import { query } from '../config/database'; import { generateAccessToken, generateRefreshToken, verifyRefreshToken, } from '../utils/jwt'; import { comparePassword } from '../utils/password'; import crypto from 'crypto'; /** * Hash a token for storage */ function hashToken(token: string): string { return crypto.createHash('sha256').update(token).digest('hex'); } /** * Authentication service response types */ export interface AuthTokens { accessToken: string; refreshToken: string; } export interface UserProfile { id: string; email: string; name: string; role: string; avatarUrl?: string | null; projectId?: string | null; organismoOperadorId?: string | null; organismoName?: string | null; createdAt: Date; } export interface LoginResult extends AuthTokens { user: UserProfile; } /** * Authenticate user with email and password * Generates access and refresh tokens on successful login * Stores hashed refresh token in database * @param email - User email * @param password - User password * @returns Access and refresh tokens with user info */ export async function login( email: string, password: string ): Promise { // Find user by email with role name and organismo const userResult = await query<{ id: string; email: string; name: string; password_hash: string; avatar_url: string | null; role_name: string; project_id: string | null; organismo_operador_id: string | null; created_at: Date; }>( `SELECT u.id, u.email, u.name, u.password_hash, u.avatar_url, r.name as role_name, u.project_id, u.organismo_operador_id, u.created_at FROM users u JOIN roles r ON u.role_id = r.id WHERE LOWER(u.email) = LOWER($1) AND u.is_active = true LIMIT 1`, [email] ); const user = userResult.rows[0]; if (!user) { throw new Error('Invalid email or password'); } // Verify password const isValidPassword = await comparePassword(password, user.password_hash); if (!isValidPassword) { throw new Error('Invalid email or password'); } const accessToken = generateAccessToken({ userId: user.id, email: user.email, roleId: user.id, roleName: user.role_name, projectId: user.project_id, organismoOperadorId: user.organismo_operador_id, }); const refreshToken = generateRefreshToken({ userId: user.id, }); // Hash and store refresh token const hashedRefreshToken = hashToken(refreshToken); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days await query( `INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)`, [user.id, hashedRefreshToken, expiresAt] ); // Update last login await query( `UPDATE users SET last_login = NOW() WHERE id = $1`, [user.id] ); return { accessToken, refreshToken, user: { id: user.id, email: user.email, name: user.name, role: user.role_name, avatarUrl: user.avatar_url, createdAt: user.created_at, }, }; } /** * Refresh access token using a valid refresh token * Verifies the refresh token exists in database and is not expired * @param refreshToken - The refresh token * @returns New access token */ export async function refresh(refreshToken: string): Promise<{ accessToken: string }> { // Verify JWT signature const decoded = verifyRefreshToken(refreshToken); if (!decoded) { throw new Error('Invalid refresh token'); } // Hash token to check against database const hashedToken = hashToken(refreshToken); const userId = (decoded as any).userId || (decoded as any).id; const tokenResult = await query<{ id: string; expires_at: Date; }>( `SELECT id, expires_at FROM refresh_tokens WHERE token_hash = $1 AND user_id = $2 AND revoked_at IS NULL LIMIT 1`, [hashedToken, userId] ); const storedToken = tokenResult.rows[0]; if (!storedToken) { throw new Error('Refresh token not found or revoked'); } // Check if token is expired if (new Date() > storedToken.expires_at) { // Clean up expired token await query( `DELETE FROM refresh_tokens WHERE id = $1`, [storedToken.id] ); throw new Error('Refresh token expired'); } // Get user data for new access token const userResult = await query<{ id: string; email: string; role_name: string; project_id: string | null; organismo_operador_id: string | null; }>( `SELECT u.id, u.email, r.name as role_name, u.project_id, u.organismo_operador_id FROM users u JOIN roles r ON u.role_id = r.id WHERE u.id = $1 AND u.is_active = true LIMIT 1`, [userId] ); const user = userResult.rows[0]; if (!user) { throw new Error('User not found'); } const accessToken = generateAccessToken({ userId: user.id, email: user.email, roleId: user.id, roleName: user.role_name, projectId: user.project_id, organismoOperadorId: user.organismo_operador_id, }); return { accessToken }; } /** * Logout user by revoking the specified refresh token * @param userId - The user ID * @param refreshToken - The refresh token to revoke */ export async function logout( userId: string, refreshToken: string ): Promise { const hashedToken = hashToken(refreshToken); // Revoke the specific refresh token await query( `UPDATE refresh_tokens SET revoked_at = NOW() WHERE token_hash = $1 AND user_id = $2`, [hashedToken, userId] ); } /** * Get authenticated user's profile * @param userId - The user ID * @returns User profile data */ export async function getMe(userId: string): Promise { const userResult = await query<{ id: string; email: string; name: string; avatar_url: string | null; role_name: string; project_id: string | null; organismo_operador_id: string | null; organismo_name: string | null; created_at: Date; }>( `SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id, u.organismo_operador_id, oo.name as organismo_name, u.created_at FROM users u JOIN roles r ON u.role_id = r.id LEFT JOIN organismos_operadores oo ON u.organismo_operador_id = oo.id WHERE u.id = $1 AND u.is_active = true LIMIT 1`, [userId] ); const user = userResult.rows[0]; if (!user) { throw new Error('User not found'); } return { id: user.id, email: user.email, name: user.name, role: user.role_name, avatarUrl: user.avatar_url, projectId: user.project_id, organismoOperadorId: user.organismo_operador_id, organismoName: user.organismo_name, createdAt: user.created_at, }; }