285 lines
8.6 KiB
TypeScript
285 lines
8.6 KiB
TypeScript
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<Session> => {
|
|
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<boolean> => {
|
|
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<Session> => {
|
|
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<User> => {
|
|
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<boolean> => {
|
|
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<boolean> => {
|
|
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();
|
|
}
|
|
}
|