import * as dotenv from 'dotenv'; import * as bcrypt from 'bcrypt'; import {Guid} from 'guid-typescript'; dotenv.config(); const mariadb = require('mariadb'); const pool = mariadb.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, connectionLimit: 5 }); /** * Data Model Interfaces */ import {User} from './user.interface'; import {Users} from './users.interface'; import {Session} from './session.interface'; /** * Service Methods */ /** * Creates a user record in the database, also creates a session. Returns the session if successful. */ export const createUser = async (username: string, password: string, email: string, ip: string): Promise => { let conn; try { // Hash password and generate + hash session key const pwHash = bcrypt.hashSync(password, 10); const sessionKey = Guid.create().toString(); const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); // Create user entry in SQL conn = await pool.getConnection(); const userQuery = 'INSERT INTO users (username, email, bcrypt_password_hash) VALUES (?, ?, ?) RETURNING user_id'; const userIdRes = await conn.query(userQuery, [username, email, pwHash]); await conn.commit(); // Get user id of the created user let userId: number = -1; for (const row in userIdRes) { if (row !== 'meta' && userIdRes[row].user_id != null) { userId = userIdRes[row].user_id; } } // Create session const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, createdDate, lastLogin, validUntil, validDays, last_IP) VALUES (?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),30,?) RETURNING session_id'; const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]); await conn.commit(); // Get session id of the created session let sessionId: number = -1; for (const row in sessionIdRes) { if (row !== 'meta' && sessionIdRes[row].session_id != null) { sessionId = sessionIdRes[row].session_id; } } return { session_id: sessionId, session_key: sessionKey, session_key_hash: '', last_IP: ip }; } catch (err) { throw err; } finally { if (conn) { conn.end(); } } return {} as Session; }; /** * Checks if the given credentials are valid and creates a new session if they are. * Returns the session information in case of a successful login */ export const login = async (username: string, password: string, ip: string): Promise => { let conn; try { // Get saved password hash conn = await pool.getConnection(); const query = 'SELECT user_id, bcrypt_password_hash FROM users WHERE username = ?'; const userRows = await conn.query(query, username); let savedHash = ''; let userId = -1; for (const row in userRows) { if (row !== 'meta' && userRows[row].user_id != null) { savedHash = userRows[row].bcrypt_password_hash; userId = userRows[row].user_id; } } // Check for correct password if (!bcrypt.compareSync(password, savedHash)) { // Wrong password, return invalid return {} as Session; } // Password is valid, continue // Generate + hash session key const sessionKey = Guid.create().toString(); const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); // Update user entry in SQL const userQuery = 'UPDATE users SET last_login_date = NOW()'; const userIdRes = await conn.query(userQuery); await conn.commit(); // Create session const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, createdDate, lastLogin, validUntil, validDays, last_IP) VALUES (?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),30,?) RETURNING session_id'; const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]); await conn.commit(); // Get session id of the created session let sessionId: number = -1; for (const row in sessionIdRes) { if (row !== 'meta' && sessionIdRes[row].session_id != null) { sessionId = sessionIdRes[row].session_id; } } return { session_id: sessionId, session_key: sessionKey, session_key_hash: '', last_IP: ip }; } catch (err) { throw err; } finally { if (conn) { conn.end(); } } return {} as Session; }; /** * Checks if the given session information are valid and returns the user information if they are */ export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise => { let conn; try { // Get saved session key hash conn = await pool.getConnection(); const query = 'SELECT user_id, session_key_hash, validUntil FROM sessions WHERE session_id = ?'; const sessionRows = await conn.query(query, sessionId); let savedHash = ''; let userId = -1; let validUntil = new Date(); for (const row in sessionRows) { if (row !== 'meta' && sessionRows[row].user_id != null) { savedHash = sessionRows[row].session_key_hash; userId = sessionRows[row].user_id; validUntil = sessionRows[row].validUntil; } } // Check for correct key if (!bcrypt.compareSync(sessionKey, savedHash)) { // Wrong key, return invalid return {} as User; } // Key is valid, continue // Check if the session is still valid if(validUntil <= new Date()) { // Session expired, return invalid return {} as User; } // Session still valid, continue // Update session entry in SQL const updateSessionsQuery = 'UPDATE sessions SET lastLogin = NOW(), last_IP = ? WHERE session_id = ?'; const updateUsersQuery = 'UPDATE users SET last_login_date = NOW() WHERE user_id = ?'; const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]); await conn.query(updateUsersQuery, userId); await conn.commit(); // Get the other required user information and update the user const userQuery = "SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?"; const userRows = await conn.query(userQuery, userId); let username = ''; let email = ''; let registrationDate = new Date(); let lastLoginDate = new Date(); for (const row in userRows) { if (row !== 'meta' && userRows[row].user_id != null) { username = userRows[row].username; email = userRows[row].email; registrationDate = userRows[row].registration_date; lastLoginDate = userRows[row].last_login_date; } } // Everything is fine, return user information return { user_id: userId, username: username, email: email, password_hash: '', registration_date: registrationDate, last_login_date: lastLoginDate }; } catch (err) { throw err; } finally { if (conn) { conn.end(); } } return {} as User; }; /** * Used in the checkUsernameAndEmail method as return value */ export interface Status { hasProblems: boolean; messages: string[]; codes: number[]; // 0 = all good, 1 = wrong username, 2 = wrong email, 3 = server error, 4 = wrong password, 5 = wrong session } /** * Checks if the given username and email are not used yet by another user * @param username The username to check * @param email The email to check */ export const checkUsernameAndEmail = async (username: string, email: string): Promise => { let conn; try { // Create user entry in SQL conn = await pool.getConnection(); const usernameQuery = 'SELECT username FROM users WHERE username = ?'; const emailQuery = 'SELECT email FROM users WHERE email = ?'; const usernameRes = await conn.query(usernameQuery, username); const emailRes = await conn.query(emailQuery, email); let res: Status = { hasProblems: false, messages: [], codes: [] }; const usernameRegex = RegExp('^[a-zA-Z0-9\\-\\_]{4,20}$'); // Can contain a-z, A-Z, 0-9, -, _ and has to be 4-20 chars long if (!usernameRegex.test(username)) { // Username doesn't match requirements res.hasProblems = true; res.messages.push('Invalid username'); res.codes.push(1); } const emailRegex = RegExp('^[a-zA-Z0-9\\-\\_.]{1,30}\\@[a-zA-Z0-9\\-.]{1,20}\\.[a-z]{1,20}$'); // Normal email regex, user@betterzon.xyz if (!emailRegex.test(email)) { // Username doesn't match requirements res.hasProblems = true; res.messages.push('Invalid email'); res.codes.push(2); } if (usernameRes.length > 0) { // Username is a duplicate res.hasProblems = true; res.messages.push('Duplicate username'); res.codes.push(1); } if (emailRes.length > 0) { // Email is a duplicate res.hasProblems = true; res.messages.push('Duplicate email'); res.codes.push(2); } return res; } catch (err) { throw err; } finally { if (conn) { conn.end(); } } return {hasProblems: true, messages: ['Internal server error'], codes: [3]}; };