diff --git a/backend-nest/package-lock.json b/backend-nest/package-lock.json index 923b1eb..a068ba3 100644 --- a/backend-nest/package-lock.json +++ b/backend-nest/package-lock.json @@ -39,6 +39,7 @@ "@nestjs/cli": "^10.4.0", "@nestjs/schematics": "^10.1.0", "@types/express": "^4.17.21", + "@types/multer": "^2.0.0", "@types/node": "^20.12.0", "@types/passport-jwt": "^4.0.1", "@types/qrcode": "^1.5.5", @@ -1256,6 +1257,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", diff --git a/backend-nest/package.json b/backend-nest/package.json index 15a58d1..63ee7cf 100644 --- a/backend-nest/package.json +++ b/backend-nest/package.json @@ -44,6 +44,7 @@ "@nestjs/cli": "^10.4.0", "@nestjs/schematics": "^10.1.0", "@types/express": "^4.17.21", + "@types/multer": "^2.0.0", "@types/node": "^20.12.0", "@types/passport-jwt": "^4.0.1", "@types/qrcode": "^1.5.5", diff --git a/backend-nest/src/modules/auth/auth.service.ts b/backend-nest/src/modules/auth/auth.service.ts index 84864bb..579859b 100644 --- a/backend-nest/src/modules/auth/auth.service.ts +++ b/backend-nest/src/modules/auth/auth.service.ts @@ -4,6 +4,8 @@ import { ConflictException, BadRequestException, NotFoundException, + NotImplementedException, + Logger, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -21,6 +23,8 @@ import { UpdateProfileDto } from './dto/update-profile.dto'; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); + constructor( @InjectRepository(User) private userRepository: Repository, @@ -294,21 +298,16 @@ export class AuthService { } async forgotPassword(email: string) { - // Stub - SMTP integration later - console.log(`Password reset requested for: ${email}`); - // In production, generate reset token, save to DB, send email + // SMTP integration pending — returns 200 to avoid leaking account enumeration + this.logger.warn(`Password reset requested for ${email} — SMTP not yet configured`); return { - message: 'If an account with that email exists, a password reset link has been sent.', + message: 'Password reset is not yet configured. Contact your administrator.', }; } - async resetPassword(token: string, password: string) { - // Stub - SMTP integration later - console.log(`Password reset with token: ${token}`); - // In production, validate token, update password - return { - message: 'Password has been reset successfully.', - }; + async resetPassword(token: string, _password: string) { + this.logger.warn(`Password reset attempted with token ${token} — SMTP not yet configured`); + throw new NotImplementedException('Password reset not yet configured'); } // Helper methods diff --git a/backend-nest/src/modules/changelog/changelog.module.ts b/backend-nest/src/modules/changelog/changelog.module.ts index cbcf0f4..b75f968 100644 --- a/backend-nest/src/modules/changelog/changelog.module.ts +++ b/backend-nest/src/modules/changelog/changelog.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { ChangelogController } from './changelog.controller'; import { ChangelogService } from './changelog.service'; +import { PlatformChangelog } from '../../entities/platform-changelog.entity'; @Module({ + imports: [TypeOrmModule.forFeature([PlatformChangelog])], controllers: [ChangelogController], providers: [ChangelogService], }) diff --git a/backend-nest/src/modules/changelog/changelog.service.ts b/backend-nest/src/modules/changelog/changelog.service.ts index 0132520..07583f3 100644 --- a/backend-nest/src/modules/changelog/changelog.service.ts +++ b/backend-nest/src/modules/changelog/changelog.service.ts @@ -1,21 +1,27 @@ import { Injectable } from '@nestjs/common'; - -export interface ChangelogEntry { - id: string; - version: string; - title: string; - description: string; - type: 'feature' | 'fix' | 'improvement' | 'breaking'; - created_at: string; -} +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PlatformChangelog } from '../../entities/platform-changelog.entity'; @Injectable() export class ChangelogService { + constructor( + @InjectRepository(PlatformChangelog) + private readonly changelogRepo: Repository, + ) {} + async getEntries( page: number, limit: number, - ): Promise<{ entries: ChangelogEntry[]; total: number }> { - // Stub — returns empty until changelog entries are seeded or managed - return { entries: [], total: 0 }; + ): Promise<{ entries: PlatformChangelog[]; total: number }> { + const skip = (page - 1) * limit; + + const [entries, total] = await this.changelogRepo.findAndCount({ + order: { published_at: 'DESC' }, + skip, + take: limit, + }); + + return { entries, total }; } } diff --git a/backend-nest/src/modules/maps/maps.controller.ts b/backend-nest/src/modules/maps/maps.controller.ts index efc3f53..8d66b02 100644 --- a/backend-nest/src/modules/maps/maps.controller.ts +++ b/backend-nest/src/modules/maps/maps.controller.ts @@ -1,7 +1,21 @@ -import { Controller, Get, Delete, Put, Body, Param, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { + Controller, + Get, + Delete, + Put, + Post, + Body, + Param, + UseGuards, + UseInterceptors, + UploadedFile, + BadRequestException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiTags, ApiBearerAuth, ApiOperation, ApiConsumes } from '@nestjs/swagger'; import { MapsService } from './maps.service'; import { UpdateRotationDto } from './dto/update-rotation.dto'; +import { UploadMapDto } from './dto/upload-map.dto'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; @@ -14,6 +28,22 @@ import { PermissionsGuard } from '../../common/guards/permissions.guard'; export class MapsController { constructor(private readonly mapsService: MapsService) {} + @Post('upload') + @RequirePermission('map.manage') + @ApiOperation({ summary: 'Upload a map to the library' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('file')) + uploadMap( + @CurrentTenant() licenseId: string, + @Body() dto: UploadMapDto, + @UploadedFile() file: Express.Multer.File, + ) { + if (!file) { + throw new BadRequestException('No file provided'); + } + return this.mapsService.uploadMap(licenseId, dto, file); + } + @Get() @RequirePermission('map.view') @ApiOperation({ summary: 'Get all maps for tenant' }) diff --git a/backend-nest/src/modules/maps/maps.service.ts b/backend-nest/src/modules/maps/maps.service.ts index ae259bc..a8a58af 100644 --- a/backend-nest/src/modules/maps/maps.service.ts +++ b/backend-nest/src/modules/maps/maps.service.ts @@ -1,9 +1,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { createHash } from 'crypto'; import { MapLibrary } from '../../entities/map-library.entity'; import { MapRotation } from '../../entities/map-rotation.entity'; import { UpdateRotationDto } from './dto/update-rotation.dto'; +import { UploadMapDto } from './dto/upload-map.dto'; @Injectable() export class MapsService { @@ -14,6 +16,30 @@ export class MapsService { private readonly mapRotationRepo: Repository, ) {} + async uploadMap( + licenseId: string, + dto: UploadMapDto, + file: Express.Multer.File, + ): Promise { + const checksum = createHash('sha256').update(file.buffer).digest('hex'); + const storagePath = `/maps/${licenseId}/${Date.now()}_${file.originalname}`; + + const map = this.mapLibraryRepo.create({ + license_id: licenseId, + filename: file.originalname, + display_name: dto.display_name, + storage_path: storagePath, + file_size_bytes: file.size, + map_type: dto.map_type, + seed: dto.seed ?? null, + world_size: dto.world_size ?? null, + thumbnail_path: null, + checksum, + }); + + return this.mapLibraryRepo.save(map); + } + async getMaps(licenseId: string): Promise<{ maps: MapLibrary[] }> { const maps = await this.mapLibraryRepo.find({ where: { license_id: licenseId }, diff --git a/backend-nest/src/modules/migration/migration.service.ts b/backend-nest/src/modules/migration/migration.service.ts index ad2e395..f57db03 100644 --- a/backend-nest/src/modules/migration/migration.service.ts +++ b/backend-nest/src/modules/migration/migration.service.ts @@ -1,16 +1,22 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotImplementedException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { MigrationExport } from '../../entities/migration-export.entity'; @Injectable() export class MigrationService { + private readonly logger = new Logger(MigrationService.name); + constructor( @InjectRepository(MigrationExport) private readonly exportRepo: Repository, ) {} - async exportConfig(licenseId: string, userId: string, exportType: string = 'full'): Promise { + async exportConfig(licenseId: string, userId: string, exportType: string = 'full'): Promise { + this.logger.warn( + `Export requested for license ${licenseId} by user ${userId} — file generation not yet implemented, DB record only`, + ); + const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // 7 days expiry @@ -18,12 +24,17 @@ export class MigrationService { license_id: licenseId, export_type: exportType, storage_path: `/exports/${licenseId}/${Date.now()}.json`, - file_size_bytes: 0, // Stub - would calculate after actual export + file_size_bytes: 0, created_by: userId, expires_at: expiresAt, }); - return this.exportRepo.save(exportRecord); + const saved = await this.exportRepo.save(exportRecord); + + return { + ...saved, + note: 'Export record created. File generation is pending implementation.', + }; } async getExports(licenseId: string): Promise { @@ -33,8 +44,8 @@ export class MigrationService { }); } - async importConfig(licenseId: string, data: any): Promise<{ message: string }> { - // Stub implementation - would validate and import data in production - return { message: 'Import complete' }; + async importConfig(licenseId: string, _data: any): Promise { + this.logger.warn(`Import attempted for license ${licenseId} — not yet implemented`); + throw new NotImplementedException('Migration import not yet available'); } } diff --git a/backend-nest/src/modules/plugins/plugins.service.ts b/backend-nest/src/modules/plugins/plugins.service.ts index 6251b09..a8f3aec 100644 --- a/backend-nest/src/modules/plugins/plugins.service.ts +++ b/backend-nest/src/modules/plugins/plugins.service.ts @@ -1,15 +1,19 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PluginRegistry } from '../../entities/plugin-registry.entity'; import { InstallPluginDto } from './dto/install-plugin.dto'; import { UpdatePluginConfigDto } from './dto/update-plugin-config.dto'; +import { NatsService } from '../../services/nats.service'; @Injectable() export class PluginsService { + private readonly logger = new Logger(PluginsService.name); + constructor( @InjectRepository(PluginRegistry) private readonly pluginRegistryRepo: Repository, + private readonly natsService: NatsService, ) {} async getPlugins(licenseId: string): Promise { @@ -45,14 +49,22 @@ export class PluginsService { } async uninstallPlugin(licenseId: string, pluginId: string): Promise { - const result = await this.pluginRegistryRepo.delete({ - id: pluginId, - license_id: licenseId, + const plugin = await this.pluginRegistryRepo.findOne({ + where: { id: pluginId, license_id: licenseId }, }); - if (result.affected === 0) { + if (!plugin) { throw new NotFoundException(`Plugin ${pluginId} not found`); } + + await this.pluginRegistryRepo.delete({ id: pluginId, license_id: licenseId }); + + await this.natsService.publish(`corrosion.${licenseId}.cmd.plugin`, { + action: 'unload', + plugin_name: plugin.plugin_name, + timestamp: new Date().toISOString(), + }); + this.logger.log(`Plugin uninstall dispatched for ${plugin.plugin_name} on license ${licenseId}`); } async reloadPlugin( @@ -67,8 +79,13 @@ export class PluginsService { throw new NotFoundException(`Plugin ${pluginId} not found`); } - // Stub implementation - in production would trigger NATS command - // to reload plugin on game server + await this.natsService.publish(`corrosion.${licenseId}.cmd.plugin`, { + action: 'reload', + plugin_name: plugin.plugin_name, + timestamp: new Date().toISOString(), + }); + this.logger.log(`Plugin reload dispatched for ${plugin.plugin_name} on license ${licenseId}`); + return { reloaded: true, plugin_name: plugin.plugin_name }; } @@ -90,9 +107,11 @@ export class PluginsService { return this.pluginRegistryRepo.save(plugin); } - async searchUmod(query: string): Promise { - // Stub implementation - in production would proxy to uMod API - // or use cached plugin directory - return []; + async searchUmod(query: string): Promise<{ results: any[]; message: string }> { + // uMod API integration pending + return { + results: [], + message: 'uMod search integration not yet configured', + }; } } diff --git a/backend-nest/src/modules/settings/settings.service.ts b/backend-nest/src/modules/settings/settings.service.ts index d457e56..3e6ac69 100644 --- a/backend-nest/src/modules/settings/settings.service.ts +++ b/backend-nest/src/modules/settings/settings.service.ts @@ -3,6 +3,7 @@ import { NotFoundException, BadRequestException, ConflictException, + Logger, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -13,6 +14,8 @@ import { UpdateDomainDto } from './dto/update-domain.dto'; @Injectable() export class SettingsService { + private readonly logger = new Logger(SettingsService.name); + constructor( @InjectRepository(PublicSiteConfig) private publicSiteConfigRepository: Repository, @@ -97,12 +100,9 @@ export class SettingsService { ); } - // TODO: Stub Cloudflare DNS provisioning - // In production, this would: - // 1. Create DNS CNAME record: {subdomain}.corrosionmgmt.com → panel.corrosionmgmt.com - // 2. Wait for DNS propagation - // 3. Verify SSL certificate provisioning - // For now, we just update the database + this.logger.warn( + `Cloudflare DNS integration not configured — subdomain ${dto.subdomain} updated in DB only`, + ); license.subdomain = dto.subdomain; } @@ -115,13 +115,9 @@ export class SettingsService { throw new BadRequestException('Invalid custom domain format'); } - // TODO: Stub Cloudflare DNS verification - // In production, this would: - // 1. Instruct user to create CNAME pointing to panel.corrosionmgmt.com - // 2. Verify DNS record exists - // 3. Provision SSL certificate via Cloudflare - // 4. Mark domain as verified - // For now, we just update the database + this.logger.warn( + `Cloudflare DNS integration not configured — custom domain ${dto.custom_domain} updated in DB only`, + ); license.custom_domain = dto.custom_domain; } else if (dto.custom_domain === null || dto.custom_domain === '') { // Allow clearing custom domain diff --git a/backend-nest/src/services/steam.service.ts b/backend-nest/src/services/steam.service.ts index 026d51b..2ce36dd 100644 --- a/backend-nest/src/services/steam.service.ts +++ b/backend-nest/src/services/steam.service.ts @@ -11,14 +11,16 @@ export class SteamService { } async checkForceWipe(): Promise<{ isForceWipe: boolean; expectedDate: string | null }> { - // Stub — would check Steam API for Rust staging branch updates + this.logger.warn('Steam API not configured — checkForceWipe returning stub response'); return { isForceWipe: false, expectedDate: null }; } async getPlayerSummary(steamId: string): Promise<{ personaname: string; avatarfull: string } | null> { - if (!this.apiKey) return null; - // Stub — would call ISteamUser/GetPlayerSummaries/v2 - this.logger.debug(`Would fetch Steam profile for ${steamId}`); + if (!this.apiKey) { + this.logger.warn('Steam API not configured — getPlayerSummary returning null'); + return null; + } + this.logger.warn(`Steam API not configured — skipping profile fetch for ${steamId}`); return null; } }