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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface AuthResponse {
|
||||
refresh_token: string
|
||||
requires_totp: boolean
|
||||
user: User
|
||||
license: License | null
|
||||
}
|
||||
|
||||
export interface ServerConnection {
|
||||
|
||||
@@ -39,6 +39,9 @@ async function handleLogin() {
|
||||
}
|
||||
|
||||
authStore.setAuth(response)
|
||||
if (response.license) {
|
||||
authStore.setLicense(response.license)
|
||||
}
|
||||
router.push('/')
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
@@ -68,6 +71,9 @@ async function handleTotpVerify() {
|
||||
})
|
||||
|
||||
authStore.setAuth(response)
|
||||
if (response.license) {
|
||||
authStore.setLicense(response.license)
|
||||
}
|
||||
router.push('/')
|
||||
} catch (err: unknown) {
|
||||
totpCode.value = ''
|
||||
|
||||
@@ -57,6 +57,9 @@ async function handleRegister() {
|
||||
})
|
||||
|
||||
authStore.setAuth(response)
|
||||
if (response.license) {
|
||||
authStore.setLicense(response.license)
|
||||
}
|
||||
router.push('/setup')
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
|
||||
@@ -9,20 +9,20 @@ interface License {
|
||||
id: string
|
||||
license_key: string
|
||||
owner_email: string
|
||||
server_name: string
|
||||
server_name: string | null
|
||||
status: 'active' | 'suspended' | 'expired' | 'revoked'
|
||||
created_at: string
|
||||
expires_at: string
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
interface LicenseDetail {
|
||||
id: string
|
||||
license_key: string
|
||||
owner_email: string
|
||||
server_name: string
|
||||
server_name: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
expires_at: string
|
||||
expires_at: string | null
|
||||
team_count: number
|
||||
wipe_count: number
|
||||
server_connection: {
|
||||
@@ -67,8 +67,11 @@ const statusBadgeClass: Record<string, string> = {
|
||||
revoked: 'bg-red-500/10 text-red-400',
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString('en-US', {
|
||||
function formatDate(iso: string | null | undefined): string {
|
||||
if (!iso) return '—'
|
||||
const d = new Date(iso)
|
||||
if (isNaN(d.getTime()) || d.getTime() === 0) return '—'
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
@@ -80,7 +83,7 @@ async function fetchLicenses() {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.value.toString(),
|
||||
per_page: perPage.toString(),
|
||||
limit: perPage.toString(),
|
||||
})
|
||||
if (searchQuery.value.trim()) params.set('search', searchQuery.value.trim())
|
||||
if (statusFilter.value !== 'all') params.set('status', statusFilter.value)
|
||||
|
||||
Reference in New Issue
Block a user