← back to Jill Website

src/models/AdminUser.ts

188 lines

import { Pool, QueryResult } from 'pg';
import { getPool } from '../config/database';
import bcrypt from 'bcrypt';

export interface AdminUser {
  id?: number;
  username: string;
  password_hash?: string;
  email: string;
  created_at?: Date;
  updated_at?: Date;
  last_login?: Date;
}

export interface AdminUserValidationError {
  field: string;
  message: string;
}

export class AdminUserModel {
  private pool: Pool;
  private readonly SALT_ROUNDS = 10;

  constructor() {
    this.pool = getPool();
  }

  /**
   * Validate admin user data
   */
  private validate(user: Partial<AdminUser>): AdminUserValidationError[] {
    const errors: AdminUserValidationError[] = [];

    if (!user.username || user.username.trim().length === 0) {
      errors.push({ field: 'username', message: 'Username is required' });
    }

    if (!user.email || user.email.trim().length === 0) {
      errors.push({ field: 'email', message: 'Email is required' });
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) {
      errors.push({ field: 'email', message: 'Invalid email format' });
    }

    return errors;
  }

  /**
   * Hash password
   */
  async hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, this.SALT_ROUNDS);
  }

  /**
   * Verify password
   */
  async verifyPassword(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }

  /**
   * Authenticate user
   */
  async authenticate(username: string, password: string): Promise<AdminUser | null> {
    try {
      const query = 'SELECT * FROM admin_users WHERE username = $1';
      const result: QueryResult<AdminUser> = await this.pool.query(query, [username]);

      if (result.rows.length === 0) {
        return null;
      }

      const user = result.rows[0];

      if (!user.password_hash) {
        return null;
      }

      const isValid = await this.verifyPassword(password, user.password_hash);

      if (!isValid) {
        return null;
      }

      // Update last login
      await this.pool.query('UPDATE admin_users SET last_login = NOW() WHERE id = $1', [user.id]);

      // Return user without password hash
      const { password_hash, ...userWithoutPassword } = user;
      return userWithoutPassword;
    } catch (error) {
      console.error('Error authenticating user:', error);
      throw new Error(`Failed to authenticate user: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Create new admin user
   */
  async create(username: string, password: string, email: string): Promise<AdminUser> {
    const errors = this.validate({ username, email });

    if (errors.length > 0) {
      throw new Error(`Validation failed: ${errors.map(e => `${e.field}: ${e.message}`).join(', ')}`);
    }

    try {
      const passwordHash = await this.hashPassword(password);

      const query = `
        INSERT INTO admin_users (username, password_hash, email)
        VALUES ($1, $2, $3)
        RETURNING id, username, email, created_at, updated_at
      `;

      const values = [username.trim(), passwordHash, email.trim().toLowerCase()];

      const result: QueryResult<AdminUser> = await this.pool.query(query, values);
      return result.rows[0];
    } catch (error) {
      console.error('Error creating admin user:', error);
      throw new Error(`Failed to create admin user: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Find user by username
   */
  async findByUsername(username: string): Promise<AdminUser | null> {
    try {
      const query = 'SELECT id, username, email, created_at, updated_at, last_login FROM admin_users WHERE username = $1';
      const result: QueryResult<AdminUser> = await this.pool.query(query, [username]);

      if (result.rows.length === 0) {
        return null;
      }

      return result.rows[0];
    } catch (error) {
      console.error('Error finding admin user:', error);
      throw new Error(`Failed to find admin user: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Get all admin users
   */
  async findAll(): Promise<AdminUser[]> {
    try {
      const query = 'SELECT id, username, email, created_at, updated_at, last_login FROM admin_users ORDER BY created_at DESC';
      const result: QueryResult<AdminUser> = await this.pool.query(query);
      return result.rows;
    } catch (error) {
      console.error('Error finding admin users:', error);
      throw new Error(`Failed to find admin users: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Update admin user password
   */
  async updatePassword(username: string, newPassword: string): Promise<boolean> {
    try {
      const passwordHash = await this.hashPassword(newPassword);
      const query = 'UPDATE admin_users SET password_hash = $1, updated_at = NOW() WHERE username = $2';
      const result = await this.pool.query(query, [passwordHash, username]);
      return result.rowCount !== null && result.rowCount > 0;
    } catch (error) {
      console.error('Error updating password:', error);
      throw new Error(`Failed to update password: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  /**
   * Delete admin user
   */
  async delete(username: string): Promise<boolean> {
    try {
      const query = 'DELETE FROM admin_users WHERE username = $1';
      const result = await this.pool.query(query, [username]);
      return result.rowCount !== null && result.rowCount > 0;
    } catch (error) {
      console.error('Error deleting admin user:', error);
      throw new Error(`Failed to delete admin user: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }
}