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,
} 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,9 +1057,19 @@ 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
@ -1092,19 +1090,18 @@ 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(
@ -1115,6 +1112,18 @@ export async function ExportUserAccount(req: Request, res: Response) {
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" });
}
}

View File

@ -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();

View File

@ -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: {

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,
},
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,
},
},

View File

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

View File

@ -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 {

View File

@ -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) {