380 lines
16 KiB
JavaScript
380 lines
16 KiB
JavaScript
import express from 'express';
|
|
import { error } from '../modules/logManager';
|
|
import { pool } from '../modules/databaseManager';
|
|
import { sendVerification } from '../modules/mailHandler';
|
|
import { verifyToken, generateToken } from '../modules/tokenManager';
|
|
import { isEmailDomainValid, isValidEmail, isPhoneNumber } from '../modules/formatManager';
|
|
import { antiBruteForce, antiVerificationSpam, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler';
|
|
import { checkBanned, checkPermissions, userExists, isBanned, verifyPermissions } from '../modules/permissionManager';
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/register', antiBruteForce, async (req, res) => {
|
|
const { username, email, password, first_name, last_name, phone = null } = req.body;
|
|
if ([ username, email, password, first_name, last_name ].every(Boolean)) {
|
|
try {
|
|
if (isValidEmail(email) && isEmailDomainValid(email)) {
|
|
const [existingUsername] = await pool.execute('SELECT * FROM users WHERE username = ? LIMIT 1', [username]);
|
|
if (existingUsername.length) return await respondWithStatus(res, 400, 'Username is already taken');
|
|
|
|
const [existingEmail] = await pool.execute('SELECT * FROM users WHERE email = ? LIMIT 1', [email]);
|
|
if (existingEmail.length) return await respondWithStatus(res, 400, 'Email is already taken');
|
|
|
|
if (phone && !isPhoneNumber(phone)) return await respondWithStatus(res, 400, 'Invalid phone number');
|
|
|
|
const hashedPassword = await Bun.password.hash(password);
|
|
|
|
const [result] = await pool.execute(
|
|
'INSERT INTO users (first_name, last_name, username, email, password, phone) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[ first_name, last_name, username, email, hashedPassword, phone ? phone : 'None'],
|
|
);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing user');
|
|
|
|
if (process.env.DISABLE_EMAIL_VERIFICATION == 'true') return await respondWithStatus(res, 200, 'Successfully registered');
|
|
|
|
const [rows] = await pool.execute('SELECT id FROM users WHERE email = ? LIMIT 1', [email]);
|
|
const code = sendVerification(email, rows[0].id, 'email');
|
|
pool.execute('INSERT INTO verification_codes (user_id, verification_code, type) VALUES (?, ?, ?)', [ rows[0].id, code, 'email' ]);
|
|
return await respondWithStatus(res, 200, 'Successfully registered');
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Invalid email address');
|
|
}
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing fields');
|
|
}
|
|
});
|
|
|
|
router.post('/login', antiBruteForce, async (req, res) => {
|
|
const { usernameOrEmail, password } = req.body;
|
|
if ([usernameOrEmail, password].every(Boolean)) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM users WHERE username = ? OR email = ? LIMIT 1',
|
|
[usernameOrEmail, usernameOrEmail],
|
|
);
|
|
if (!rows.length) return await respondWithStatus(res, 404, 'Incorrect username or email');
|
|
|
|
const user = rows[0];
|
|
const passwordMatch = await Bun.password.verify(password, user.password);
|
|
if (!passwordMatch) return await respondWithStatus(res, 401, 'Incorrect password');
|
|
|
|
if (await isBanned(user.id)) return await respondWithStatus(res, 403, 'User is banned or an issue occured');
|
|
const token = await generateToken(user.id, password);
|
|
return await respondWithStatusJSON(res, 200, {
|
|
message: 'Login successful',
|
|
token: token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
first_name: user.first_name,
|
|
last_name: user.last_name,
|
|
verified_status: process.env.DISABLE_EMAIL_VERIFICATION == 'true' ? true : user.email_verified,
|
|
},
|
|
});
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing fields');
|
|
}
|
|
});
|
|
|
|
router.get('/', verifyToken, checkBanned, checkPermissions('user', 1), async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.execute('SELECT id, first_name, last_name, username, email, phone FROM users WHERE 1');
|
|
if (rows.length === 0) return await respondWithStatus(res, 404, 'Users not found');
|
|
return await respondWithStatusJSON(res, 200, rows);
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.post('/', verifyToken, checkBanned, checkPermissions('user', 2), async (req, res) => {
|
|
const { first_name, last_name, username, email, password, phone = 'None' } = req.body;
|
|
if ([ first_name, last_name, username, email, password ].every(Boolean)) {
|
|
try {
|
|
const hashedPassword = await Bun.password.hash(password);
|
|
await pool.execute(
|
|
'INSERT INTO users (first_name, last_name, username, email, password, phone) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[ first_name, last_name, username, email, hashedPassword, phone ],
|
|
);
|
|
return await respondWithStatus(res, 200, 'User created successfully');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing fields');
|
|
}
|
|
});
|
|
|
|
// Email verification endpoints
|
|
router.get('/email/request', antiVerificationSpam, verifyToken, checkBanned, async (req, res) => {
|
|
const userId = req.userId;
|
|
try {
|
|
const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
|
|
if (!rows.length) return await respondWithStatus(res, 404, 'User not found');
|
|
const user = rows[0];
|
|
if (user.email_verified) return await respondWithStatus(res, 400, 'Email is already verified');
|
|
const code = await sendVerification(user.email, userId, 'email');
|
|
pool.execute('INSERT INTO verification_codes (user_id, verification_code, type) VALUES (?, ?, ?)', [ userId, code, 'email' ]);
|
|
return await respondWithStatus(res, 200, 'Successfully sent verification email');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.get('/email/verify', antiVerificationSpam, verifyToken, checkBanned, async (req, res) => {
|
|
const { code } = req.query;
|
|
const userId = req.userId;
|
|
if (code) {
|
|
try {
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM verification_codes WHERE user_id = ? AND verification_code = ? AND type = ? AND created_at >= NOW() - INTERVAL 10 MINUTE LIMIT 1',
|
|
[userId, code, 'email'],
|
|
);
|
|
if (!rows.length) return await respondWithStatus(res, 400, 'Invalid code');
|
|
|
|
const [result] = await pool.execute('UPDATE users SET email_verified = ? WHERE id = ?', [ true, userId ]);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error updating user');
|
|
await pool.execute('DELETE FROM verification_codes WHERE id = ?', [ rows[0].id ]);
|
|
|
|
return await respondWithStatus(res, 200, 'Successfully verified user');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing code');
|
|
}
|
|
});
|
|
|
|
// Phone verification endpoints (requires a mobile providers API)
|
|
// POST /phone/request
|
|
// PATCH /phone/verify
|
|
|
|
// Password reset endpoints
|
|
router.post('/password/request', antiVerificationSpam, async (req, res) => {
|
|
const { usernameOrEmail } = req.body;
|
|
if (usernameOrEmail) {
|
|
try {
|
|
const [user] = await pool.execute('SELECT * FROM users WHERE email = ? OR username = ? LIMIT 1', [usernameOrEmail, usernameOrEmail]);
|
|
if (user.length === 0) return await respondWithStatus(res, 404, 'User not found');
|
|
|
|
let code;
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM verification_codes WHERE user_id = ? AND type = ? AND created_at >= NOW() - INTERVAL 10 MINUTE LIMIT 1', [user[0].id, 'password'],
|
|
);
|
|
if (!rows.length) {
|
|
code = sendVerification(user[0].email, user[0].id, 'password');
|
|
pool.execute('INSERT INTO verification_codes (user_id, verification_code, type) VALUES (?, ?, ?)', [ user[0].id, code, 'password' ]);
|
|
return await respondWithStatus(res, 200, 'Successfully sent password reset email');
|
|
}
|
|
else {
|
|
code = sendVerification(user[0].email, user[0].id, 'password', rows[0].verification_code);
|
|
return await respondWithStatus(res, 200, 'Successfully sent password reset email');
|
|
}
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing username or email');
|
|
}
|
|
});
|
|
|
|
router.patch('/password/verify', antiVerificationSpam, async (req, res) => {
|
|
const { usernameOrEmail, password, code } = req.body;
|
|
if ([usernameOrEmail, password, code].every(Boolean)) {
|
|
try {
|
|
const [user] = await pool.execute(
|
|
'SELECT id FROM users WHERE username = ? OR email = ? LIMIT 1',
|
|
[usernameOrEmail, usernameOrEmail],
|
|
);
|
|
if (!user.length) return await respondWithStatus(res, 404, 'Incorrect username or email');
|
|
|
|
const [rows] = await pool.execute(
|
|
'SELECT * FROM verification_codes WHERE user_id = ? AND verification_code = ? AND type = ? AND created_at >= NOW() - INTERVAL 10 MINUTE ORDER BY 1 DESC LIMIT 1',
|
|
[user[0].id, code, 'password'],
|
|
);
|
|
if (!rows.length) return await respondWithStatus(res, 400, 'Invalid code');
|
|
|
|
const [result] = await pool.execute('DELETE FROM verification_codes WHERE user_id = ? AND verification_code = ?', [ user[0].id, code ]);
|
|
if (result.affectedRows === 0) {
|
|
return await respondWithStatus(res, 500, 'Error removing verification');
|
|
}
|
|
const token = generateToken(user[0].id, password);
|
|
const [rest] = await pool.execute('UPDATE users SET password = ? WHERE id = ?', [await Bun.password.hash(password), user[0].id]);
|
|
if (rest.affectedRows === 0) return await respondWithStatus(res, 500, 'Error updating user');
|
|
return await respondWithStatusJSON(res, 200, {
|
|
message: 'Password reset successful',
|
|
token: token,
|
|
});
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing fields');
|
|
}
|
|
});
|
|
|
|
router.get('/:userId', verifyToken, checkBanned, async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 1)) return await respondWithStatus(res, 403, 'Missing permission');
|
|
const [rows] = await pool.execute('SELECT id, first_name, last_name, username, email, phone FROM users WHERE id = ? LIMIT 1', [req.params.userId]);
|
|
if (rows.length === 0) return await respondWithStatus(res, 404, 'User not found');
|
|
|
|
const user = rows[0];
|
|
delete user.password;
|
|
return await respondWithStatusJSON(res, 200, user);
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.patch('/:userId', verifyToken, checkBanned, async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 2)) return await respondWithStatus(res, 403, 'Missing permission');
|
|
const { type } = req.body;
|
|
let { value } = req.body;
|
|
const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [req.params.userId]);
|
|
if (rows.length === 0) return await respondWithStatus(res, 404, 'User not found');
|
|
const excludedKeys = ['id'];
|
|
const fields = rows.map(row => Object.keys(row).filter(key => !excludedKeys.includes(key)));
|
|
if (fields[0].includes(type)) {
|
|
if (type === 'password') value = await Bun.password.hash(value);
|
|
const [result] = await pool.execute(`UPDATE users SET ${type} = ? WHERE id = ?`, [value, req.params.userId]);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error updating user');
|
|
return respondWithStatus(res, 200, 'User updated successfully');
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Invalid type or disallowed');
|
|
}
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.put('/:userId', verifyToken, checkBanned, async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 2)) return await respondWithStatus(res, 403, 'Missing permission');
|
|
const { first_name, last_name, username, password = null, email, phone = null } = req.body;
|
|
if ([first_name, last_name, username, email].every(Boolean)) {
|
|
let sqlQuery = 'UPDATE users SET first_name = ?, last_name = ?, username = ?, email = ?';
|
|
const queryParams = [first_name, last_name, username, email];
|
|
if (password) {
|
|
const hashedPassword = await Bun.password.hash(password);
|
|
sqlQuery = +' password = ?';
|
|
queryParams.append(hashedPassword);
|
|
}
|
|
else if (phone && isPhoneNumber(phone)) {
|
|
sqlQuery = ' phone = ?';
|
|
queryParams.append(phone);
|
|
}
|
|
const [result] = await pool.execute(sqlQuery + ' WHERE id = ?', queryParams.append(req.params.userId));
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error updating user');
|
|
return respondWithStatus(res, 200, 'User updated successfully');
|
|
}
|
|
if (!userExists(req.params.userId)) return await respondWithStatus(res, 404, 'User not found');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.delete('/:userId', verifyToken, checkBanned, async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 4)) return await respondWithStatus(res, 403, 'Missing permission');
|
|
if (!userExists(req.params.userId)) return await respondWithStatus(res, 404, 'User not found');
|
|
const [result] = await pool.execute('DELETE FROM users WHERE id = ?', [ req.params.userId ]);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error removing user');
|
|
return respondWithStatus(res, 200, 'User deleted successfully');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.get('/:userId/roles', verifyToken, checkBanned, async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 1)) return await respondWithStatus(res, 403, 'Missing permission');
|
|
const [rows] = await pool.execute('SELECT r.* FROM users u INNER JOIN user_roles ur ON u.id = ur.user_id INNER JOIN roles r ON ur.role_id = r.id WHERE u.id = ?', [ req.params.userId ]);
|
|
if (rows.length === 0) return await respondWithStatus(res, 404, 'No roles found');
|
|
return await respondWithStatusJSON(res, rows);
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
router.post('/:userId/roles', verifyToken, checkBanned, checkPermissions('user', 2), async (req, res) => {
|
|
const { roleId } = req.body;
|
|
if (roleId) {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
const [rows] = await pool.execute('SELECT * FROM roles WHERE id = ? LIMIT 1', [ roleId ]);
|
|
if (rows.length === 0) return await respondWithStatus(res, 404, 'Role not found');
|
|
const [result] = await pool.execute('INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', [ req.params.userId, roleId ]);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error assigning role');
|
|
return await respondWithStatus(res, 200, 'Role assigned successfully');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
}
|
|
else {
|
|
return await respondWithStatus(res, 400, 'Missing fields');
|
|
}
|
|
});
|
|
|
|
router.delete('/:userId/roles/:roleId', verifyToken, checkBanned, checkPermissions('user', 4), async (req, res) => {
|
|
try {
|
|
if (req.params.userId == '@me') req.params.userId = req.userId;
|
|
const [result] = await pool.execute('DELETE FROM user_roles WHERE user_id = ? AND role_id = ?', [ req.params.userId, req.params.roleId ]);
|
|
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error removing role');
|
|
return await respondWithStatus(res, 200, 'Role removed successfully');
|
|
}
|
|
catch (err) {
|
|
error(err);
|
|
return await respondWithStatus(res, 500, 'An error has occured');
|
|
}
|
|
});
|
|
|
|
export default router; |