const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const { getDb } = require('../config/db'); const { AppError } = require('../middleware/error-handler'); const ACCESS_TOKEN_TTL = '15m'; const REFRESH_TOKEN_BYTES = 48; const RESET_TOKEN_BYTES = 32; const RESET_TOKEN_TTL_MINUTES = 30; function getJwtSecret() { return process.env.JWT_SECRET || 'development_only_change_me'; } function hashToken(token) { return crypto.createHash('sha256').update(token).digest('hex'); } function addMinutes(date, minutes) { return new Date(date.getTime() + minutes * 60 * 1000); } function addDays(date, days) { return new Date(date.getTime() + days * 24 * 60 * 60 * 1000); } function sanitizeUser(user) { return { id: user.id, username: user.username, email: user.email, role: user.role, status: user.status, balance: user.balance }; } function createAccessToken(user) { return jwt.sign( { user_id: user.id, role: user.role }, getJwtSecret(), { expiresIn: ACCESS_TOKEN_TTL } ); } async function createRefreshToken(userId, trx = getDb()) { const refreshToken = crypto.randomBytes(REFRESH_TOKEN_BYTES).toString('base64url'); const tokenHash = hashToken(refreshToken); await trx('refresh_tokens').insert({ user_id: userId, token_hash: tokenHash, expires_at: addDays(new Date(), 30) }); return refreshToken; } async function register(input) { const db = getDb(); const passwordHash = await bcrypt.hash(input.password, 12); try { const [userId] = await db('users').insert({ username: input.username, email: input.email, password_hash: passwordHash, balance: '0.0000' }); const user = await db('users').where({ id: userId }).first(); return { user: sanitizeUser(user) }; } catch (error) { if (error.code === 'ER_DUP_ENTRY') { throw new AppError('Username or email is already in use', 409, 'ACCOUNT_EXISTS'); } throw error; } } async function login(input) { const db = getDb(); const genericError = new AppError('Invalid email or password', 401, 'INVALID_CREDENTIALS'); const user = await db('users').where({ email: input.email }).first(); if (!user) { throw genericError; } const passwordMatches = await bcrypt.compare(input.password, user.password_hash); if (!passwordMatches) { throw genericError; } if (user.status !== 'active') { throw new AppError('Account is not active', 403, 'ACCOUNT_INACTIVE'); } const accessToken = createAccessToken(user); const refreshToken = await createRefreshToken(user.id); return { accessToken, refreshToken, tokenType: 'Bearer', expiresIn: 15 * 60, user: sanitizeUser(user) }; } async function logout(refreshToken) { if (!refreshToken) { return; } const db = getDb(); await db('refresh_tokens') .where({ token_hash: hashToken(refreshToken) }) .whereNull('revoked_at') .update({ revoked_at: new Date() }); } async function forgotPassword(email) { const db = getDb(); const user = await db('users').select('id').where({ email }).first(); const response = { status: 'ok', message: 'If the account exists, a reset link will be sent' }; if (!user) { return response; } const token = crypto.randomBytes(RESET_TOKEN_BYTES).toString('base64url'); await db('password_reset_tokens').insert({ user_id: user.id, token_hash: hashToken(token), expires_at: addMinutes(new Date(), RESET_TOKEN_TTL_MINUTES) }); if (process.env.NODE_ENV !== 'production') { response.resetToken = token; } return response; } async function resetPassword(input) { const db = getDb(); const tokenHash = hashToken(input.token); await db.transaction(async (trx) => { const resetToken = await trx('password_reset_tokens') .where({ token_hash: tokenHash }) .whereNull('used_at') .where('expires_at', '>', new Date()) .forUpdate() .first(); if (!resetToken) { throw new AppError('Invalid or expired reset token', 400, 'INVALID_RESET_TOKEN'); } const passwordHash = await bcrypt.hash(input.password, 12); await trx('users').where({ id: resetToken.user_id }).update({ password_hash: passwordHash }); await trx('password_reset_tokens').where({ id: resetToken.id }).update({ used_at: new Date() }); await trx('refresh_tokens') .where({ user_id: resetToken.user_id }) .whereNull('revoked_at') .update({ revoked_at: new Date() }); }); } async function getMe(userId) { const db = getDb(); const user = await db('users') .select('id', 'username', 'email', 'role', 'status', 'balance') .where({ id: userId }) .first(); if (!user) { throw new AppError('User not found', 404, 'USER_NOT_FOUND'); } return { user: sanitizeUser(user) }; } module.exports = { forgotPassword, getMe, login, logout, register, resetPassword };