account export

main
alex 2024-02-19 20:09:23 +01:00
parent 7f0369b0d5
commit cbc916088c
8 changed files with 184 additions and 84 deletions

View File

@ -12,7 +12,8 @@ import {
isUsernameValid, isUsernameValid,
} from "../validator/validator"; } from "../validator/validator";
import { import {
ACCOUNT_DEMO_DAYS, ACCOUNT_EXPORT_EXPIRY,
ACCOUNT_EXPORT_URL,
ACCOUNT_STATE, ACCOUNT_STATE,
CALENDAR_MAX_SERVICE_DURATION, CALENDAR_MAX_SERVICE_DURATION,
CALENDAR_MIN_EARLIEST_BOOKING_TIME, CALENDAR_MIN_EARLIEST_BOOKING_TIME,
@ -53,6 +54,7 @@ import {
isTerminPlanerGoogleCalendarConnected, isTerminPlanerGoogleCalendarConnected,
terminPlanerRequest, terminPlanerRequest,
} from "../utils/terminPlaner"; } from "../utils/terminPlaner";
import UserAccountExport from "../models/userAccountExport";
export async function SignUp(req: Request, res: Response) { export async function SignUp(req: Request, res: Response) {
try { try {
@ -505,26 +507,6 @@ export async function GetUser(req: Request, res: Response) {
"calendar", "calendar",
"paymentPlan" "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 // 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) { export async function ExportUserAccount(req: Request, res: Response) {
try { try {
const { email, password } = req.body; const { email, password } = req.body;
@ -1042,15 +1026,17 @@ export async function ExportUserAccount(req: Request, res: Response) {
if ( if (
!email || !email ||
!password || !password ||
!(await isEmailValid(email)) || !(await isEmailValid(email, false)) ||
!isPasswordValid(password) !isPasswordValid(password)
) { ) {
logger.error("export user account error", "invalid request");
return res.status(400).send({ err: "invalid request" }); return res.status(400).send({ err: "invalid request" });
} }
const session = await getUserSession(req); const session = await getUserSession(req);
if (!session) { if (!session) {
logger.error("export user account error", "unauthorized");
return res.status(401).send({ err: "unauthorized" }); return res.status(401).send({ err: "unauthorized" });
} }
@ -1058,9 +1044,11 @@ export async function ExportUserAccount(req: Request, res: Response) {
where: { where: {
user_id: session.user_id, user_id: session.user_id,
}, },
attributes: ["password"],
}); });
if (!user) { if (!user) {
logger.error("export user account error", "unauthorized");
return res.status(401).send({ err: "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); const match = await matchPassword(decodedPassword, user.password);
if (!match) { if (!match) {
logger.error("export user account error", "invalid password");
return res.status(400).send({ err: "invalid request" }); return res.status(400).send({ err: "invalid request" });
} }
const accountExportId = newAccountExportId();
await UserAccountExport.create({
export_id: accountExportId,
user_id: session.user_id,
});
/*
(async () => { (async () => {
try { try {
// create json file with user data // create json file with user data
const exportData = { const exportData = {
user: { user: {
user_id: user.user_id, user_id: user.user_id,
@ -1092,29 +1090,40 @@ export async function ExportUserAccount(req: Request, res: Response) {
}, },
}; };
const accountExportId = newAccountExportId();
fs.writeJson( fs.writeJson(
`./user-profile-exports/${accountExportId}.json`, `./user-profile-exports/${accountExportId}.json`,
exportData exportData
); );
const accountExportId = newAccountExportId();
// send email with file // send email with file
rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", { rabbitmq.sendEmail(email, "dashboardUserAccountExportFinish", "de", {
accountExportDownloadUrl: `${ accountExportDownloadUrl: `${ACCOUNT_EXPORT_URL}${accountExportId}`,
process.env.ACCOUNT_EXPORT_URL as string
}${accountExportId}`,
}); });
userLogger.info( userLogger.info(
session.user_id, session.user_id,
"User account exported and sent via email" "User account exported and sent via email"
); );
} catch (error) { } catch (error) {
logger.error("export user account error", error as string); 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"); 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) { export async function GetExportedUserAccount(req: Request, res: Response) {
try { try {
const { id } = req.params; const { id } = req.params;
if (!id) { if (!id) {
logger.warn(
"GetExportedUserAccount error",
"invalid request (id missing)"
);
return res.status(400).send({ err: "invalid request" }); 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) => { const userAccountExport = await UserAccountExport.findOne({
if (err) { where: {
res.status(500).send({ err: "invalid request" }); 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) { } catch (error) {
logger.error("get exported user account error", error as string); logger.error("get exported user account error", error as string);
res.status(500).send({ err: "invalid request" }); 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" }); 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" });
}
}

View File

@ -8,16 +8,18 @@ import StoreServiceActivityUsers from "./storeServiceActivityUsers";
import User from "./user"; import User from "./user";
import UserPendingEmailChange from "./userPendingEmailChange"; import UserPendingEmailChange from "./userPendingEmailChange";
import UserPendingPayment from "./userPendingPayment"; import UserPendingPayment from "./userPendingPayment";
import UserAccountExport from "./userAccountExport";
function syncModels() { function syncModels() {
EmailVerification.sync(); EmailVerification.sync();
User.sync(); Feedback.sync();
Session.sync(); Session.sync();
Store.sync(); Store.sync();
StoreService.sync(); StoreService.sync();
StoreServiceActivity.sync(); StoreServiceActivity.sync();
StoreServiceActivityUsers.sync(); StoreServiceActivityUsers.sync();
Feedback.sync(); User.sync();
UserAccountExport.sync();
UserPendingEmailChange.sync(); UserPendingEmailChange.sync();
UserPendingPayment.sync(); UserPendingPayment.sync();

View File

@ -94,7 +94,7 @@ User.init(
allowNull: true, allowNull: true,
}, },
google_account_picture: { google_account_picture: {
type: DataTypes.STRING, type: DataTypes.STRING(1050),
allowNull: true, // varchar(1050) needs to be set manually allowNull: true, // varchar(1050) needs to be set manually
}, },
analytics_enabled: { analytics_enabled: {

View File

@ -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<UserAccountExportAttributes>
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;

View File

@ -28,7 +28,7 @@ UserPendingPayment.init(
allowNull: false, allowNull: false,
}, },
payment_session_url: { 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, allowNull: true,
}, },
}, },

View File

@ -52,6 +52,5 @@ router.post(
); );
router.get("/profile/export/:id", userController.GetExportedUserAccount); router.get("/profile/export/:id", userController.GetExportedUserAccount);
router.post("/verify/:state/:emailVerificationId", userController.VerifyEmail); router.post("/verify/:state/:emailVerificationId", userController.VerifyEmail);
router.get("/demo", userController.GetDemo);
export default router; export default router;

View File

@ -1,8 +1,8 @@
import Stripe from "stripe";
export const DEFAULT_SESSION_EXPIRY = 365 * 24 * 60 * 60 * 1000; // 365 days 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 SESSION_EXPIRY_NOT_REMEMBER_ME = 60 * 60 * 1000; // 1 hour
export const USER_SESSION_LENGTH = 32; 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_MIN_LENGTH = 3;
export const USERNAME_MAX_LENGTH = 20; export const USERNAME_MAX_LENGTH = 20;
@ -71,10 +71,6 @@ export const Roles = {
Worker: "worker", 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 { export enum EMAIL_VERIFICATION_STATE {
PENDING_EMAIL_VERIFICATION = 0, // account is created but email is not verified yet 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 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 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_FAILURE_REDIRECT_URL = `${DASHBOARD_URL}/store/calendar/auth/failed`;
export const PASSPORT_SUCCESS_REDIRECT_URL = `${DASHBOARD_URL}/store/calendar/auth/finish`; 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 const TERMIN_PLANNER_URL = process.env.TERMIN_PLANNER_URL;
export enum PAYMENT_PLAN { export enum PAYMENT_PLAN {

View File

@ -4,6 +4,7 @@ import { v4 as uuidv4 } from "uuid";
import { Request, Response } from "express"; import { Request, Response } from "express";
import Session from "../models/session"; import Session from "../models/session";
import { import {
ACCOUNT_EXPORT_ID_LENGTH,
DASHBOARD_URL, DASHBOARD_URL,
DEFAULT_SESSION_EXPIRY, DEFAULT_SESSION_EXPIRY,
HEADER_X_AUTHORIZATION, HEADER_X_AUTHORIZATION,
@ -57,7 +58,7 @@ export function newFeedbackId() {
} }
export function newAccountExportId() { export function newAccountExportId() {
return uuidv4(); return crypto.randomBytes(ACCOUNT_EXPORT_ID_LENGTH).toString("hex");
} }
export function getUserAgentOS(req: Request) { export function getUserAgentOS(req: Request) {