feat: Complete NestJS backend scaffold — 22 modules, 39 entities, WebSocket gateway
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:
Vantz Stockwell
2026-02-15 21:29:25 -05:00
parent 0f8d0dd14f
commit d20493d533
141 changed files with 13552 additions and 4 deletions

View File

@@ -0,0 +1,106 @@
import {
WebSocketGateway,
WebSocketServer,
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { NatsBridgeService } from '../services/nats-bridge.service';
import { NatsService } from '../services/nats.service';
interface AuthenticatedSocket extends Socket {
data: {
userId: string;
licenseId: string;
email: string;
};
}
@WebSocketGateway({
namespace: '/ws',
cors: { origin: '*' },
})
export class NatsBridgeGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly logger = new Logger(NatsBridgeGateway.name);
@WebSocketServer()
server!: Server;
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private natsBridge: NatsBridgeService,
private natsService: NatsService,
) {}
async handleConnection(client: AuthenticatedSocket) {
try {
const token = client.handshake.query.token as string;
if (!token) {
client.emit('error', { message: 'Authentication required' });
client.disconnect();
return;
}
const secret = this.configService.get<string>('jwt.secret');
const payload = this.jwtService.verify(token, { secret });
client.data = {
userId: payload.sub,
licenseId: payload.license_id,
email: payload.email,
};
if (payload.license_id) {
await client.join(`license:${payload.license_id}`);
}
if (payload.license_id) {
const listener = (event: string, data: unknown) => {
client.emit('event', {
type: 'event',
license_id: payload.license_id,
event,
data,
});
};
this.natsBridge.addListener(payload.license_id, listener);
(client as Socket & { _natsListener?: typeof listener })._natsListener = listener;
}
client.emit('connected', { type: 'connected', license_id: payload.license_id });
this.logger.log(`Client connected: ${payload.email} (license: ${payload.license_id})`);
} catch {
client.emit('error', { message: 'Invalid token' });
client.disconnect();
}
}
handleDisconnect(client: AuthenticatedSocket) {
if (client.data?.licenseId) {
const listener = (client as Socket & { _natsListener?: (event: string, data: unknown) => void })._natsListener;
if (listener) {
this.natsBridge.removeListener(client.data.licenseId, listener);
}
}
}
@SubscribeMessage('console_input')
async handleConsoleInput(client: AuthenticatedSocket, data: { command: string }) {
if (!client.data?.licenseId) return;
await this.natsService.sendServerCommand(client.data.licenseId, 'command', { command: data.command });
}
sendToLicense(licenseId: string, event: string, data: unknown): void {
this.server.to(`license:${licenseId}`).emit(event, {
type: 'event',
license_id: licenseId,
event,
data,
});
}
}