Files
SS/src/services/auth.service.js
T

220 lines
4.9 KiB
JavaScript

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
};