fix: License key format, login populates license, case-insensitive email
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user