const express = require('express'); const argon2 = require('argon2'); const router = express.Router(); const { pool } = require('../modules/database'); const { sendVerification, sendResetVerification } = require('../modules/mailHandler'); const { isEmailDomainValid, isValidEmail, isNumber } = require('../modules/formatHandler'); const { hasPermission, checkBanned, isBanned } = require('../modules/permission'); const { verifyToken, generateToken, revokeUserTokens } = require('../modules/token'); const { requestLimiter, respondWithStatus, respondWithStatusJSON } = require('../modules/requestHandler'); router.post('/register', requestLimiter, async (req, res) => { const { username, email, password, first_name, last_name, phone = 'none' } = 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'); } const hashedPassword = await argon2.hash(password); const [unverifiedId] = await pool.execute( 'SELECT id FROM user_types WHERE name = \'unverified\' LIMIT 1', ); const [result] = await pool.execute( 'INSERT INTO users (first_name, last_name, username, email, password, user_type_id, phone) VALUES (?, ?, ?, ?, ?, ?, ?)', [ first_name, last_name, username, email, hashedPassword, unverifiedId[0].id, phone ], ); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error storing user'); } const [rows] = await pool.execute('SELECT id FROM users WHERE email = ? LIMIT 1', [email]); const code = sendVerification(email, rows[0].id); pool.execute('INSERT INTO user_email_verifications (user_id, verification_code, type) VALUES (?, ?, ?)', [ rows[0].id, code, 'register' ]); return await respondWithStatus(res, 200, 'Successfully registered'); } else { return await respondWithStatus(res, 400, 'Invalid email address'); } } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } } else { return await respondWithStatus(res, 400, 'Missing fields'); } }); router.post('/login', requestLimiter, 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 argon2.verify(user.password, password); if (!passwordMatch) { return await respondWithStatus(res, 401, 'Incorrect password'); } if (isBanned(user.id)) { const token = await generateToken(user.id, password); res.cookie('token', token, { expires: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), httpOnly: true, secure: true, sameSite: 'strict', }); return await respondWithStatusJSON(res, 200, { message: 'Login successful', token: token, user: { id: user.id, username: user.username, email: user.email, name: user.name, }, }); } else { return await respondWithStatus(res, 403, 'User is banned or an issue occured'); } } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } } else { return await respondWithStatus(res, 400, 'Missing fields'); } }); router.get('/verify', async (req, res) => { const { c, u } = req.query; if ([c, u].every(Boolean)) { try { let userId = u; if (!isNumber(u)) { const [user] = await pool.execute( 'SELECT id FROM users WHERE username = ? OR email = ? LIMIT 1', [u, u], ); if (!user.length) { return await respondWithStatus(res, 404, 'Incorrect username or email'); } userId = user[0].id; } const [rows] = await pool.execute( 'SELECT * FROM user_email_verifications WHERE user_id = ? AND verification_code = ? LIMIT 1', [userId, c], ); if (!rows.length) { return await respondWithStatus(res, 400, 'Invalid code'); } if (rows[0].type == 'register') { const [customerId] = await pool.execute( 'SELECT id FROM user_types WHERE name = \'customer\' LIMIT 1', ); const [result] = await pool.execute('UPDATE users SET user_type_id = ? WHERE id = ?', [ customerId[0].id, userId ]); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error updating user'); } } return await respondWithStatus(res, 200, 'Successfully verified user'); } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } } else { return await respondWithStatus(res, 400, 'Missing fields'); } }); router.post('/verify', verifyToken, checkBanned, async (req, res) => { const { code } = req.body; const userId = req.userId; if ([code, userId].every(Boolean)) { try { const [rows] = await pool.execute( 'SELECT * FROM user_email_verifications WHERE user_id = ? AND verification_code = ? LIMIT 1', [ userId, code ], ); if (!rows.length) { return await respondWithStatus(res, 400, 'Invalid code'); } const [customerId] = await pool.execute( 'SELECT id FROM user_types WHERE name = \'customer\' LIMIT 1', ); const [result] = await pool.execute('UPDATE users SET user_type_id = ? WHERE userId = ?', [ customerId[0].id, userId ]); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error updating user'); } return await respondWithStatus(res, 200, 'Successfully verified user'); } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } } else { return await respondWithStatus(res, 400, 'Missing fields'); } }); router.post('/changepassword', async (req, res) => { const { usernameOrEmail } = req.body; if ([ usernameOrEmail ].every(Boolean)) { 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 user_email_verifications WHERE user_id = ? AND type = \'password\' LIMIT 1', [user[0].id], ); if (!rows.length) { code = sendResetVerification(user[0].email); } else { code = sendResetVerification(user[0].email, rows[0].verification_code); } if (code) { pool.execute('INSERT INTO user_email_verifications (user_id, verification_code, type) VALUES (?, ?, ?)', [ user[0].id, code, 'password' ]); return await respondWithStatus(res, 200, 'Successfully sent password reset email'); } else { return await respondWithStatus(res, 500, 'An error has occured'); } } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } } }); router.patch('/changepassword', async (req, res) => { const { usernameOrEmail, password, code } = req.body; 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 user_email_verifications WHERE user_id = ? AND verification_code = ? AND type = \'password\' ORDER BY 1 DESC LIMIT 1', [user[0].id, code], ); if (!rows.length) { return await respondWithStatus(res, 400, 'Invalid code'); } const [result] = await pool.execute('DELETE FROM user_email_verifications WHERE user_id = ? AND verification_code = ?', [ user[0].id, code ]); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error removing verification'); } revokeUserTokens(user[0].id); const token = generateToken(user[0].id, password); res.cookie('token', token, { expires: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), httpOnly: true, secure: true, sameSite: 'strict', }); return userPATCH(res, user[0].id, 'password', password); } catch (error) { console.error(error); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.get('/', verifyToken, checkBanned, hasPermission('view_users'), async (req, res) => { try { const [rows] = await pool.execute('SELECT * FROM users WHERE 1'); if (rows.length === 0) { return await respondWithStatus(res, 404, 'Users not found'); } return await respondWithStatusJSON(res, 200, rows); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.post('/', verifyToken, checkBanned, hasPermission('add_users'), async (req, res) => { const { first_name, last_name, username, email, password, user_type } = req.body; if ([first_name, last_name, username, email, password, user_type].every(Boolean)) { try { const hashedPassword = await argon2.hash(password); await pool.execute( 'INSERT INTO users (first_name, last_name, username, email, password, user_type) VALUES (?, ?, ?, ?, ?, ?)', [ first_name, last_name, username, email, hashedPassword, user_type ], ); return await respondWithStatus(res, 200, 'User created successfully'); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } } else { return await respondWithStatus(res, 400, 'Missing fields'); } }); router.patch('/', verifyToken, checkBanned, async (req, res) => { try { const userId = req.userId; const { type, value } = req.body; userPATCH(res, userId, type, value); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.put('/', verifyToken, checkBanned, async (req, res) => { try { const userId = req.userId; const { first_name, last_name, username, email } = req.body; userPUT(res, userId, first_name, last_name, username, email); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.delete('/', verifyToken, checkBanned, async (req, res) => { try { const userId = req.userId; userDELETE(res, userId); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.get('/:userId', verifyToken, checkBanned, hasPermission('view_users'), async (req, res) => { try { let userId = req.params.userId; if (req.params.userId == 'me') { userId = req.userId; } const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [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) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.patch('/:userId', verifyToken, checkBanned, hasPermission('edit_users'), async (req, res) => { try { const userId = req.params.userId; const { type, value } = req.body; const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [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)), ); console.log(fields[0]); if (fields[0].includes(type)) { const [result] = await pool.execute(`UPDATE users SET ${type} = ? WHERE id = ?`, [value, 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) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.put('/:userId', verifyToken, checkBanned, hasPermission('edit_users'), async (req, res) => { try { const userId = req.params.userId; const { first_name, last_name, username, email } = req.body; if ([first_name, last_name, username, email].every(Boolean)) { userPUT(res, userId, first_name, last_name, username, email); } } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); router.delete('/:userId', verifyToken, checkBanned, hasPermission('delete_users'), async (req, res) => { try { const userId = req.params.userId; userDELETE(res, userId); } catch (err) { console.error(err); return await respondWithStatus(res, 500, 'An error has occured'); } }); async function userPATCH(res, id, type, value) { const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [id]); if (rows.length === 0) { return await respondWithStatus(res, 404, 'User not found'); } const excludedKeys = ['id', 'user_type_id', 'is_banned']; const fields = rows.map(row => Object.keys(row) .filter(key => !excludedKeys.includes(key)), ); if (type == 'password') { value = await argon2.hash(value); } if (fields[0].includes(type)) { const [result] = await pool.execute(`UPDATE users SET ${type} = ? WHERE id = ?`, [value, id]); 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'); } } async function userPUT(res, userId, first_name, last_name, username, email, password = false) { const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]); if (rows.length === 0) { return await respondWithStatus(res, 404, 'User not found'); } let sqlQuery, queryParams; if (password) { const hashedPassword = await argon2.hash(password); sqlQuery = 'UPDATE users SET first_name = ?, last_name = ?, username = ?, email = ?, password = ? WHERE id = ?'; queryParams = [first_name, last_name, username, email, hashedPassword, userId]; } else { sqlQuery = 'UPDATE users SET first_name = ?, last_name = ?, username = ?, email = ? WHERE id = ?'; queryParams = [first_name, last_name, username, email, userId]; } const [result] = await pool.execute(sqlQuery, queryParams); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error updating user'); } return respondWithStatus(res, 200, 'User updated successfully'); } async function userDELETE(res, userId) { const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]); if (rows.length === 0) { return await respondWithStatus(res, 404, 'User not found'); } const [result] = await pool.execute('DELETE FROM users WHERE id = ?', [ userId ]); if (result.affectedRows === 0) { return await respondWithStatus(res, 500, 'Error removing user'); } return respondWithStatus(res, 200, 'User deleted successfully'); } module.exports.router = router;