import { Request, Response } from "express"; import logger, { userLogger } from "../logger/logger"; import User from "../models/user"; import { isCompanyNameValid, isEmailValid, isLanguageCodeValid, isPasswordValid, isUsernameValid, } from "../validator/validator"; import { ACCOUNT_DEMO_DAYS, ACCOUNT_STATE, CALENDAR_MAX_SERVICE_DURATION, CALENDAR_MIN_EARLIEST_BOOKING_TIME, EMAIL_VERIFICATION_STATE, PAYMENT_PLAN, PAYMENT_PLAN_SETTINGS, Roles, USER_ANALYTICS_ENABLED_DEFAULT, } from "../utils/constants"; import { decodeBase64, getEmailVerificationUrl, getUserAgentOS, getUserSession, hashPassword, matchPassword, newAccountExportId, newEmailVerificationId, newFeedbackId, newStoreId, newUserId, saveSession, } from "../utils/utils"; import Store from "../models/store"; import Session from "../models/session"; import Feedback from "../models/feedback"; import fs from "fs-extra"; import rabbitmq from "../rabbitmq/rabbitmq"; import verifyCaptcha from "../utils/recaptcha"; import EmailVerification from "../models/emailVerification"; import UserPendingEmailChange from "../models/userPendingEmailChange"; export async function SignUp(req: Request, res: Response) { try { let { companyName, username, email, password, language, rememberMe, recaptcha, } = req.body; // validate request if ( !companyName || !username || !email || !password || !language || !isCompanyNameValid(companyName) || !isUsernameValid(username) || !(await isEmailValid(email)) || !isLanguageCodeValid(language) || rememberMe === undefined || !recaptcha ) { return res.status(400).send({ err: "invalid request" }); } // validate recaptcha const recaptchaValid = await verifyCaptcha( recaptcha, req.headers["x-real-ip"] as string ); if (!recaptchaValid) { return res.status(400).send({ err: "invalid request" }); } email = email.toLowerCase(); // check if user already exists const existingUser = await User.findOne({ where: { email: email, }, }); if (existingUser) { return res.status(400).send({ err: "invalid request" }); } // decode password const decodedPassword = decodeBase64(password); if (!isPasswordValid(decodedPassword)) { return res.status(400).send({ err: "invalid request" }); } // hash password const hashedPassword = await hashPassword(decodedPassword); // create store let userId = newUserId(); const store = await Store.create({ store_id: newStoreId(), owner_user_id: userId, name: companyName, calendar_max_future_booking_days: PAYMENT_PLAN_SETTINGS[PAYMENT_PLAN.DEMO].calendarMaxFutureBookingDays, calendar_min_earliest_booking_time: CALENDAR_MIN_EARLIEST_BOOKING_TIME, calendar_max_service_duration: CALENDAR_MAX_SERVICE_DURATION, address: "", }); // create email verification const emailVerificationId = newEmailVerificationId(); const state = EMAIL_VERIFICATION_STATE.PENDING_EMAIL_VERIFICATION; await EmailVerification.create({ email_verification_id: emailVerificationId, user_id: userId, state: state, }); rabbitmq.sendEmail(email, "dashboardSignUpEmailVerification", language, { emailVerificationUrl: getEmailVerificationUrl(state, emailVerificationId), }); // create user await User.create({ user_id: userId, store_id: store.store_id, role: Roles.Master, email: email, username: username, password: hashedPassword, language: language, analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT, state: ACCOUNT_STATE.PENDING_EMAIL_VERIFICATION, payment_plan: PAYMENT_PLAN.DEMO, }); logger.info( `new user signed up: user_id: ${userId} email: ${email} language: ${language} company: ${companyName} username: ${username}` ); res.status(200).send({ msg: "success" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function Login(req: Request, res: Response) { try { let { email, password, rememberMe, recaptcha } = req.body; // validate request if (!email) { return res.status(400).send({ err: "invalid request" }); } email = email.toLowerCase(); if (!(await isEmailValid(email, false))) { return res.status(400).send({ err: "invalid request" }); } // check if user exists const user = await User.findOne({ where: { email: email, }, attributes: ["user_id", "password", "state", "language"], }); if (!user) { return res.status(400).send({ err: "invalid request" }); } // if password not provided, then send user state // user is on the login page on the first step of the login process // and only needs to enter their email to get the user state to know what to do next if (password === undefined) { return res.status(200).send({ state: user.state }); } // validate recaptcha const recaptchaValid = await verifyCaptcha( recaptcha, req.headers["x-real-ip"] as string ); if (!recaptchaValid) { return res.status(400).send({ err: "invalid request" }); } // decode password const decodedPassword = decodeBase64(password); if (!isPasswordValid(decodedPassword)) { return res.status(400).send({ err: "invalid request" }); } let updateData = {}; // if user state is INIT_LOGIN, then user is logging in for the first time and needs to set their password if (user.state === ACCOUNT_STATE.INIT_LOGIN) { // hash password updateData = { password: await hashPassword(decodedPassword), }; } else { // compare password const match = await matchPassword(decodedPassword, user.password); if (!match) { return res.status(400).send({ err: "invalid request" }); } } // check user state if ( user.state === ACCOUNT_STATE.PENDING_DELETION || user.state === ACCOUNT_STATE.INIT_LOGIN ) { // update user state back to active updateData = { ...updateData, state: ACCOUNT_STATE.ACTIVE, }; User.update(updateData, { where: { user_id: user.user_id, }, }); } rabbitmq.sendEmail( email, "dashboardSecurityInfoNewAccountLogin", user.language, { os: getUserAgentOS(req), email: email, } ); userLogger.info(user.user_id, "User logged in"); // create session saveSession(req, res, user.user_id, user.username, rememberMe); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function ForgotPassword(req: Request, res: Response) { try { let { email, recaptcha } = req.body; // validate request if (!email || !recaptcha || (await isEmailValid(email))) { return res.status(400).send({ err: "invalid request" }); } email = email.toLowerCase(); // validate recaptcha const recaptchaValid = await verifyCaptcha( recaptcha, req.headers["x-real-ip"] as string ); if (!recaptchaValid) { return res.status(400).send({ err: "invalid request" }); } // check if user exists const user = await User.findOne({ where: { email: email, }, attributes: ["user_id", "language"], }); if (!user) { return res.status(400).send({ err: "invalid request" }); } // create email verification const emailVerificationId = newEmailVerificationId(); const state = EMAIL_VERIFICATION_STATE.PENDING_FORGOT_PASSWORD; await EmailVerification.create({ email_verification_id: emailVerificationId, user_id: user.user_id, state: state, }); rabbitmq.sendEmail( email, "dashboardForgotPasswordEmailVerification", user.language, { emailVerificationUrl: getEmailVerificationUrl( state, emailVerificationId ), } ); userLogger.info(user.user_id, "User forgot password"); res.status(200).send({ msg: "success" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function Logout(req: Request, res: Response) { try { const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } userLogger.info(session.user_id, "User logged out"); await session.destroy(); res.status(200).send({ msg: "logout successful" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function GetUser(req: Request, res: Response) { try { const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, attributes: [ "user_id", "username", "store_id", "language", "analytics_enabled", "payment_plan", "created_at", ], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } userLogger.info(session.user_id, "GetUser"); const stores = await Store.findAll({ where: { owner_user_id: user.user_id, }, attributes: ["store_id", "name"], }); // send user data let respData = { user: { user_id: user.user_id, username: user.username, //store_id: user.store_id, language: user.language, analytics_enabled: user.analytics_enabled, payment_plan: user.payment_plan, }, stores: stores, // only temporary until we have a proper permissions system permissions: [] as string[], }; // if user is not a store master, then check if user is a worker if (!stores || stores.length === 0) { // user is a worker const store = await Store.findOne({ where: { store_id: user.store_id, }, attributes: ["store_id", "name"], }); if (!store) { return res.status(401).send({ err: "unauthorized" }); } stores.push(store); respData.stores = stores; respData.permissions.push("calendar"); } else { // user is a store owner respData.permissions.push( "settings", "employees", "services", "calendar" ); // calc account plan expiry by created_at + demo days const accountPlanExpiry = new Date(user.created_at); accountPlanExpiry.setDate( accountPlanExpiry.getDate() + ACCOUNT_DEMO_DAYS ); respData.user = { ...respData.user, account_plan_expiry: accountPlanExpiry, } as { user_id: string; username: string; language: string; analytics_enabled: boolean; account_plan_expiry: Date; payment_plan: number; }; } // update user session last_used Session.update( { last_used: new Date(), }, { where: { session_id: session.session_id, }, } ); res.status(200).send(respData); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function IsEmailAvailable(req: Request, res: Response) { try { let { email } = req.body; // validate request if (!email) { return res.status(400).send({ err: "invalid request" }); } email = email.toLowerCase(); if (!(await isEmailValid(email))) { return res.status(400).send({ err: "invalid request" }); } // check if user exists const user = await User.findOne({ where: { email: email, }, }); if (user) { return res.status(400).send({ err: "invalid request" }); } res.status(200).send({ msg: "email available" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function GetUserProfileSettings(req: Request, res: Response) { try { const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, attributes: ["language", "analytics_enabled", "username", "email"], }); userLogger.info(session.user_id, "GetUserProfileSettings"); res.status(200).json(user); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function UpdateUserProfileSettings(req: Request, res: Response) { try { const { language, analyticsEnabled, username } = req.body; if (!language && analyticsEnabled === undefined && !username) { return res.status(400).send({ err: "invalid request" }); } const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } if (language) { user.language = language; } if (analyticsEnabled !== undefined) { user.analytics_enabled = analyticsEnabled; } if (username) { if (!isUsernameValid(username)) { return res.status(400).send({ err: "invalid request" }); } user.username = username; } await user.save(); userLogger.info( session.user_id, `User updated profile setting to username: ${username} language: ${language} analyticsEnabled: ${analyticsEnabled}` ); res.status(200).send({ msg: "user profile settings updated" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function UpdateUserProfilePassword(req: Request, res: Response) { try { const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword) { return res.status(400).send({ err: "invalid request" }); } const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, attributes: ["password"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const decodedCurrentPassword = decodeBase64(currentPassword); const match = await matchPassword(decodedCurrentPassword, user.password); if (!match) { return res.status(400).send({ err: "invalid request" }); } const decodedPassword = decodeBase64(newPassword); if (!isPasswordValid(decodedPassword)) { return res.status(400).send({ err: "invalid request" }); } const hashedPassword = await hashPassword(decodedPassword); // update user password await User.update( { password: hashedPassword, }, { where: { user_id: session.user_id, }, } ); // delete all sessions of this user by deleting all sessions with this user_id await Session.destroy({ where: { user_id: session.user_id, }, }); userLogger.info(session.user_id, "User updated password"); res.status(200).send({ msg: "user password updated" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function GetUserProfileSessions(req: Request, res: Response) { try { const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const sessions = await Session.findAll({ where: { user_id: session.user_id, }, attributes: ["session_id", "id", "browser", "os", "last_used"], }); // set last_used to now if session is the user's current session let currentSession = sessions.find( (sess) => sess.session_id === session.session_id )?.id; // remove session_id from sessions for security reasons const sessionsList = sessions.map((sess) => { return { id: sess.id, browser: sess.browser, os: sess.os, last_used: sess.last_used, }; }); userLogger.info(session.user_id, "GetUserProfileSessions"); res.status(200).json({ sessions: sessionsList, currentSession: currentSession, }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function DeleteUserProfileSession(req: Request, res: Response) { try { const { id } = req.params; if (!id) { return res.status(400).send({ err: "invalid request" }); } const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } await Session.destroy({ where: { id: id, user_id: session.user_id, }, }); userLogger.info(session.user_id, `User deleted session: ${id}`); res.status(200).send({ msg: "session deleted" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function DeleteUserProfile(req: Request, res: Response) { try { const { reason, feedback, password } = req.body; if (!reason || !feedback || !password) { return res.status(400).send({ err: "invalid request" }); } const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, attributes: ["password"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const decodedPassword = decodeBase64(password); const match = await matchPassword(decodedPassword, user.password); if (!match) { return res.status(400).send({ err: "invalid request" }); } // set user state to pending deletion await User.update( { state: ACCOUNT_STATE.PENDING_DELETION, }, { where: { user_id: session.user_id, }, } ); // delete all sessions of this user by deleting all sessions with this user_id await Session.destroy({ where: { user_id: session.user_id, }, }); // send feedback Feedback.create({ feedback_id: newFeedbackId(), user_id: session.user_id, feedback: `${reason} - ${feedback}`, }); userLogger.info(session.user_id, "User deleted profile"); res.status(200).send({ msg: "user deleted" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function ExportUserAccount(req: Request, res: Response) { try { const { email, password } = req.body; if ( !email || !password || !(await isEmailValid(email)) || !isPasswordValid(password) ) { return res.status(400).send({ err: "invalid request" }); } const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const decodedPassword = decodeBase64(password); const match = await matchPassword(decodedPassword, user.password); if (!match) { return res.status(400).send({ err: "invalid request" }); } (async () => { try { // create json file with user data const exportData = { user: { user_id: user.user_id, username: user.username, email: user.email, calendar_max_future_booking_days: user.calendar_max_future_booking_days, calendar_min_earliest_booking_time: user.calendar_min_earliest_booking_time, calendar_using_primary_calendar: user.calendar_using_primary_calendar, language: user.language, analytics_enabled: user.analytics_enabled, }, }; const accountExportId = newAccountExportId(); fs.writeJson( `./user-profile-exports/${accountExportId}.json`, exportData ); // send email with file rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", { accountExportDownloadUrl: `${ process.env.ACCOUNT_EXPORT_URL as string }${accountExportId}`, }); userLogger.info( session.user_id, "User account exported and sent via email" ); } catch (error) { logger.error(error); } })(); userLogger.info(session.user_id, "User requested account export"); res.status(200).json({}); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function GetExportedUserAccount(req: Request, res: Response) { try { const { id } = req.params; if (!id) { return res.status(400).send({ err: "invalid request" }); } const file = `./user-profile-exports/${id}.json`; res.download(file, (err) => { if (err) { res.status(500).send({ err: "invalid request" }); } }); // delete file after download fs.remove(file); logger.info(`User account export downloaded: ${id}`); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function VerifyEmail(req: Request, res: Response) { try { const { state, emailVerificationId } = req.params; if (state === undefined || !emailVerificationId) { return res.status(400).send({ err: "invalid request" }); } const emailVerification = await EmailVerification.findOne({ where: { email_verification_id: emailVerificationId, }, }); if (!emailVerification) { return res.status(400).send({ err: "invalid request" }); } if ( emailVerification.state === EMAIL_VERIFICATION_STATE.PENDING_EMAIL_VERIFICATION ) { await User.update( { state: ACCOUNT_STATE.ACTIVE, }, { where: { user_id: emailVerification.user_id, }, } ); await EmailVerification.destroy({ where: { email_verification_id: emailVerificationId, }, }); userLogger.info(emailVerification.user_id, "User verified email"); res.status(200).send({ msg: "email verified" }); return; } else if ( emailVerification.state === EMAIL_VERIFICATION_STATE.PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION ) { const { password } = req.body; if (!password) { return res.status(200).send({ status: "actionRequired" }); } const user = await User.findOne({ where: { user_id: emailVerification.user_id, }, attributes: ["password"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const decodedPassword = decodeBase64(password); const match = await matchPassword(decodedPassword, user.password); if (!match) { return res.status(400).send({ err: "invalid request" }); } // update user email const userPendingEmailChange = await UserPendingEmailChange.findOne({ where: { email_verification_id: emailVerificationId, }, attributes: ["new_email"], }); if (!userPendingEmailChange) { return res.status(400).send({ err: "invalid request" }); } // update user email await User.update( { email: userPendingEmailChange.new_email, }, { where: { user_id: emailVerification.user_id, }, } ); // delete email verification and user pending email change await EmailVerification.destroy({ where: { email_verification_id: emailVerificationId, }, }); await UserPendingEmailChange.destroy({ where: { email_verification_id: emailVerificationId, }, }); // delete all sessions of this user by deleting all sessions with this user_id await Session.destroy({ where: { user_id: emailVerification.user_id, }, }); userLogger.info(emailVerification.user_id, "User verified email change"); res.status(200).send({ msg: "email changed" }); return; } else if ( emailVerification.state === EMAIL_VERIFICATION_STATE.PENDING_FORGOT_PASSWORD ) { const { password } = req.body; if (!password) { return res.status(200).send({ status: "actionRequired" }); } const user = await User.findOne({ where: { user_id: emailVerification.user_id, }, attributes: ["password"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const decodedPassword = decodeBase64(password); const hashedPassword = await hashPassword(decodedPassword); // update user password await User.update( { password: hashedPassword, }, { where: { user_id: emailVerification.user_id, }, } ); // delete email verification await EmailVerification.destroy({ where: { email_verification_id: emailVerificationId, }, }); // delete all sessions of this user by deleting all sessions with this user_id await Session.destroy({ where: { user_id: emailVerification.user_id, }, }); userLogger.info( emailVerification.user_id, "User verified forgot password" ); res.status(200).send({ msg: "password changed" }); return; } res.status(400).send({ err: "invalid request" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } } export async function UpdateUserProfileEmail(req: Request, res: Response) { try { let { email } = req.body; if (!email || !(await isEmailValid(email))) { return res.status(400).send({ err: "invalid request" }); } email = email.toLowerCase(); const session = await getUserSession(req); if (!session) { return res.status(401).send({ err: "unauthorized" }); } const user = await User.findOne({ where: { user_id: session.user_id, }, attributes: ["language"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } const emailVerificationId = newEmailVerificationId(); const state = EMAIL_VERIFICATION_STATE.PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION; await EmailVerification.create({ email_verification_id: emailVerificationId, user_id: session.user_id, state: state, }); await UserPendingEmailChange.create({ email_verification_id: emailVerificationId, user_id: session.user_id, new_email: email, }); rabbitmq.sendEmail( email, "dashboardUserProfileChangeToNewEmailVerification", user.language, { emailVerificationUrl: getEmailVerificationUrl( state, emailVerificationId ), } ); userLogger.info( session.user_id, `Userupdated email to new email: ${email}` ); res.status(200).send({ msg: "user email updated" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); } }