feat: Complete stub services with real implementations and graceful not-configured responses
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
- ChangelogService: inject PlatformChangelog repo, query with findAndCount(skip/take), return actual data
- ChangelogModule: add TypeOrmModule.forFeature([PlatformChangelog])
- MapsService: add uploadMap() — SHA-256 checksum, storage path, full entity save
- MapsController: add POST /maps/upload with FileInterceptor, map.manage permission, @UploadedFile
- AuthService: replace console.log stubs with Logger; forgotPassword returns 200 with clear message; resetPassword throws NotImplementedException
- PluginsService: searchUmod returns { results: [], message: 'not yet configured' } instead of bare []
- SteamService: add Logger.warn on every stub path (checkForceWipe, getPlayerSummary)
- SettingsService: add Logger; both Cloudflare DNS stubs emit Logger.warn before DB save
- MigrationService: add Logger; exportConfig logs warning + returns note field; importConfig throws NotImplementedException
- Install @types/multer dev dependency for Express.Multer.File type support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
backend-nest/package-lock.json
generated
11
backend-nest/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<User>,
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<PlatformChangelog>,
|
||||
) {}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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<MapRotation>,
|
||||
) {}
|
||||
|
||||
async uploadMap(
|
||||
licenseId: string,
|
||||
dto: UploadMapDto,
|
||||
file: Express.Multer.File,
|
||||
): Promise<MapLibrary> {
|
||||
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 },
|
||||
|
||||
@@ -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<MigrationExport>,
|
||||
) {}
|
||||
|
||||
async exportConfig(licenseId: string, userId: string, exportType: string = 'full'): Promise<MigrationExport> {
|
||||
async exportConfig(licenseId: string, userId: string, exportType: string = 'full'): Promise<MigrationExport & { note: string }> {
|
||||
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<MigrationExport[]> {
|
||||
@@ -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<never> {
|
||||
this.logger.warn(`Import attempted for license ${licenseId} — not yet implemented`);
|
||||
throw new NotImplementedException('Migration import not yet available');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PluginRegistry>,
|
||||
private readonly natsService: NatsService,
|
||||
) {}
|
||||
|
||||
async getPlugins(licenseId: string): Promise<PluginRegistry[]> {
|
||||
@@ -45,14 +49,22 @@ export class PluginsService {
|
||||
}
|
||||
|
||||
async uninstallPlugin(licenseId: string, pluginId: string): Promise<void> {
|
||||
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<any[]> {
|
||||
// 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PublicSiteConfig>,
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user