231 lines
6.1 KiB
TypeScript
231 lines
6.1 KiB
TypeScript
/**
|
|
* User model for Stack.Gallery Registry
|
|
*/
|
|
|
|
import * as plugins from '../plugins.ts';
|
|
import type { IUser, TUserStatus } from '../interfaces/auth.interfaces.ts';
|
|
import { db } from './db.ts';
|
|
|
|
@plugins.smartdata.Collection(() => db)
|
|
export class User extends plugins.smartdata.SmartDataDbDoc<User, User> implements IUser {
|
|
@plugins.smartdata.unI()
|
|
public id: string = '';
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.searchable()
|
|
@plugins.smartdata.index({ unique: true })
|
|
public email: string = '';
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.searchable()
|
|
@plugins.smartdata.index({ unique: true })
|
|
public username: string = '';
|
|
|
|
@plugins.smartdata.svDb()
|
|
public passwordHash: string = '';
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.searchable()
|
|
public displayName: string = '';
|
|
|
|
@plugins.smartdata.svDb()
|
|
public avatarUrl?: string;
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.index()
|
|
public status: TUserStatus = 'pending_verification';
|
|
|
|
@plugins.smartdata.svDb()
|
|
public emailVerified: boolean = false;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public mfaEnabled: boolean = false;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public mfaSecret?: string;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public lastLoginAt?: Date;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public lastLoginIp?: string;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public failedLoginAttempts: number = 0;
|
|
|
|
@plugins.smartdata.svDb()
|
|
public lockedUntil?: Date;
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.index()
|
|
public isPlatformAdmin: boolean = false;
|
|
|
|
@plugins.smartdata.svDb()
|
|
@plugins.smartdata.index()
|
|
public createdAt: Date = new Date();
|
|
|
|
@plugins.smartdata.svDb()
|
|
public updatedAt: Date = new Date();
|
|
|
|
/**
|
|
* Create a new user instance
|
|
*/
|
|
public static async createUser(data: {
|
|
email: string;
|
|
username: string;
|
|
passwordHash: string;
|
|
displayName?: string;
|
|
}): Promise<User> {
|
|
const user = new User();
|
|
user.id = await User.getNewId();
|
|
user.email = data.email.toLowerCase();
|
|
user.username = data.username.toLowerCase();
|
|
user.passwordHash = data.passwordHash;
|
|
user.displayName = data.displayName || data.username;
|
|
user.status = 'pending_verification';
|
|
user.createdAt = new Date();
|
|
user.updatedAt = new Date();
|
|
await user.save();
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Find user by email
|
|
*/
|
|
public static async findByEmail(email: string): Promise<User | null> {
|
|
return await User.getInstance({ email: email.toLowerCase() });
|
|
}
|
|
|
|
/**
|
|
* Find user by username
|
|
*/
|
|
public static async findByUsername(username: string): Promise<User | null> {
|
|
return await User.getInstance({ username: username.toLowerCase() });
|
|
}
|
|
|
|
/**
|
|
* Lifecycle hook: Update timestamps before save
|
|
*/
|
|
public async beforeSave(): Promise<void> {
|
|
this.updatedAt = new Date();
|
|
if (!this.id) {
|
|
this.id = await User.getNewId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user is active (status === 'active')
|
|
*/
|
|
public get isActive(): boolean {
|
|
return this.status === 'active';
|
|
}
|
|
|
|
/**
|
|
* Alias for isPlatformAdmin for backward compatibility
|
|
*/
|
|
public get isSystemAdmin(): boolean {
|
|
return this.isPlatformAdmin;
|
|
}
|
|
|
|
/**
|
|
* Find user by ID
|
|
*/
|
|
public static async findById(id: string): Promise<User | null> {
|
|
return await User.getInstance({ id });
|
|
}
|
|
|
|
/**
|
|
* Verify password against stored hash
|
|
*/
|
|
public async verifyPassword(password: string): Promise<boolean> {
|
|
if (!this.passwordHash) return false;
|
|
|
|
const [saltHex, expectedHash] = this.passwordHash.split(':');
|
|
if (!saltHex || !expectedHash) return false;
|
|
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(saltHex + password);
|
|
|
|
let hash = data;
|
|
for (let i = 0; i < 10000; i++) {
|
|
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
|
|
}
|
|
|
|
const hashHex = Array.from(hash)
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
return hashHex === expectedHash;
|
|
}
|
|
|
|
/**
|
|
* Hash a password for storage
|
|
*/
|
|
public static async hashPassword(password: string): Promise<string> {
|
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
const saltHex = Array.from(salt)
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(saltHex + password);
|
|
|
|
let hash = data;
|
|
for (let i = 0; i < 10000; i++) {
|
|
hash = new Uint8Array(await crypto.subtle.digest('SHA-256', hash));
|
|
}
|
|
|
|
const hashHex = Array.from(hash)
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
return `${saltHex}:${hashHex}`;
|
|
}
|
|
|
|
/**
|
|
* Create the default admin user if no admin exists
|
|
*/
|
|
public static async seedDefaultAdmin(): Promise<User | null> {
|
|
const adminEmail = Deno.env.get('ADMIN_EMAIL') || 'admin@stack.gallery';
|
|
const adminPassword = Deno.env.get('ADMIN_PASSWORD') || 'admin';
|
|
|
|
// Check if any platform admin exists
|
|
const existingAdmin = await User.getInstance({ isPlatformAdmin: true });
|
|
if (existingAdmin) {
|
|
console.log('[User] Platform admin already exists, skipping seed');
|
|
return null;
|
|
}
|
|
|
|
// Check if admin email already exists
|
|
const existingUser = await User.findByEmail(adminEmail);
|
|
if (existingUser) {
|
|
console.log('[User] User with admin email already exists, promoting to admin');
|
|
existingUser.isPlatformAdmin = true;
|
|
existingUser.status = 'active';
|
|
await existingUser.save();
|
|
return existingUser;
|
|
}
|
|
|
|
// Create new admin user
|
|
console.log('[User] Creating default admin user:', adminEmail);
|
|
const passwordHash = await User.hashPassword(adminPassword);
|
|
|
|
const admin = new User();
|
|
admin.id = await User.getNewId();
|
|
admin.email = adminEmail.toLowerCase();
|
|
admin.username = 'admin';
|
|
admin.passwordHash = passwordHash;
|
|
admin.displayName = 'System Administrator';
|
|
admin.status = 'active';
|
|
admin.emailVerified = true;
|
|
admin.isPlatformAdmin = true;
|
|
admin.createdAt = new Date();
|
|
admin.updatedAt = new Date();
|
|
|
|
await admin.save();
|
|
console.log('[User] Default admin user created successfully');
|
|
|
|
return admin;
|
|
}
|
|
}
|