import { SignJWT, jwtVerify } from "jose" import bcrypt from "bcryptjs" import { query } from "@/lib/db" import { cookies } from "next/headers" const JWT_SECRET = new TextEncoder().encode( process.env.JWT_SECRET || "fallback-dev-secret-do-not-use-in-production" ) const SESSION_COOKIE = "session" const MAX_FAILED_ATTEMPTS = 5 const LOCKOUT_DURATION_MINUTES = 15 export interface SessionUser { id: string username: string email: string firstName: string lastName: string role: string avatar: string } export async function hashPassword(password: string): Promise { return bcrypt.hash(password, 12) } export async function comparePassword( password: string, hash: string ): Promise { return bcrypt.compare(password, hash) } export async function signToken(payload: { userId: string; role: string }) { return new SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) .setExpirationTime("24h") .setIssuedAt() .sign(JWT_SECRET) } export async function verifyToken(token: string) { try { const { payload } = await jwtVerify(token, JWT_SECRET) return payload as { userId: string; role: string } } catch { return null } } export async function getUserByEmail(email: string) { const result = await query( `SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name, u.is_active, u.is_locked, u.failed_login_attempts, u.locked_until, u.password_change_required, r.name AS role_name FROM users u JOIN user_roles ur ON ur.user_id = u.id JOIN roles r ON r.id = ur.role_id WHERE u.email = $1 AND u.deleted_at IS NULL LIMIT 1`, [email.toLowerCase().trim()] ) return result.rows[0] || null } export async function getUserByUsername(username: string) { const result = await query( `SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name, u.is_active, u.is_locked, u.failed_login_attempts, u.locked_until, u.password_change_required, r.name AS role_name FROM users u JOIN user_roles ur ON ur.user_id = u.id JOIN roles r ON r.id = ur.role_id WHERE u.username = $1 AND u.deleted_at IS NULL LIMIT 1`, [username.toLowerCase().trim()] ) return result.rows[0] || null } export async function getUserById(id: string) { const result = await query( `SELECT u.id, u.username, u.email, u.first_name, u.last_name, u.is_active, r.name AS role_name FROM users u JOIN user_roles ur ON ur.user_id = u.id JOIN roles r ON r.id = ur.role_id WHERE u.id = $1 AND u.deleted_at IS NULL LIMIT 1`, [id] ) return result.rows[0] || null } export async function recordLoginAttempt( userId: string | null, usernameAttempted: string, ipAddress: string, userAgent: string | null, wasSuccessful: boolean, failureReason?: string ) { await query( `INSERT INTO login_attempts (user_id, username_attempted, ip_address, user_agent, was_successful, failure_reason) VALUES ($1, $2, $3, $4, $5, $6)`, [userId, usernameAttempted, ipAddress, userAgent, wasSuccessful, failureReason || null] ) } export async function incrementFailedAttempts(userId: string) { await query( `UPDATE users SET failed_login_attempts = failed_login_attempts + 1, locked_until = CASE WHEN failed_login_attempts + 1 >= $2 THEN NOW() + (INTERVAL '1 minute' * $3) ELSE locked_until END WHERE id = $1`, [userId, MAX_FAILED_ATTEMPTS, LOCKOUT_DURATION_MINUTES] ) } export async function resetFailedAttempts(userId: string) { await query( `UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, [userId] ) } export async function isAccountLocked(user: { is_locked: boolean locked_until: Date | null }): Promise<{ locked: boolean; reason?: string }> { if (user.is_locked) { return { locked: true, reason: "Account has been locked by an administrator." } } if (user.locked_until && new Date(user.locked_until) > new Date()) { const minutes = Math.ceil( (new Date(user.locked_until).getTime() - Date.now()) / 60000 ) return { locked: true, reason: `Account is temporarily locked due to too many failed attempts. Try again in ${minutes} minute(s).`, } } return { locked: false } } export function mapDbUserToSessionUser(dbUser: Record): SessionUser { const roleName = (dbUser.role_name as string).toLowerCase() return { id: dbUser.id as string, username: dbUser.username as string, email: dbUser.email as string, firstName: dbUser.first_name as string, lastName: dbUser.last_name as string, role: roleName, avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent( `${dbUser.first_name}+${dbUser.last_name}` )}&background=1d4ed8&color=fff&size=128`, } } export async function createSession(userId: string, role: string) { const token = await signToken({ userId, role }) const cookieStore = await cookies() cookieStore.set(SESSION_COOKIE, token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 60 * 60 * 24, // 24 hours }) return token } export async function destroySession() { const cookieStore = await cookies() cookieStore.set(SESSION_COOKIE, "", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 0, }) } export async function getSessionUser(): Promise { try { const cookieStore = await cookies() const token = cookieStore.get(SESSION_COOKIE)?.value if (!token) return null const payload = await verifyToken(token) if (!payload) return null const dbUser = await getUserById(payload.userId) if (!dbUser) return null return mapDbUserToSessionUser(dbUser) } catch { return null } }