diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a099ff0..b00d2f5 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -12,7 +12,8 @@ import { isUsernameValid, } from "../validator/validator"; import { - ACCOUNT_DEMO_DAYS, + ACCOUNT_EXPORT_EXPIRY, + ACCOUNT_EXPORT_URL, ACCOUNT_STATE, CALENDAR_MAX_SERVICE_DURATION, CALENDAR_MIN_EARLIEST_BOOKING_TIME, @@ -53,6 +54,7 @@ import { isTerminPlanerGoogleCalendarConnected, terminPlanerRequest, } from "../utils/terminPlaner"; +import UserAccountExport from "../models/userAccountExport"; export async function SignUp(req: Request, res: Response) { try { @@ -505,26 +507,6 @@ export async function GetUser(req: Request, res: Response) { "calendar", "paymentPlan" ); - - // 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 @@ -1035,6 +1017,8 @@ export async function DeleteUserProfile(req: Request, res: Response) { } } +// 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; @@ -1042,15 +1026,17 @@ export async function ExportUserAccount(req: Request, res: Response) { if ( !email || !password || - !(await isEmailValid(email)) || + !(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" }); } @@ -1058,9 +1044,11 @@ export async function ExportUserAccount(req: Request, res: Response) { where: { user_id: session.user_id, }, + attributes: ["password"], }); if (!user) { + logger.error("export user account error", "unauthorized"); return res.status(401).send({ err: "unauthorized" }); } @@ -1069,13 +1057,23 @@ export async function ExportUserAccount(req: Request, res: Response) { 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, @@ -1092,29 +1090,40 @@ export async function ExportUserAccount(req: Request, res: Response) { }, }; - const accountExportId = newAccountExportId(); - + fs.writeJson( `./user-profile-exports/${accountExportId}.json`, exportData - ); + ); + + const accountExportId = newAccountExportId(); // send email with file rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", { - accountExportDownloadUrl: `${ - process.env.ACCOUNT_EXPORT_URL as string - }${accountExportId}`, + 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"); @@ -1125,27 +1134,114 @@ export async function ExportUserAccount(req: Request, res: Response) { } } +// 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" }); } - const file = `./user-profile-exports/${id}.json`; + // check if user account export exists in database - res.download(file, (err) => { - if (err) { - res.status(500).send({ err: "invalid request" }); - } + const userAccountExport = await UserAccountExport.findOne({ + where: { + export_id: id, + }, }); - // delete file after download + if (!userAccountExport) { + logger.warn( + "GetExportedUserAccount error", + "invalid request (user account export not found)" + ); + return res.status(400).send({ err: "invalid request" }); + } - fs.remove(file); + if ( + userAccountExport.created_at < + new Date(Date.now() - ACCOUNT_EXPORT_EXPIRY) + ) { + logger.warn( + "GetExportedUserAccount error", + "invalid request (user account export expired)" + ); - logger.info(`User account export downloaded: ${id}`); + // 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" }); @@ -1420,36 +1516,3 @@ export async function UpdateUserProfileEmail(req: Request, res: Response) { res.status(500).send({ err: "invalid request" }); } } - -export async function GetDemo(req: Request, res: Response) { - try { - const demoAccount = await User.findOne({ - where: { - user_id: process.env.DEMO_ACCOUNT_USER_ID as string, - }, - attributes: ["user_id", "email", "language"], - }); - - if (!demoAccount) { - logger.error("get demo error", "demo account not found"); - return res.status(400).send({ err: "invalid request" }); - } - - rabbitmq.sendEmail( - demoAccount.email, - "dashboardSecurityInfoNewAccountLogin", - demoAccount.language, - { - os: getUserAgentOS(req), - email: demoAccount.email, - } - ); - - userLogger.info(demoAccount.user_id, "A demo account was requested"); - - saveSession(req, res, demoAccount.user_id, true); - } catch (error) { - logger.error("get demo error", error as string); - res.status(500).send({ err: "invalid request" }); - } -} diff --git a/src/models/index.ts b/src/models/index.ts index c173945..25d4828 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -8,16 +8,18 @@ import StoreServiceActivityUsers from "./storeServiceActivityUsers"; import User from "./user"; import UserPendingEmailChange from "./userPendingEmailChange"; import UserPendingPayment from "./userPendingPayment"; +import UserAccountExport from "./userAccountExport"; function syncModels() { EmailVerification.sync(); - User.sync(); + Feedback.sync(); Session.sync(); Store.sync(); StoreService.sync(); StoreServiceActivity.sync(); StoreServiceActivityUsers.sync(); - Feedback.sync(); + User.sync(); + UserAccountExport.sync(); UserPendingEmailChange.sync(); UserPendingPayment.sync(); diff --git a/src/models/user.ts b/src/models/user.ts index a2251b4..2e543e7 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -94,7 +94,7 @@ User.init( allowNull: true, }, google_account_picture: { - type: DataTypes.STRING, + type: DataTypes.STRING(1050), allowNull: true, // varchar(1050) needs to be set manually }, analytics_enabled: { diff --git a/src/models/userAccountExport.ts b/src/models/userAccountExport.ts new file mode 100644 index 0000000..4ccf70a --- /dev/null +++ b/src/models/userAccountExport.ts @@ -0,0 +1,39 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; +import { ACCOUNT_EXPORT_ID_LENGTH } from "../utils/constants"; + +interface UserAccountExportAttributes { + export_id: string; + user_id: string; +} + +class UserAccountExport + extends Model + implements UserAccountExportAttributes +{ + declare export_id: string; + declare user_id: string; + declare created_at: Date; +} + +UserAccountExport.init( + { + export_id: { + primaryKey: true, + type: DataTypes.STRING(ACCOUNT_EXPORT_ID_LENGTH * 2), + allowNull: false, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + modelName: "user_account_export", + createdAt: "created_at", + updatedAt: "updated_at", + } +); + +export default UserAccountExport; diff --git a/src/models/userPendingPayment.ts b/src/models/userPendingPayment.ts index 714828d..a9a233d 100644 --- a/src/models/userPendingPayment.ts +++ b/src/models/userPendingPayment.ts @@ -28,7 +28,7 @@ UserPendingPayment.init( allowNull: false, }, payment_session_url: { - type: DataTypes.STRING, // varchar(1000) needs to be set manually + type: DataTypes.STRING(1000), // varchar(1000) needs to be set manually allowNull: true, }, }, diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 3750de6..a95730f 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -52,6 +52,5 @@ router.post( ); router.get("/profile/export/:id", userController.GetExportedUserAccount); router.post("/verify/:state/:emailVerificationId", userController.VerifyEmail); -router.get("/demo", userController.GetDemo); export default router; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6018203..1906005 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,8 +1,8 @@ -import Stripe from "stripe"; - export const DEFAULT_SESSION_EXPIRY = 365 * 24 * 60 * 60 * 1000; // 365 days export const SESSION_EXPIRY_NOT_REMEMBER_ME = 60 * 60 * 1000; // 1 hour export const USER_SESSION_LENGTH = 32; +export const ACCOUNT_EXPORT_ID_LENGTH = 64; +export const ACCOUNT_EXPORT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours export const USERNAME_MIN_LENGTH = 3; export const USERNAME_MAX_LENGTH = 20; @@ -71,10 +71,6 @@ export const Roles = { Worker: "worker", }; -export const ACCOUNT_DEMO_DAYS = Number( - process.env.CONSTANTS_ACCOUNT_DEMO_DAYS -); // how many days a demo account is valid until payment is required or account will be deleted - export enum EMAIL_VERIFICATION_STATE { PENDING_EMAIL_VERIFICATION = 0, // account is created but email is not verified yet PENDING_USER_PROFILE_EMAIL_CHANGE_VERIFICATION = 1, // user wants to change email, new email is not verified yet @@ -85,7 +81,7 @@ export const DASHBOARD_URL = process.env.DASHBOARD_URL as string; export const GOOGLE_CALLBACK_URL = `${process.env.DASHBOARD_API_URL}/v1/calendar/auth/google/callback`; export const PASSPORT_FAILURE_REDIRECT_URL = `${DASHBOARD_URL}/store/calendar/auth/failed`; export const PASSPORT_SUCCESS_REDIRECT_URL = `${DASHBOARD_URL}/store/calendar/auth/finish`; -export const ACCOUNT_EXPORT_URL = `${DASHBOARD_URL}/v1/user/profile/export/`; +export const ACCOUNT_EXPORT_URL = `${DASHBOARD_URL}/api/v1/user/profile/export/`; export const TERMIN_PLANNER_URL = process.env.TERMIN_PLANNER_URL; export enum PAYMENT_PLAN { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 828ff59..156b952 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from "uuid"; import { Request, Response } from "express"; import Session from "../models/session"; import { + ACCOUNT_EXPORT_ID_LENGTH, DASHBOARD_URL, DEFAULT_SESSION_EXPIRY, HEADER_X_AUTHORIZATION, @@ -57,7 +58,7 @@ export function newFeedbackId() { } export function newAccountExportId() { - return uuidv4(); + return crypto.randomBytes(ACCOUNT_EXPORT_ID_LENGTH).toString("hex"); } export function getUserAgentOS(req: Request) {