224 lines
6.3 KiB
TypeScript
224 lines
6.3 KiB
TypeScript
|
|
import { pool } from '../config/database.js';
|
||
|
|
import { User, CreateUserRequest, UpdateUserRequest, UserResponse } from '../models/User.js';
|
||
|
|
import { $log } from '@tsed/logger';
|
||
|
|
import bcrypt from 'bcryptjs';
|
||
|
|
import { randomUUID } from 'crypto';
|
||
|
|
|
||
|
|
export class UserService {
|
||
|
|
async createUser(userData: CreateUserRequest): Promise<UserResponse> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Check if user already exists
|
||
|
|
const [existingUsers] = await connection.execute(
|
||
|
|
'SELECT id FROM users WHERE email = ? AND deleted_at IS NULL',
|
||
|
|
[userData.email]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (Array.isArray(existingUsers) && existingUsers.length > 0) {
|
||
|
|
throw new Error('User with this email already exists');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Hash password
|
||
|
|
const password_hash = await bcrypt.hash(userData.password, 10);
|
||
|
|
|
||
|
|
// Generate UUID for user ID
|
||
|
|
const userId = randomUUID();
|
||
|
|
|
||
|
|
// Insert user
|
||
|
|
const [result] = await connection.execute(
|
||
|
|
`INSERT INTO users (id, email, password_hash, first_name, last_name, role, company_name, is_active, email_verified_at, created_at, updated_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW())`,
|
||
|
|
[
|
||
|
|
userId,
|
||
|
|
userData.email,
|
||
|
|
password_hash,
|
||
|
|
userData.first_name,
|
||
|
|
userData.last_name,
|
||
|
|
userData.role || 'recruiter',
|
||
|
|
userData.company_name || null,
|
||
|
|
true
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get the created user
|
||
|
|
const user = await this.getUserById(userId);
|
||
|
|
if (!user) {
|
||
|
|
throw new Error('Failed to create user');
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.mapUserToResponse(user);
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error creating user:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async getUserByEmail(email: string): Promise<User | null> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const [rows] = await connection.execute(
|
||
|
|
'SELECT * FROM users WHERE email = ? AND deleted_at IS NULL',
|
||
|
|
[email]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (Array.isArray(rows) && rows.length > 0) {
|
||
|
|
return rows[0] as User;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error getting user by email:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async getUserById(id: string): Promise<User | null> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const [rows] = await connection.execute(
|
||
|
|
'SELECT * FROM users WHERE id = ? AND deleted_at IS NULL',
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
|
||
|
|
if (Array.isArray(rows) && rows.length > 0) {
|
||
|
|
return rows[0] as User;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error getting user by ID:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async updateUser(id: string, userData: UpdateUserRequest): Promise<UserResponse | null> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const updateFields = [];
|
||
|
|
const values = [];
|
||
|
|
|
||
|
|
if (userData.first_name) {
|
||
|
|
updateFields.push('first_name = ?');
|
||
|
|
values.push(userData.first_name);
|
||
|
|
}
|
||
|
|
if (userData.last_name) {
|
||
|
|
updateFields.push('last_name = ?');
|
||
|
|
values.push(userData.last_name);
|
||
|
|
}
|
||
|
|
if (userData.company_name !== undefined) {
|
||
|
|
updateFields.push('company_name = ?');
|
||
|
|
values.push(userData.company_name);
|
||
|
|
}
|
||
|
|
if (userData.avatar_url !== undefined) {
|
||
|
|
updateFields.push('avatar_url = ?');
|
||
|
|
values.push(userData.avatar_url);
|
||
|
|
}
|
||
|
|
if (userData.is_active !== undefined) {
|
||
|
|
updateFields.push('is_active = ?');
|
||
|
|
values.push(userData.is_active);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (updateFields.length === 0) {
|
||
|
|
throw new Error('No fields to update');
|
||
|
|
}
|
||
|
|
|
||
|
|
updateFields.push('updated_at = NOW()');
|
||
|
|
values.push(id);
|
||
|
|
|
||
|
|
await connection.execute(
|
||
|
|
`UPDATE users SET ${updateFields.join(', ')} WHERE id = ? AND deleted_at IS NULL`,
|
||
|
|
values
|
||
|
|
);
|
||
|
|
|
||
|
|
const user = await this.getUserById(id);
|
||
|
|
return user ? this.mapUserToResponse(user) : null;
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error updating user:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async updateLastLogin(id: string): Promise<void> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
await connection.execute(
|
||
|
|
'UPDATE users SET last_login_at = NOW(), updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error updating last login:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async verifyPassword(user: User, password: string): Promise<boolean> {
|
||
|
|
return await bcrypt.compare(password, user.password_hash);
|
||
|
|
}
|
||
|
|
|
||
|
|
async changePassword(id: string, newPassword: string): Promise<void> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const password_hash = await bcrypt.hash(newPassword, 10);
|
||
|
|
|
||
|
|
await connection.execute(
|
||
|
|
'UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
|
||
|
|
[password_hash, id]
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error changing password:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async softDeleteUser(id: string): Promise<void> {
|
||
|
|
const connection = await pool.getConnection();
|
||
|
|
|
||
|
|
try {
|
||
|
|
await connection.execute(
|
||
|
|
'UPDATE users SET deleted_at = NOW(), is_active = FALSE, updated_at = NOW() WHERE id = ? AND deleted_at IS NULL',
|
||
|
|
[id]
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
$log.error('Error soft deleting user:', error);
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
connection.release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private mapUserToResponse(user: User): UserResponse {
|
||
|
|
return {
|
||
|
|
id: user.id,
|
||
|
|
email: user.email,
|
||
|
|
first_name: user.first_name,
|
||
|
|
last_name: user.last_name,
|
||
|
|
role: user.role,
|
||
|
|
company_name: user.company_name,
|
||
|
|
avatar_url: user.avatar_url,
|
||
|
|
is_active: user.is_active,
|
||
|
|
last_login_at: user.last_login_at,
|
||
|
|
email_verified_at: user.email_verified_at,
|
||
|
|
created_at: user.created_at,
|
||
|
|
updated_at: user.updated_at
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|