import jwt from "jsonwebtoken"; import db from "./db.js"; import bcrypt from "bcryptjs"; /** @typedef User * @property {number} id - The unique identifier for the user. * @property {string} username - The username of the user. * @property {string} password - The hashed password of the user. * @property {string} role - The role of the user, e.g., 'user' or 'admin'. */ /** * Fetches all users from the database. * @return {Promise} */ async function getAllUsers() { try { return await db.get('users').then((users) => users); } catch (error) { console.error('Error fetching users:', error); throw error; } } /** * gets user by id or username * @param {string | number} identifier can be either user id or username * @return {Promise<{id: number, username: string, password: string, role: string}>} */ async function getUser(identifier) { /** @type {User[]} users */ let users; if (/^\d+$/.test(identifier)) users = await db.get('users',{"id": identifier}); else users = await db.get('users', {'username': identifier}); if (users.length === 0) throw new Error("User not found"); else if (users.length > 1) throw new Error("Multiple users found with the same identifier, something has gone wrong"); return users[0]; } /** * adds new user to db as non-admin. only direct db manipulation can make users admins * @param {string} username * @param {string} password * @return {Promise} */ async function addNewUser(username, password) { if (!username || !password) throw new Error('Username and password are required'); // Check if the user already exists if ((await db.get('users', {'username': username})).length > 0) throw new Error('User already exists with this username'); // Hash the password before storing it const hashedPassword = await bcrypt.hash(password, 10); /** @type {Promise} */ const result = await db.insert('users', {username, password: hashedPassword, role: 'user'}); if (!result) throw new Error('User could not be created'); if (result instanceof Error) throw result; return result; } /** * Logs in a user by validating their credentials and generating a JWT token. * @param {string} username * @param {string} password * @return {Promise<{id: number, username: string, role: string, token: (*)}>} */ async function login (username, password) { /** @type {User} user*/ const user = await db.get('users', {'username': username}).then(response => { if (response instanceof Error) throw response; if (response.length === 0) throw new Error('User not found'); if (response.length > 1) throw new Error('Multiple users found with the same username, something has gone wrong'); return response[0]; }); if (!user || !bcrypt.compareSync(password, user.password)) { throw new Error('Invalid username or password'); } console.log(user); const token = jwt.sign({id: user.id, role: user.role}, process.env.JWT_SECRET, {expiresIn: "1d"}); return { id: user.id, username: user.username, role: user.role, token: token } } /** * Updates a user's information. * @param {{id?: number, username?: string, password?: string, newPassword?: string}} mapping * @return {Promise} */ async function updateUser(mapping) { if (!mapping) throw new Error('User mapping is required'); if (!mapping.id && !mapping.username) throw new Error('User id or username is required for update'); let user; if (mapping.id) user = await getUser(mapping.id); else user = await getUser(mapping.username); if (!user) throw new Error('User not found'); if (mapping.newPassword) { // validate the old password to make sure the user is authenticated if (!mapping.password) throw new Error('Old password is required to update password'); if (!bcrypt.compareSync(mapping.password, user.password)) throw new Error('Old password is incorrect'); // Hash the new password before updating mapping.password = await bcrypt.hash(mapping.newPassword, 10); delete mapping.newPassword; // Remove newPassword from the mapping } /** @type {User[] | Error} */ let result; if (mapping.id) { // remove id from mapping to avoid updating it const id = mapping.id; delete mapping.id; result = await db.update('users', mapping, {'id': id}); } else { // remove username from mapping to avoid updating it const username = mapping.username; delete mapping.username; result = await db.update('users', mapping, {'username': username}); } if (result instanceof Error) throw result; if (result.length > 1) throw new Error('Multiple users updated, something has gone wrong'); return result[0]; } /** * Deletes a user by their ID. * @param {string | number} identifier can be either user id or username * @return {Promise} */ async function deleteUser(identifier) { if (!identifier) throw new Error('User identifier is required'); /** @type {User[] | Error} */ let result; if (/^\d+$/.test(identifier)) result = await db.remove('users', {"id": identifier}); else result = await db.remove('users', {'username': identifier}); if (result instanceof Error) throw result; return result[0]; } export default { getAllUsers, getUser, addNewUser, login, updateUser, deleteUser }