const path = require('path'); require('dotenv').config(); const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js')); const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js')); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const { format, differenceInHours, addHours } = require('date-fns'); const sendMail = require('../utils/sendMail.js'); const HaveIBeenPwnedAPI = require('../utils/hibp.js'); class AuthController extends BaseController { constructor() { super(); this.protected('me', 'refreshToken', 'login'); } /** * @swagger * /auth/login: * post: * summary: Authenticate a user * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - username * - password * properties: * username: * type: string * description: Username or email * password: * type: string * format: password * responses: * 200: * description: Login successful * content: * application/json: * schema: * type: object * properties: * api_key: * type: string * description: JWT token for authentication * 401: * description: Invalid credentials */ async login(req, res) { const { username, password } = req.body; const user = await models.User.query().where('username', username).orWhere('email', username).first(); if (!user) { return res.status(401).json({ error: 'Invalid username or password' }); } const passwordMatch = await bcrypt.compare(password, user.password); if (!passwordMatch) { return res.status(401).json({ error: 'Invalid username or password' }); } const validToken = jwt.verify(user.api_key, process.env.JWT_SECRET); if (!validToken) { const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' }); await models.User.query().update({ api_key: token, last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }).where('id', user.id); return res.json({ api_key: token }); } await models.User.query().update({ last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }).where('id', user.id); res.json({ api_key: user.api_key }); } /** * @swagger * /auth/me: * get: * summary: Get current user details * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: User details * content: * application/json: * schema: * type: object * properties: * id: * type: integer * username: * type: string * email: * type: string * is_admin: * type: boolean * is_active: * type: boolean * created: * type: string * format: date-time * modified: * type: string * format: date-time * token_expiration: * type: string * format: date-time * is_token_expired: * type: boolean * 401: * description: Unauthorized */ async me(req, res) { const user = await models.User.query().findById(req.user.id); const token = req.headers.authorization?.split(' ')[1]; const decodedToken = token ? jwt.decode(token) : null; const tokenExpiration = decodedToken?.exp ? new Date(decodedToken.exp * 1000) : null; const isTokenExpired = tokenExpiration ? tokenExpiration < new Date() : true; res.json({ id: user.id, username: user.username, email: user.email, is_admin: user.is_admin, is_active: user.is_active, created: user.created, modified: user.modified, token_expiration: tokenExpiration, is_token_expired: isTokenExpired }); } /** * @swagger * /auth/refresh-token: * post: * summary: Refresh authentication token * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: Token refreshed successfully * content: * application/json: * schema: * type: object * properties: * api_key: * type: string * description: New JWT token * 401: * description: Unauthorized */ async refreshToken(req, res) { const user = await models.User.query().findById(req.user.id); const token = jwt.sign({ id: user.id, username: user.username, is_admin: user.is_admin, email: user.email }, process.env.JWT_SECRET, { expiresIn: '6h' }); res.json({ api_key: token }); } async registerView(req, res) { res.render('auth/register'); } async register(req, res) { console.log(req.body); const { username, email, password } = req.body; const hibp = new HaveIBeenPwnedAPI(); try { const checkBreached = await hibp.checkPassword(password); if (checkBreached.isCompromised) { return res.status(400).json({ error: 'Password is compromised. Checked against haveibeenpwned.com' }); } const validatePassword = await hibp.validatePassword(password, { maxExposures: 0 }); if (!validatePassword.isValid) { return res.status(400).json({ error: 'Password is not valid. Checked against haveibeenpwned.com' }); } } catch (error) { console.error(error); return res.status(500).json({ error: 'Failed to check password' }); } const dupUser = await models.User.query().where('username', username).orWhere('email', email).first(); if (dupUser) { return res.status(400).json({ error: 'Username or email already exists' }); } const invite_token = crypto.randomBytes(32).toString('hex'); const salt = await bcrypt.genSalt(10); const passwordEncrypted = await bcrypt.hash(password, salt); const newEmail = `${username}@${crypto.randomBytes(10).toString('hex')}.com`; const user = await models.User.query().insert({ username, password: passwordEncrypted, email: newEmail, is_admin: 0, is_active: 0, created: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }); await models.Invite.query().insert({ user_id: user.id, token: invite_token, expires: format(addHours(new Date(), 12), 'yyyy-MM-dd HH:mm:ss'), created: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }); await sendMail( email, '2weekmail - Invite Code', `Welcome to the 2weekmail. Please use the link below to activate your account.
activate your account`, `Welcome to the 2weekmail. Please use it to activate your account.` ); res.json({ message: 'User registered successfully' }); } async activateView(req, res) { res.render('auth/activate'); } async activate(req, res) { const invite_token = req.params.token; const invite = await models.Invite.query().where('token', invite_token).first(); if (!invite) { return res.status(400).json({ error: 'Invalid invite token' }); } if (differenceInHours(new Date(), new Date(invite.expires_at)) > 0) { return res.status(400).json({ error: 'Invite token expired' }); } const user = await models.User.query().where('id', invite.user_id).first(); if (!user) { return res.status(400).json({ error: 'User not found' }); } await models.User.query().update({ is_active: 1, modified: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), last_login: format(new Date(), 'yyyy-MM-dd HH:mm:ss') }).where('id', user.id); await models.Invite.query().delete().where('id', invite.id); return res.json({ success: true, message: 'User activated successfully' }); } } module.exports = new AuthController();