import * as dotenv from 'dotenv'; import * as bcrypt from 'bcrypt'; import {Guid} from 'guid-typescript'; import {User} from './user.interface'; import {Session} from './session.interface'; import {NachklangCalendarDB} from '../Calendar.db'; import {MailService} from "../../../common/common.mail.nodemailer"; dotenv.config(); /** * Data Model Interfaces */ /** * Service Methods */ /** * Creates a user record in the database, also creates a session. Returns the session if successful. */ export const createUser = async (email: string, password: string, fullName: string, ip: string): Promise => { let conn = await NachklangCalendarDB.getConnection(); 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); const activationToken = Guid.create().toString(); // Create user entry in SQL const userQuery = 'INSERT INTO users (email, password_hash, full_name, activation_token) VALUES (?, ?, ?, ?) RETURNING user_id'; const userIdRes = await conn.query(userQuery, [email, pwHash, fullName, activationToken]); // Get user id of the created user let userId: number = -1; for (const row of userIdRes) { userId = row.user_id; } // Send email with activation link await MailService.sendMail(email, 'Activate your Nachklang account', `Hi ${fullName},\n\nPlease click on the following link to activate your account:\n\nhttps://api.nachklang.art/calendar/users/activate?id=${userId}&token=${activationToken}`); // Create session const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) 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 of sessionIdRes) { sessionId = row.session_id; } return { sessionId: sessionId, userId: userId, sessionKey: sessionKey, sessionKeyHash: 'HIDDEN', lastIP: ip }; } catch (err) { throw err; } finally { // Return connection await conn.end(); } }; export const activateUser = async (userId: number, token: string): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { const checkTokenQuery = 'SELECT user_id, activation_token FROM users WHERE user_id = ? AND is_active = 0'; const userNameRes = await conn.query(checkTokenQuery, [userId]); let storedToken = ''; for (const row of userNameRes) { storedToken = row.activation_token; } if (storedToken!== token) { return false; } const activateQuery = 'UPDATE users SET is_active = 1, activation_token = null WHERE user_id =?'; const activateRes = await conn.execute(activateQuery, [userId]); return activateRes.affectedRows !== 0; } catch (err) { throw err; } finally { // Return connection await conn.end(); } } /** * 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 (email: string, password: string, ip: string): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { // Get saved password hash const query = 'SELECT user_id, password_hash FROM users WHERE email = ?'; const userRows = await conn.query(query, email); let savedHash = ''; let userId = -1; for (const row of userRows) { savedHash = row.password_hash; userId = row.user_id; } // Check for correct password if (!bcrypt.compareSync(password, savedHash)) { // Wrong password, return invalid return {} as Session; } // Generate + hash session key const sessionKey = Guid.create().toString(); const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); // Create session const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_date, valid_until, last_ip) VALUES (?,?,NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),?) 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 of sessionIdRes) { sessionId = row.session_id; } return { sessionId: sessionId, userId: userId, sessionKey: sessionKey, sessionKeyHash: 'HIDDEN', lastIP: ip }; } catch (err) { throw err; } finally { // Return connection await conn.end(); } }; /** * 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 = await NachklangCalendarDB.getConnection(); try { // Get saved session key hash const query = 'SELECT user_id, session_key_hash, valid_until FROM sessions WHERE session_id = ?'; const sessionRows = await conn.query(query, sessionId); let savedHash = ''; let userId = -1; let validUntil = new Date(); for (const row of sessionRows) { savedHash = row.session_key_hash; userId = row.user_id; validUntil = row.valid_until; } // Check for correct key if (!bcrypt.compareSync(sessionKey, savedHash)) { // Wrong key, return invalid return {} as User; } // Check if the session is still valid if (validUntil <= new Date()) { // Session expired, return invalid return {} as User; } // Update session entry in SQL const updateSessionsQuery = 'UPDATE sessions SET last_IP = ? WHERE session_id = ?'; const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]); await conn.commit(); // Get the other required user information const userQuery = 'SELECT user_id, email, full_name, is_active FROM users WHERE user_id = ?'; const userRows = await conn.query(userQuery, userId); let username = ''; let email = ''; let fullName = ''; let is_active = false; for (const row of userRows) { username = row.username; email = row.email; fullName = row.full_name; is_active = row.is_active; } // Everything is fine, return user information return { userId: userId, email: email, passwordHash: 'HIDDEN', fullName: fullName, isActive: is_active }; } catch (err) { throw err; } finally { // Return connection await conn.end(); } }; export const initiatePasswordReset = async (email: string): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { const checkUsernameQuery = 'SELECT user_id, full_Name FROM users WHERE email = ?'; const userNameRes = await conn.query(checkUsernameQuery, [email]); if (userNameRes.length === 0) { return false; } let userId: number = -1; let fullName: string = ''; for(let row of userNameRes) { userId = row.user_id; fullName = row.full_Name; } let resetToken = Guid.create().toString(); let resetTokenHash = bcrypt.hashSync(resetToken, 10); const updateQuery = 'UPDATE users SET pw_reset_token_hash = ? WHERE user_id = ?'; const updateRes = await conn.execute(updateQuery, [resetTokenHash, userId]); if(updateRes.affectedRows === 0) { return false; } await conn.commit(); await MailService.sendMail(email, 'Password Reset', `Hello ${fullName},\n\nYou requested a password reset for your BonkApp account. If you did not request this, please ignore this email.\n\nTo reset your password, please use the following reset token:\n\n${resetToken}`); return true; } catch (err) { throw err; } finally { // Return connection await conn.end(); } } export const finalizePasswordReset = async (email: string, token: string, newPassword: string): Promise => { let conn = await NachklangCalendarDB.getConnection(); try { const checkTokenQuery = 'SELECT user_id, pw_reset_token_hash FROM users WHERE email = ?'; const userNameRes = await conn.query(checkTokenQuery, [email]); if (userNameRes.length === 0) { return false; } let userId: string = ''; let tokenHash: string = ''; for(let row of userNameRes) { userId = row.user_id; tokenHash = row.pw_reset_token_hash; } if(!bcrypt.compareSync(token, tokenHash)) { return false; } const pwHash = bcrypt.hashSync(newPassword, 10); const updatePasswordQuery = 'UPDATE users SET password_hash = ?, pw_reset_token_hash = NULL WHERE user_id = ?'; const updateRes = await conn.execute(updatePasswordQuery, [pwHash, userId]); if(updateRes.affectedRows > 0) { await conn.commit(); return true; } return false; } catch (err) { throw err; } finally { // Return connection await conn.end(); } }