Initial commit
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
// 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) => {},
|
||||
};
|
||||
Reference in New Issue
Block a user