// node dependencies const { v4: uuidv4 } = require('uuid'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); require('dotenv').config({ path: '../.env' }); // variables const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS); const JWT_SECRET = process.env.JWT_SECRET; const JWT_EXPIRES = '30d'; // file paths const USER_PIC_DIR = '/mnt/openarchive/.lnx_connect/userPictures'; const TMP_UPLOAD_DIR = '/mnt/openarchive/.lnx_connect/tmp'; // dependencies const pools = require('../services/database/pools'); const { sendMail } = require('./activator'); // helper async function saveUserPicture(userId, tmpFilePath) { if (!fs.existsSync(USER_PIC_DIR)) fs.mkdirSync(USER_PIC_DIR, { recursive: true }); const destPath = path.join(USER_PIC_DIR, `${userId}.png`); if (tmpFilePath && fs.existsSync(tmpFilePath)) { // resize uploaded image await sharp(tmpFilePath) .resize(250, 250) .png({ compressionLevel: 9 }) .toFile(destPath); // delete tmp file fs.unlinkSync(tmpFilePath); return `${userId}.png` } else { return `default.png`; } } // export functions module.exports = { // register function register: async (data) => { // 0. Declare dbs to use const read = pools.users.r.s; const write = pools.users.w.s; // 1. Get incoming variables const { userEmail: user_email, userName: user_name, userPass: user_pass, userPicture: user_picture, fromProject: from_project } = data; // 2. Check if email or username already exists const existing = await read.query(` SELECT user_email, user_name FROM users WHERE user_email = ? OR user_name = ? `, [user_email, user_name]); // If exists throw error if (existing.length > 0) { const existingUser = existing[0]; if (existingUser.user_email === user_email) throw new Error('User with that mail already exists.'); if (existingUser.user_name === user_name) throw new Error('User with that name already exists.'); } // 3. Generate IDs and keys const user_id = uuidv4(); const authKeyPlain = uuidv4(); const auth_key = await bcrypt.hash(authKeyPlain, SALT_ROUNDS); const pass_hash = await bcrypt.hash(user_pass, SALT_ROUNDS); const session_key = jwt.sign({ user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES }); // 4. Handle user picture const tmpFileName = path.basename(user_picture || ''); const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName); const user_picture_file = await saveUserPicture(user_id, tmpFilePath); // 5. Set default role const default_role = ['default']; // 5. Write data to db await write.query(` INSERT INTO users ( user_id, auth_key, session_key, user_name, user_email, user_pass, user_picture, from_project, assigned_roles, state, last_active ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) `, [ user_id, auth_key, session_key, user_name, user_email, pass_hash, user_picture_file, from_project, JSON.stringify(default_role), 'active' ]); // send activation mail sendMail(user_id, user_name, user_email); // Objectize result const result = { userId: user_id, authKey: authKeyPlain, sessionKey: session_key, userName: user_name, userEmail: user_email, userPicture: user_picture_file, fromProject: from_project, assignedRoles: default_role } // Return result return result; }, // login function login: async (data) => { // Dummy hash used to normalize login timing const DUMMY_HASH = '$2b$10$nC7jBLRF/HwpZLkt79xCreF5Npsh5NeVFnGzOrRF7sVxlI9yJk99i'; // 0. Declare dbs to use const read = pools.users.r.s; const write = pools.users.w.s; // 1. Get incoming variables const { userLogin: user_login, userPass: user_pass, sessionKey: session_key } = data; const login = (user_login || '').trim(); // trim login name/mail // 2. Check if user exists const existing = await read.query(` SELECT user_id, auth_key, session_key, user_name, user_email, user_pass, user_picture, from_project, assigned_roles, additional_permissions, state FROM users WHERE user_email = ? OR user_name = ? `, [login, login]); let user = existing[0]; // Run fake bcrypt compare to match timing if (!user) { await bcrypt.compare(user_pass || '', DUMMY_HASH); throw new Error('Invalid login credentials.'); } // 3. Check user state if (user.state === 'banned' || user.state === 'pending') throw new Error(`User ${user.user_name} is ${user.state}.`); // 4. Login validation let loginValid = false; let autoLogin = false; // 4.1. Session login (auto login) if (session_key) { try { const decoded = jwt.verify(session_key, JWT_SECRET); if (decoded.user_id === user.user_id && session_key === user.session_key) { loginValid = true; autoLogin = true; }; } catch (err) { loginValid = false; autoLogin = false; } } // 4.2. Password login let new_session_key; if (!loginValid) { // match passwords const passMatch = await bcrypt.compare(user_pass || '', user.user_pass); if (!passMatch) throw new Error('Invalid login credentials.'); loginValid = true; // generate new session key new_session_key = jwt.sign({ user_id: user.user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES }); } const newAuthKeyPlain = uuidv4(); const newAuthKeyHash = await bcrypt.hash(newAuthKeyPlain, SALT_ROUNDS); // 6. Update session after login if (loginValid && !autoLogin) { await write.query(` UPDATE users SET auth_key = ?, session_key = ?, last_active = NOW() WHERE user_id = ? `, [newAuthKeyHash, new_session_key, user.user_id]); } if (loginValid && autoLogin) { await write.query(` UPDATE users SET auth_key = ?, last_active = NOW() WHERE user_id = ? `, [newAuthKeyHash, user.user_id]); } // Objectize result const result = { userId: user.user_id, authKey: autoLogin ? user.auth_key : newAuthKeyPlain, sessionKey: autoLogin ? user.session_key : new_session_key, userName: user.user_name, userEmail: user.user_email, userPicture: user.user_picture, fromProject: user.from_project, assignedRoles: user.assigned_roles ? JSON.parse(user.assigned_roles) : [], additionalPermissions: user.additional_permissions ? JSON.parse(user.additional_permissions) : [] } // Return result return result; }, // update function update: async (data) => { // 0. Declare dbs to use const read = pools.users.r.s; const write = pools.users.w.s; // 1. Get incoming variables const { userId: user_id, authKey: auth_key, newUserName: user_name, newUserEmail: user_email, newUserPass: user_pass, newUserPicture: user_picture, assignedRoles: assigned_roles, additionalPermissions: additional_permissions, state: new_state } = data; // 2. Fetch current user const existing = await read.query(` SELECT user_id, auth_key, user_name, user_email, user_pass, user_picture, assigned_roles, additional_permissions, state FROM users WHERE user_id = ? `, [user_id]); if (!existing[0]) throw new Error('User not found.'); const user = existing[0]; // 3. Check auth key const authMatch = await bcrypt.compare(auth_key, user.auth_key); if (!authMatch) throw new Error('Invalid auth key.'); // 4. Prepare fields to update const updates = {}; if (user_name && user_name !== user.user_name) updates.user_name = user_name; if (user_email && user_email !== user.user_email) updates.user_email = user_email; if (user_pass) updates.user_pass = await bcrypt.hash(user_pass, SALT_ROUNDS); if (assigned_roles) updates.assigned_roles = JSON.stringify(assigned_roles); if (additional_permissions) updates.additional_permissions = JSON.stringify(additional_permissions); if (new_state && new_state !== user.state) updates.state = new_state; // 5. Handle new user picture if (user_picture) { const tmpFileName = path.basename(user_picture || ''); const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName); updates.user_picture = await saveUserPicture(user_id, tmpFilePath); } // 6. Build dynamic update query const fields = Object.keys(updates); if (fields.length === 0) return { message: 'Nothing to update.' }; const placeholders = fields.map(f => `${f} = ?`).join(', '); const values = fields.map(f => updates[f]); values.push(user_id); await write.query(` UPDATE users SET ${placeholders}, last_active = NOW() WHERE user_id = ? `, values); // 7. Return updated user data const updatedUser = await read.query(` SELECT user_id, user_name, user_email, user_picture, from_project, assigned_roles, additional_permissions, state FROM users WHERE user_id = ? `, [user_id]); return updatedUser[0]; }, // activation function activate: async (data) => { // 0. Declare dbs to use const read = pools.users.r.s; const write = pools.users.w.s; // 1. Get incoming token const { activationToken } = data; if (!activationToken) throw new Error('No activation token provided.'); let payload; try { // 2. Verify token payload = jwt.verify(activationToken, JWT_SECRET); } catch (err) { throw new Error('Invalid or expired activation token.'); } const userId = payload.user_id; // 3. Fetch user const existing = await read.query(` SELECT user_id, assigned_roles FROM users WHERE user_id = ? `, [userId]); if (!existing[0]) throw new Error('User not found.'); const user = existing[0]; // 4. Add "activated" role if not present const roles = user.assigned_roles ? JSON.parse(user.assigned_roles) : []; if (!roles.includes('activated')) roles.push('activated'); // 5. Update DB await write.query(` UPDATE users SET assigned_roles = ?, last_active = NOW() WHERE user_id = ? `, [JSON.stringify(roles), userId]); return { message: 'Account successfully activated.', assignedRoles: roles }; }, // check auth function check: async (data) => {}, };