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:
168
backend-nest/src/modules/admin/admin.service.ts
Normal file
168
backend-nest/src/modules/admin/admin.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user