feat: Complete NestJS backend scaffold — 22 modules, 39 entities, WebSocket gateway
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Full backend rewrite from Rust/Axum to NestJS/TypeScript. - 22 feature modules (auth, servers, wipes, maps, plugins, players, console, chat, team, notifications, settings, schedules, analytics, alerts, status, store, webstore, admin, setup, migration, users, licenses) - 39 TypeORM entities matching PostgreSQL schema (12 migrations) - Common infrastructure: JWT/RBAC guards, decorators, exception filter - NATS service with pub/sub/request-reply - Socket.IO WebSocket gateway with NATS bridge - Docker: NestJS Dockerfile + updated docker-compose.yml - Zero compile errors (npx tsc --noEmit clean) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
24
backend-nest/src/modules/wipes/dto/create-profile.dto.ts
Normal file
24
backend-nest/src/modules/wipes/dto/create-profile.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsString, IsOptional, MaxLength, IsObject } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateProfileDto {
|
||||
@ApiProperty({ example: 'Standard Monthly Wipe', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
profile_name: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Complete wipe with all plugins reset' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: { backup: true, notify_players: true } })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
pre_wipe_config?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional({ example: { start_server: true, send_discord_notification: true } })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
post_wipe_config?: Record<string, any>;
|
||||
}
|
||||
33
backend-nest/src/modules/wipes/dto/create-schedule.dto.ts
Normal file
33
backend-nest/src/modules/wipes/dto/create-schedule.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { IsString, IsEnum, IsUUID, IsOptional, IsBoolean, MaxLength } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class CreateScheduleDto {
|
||||
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' })
|
||||
@IsUUID()
|
||||
wipe_profile_id: string;
|
||||
|
||||
@ApiProperty({ example: 'Weekly Thursday Wipe', maxLength: 100 })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
schedule_name: string;
|
||||
|
||||
@ApiProperty({ example: 'map', enum: ['map', 'blueprint', 'full'] })
|
||||
@IsEnum(['map', 'blueprint', 'full'])
|
||||
wipe_type: 'map' | 'blueprint' | 'full';
|
||||
|
||||
@ApiProperty({ example: '0 14 * * 4', description: 'Cron expression for schedule' })
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
cron_expression: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'America/New_York', default: 'America/New_York' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(50)
|
||||
timezone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: false, default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
wipe_blueprints?: boolean;
|
||||
}
|
||||
13
backend-nest/src/modules/wipes/dto/trigger-wipe.dto.ts
Normal file
13
backend-nest/src/modules/wipes/dto/trigger-wipe.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsEnum, IsUUID, IsOptional } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class TriggerWipeDto {
|
||||
@ApiProperty({ example: 'map', enum: ['map', 'blueprint', 'full'] })
|
||||
@IsEnum(['map', 'blueprint', 'full'])
|
||||
wipe_type: 'map' | 'blueprint' | 'full';
|
||||
|
||||
@ApiPropertyOptional({ example: '550e8400-e29b-41d4-a716-446655440000' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
wipe_profile_id?: string;
|
||||
}
|
||||
4
backend-nest/src/modules/wipes/dto/update-profile.dto.ts
Normal file
4
backend-nest/src/modules/wipes/dto/update-profile.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateProfileDto } from './create-profile.dto';
|
||||
|
||||
export class UpdateProfileDto extends PartialType(CreateProfileDto) {}
|
||||
102
backend-nest/src/modules/wipes/wipes.controller.ts
Normal file
102
backend-nest/src/modules/wipes/wipes.controller.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { WipesService } from './wipes.service';
|
||||
import { CreateProfileDto } from './dto/create-profile.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { CreateScheduleDto } from './dto/create-schedule.dto';
|
||||
import { TriggerWipeDto } from './dto/trigger-wipe.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';
|
||||
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
||||
|
||||
@ApiTags('wipes')
|
||||
@ApiBearerAuth()
|
||||
@Controller('wipes')
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class WipesController {
|
||||
constructor(private readonly wipesService: WipesService) {}
|
||||
|
||||
@Get('profiles')
|
||||
@RequirePermission('wipe.view')
|
||||
@ApiOperation({ summary: 'Get all wipe profiles for tenant' })
|
||||
getProfiles(@CurrentTenant() licenseId: string) {
|
||||
return this.wipesService.getProfiles(licenseId);
|
||||
}
|
||||
|
||||
@Post('profiles')
|
||||
@RequirePermission('wipe.manage')
|
||||
@ApiOperation({ summary: 'Create new wipe profile' })
|
||||
createProfile(@CurrentTenant() licenseId: string, @Body() dto: CreateProfileDto) {
|
||||
return this.wipesService.createProfile(licenseId, dto);
|
||||
}
|
||||
|
||||
@Put('profiles/:id')
|
||||
@RequirePermission('wipe.manage')
|
||||
@ApiOperation({ summary: 'Update wipe profile' })
|
||||
updateProfile(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Param('id') profileId: string,
|
||||
@Body() dto: UpdateProfileDto,
|
||||
) {
|
||||
return this.wipesService.updateProfile(licenseId, profileId, dto);
|
||||
}
|
||||
|
||||
@Delete('profiles/:id')
|
||||
@RequirePermission('wipe.manage')
|
||||
@ApiOperation({ summary: 'Delete wipe profile' })
|
||||
async deleteProfile(@CurrentTenant() licenseId: string, @Param('id') profileId: string) {
|
||||
await this.wipesService.deleteProfile(licenseId, profileId);
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
@Get('schedules')
|
||||
@RequirePermission('wipe.view')
|
||||
@ApiOperation({ summary: 'Get all wipe schedules for tenant' })
|
||||
getSchedules(@CurrentTenant() licenseId: string) {
|
||||
return this.wipesService.getSchedules(licenseId);
|
||||
}
|
||||
|
||||
@Post('schedules')
|
||||
@RequirePermission('wipe.manage')
|
||||
@ApiOperation({ summary: 'Create new wipe schedule' })
|
||||
createSchedule(@CurrentTenant() licenseId: string, @Body() dto: CreateScheduleDto) {
|
||||
return this.wipesService.createSchedule(licenseId, dto);
|
||||
}
|
||||
|
||||
@Get('history')
|
||||
@RequirePermission('wipe.view')
|
||||
@ApiOperation({ summary: 'Get wipe history for tenant' })
|
||||
@ApiQuery({ name: 'limit', required: false, example: 50 })
|
||||
getHistory(
|
||||
@CurrentTenant() licenseId: string,
|
||||
@Query('limit', new ParseIntPipe({ optional: true })) limit?: number,
|
||||
) {
|
||||
return this.wipesService.getHistory(licenseId, limit || 50);
|
||||
}
|
||||
|
||||
@Post('trigger')
|
||||
@RequirePermission('wipe.execute')
|
||||
@ApiOperation({ summary: 'Trigger manual wipe' })
|
||||
triggerWipe(@CurrentTenant() licenseId: string, @Body() dto: TriggerWipeDto) {
|
||||
return this.wipesService.triggerWipe(licenseId, dto);
|
||||
}
|
||||
|
||||
@Post('dry-run')
|
||||
@RequirePermission('wipe.execute')
|
||||
@ApiOperation({ summary: 'Simulate wipe and return what would be affected' })
|
||||
triggerDryRun(@CurrentTenant() licenseId: string, @Body() dto: TriggerWipeDto) {
|
||||
return this.wipesService.triggerDryRun(licenseId, dto);
|
||||
}
|
||||
}
|
||||
15
backend-nest/src/modules/wipes/wipes.module.ts
Normal file
15
backend-nest/src/modules/wipes/wipes.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WipesController } from './wipes.controller';
|
||||
import { WipesService } from './wipes.service';
|
||||
import { WipeProfile } from '../../entities/wipe-profile.entity';
|
||||
import { WipeSchedule } from '../../entities/wipe-schedule.entity';
|
||||
import { WipeHistory } from '../../entities/wipe-history.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([WipeProfile, WipeSchedule, WipeHistory])],
|
||||
controllers: [WipesController],
|
||||
providers: [WipesService],
|
||||
exports: [WipesService],
|
||||
})
|
||||
export class WipesModule {}
|
||||
130
backend-nest/src/modules/wipes/wipes.service.ts
Normal file
130
backend-nest/src/modules/wipes/wipes.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { WipeProfile } from '../../entities/wipe-profile.entity';
|
||||
import { WipeSchedule } from '../../entities/wipe-schedule.entity';
|
||||
import { WipeHistory } from '../../entities/wipe-history.entity';
|
||||
import { CreateProfileDto } from './dto/create-profile.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { CreateScheduleDto } from './dto/create-schedule.dto';
|
||||
import { TriggerWipeDto } from './dto/trigger-wipe.dto';
|
||||
|
||||
@Injectable()
|
||||
export class WipesService {
|
||||
constructor(
|
||||
@InjectRepository(WipeProfile)
|
||||
private readonly wipeProfileRepo: Repository<WipeProfile>,
|
||||
@InjectRepository(WipeSchedule)
|
||||
private readonly wipeScheduleRepo: Repository<WipeSchedule>,
|
||||
@InjectRepository(WipeHistory)
|
||||
private readonly wipeHistoryRepo: Repository<WipeHistory>,
|
||||
) {}
|
||||
|
||||
async getProfiles(licenseId: string): Promise<WipeProfile[]> {
|
||||
return this.wipeProfileRepo.find({
|
||||
where: { license_id: licenseId },
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async createProfile(licenseId: string, dto: CreateProfileDto): Promise<WipeProfile> {
|
||||
const profile = this.wipeProfileRepo.create({
|
||||
license_id: licenseId,
|
||||
...dto,
|
||||
});
|
||||
return this.wipeProfileRepo.save(profile);
|
||||
}
|
||||
|
||||
async updateProfile(
|
||||
licenseId: string,
|
||||
profileId: string,
|
||||
dto: UpdateProfileDto,
|
||||
): Promise<WipeProfile> {
|
||||
const profile = await this.wipeProfileRepo.findOne({
|
||||
where: { id: profileId, license_id: licenseId },
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
throw new NotFoundException(`Wipe profile ${profileId} not found`);
|
||||
}
|
||||
|
||||
Object.assign(profile, dto);
|
||||
profile.updated_at = new Date();
|
||||
return this.wipeProfileRepo.save(profile);
|
||||
}
|
||||
|
||||
async deleteProfile(licenseId: string, profileId: string): Promise<void> {
|
||||
const result = await this.wipeProfileRepo.delete({
|
||||
id: profileId,
|
||||
license_id: licenseId,
|
||||
});
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Wipe profile ${profileId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async getSchedules(licenseId: string): Promise<WipeSchedule[]> {
|
||||
return this.wipeScheduleRepo.find({
|
||||
where: { license_id: licenseId },
|
||||
relations: ['wipe_profile'],
|
||||
order: { created_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async createSchedule(licenseId: string, dto: CreateScheduleDto): Promise<WipeSchedule> {
|
||||
const schedule = this.wipeScheduleRepo.create({
|
||||
license_id: licenseId,
|
||||
...dto,
|
||||
});
|
||||
return this.wipeScheduleRepo.save(schedule);
|
||||
}
|
||||
|
||||
async getHistory(licenseId: string, limit: number = 50): Promise<WipeHistory[]> {
|
||||
return this.wipeHistoryRepo.find({
|
||||
where: { license_id: licenseId },
|
||||
relations: ['wipe_profile', 'wipe_schedule', 'map'],
|
||||
order: { created_at: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
async triggerWipe(
|
||||
licenseId: string,
|
||||
dto: TriggerWipeDto,
|
||||
): Promise<{ wipe_history_id: string }> {
|
||||
const history = this.wipeHistoryRepo.create({
|
||||
license_id: licenseId,
|
||||
wipe_type: dto.wipe_type,
|
||||
wipe_profile_id: dto.wipe_profile_id,
|
||||
trigger_type: 'manual',
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
const saved = await this.wipeHistoryRepo.save(history);
|
||||
return { wipe_history_id: saved.id };
|
||||
}
|
||||
|
||||
async triggerDryRun(
|
||||
licenseId: string,
|
||||
dto: TriggerWipeDto,
|
||||
): Promise<{
|
||||
would_delete: string[];
|
||||
would_preserve: string[];
|
||||
estimated_duration_seconds: number;
|
||||
}> {
|
||||
// Stub implementation - real logic would analyze wipe profile config
|
||||
const mockResult = {
|
||||
would_delete: ['*.sav', '*.db', 'player.deaths.db', 'player.identities.db'],
|
||||
would_preserve: ['oxide/', 'oxide/plugins/', 'oxide/data/', 'backups/'],
|
||||
estimated_duration_seconds: 45,
|
||||
};
|
||||
|
||||
if (dto.wipe_type === 'full') {
|
||||
mockResult.would_delete.push('oxide/data/*');
|
||||
mockResult.estimated_duration_seconds = 120;
|
||||
}
|
||||
|
||||
return mockResult;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user