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,168 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like } from 'typeorm';
import { User } from '../../entities/user.entity';
import { License } from '../../entities/license.entity';
import { ServerConnection } from '../../entities/server-connection.entity';
import { WebstoreSubscription } from '../../entities/webstore-subscription.entity';
import * as crypto from 'crypto';
import * as argon2 from 'argon2';
@Injectable()
export class AdminService {
constructor(
@InjectRepository(User)
private readonly userRepo: Repository<User>,
@InjectRepository(License)
private readonly licenseRepo: Repository<License>,
@InjectRepository(ServerConnection)
private readonly serverConnectionRepo: Repository<ServerConnection>,
@InjectRepository(WebstoreSubscription)
private readonly webstoreSubRepo: Repository<WebstoreSubscription>,
) {}
async getStats() {
const [totalUsers, totalLicenses, activeServers] = await Promise.all([
this.userRepo.count(),
this.licenseRepo.count(),
this.serverConnectionRepo.count({
where: { connection_status: 'connected' },
}),
]);
return {
total_users: totalUsers,
total_licenses: totalLicenses,
active_servers: activeServers,
};
}
async getLicenses(page: number = 1, limit: number = 25, search?: string) {
const skip = (page - 1) * limit;
const queryBuilder = this.licenseRepo
.createQueryBuilder('license')
.leftJoinAndSelect('license.owner', 'owner')
.orderBy('license.created_at', 'DESC')
.skip(skip)
.take(limit);
if (search) {
queryBuilder.where(
'(license.license_key ILIKE :search OR license.server_name ILIKE :search OR license.subdomain ILIKE :search OR owner.email ILIKE :search)',
{ search: `%${search}%` },
);
}
const [licenses, total] = await queryBuilder.getManyAndCount();
return {
data: licenses,
pagination: {
page,
limit,
total,
total_pages: Math.ceil(total / limit),
},
};
}
async getLicenseById(id: string) {
return this.licenseRepo.findOne({
where: { id },
relations: ['owner'],
});
}
async createLicense(email: string) {
// Find or create user
let user = await this.userRepo.findOne({ where: { email } });
if (!user) {
// Create new user with random password
const randomPassword = crypto.randomBytes(16).toString('hex');
const passwordHash = await argon2.hash(randomPassword);
const username = email.split('@')[0] + '_' + Math.random().toString(36).substr(2, 5);
user = this.userRepo.create({
email,
username,
password_hash: passwordHash,
});
await this.userRepo.save(user);
}
// Create license
const licenseKey = crypto.randomBytes(32).toString('hex');
const license = this.licenseRepo.create({
license_key: licenseKey,
owner_user_id: user.id,
status: 'active',
});
return this.licenseRepo.save(license);
}
async getUsers(page: number = 1, limit: number = 25) {
const skip = (page - 1) * limit;
const [users, total] = await this.userRepo.findAndCount({
order: { created_at: 'DESC' },
skip,
take: limit,
});
return {
data: users.map(u => ({
id: u.id,
email: u.email,
username: u.username,
is_super_admin: u.is_super_admin,
email_verified: u.email_verified,
totp_enabled: u.totp_enabled,
created_at: u.created_at,
last_login_at: u.last_login_at,
})),
pagination: {
page,
limit,
total,
total_pages: Math.ceil(total / limit),
},
};
}
async updateUser(userId: string, data: Partial<User>) {
const user = await this.userRepo.findOne({ where: { id: userId } });
if (!user) {
throw new BadRequestException('User not found');
}
// Only allow updating specific fields
if (typeof data.is_super_admin !== 'undefined') {
user.is_super_admin = data.is_super_admin;
}
if (typeof data.email_verified !== 'undefined') {
user.email_verified = data.email_verified;
}
return this.userRepo.save(user);
}
async getSubscriptions() {
return this.webstoreSubRepo.find({
relations: ['license'],
order: { created_at: 'DESC' },
});
}
async getServers() {
return this.serverConnectionRepo
.createQueryBuilder('conn')
.leftJoinAndSelect('conn.license', 'license')
.leftJoinAndSelect('license.owner', 'owner')
.orderBy('conn.created_at', 'DESC')
.getMany();
}
}