Files
corrosion-admin-panel/backend-nest/src/modules/admin/admin.service.ts
Vantz Stockwell 8253680fbd
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
fix: License key format, login populates license, case-insensitive email
- admin.service.ts: createLicense() now uses CORR-XXXX-XXXX-XXXX format
  instead of raw hex hash
- admin.service.ts: getLicenses() flattens owner_email in response to
  match frontend expected shape
- auth.service.ts: Login/register responses now include full license
  object so frontend can populate auth store
- auth.service.ts: Email lookups are case-insensitive (LOWER()) to
  prevent duplicate accounts from case variations
- LoginView/RegisterView: Call setLicense() after setAuth()
- AdminLicenses: Handle null expires_at (was showing Dec 31, 1969),
  fix nullable types, fix query param name (per_page → limit)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:32:35 -05:00

183 lines
5.3 KiB
TypeScript

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.map(l => ({
id: l.id,
license_key: l.license_key,
owner_email: l.owner?.email ?? '',
server_name: l.server_name,
status: l.status,
created_at: l.created_at,
expires_at: l.expires_at,
})),
total,
};
}
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 (branded CORR-XXXX-XXXX-XXXX format)
const part1 = crypto.randomBytes(2).toString('hex').toUpperCase();
const part2 = crypto.randomBytes(2).toString('hex').toUpperCase();
const part3 = crypto.randomBytes(2).toString('hex').toUpperCase();
const licenseKey = `CORR-${part1}-${part2}-${part3}`;
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() {
const subs = await this.webstoreSubRepo.find({
relations: ['license', 'license.owner'],
order: { created_at: 'DESC' },
});
return {
subscriptions: subs.map(sub => ({
owner_email: sub.license?.owner?.email ?? 'Unknown',
module_name: sub.plan_id,
license_id: sub.license_id,
})),
};
}
async getServers() {
return this.serverConnectionRepo
.createQueryBuilder('conn')
.leftJoinAndSelect('conn.license', 'license')
.leftJoinAndSelect('license.owner', 'owner')
.orderBy('conn.created_at', 'DESC')
.getMany();
}
}