customer-dashboard-api/src/controllers/userController.ts

1196 lines
28 KiB
TypeScript

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" });
}
}