import { Controller, Get, Post, Put, Patch, Delete, Inject, Param, Query, Body, UseGuards, UseInterceptors, UploadedFile, Req, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiConsumes, ApiQuery } from '@nestjs/swagger'; import { JwtAuthGuard, Roles, RolesGuard, UserRole } from '@genex/common'; import { AppVersionService } from '../../../application/services/app-version.service'; import { FileStorageService } from '../../../application/services/file-storage.service'; import { PACKAGE_PARSER, IPackageParser } from '../../../domain/ports/package-parser.interface'; import { Platform } from '../../../domain/enums/platform.enum'; import { AppType } from '../../../domain/enums/app-type.enum'; @ApiTags('admin-versions') @Controller('admin/versions') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @ApiBearerAuth() export class AdminVersionController { constructor( private readonly versionService: AppVersionService, private readonly fileStorage: FileStorageService, @Inject(PACKAGE_PARSER) private readonly packageParser: IPackageParser, ) {} @Get() @ApiOperation({ summary: 'List app versions' }) @ApiQuery({ name: 'appType', required: false, enum: AppType }) @ApiQuery({ name: 'platform', required: false, enum: Platform }) async listVersions( @Query('appType') appType?: string, @Query('platform') platform?: string, @Query('includeDisabled') includeDisabled?: string, ) { const appTypeEnum = appType ? (appType.toUpperCase() as AppType) : undefined; const platformEnum = platform ? (platform.toUpperCase() as Platform) : undefined; const versions = await this.versionService.listVersions( appTypeEnum, platformEnum, includeDisabled === 'true', ); return { code: 0, data: versions }; } @Get(':id') @ApiOperation({ summary: 'Get version details' }) async getVersion(@Param('id') id: string) { const version = await this.versionService.getVersion(id); return { code: 0, data: version }; } @Post() @ApiOperation({ summary: 'Create version manually' }) async createVersion( @Body() body: { appType: string; platform: string; versionCode: number; versionName: string; buildNumber: string; downloadUrl: string; fileSize: string; fileSha256: string; changelog: string; isForceUpdate: boolean; minOsVersion?: string; releaseDate?: string; }, @Req() req: any, ) { const version = await this.versionService.createVersion({ ...body, appType: (body.appType || 'GENEX_MOBILE').toUpperCase() as AppType, platform: body.platform.toUpperCase() as Platform, releaseDate: body.releaseDate ? new Date(body.releaseDate) : undefined, createdBy: req.user?.sub, }); return { code: 0, data: version }; } @Post('upload') @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload APK/IPA and create version' }) async uploadVersion( @UploadedFile() file: Express.Multer.File, @Body() body: { appType?: string; platform: string; versionCode?: string; versionName?: string; buildNumber?: string; changelog?: string; isForceUpdate?: string; minOsVersion?: string; releaseDate?: string; }, @Req() req: any, ) { // Parse package to extract metadata (auto-fill when not provided) const parsedInfo = await this.packageParser.parse(file.buffer, file.originalname); const appType: AppType = (body.appType || 'GENEX_MOBILE').toUpperCase() as AppType; const platform: Platform = body.platform ? (body.platform.toUpperCase() as Platform) : (parsedInfo.platform as Platform); const versionCode = body.versionCode ? parseInt(body.versionCode, 10) : parsedInfo.versionCode || Date.now(); const versionName = body.versionName || parsedInfo.versionName || '1.0.0'; const buildNumber = body.buildNumber || versionCode.toString(); // Upload to MinIO const uploadResult = await this.fileStorage.uploadFile( file.buffer, file.originalname, platform, versionName, ); const version = await this.versionService.createVersion({ appType, platform, versionCode, versionName, buildNumber, storageKey: uploadResult.objectName, downloadUrl: '', // will be updated after id is known fileSize: uploadResult.fileSize, fileSha256: uploadResult.sha256, changelog: body.changelog || '', isForceUpdate: body.isForceUpdate === 'true', minOsVersion: body.minOsVersion || parsedInfo.minSdkVersion, releaseDate: body.releaseDate ? new Date(body.releaseDate) : undefined, createdBy: req.user?.sub, }); // Set stable download URL pointing to this service's download endpoint const updated = await this.versionService.updateVersion(version.id, { downloadUrl: `/api/v1/app/version/download/${version.id}`, }); return { code: 0, data: updated }; } @Post('parse') @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Parse APK/IPA without saving (preview metadata)' }) async parsePackage(@UploadedFile() file: Express.Multer.File) { const info = await this.packageParser.parse(file.buffer, file.originalname); return { code: 0, data: info }; } @Put(':id') @ApiOperation({ summary: 'Update version' }) async updateVersion( @Param('id') id: string, @Body() body: any, @Req() req: any, ) { const version = await this.versionService.updateVersion(id, { ...body, updatedBy: req.user?.sub, }); return { code: 0, data: version }; } @Patch(':id/toggle') @ApiOperation({ summary: 'Enable/disable version' }) async toggleVersion( @Param('id') id: string, @Body() body: { isEnabled: boolean }, ) { const result = await this.versionService.toggleVersion(id, body.isEnabled); return { code: 0, data: result }; } @Delete(':id') @ApiOperation({ summary: 'Delete version' }) async deleteVersion(@Param('id') id: string) { const result = await this.versionService.deleteVersion(id); return { code: 0, data: result }; } }