← 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'}`);
}
}
}