account export
parent
7f0369b0d5
commit
cbc916088c
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue