/** * 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 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 { 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 { return await User.getInstance({ email: email.toLowerCase() }); } /** * Find user by username */ public static async findByUsername(username: string): Promise { return await User.getInstance({ username: username.toLowerCase() }); } /** * Lifecycle hook: Update timestamps before save */ public async beforeSave(): Promise { 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 { return await User.getInstance({ id }); } /** * Verify password against stored hash */ public async verifyPassword(password: string): Promise { 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 { 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 { 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; } }