2weekmail/api/controllers/AuthController.js
2025-03-23 13:14:14 +00:00

270 lines
8.8 KiB
JavaScript

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.
<br>
<a href="https://2weekmail.fyi/auth/activate/${invite_token}">activate your account</a>`,
`Welcome to the 2weekmail. Please use it to <a href="https://2weekmail.fyi/auth/activate/${invite_token}">activate your account</a>.`
);
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();