fix: License key format, login populates license, case-insensitive email
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

- 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>
This commit is contained in:
Vantz Stockwell
2026-02-21 15:32:35 -05:00
parent 14b099b075
commit 8253680fbd
6 changed files with 77 additions and 28 deletions

View File

@@ -57,13 +57,16 @@ export class AdminService {
const [licenses, total] = await queryBuilder.getManyAndCount();
return {
data: licenses,
pagination: {
page,
limit,
total,
total_pages: Math.ceil(total / limit),
},
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,
};
}
@@ -92,8 +95,11 @@ export class AdminService {
await this.userRepo.save(user);
}
// Create license
const licenseKey = crypto.randomBytes(32).toString('hex');
// 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,

View File

@@ -35,13 +35,20 @@ export class AuthService {
) {}
async register(dto: RegisterDto) {
// Normalize email to lowercase to prevent case-sensitive duplicates
const normalizedEmail = dto.email.toLowerCase();
// Check if user already exists
const existingUser = await this.userRepository.findOne({
where: [{ email: dto.email }, { username: dto.username }],
});
const existingUser = await this.userRepository
.createQueryBuilder('user')
.where('LOWER(user.email) = :email OR user.username = :username', {
email: normalizedEmail,
username: dto.username,
})
.getOne();
if (existingUser) {
if (existingUser.email === dto.email) {
if (existingUser.email.toLowerCase() === normalizedEmail) {
throw new ConflictException('Email already registered');
}
throw new ConflictException('Username already taken');
@@ -50,9 +57,9 @@ export class AuthService {
// Hash password
const password_hash = await argon2.hash(dto.password);
// Create user
// Create user (email stored lowercase)
const user = this.userRepository.create({
email: dto.email,
email: normalizedEmail,
username: dto.username,
password_hash,
email_verified: false,
@@ -85,16 +92,28 @@ export class AuthService {
username: user.username,
is_super_admin: user.is_super_admin,
totp_enabled: user.totp_enabled,
license_key: licenseKey,
},
license: {
id: license.id,
license_key: license.license_key,
status: license.status,
server_name: license.server_name ?? null,
subdomain: license.subdomain ?? null,
custom_domain: license.custom_domain ?? null,
modules_enabled: license.modules_enabled,
webstore_active: license.webstore_active,
created_at: license.created_at,
expires_at: license.expires_at ?? null,
},
};
}
async login(dto: LoginDto) {
// Find user by email
const user = await this.userRepository.findOne({
where: { email: dto.email },
});
// Find user by email (case-insensitive)
const user = await this.userRepository
.createQueryBuilder('user')
.where('LOWER(user.email) = :email', { email: dto.email.toLowerCase() })
.getOne();
if (!user) {
throw new UnauthorizedException('Invalid credentials');
@@ -142,8 +161,19 @@ export class AuthService {
username: user.username,
is_super_admin: user.is_super_admin,
totp_enabled: user.totp_enabled,
license_key: license?.license_key,
},
license: license ? {
id: license.id,
license_key: license.license_key,
status: license.status,
server_name: license.server_name,
subdomain: license.subdomain,
custom_domain: license.custom_domain,
modules_enabled: license.modules_enabled,
webstore_active: license.webstore_active,
created_at: license.created_at,
expires_at: license.expires_at,
} : null,
};
}