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;