1561 lines
39 KiB
TypeScript
1561 lines
39 KiB
TypeScript
import { Request, Response } from "express";
|
|
import logger, { userLogger } from "../logger/logger";
|
|
import User from "../models/user";
|
|
import {
|
|
isCompanyAddressValid,
|
|
isCompanyNameValid,
|
|
isEmailValid,
|
|
isFeedbackValid,
|
|
isLanguageCodeValid,
|
|
isPasswordValid,
|
|
isPaymentPlanValid,
|
|
isUsernameValid,
|
|
} from "../validator/validator";
|
|
import {
|
|
ACCOUNT_EXPORT_EXPIRY,
|
|
ACCOUNT_EXPORT_URL,
|
|
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,
|
|
stripe,
|
|
} 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";
|
|
import StoreServiceActivity from "../models/storeServiceActivity";
|
|
import StoreService from "../models/storeService";
|
|
import StoreServiceActivityUsers from "../models/storeServiceActivityUsers";
|
|
import {
|
|
isTerminPlanerGoogleCalendarConnected,
|
|
terminPlanerRequest,
|
|
} from "../utils/terminPlaner";
|
|
import UserAccountExport from "../models/userAccountExport";
|
|
import { telegramNotification } from "../utils/adminDashboard";
|
|
|
|
export async function SignUp(req: Request, res: Response) {
|
|
try {
|
|
let {
|
|
companyName,
|
|
companyAddress,
|
|
username,
|
|
email,
|
|
password,
|
|
language,
|
|
recaptcha,
|
|
} = req.body;
|
|
|
|
// validate request
|
|
|
|
if (
|
|
!companyName ||
|
|
!username ||
|
|
!email ||
|
|
!password ||
|
|
!language ||
|
|
!isCompanyNameValid(companyName) ||
|
|
!isUsernameValid(username) ||
|
|
!(await isEmailValid(email)) ||
|
|
!isLanguageCodeValid(language) ||
|
|
!recaptcha ||
|
|
!isCompanyAddressValid(companyAddress)
|
|
) {
|
|
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: companyAddress,
|
|
});
|
|
|
|
// 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.INIT_PAYMENT,
|
|
payment_plan: PAYMENT_PLAN.DEMO,
|
|
// payment_plan_status: PAYMENT_PLAN_STATUS.TRAILING,
|
|
});
|
|
/*
|
|
const checkoutSessionUrl = await CreateCheckoutSession(
|
|
await getPriceId(paymentPlan, paymentInterval),
|
|
userId
|
|
);
|
|
|
|
if (!checkoutSessionUrl) {
|
|
return res.status(500).send({ err: "invalid request" });
|
|
} */
|
|
|
|
logger.info(
|
|
`new user signed up: user_id: ${userId} email: ${email} language: ${language} company: ${companyName} username: ${username}`
|
|
);
|
|
|
|
res.status(200).send({ msg: "success" });
|
|
|
|
telegramNotification(
|
|
1,
|
|
`New user signed up: user_id: ${userId} email: ${email} language: ${language} company: ${companyName} username: ${username}`
|
|
);
|
|
|
|
/*
|
|
saveSession(
|
|
req,
|
|
res,
|
|
userId,
|
|
true
|
|
, {
|
|
redirectUrl: checkoutSessionUrl,
|
|
}
|
|
);*/
|
|
} catch (error) {
|
|
logger.error("signup error", error as string);
|
|
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) {
|
|
// user has signed up but not completed payment
|
|
// happens when user closed stripe checkout before completing payment
|
|
// and then tries to login
|
|
if (user.state === ACCOUNT_STATE.INIT_PAYMENT) {
|
|
const userPendingPayment = await UserPendingPayment.findOne({
|
|
where: {
|
|
user_id: user.user_id,
|
|
},
|
|
});
|
|
|
|
if (
|
|
userPendingPayment &&
|
|
userPendingPayment.payment_session_url !== ""
|
|
) {
|
|
userLogger.info(
|
|
user.user_id,
|
|
"Sent email to complete init payment from signup"
|
|
);
|
|
|
|
rabbitmq.sendEmail(
|
|
email,
|
|
"dashboardSignUpUrlToCompletePayment",
|
|
user.language,
|
|
{
|
|
paymentSessionUrl: userPendingPayment.payment_session_url,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (user.state === ACCOUNT_STATE.ACTIVE) {
|
|
// create session
|
|
saveSession(req, res, user.user_id, rememberMe);
|
|
|
|
rabbitmq.sendEmail(
|
|
email,
|
|
"dashboardSecurityInfoNewAccountLogin",
|
|
user.language,
|
|
{
|
|
os: getUserAgentOS(req),
|
|
email: email,
|
|
}
|
|
);
|
|
|
|
userLogger.info(user.user_id, "User logged in");
|
|
} else {
|
|
res.status(200).send({ state: user.state });
|
|
|
|
userLogger.info(
|
|
user.user_id,
|
|
"User logged in, but account state is not active"
|
|
);
|
|
}
|
|
|
|
telegramNotification(
|
|
1,
|
|
`User logged in: user_id: ${user.user_id} email: ${email} state: ${user.state}`
|
|
);
|
|
} catch (error) {
|
|
logger.error("login error", error as string);
|
|
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, false))) {
|
|
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) {
|
|
// sending success to prevent email enumeration
|
|
return res.status(200).send({ msg: "success" });
|
|
}
|
|
|
|
// 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("forgot password error", error as string);
|
|
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("logout error", error as string);
|
|
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",
|
|
"payment_plan_status",
|
|
"payment_plan_trial_end",
|
|
"payment_plan_canceled_at",
|
|
"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,
|
|
// payment_plan_status: user.payment_plan_status,
|
|
payment_plan_trial_end: user.payment_plan_trial_end,
|
|
payment_plan_canceled_at: user.payment_plan_canceled_at,
|
|
},
|
|
stores: stores,
|
|
// only temporary until we have a proper permissions system
|
|
permissions: [] as string[],
|
|
paymentPlanSettings: PAYMENT_PLAN_SETTINGS[user.payment_plan],
|
|
};
|
|
|
|
// 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",
|
|
"paymentPlan"
|
|
);
|
|
}
|
|
|
|
// send error if user has invalid payment plan
|
|
if (!isPaymentPlanValid(user.payment_plan)) {
|
|
userLogger.error(
|
|
session.user_id,
|
|
`User has invalid payment plan: ${
|
|
user.payment_plan
|
|
}. Possible highest payment plan is ${PAYMENT_PLAN_SETTINGS.length - 1}`
|
|
);
|
|
}
|
|
|
|
// 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("get user error", error as string);
|
|
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("is email available error", error as string);
|
|
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).send(user);
|
|
} catch (error) {
|
|
logger.error("get user profile settings error", error as string);
|
|
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("update user profile settings error", error as string);
|
|
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("update user profile password error", error as string);
|
|
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).send({
|
|
sessions: sessionsList,
|
|
currentSession: currentSession,
|
|
});
|
|
} catch (error) {
|
|
logger.error("get user profile sessions error", error as string);
|
|
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("delete user profile session error", error as string);
|
|
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 || !isFeedbackValid(feedback)) {
|
|
console.log("invalid request");
|
|
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", "stripe_customer_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" });
|
|
}
|
|
|
|
// delete stripe subscription
|
|
|
|
if (user.stripe_customer_id) {
|
|
const subscription = await stripe.subscriptions.list({
|
|
customer: user.stripe_customer_id,
|
|
});
|
|
|
|
if (subscription.data.length > 0) {
|
|
const subscriptionId = subscription.data[0].id;
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Canceling stripe subscription id:",
|
|
subscriptionId
|
|
);
|
|
|
|
await stripe.subscriptions.cancel(subscriptionId);
|
|
}
|
|
|
|
await stripe.customers.del(user.stripe_customer_id);
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted stripe customer id:",
|
|
user.stripe_customer_id
|
|
);
|
|
}
|
|
|
|
// delete store if user is a store owner
|
|
|
|
const store = await Store.findOne({
|
|
where: {
|
|
owner_user_id: session.user_id,
|
|
},
|
|
attributes: ["store_id"],
|
|
});
|
|
|
|
// user is a store owner
|
|
if (store) {
|
|
await Store.destroy({
|
|
where: {
|
|
store_id: store.store_id,
|
|
},
|
|
});
|
|
|
|
userLogger.info(session.user_id, "Deleted store id:", store.store_id);
|
|
|
|
// delete all employees of this store
|
|
|
|
const employees = await User.findAll({
|
|
where: {
|
|
store_id: store.store_id,
|
|
},
|
|
attributes: ["user_id"],
|
|
});
|
|
|
|
if (employees) {
|
|
for (const employee of employees) {
|
|
const googleCalendarConnected =
|
|
await isTerminPlanerGoogleCalendarConnected(employee.user_id);
|
|
|
|
if (googleCalendarConnected) {
|
|
await terminPlanerRequest("/api/v1/removeGoogleAccount", "POST", {
|
|
userId: employee.user_id,
|
|
});
|
|
}
|
|
|
|
await Session.destroy({
|
|
where: {
|
|
user_id: employee.user_id,
|
|
},
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted store employee id:",
|
|
employee.user_id
|
|
);
|
|
}
|
|
|
|
await User.destroy({
|
|
where: {
|
|
user_id: employees.map((employee) => employee.user_id),
|
|
},
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted store employees, count:",
|
|
employees.length.toString()
|
|
);
|
|
}
|
|
|
|
// delete all store services, store service activities, and store service activity users
|
|
|
|
const storeServices = await StoreService.findAll({
|
|
where: {
|
|
store_id: store.store_id,
|
|
},
|
|
attributes: ["service_id"],
|
|
});
|
|
|
|
if (storeServices) {
|
|
const storeServiceActivities = await StoreServiceActivity.findAll({
|
|
where: {
|
|
service_id: storeServices.map((service) => service.service_id),
|
|
},
|
|
});
|
|
|
|
if (storeServiceActivities) {
|
|
const storeServiceActivityUsers =
|
|
await StoreServiceActivityUsers.findAll({
|
|
where: {
|
|
activity_id: storeServiceActivities.map(
|
|
(activity) => activity.activity_id
|
|
),
|
|
},
|
|
});
|
|
|
|
if (storeServiceActivityUsers) {
|
|
await StoreServiceActivityUsers.destroy({
|
|
where: {
|
|
activity_id: storeServiceActivityUsers.map(
|
|
(activity) => activity.activity_id
|
|
),
|
|
},
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted store service activity users, count:",
|
|
storeServiceActivityUsers.length.toString()
|
|
);
|
|
}
|
|
|
|
await StoreServiceActivity.destroy({
|
|
where: {
|
|
activity_id: storeServiceActivities.map(
|
|
(activity) => activity.activity_id
|
|
),
|
|
},
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted store service activities, count:",
|
|
storeServiceActivities.length.toString()
|
|
);
|
|
|
|
await StoreService.destroy({
|
|
where: {
|
|
service_id: storeServices.map((service) => service.service_id),
|
|
},
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"Deleted store services, count:",
|
|
storeServices.length.toString()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// destroy user
|
|
|
|
await User.destroy({
|
|
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, "Deleted user account");
|
|
|
|
res.status(200).send({ msg: "user deleted" });
|
|
} catch (error) {
|
|
logger.error("delete user profile error", error as string);
|
|
res.status(500).send({ err: "invalid request" });
|
|
}
|
|
}
|
|
|
|
// This will create a new user account export and send the user an email with a link to download the exported account data
|
|
// There is no export file created, the file will be created when the user clicks the link in the email
|
|
export async function ExportUserAccount(req: Request, res: Response) {
|
|
try {
|
|
const { email, password } = req.body;
|
|
|
|
if (
|
|
!email ||
|
|
!password ||
|
|
!(await isEmailValid(email, false)) ||
|
|
!isPasswordValid(password)
|
|
) {
|
|
logger.error("export user account error", "invalid request");
|
|
return res.status(400).send({ err: "invalid request" });
|
|
}
|
|
|
|
const session = await getUserSession(req);
|
|
|
|
if (!session) {
|
|
logger.error("export user account error", "unauthorized");
|
|
return res.status(401).send({ err: "unauthorized" });
|
|
}
|
|
|
|
const user = await User.findOne({
|
|
where: {
|
|
user_id: session.user_id,
|
|
},
|
|
attributes: ["password"],
|
|
});
|
|
|
|
if (!user) {
|
|
logger.error("export user account error", "unauthorized");
|
|
return res.status(401).send({ err: "unauthorized" });
|
|
}
|
|
|
|
const decodedPassword = decodeBase64(password);
|
|
|
|
const match = await matchPassword(decodedPassword, user.password);
|
|
|
|
if (!match) {
|
|
logger.error("export user account error", "invalid password");
|
|
return res.status(400).send({ err: "invalid request" });
|
|
}
|
|
|
|
const accountExportId = newAccountExportId();
|
|
|
|
await UserAccountExport.create({
|
|
export_id: accountExportId,
|
|
user_id: session.user_id,
|
|
});
|
|
|
|
/*
|
|
|
|
(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,
|
|
},
|
|
};
|
|
|
|
|
|
fs.writeJson(
|
|
`./user-profile-exports/${accountExportId}.json`,
|
|
exportData
|
|
);
|
|
|
|
const accountExportId = newAccountExportId();
|
|
|
|
// send email with file
|
|
|
|
rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", {
|
|
accountExportDownloadUrl: `${ACCOUNT_EXPORT_URL}${accountExportId}`,
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"User account exported and sent via email"
|
|
);
|
|
} catch (error) {
|
|
logger.error("export user account error", error as string);
|
|
}
|
|
})();
|
|
*/
|
|
|
|
// send email for the user to download his exported account data
|
|
|
|
rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", {
|
|
accountExportDownloadUrl: `${ACCOUNT_EXPORT_URL}${accountExportId}`,
|
|
});
|
|
|
|
userLogger.info(
|
|
session.user_id,
|
|
"User account exported and sent via email"
|
|
);
|
|
|
|
userLogger.info(session.user_id, "User requested account export");
|
|
|
|
res.status(200).send({});
|
|
} catch (error) {
|
|
logger.error("export user account error", error as string);
|
|
res.status(500).send({ err: "invalid request" });
|
|
}
|
|
}
|
|
|
|
// This will create a new user account export file and send it to the user and then delete the file
|
|
export async function GetExportedUserAccount(req: Request, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
if (!id) {
|
|
logger.warn(
|
|
"GetExportedUserAccount error",
|
|
"invalid request (id missing)"
|
|
);
|
|
return res.status(400).send({ err: "invalid request" });
|
|
}
|
|
|
|
// check if user account export exists in database
|
|
|
|
const userAccountExport = await UserAccountExport.findOne({
|
|
where: {
|
|
export_id: id,
|
|
},
|
|
});
|
|
|
|
if (!userAccountExport) {
|
|
logger.warn(
|
|
"GetExportedUserAccount error",
|
|
"invalid request (user account export not found)"
|
|
);
|
|
return res.status(400).send({ err: "invalid request" });
|
|
}
|
|
|
|
if (
|
|
userAccountExport.created_at <
|
|
new Date(Date.now() - ACCOUNT_EXPORT_EXPIRY)
|
|
) {
|
|
logger.warn(
|
|
"GetExportedUserAccount error",
|
|
"invalid request (user account export expired)"
|
|
);
|
|
|
|
// delete expired user account export
|
|
|
|
await UserAccountExport.destroy({
|
|
where: {
|
|
export_id: id,
|
|
},
|
|
});
|
|
|
|
return res.status(400).send({ err: "invalid request" });
|
|
}
|
|
|
|
const user = await User.findOne({
|
|
where: {
|
|
user_id: userAccountExport.user_id,
|
|
},
|
|
attributes: [
|
|
"user_id",
|
|
"username",
|
|
"email",
|
|
"calendar_max_future_booking_days",
|
|
"calendar_min_earliest_booking_time",
|
|
"calendar_using_primary_calendar",
|
|
"language",
|
|
"analytics_enabled",
|
|
],
|
|
});
|
|
|
|
if (!user) {
|
|
logger.error("export user account error", "unauthorized");
|
|
return res.status(401).send({ err: "unauthorized" });
|
|
}
|
|
|
|
const filePath = `./user-profile-exports/${id}.json`;
|
|
|
|
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,
|
|
},
|
|
};
|
|
|
|
// spaces is for pretty print
|
|
await fs.writeJson(filePath, exportData, { spaces: 2 });
|
|
|
|
res.download(filePath, (err) => {
|
|
if (err) {
|
|
res.status(500).send({ err: "invalid request" });
|
|
} else {
|
|
// delete file after successful download
|
|
fs.remove(filePath)
|
|
.then(() => {
|
|
logger.info(
|
|
`User account export downloaded and file deleted: ${id}`
|
|
);
|
|
})
|
|
.catch((deleteError) => {
|
|
logger.error(
|
|
"Error deleting file after download",
|
|
deleteError as string
|
|
);
|
|
});
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error("get exported user account error", error as string);
|
|
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("verify email error", error as string);
|
|
res.status(500).send({ err: "invalid request" });
|
|
}
|
|
}
|
|
|
|
export async function UpdateUserProfileEmail(req: Request, res: Response) {
|
|
try {
|
|
let { email, password } = req.body;
|
|
|
|
if (!email || !(await isEmailValid(email)) || !password) {
|
|
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", "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" });
|
|
}
|
|
|
|
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("update user profile email error", error as string);
|
|
res.status(500).send({ err: "invalid request" });
|
|
}
|
|
}
|