Files
2025-08-19 09:55:57 -04:00

146 lines
5.4 KiB
JavaScript

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<User[]>}
*/
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<User|void>}
*/
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<User|Error>} */
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<User>}
*/
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<User>}
*/
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
}