diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 3235462..a0780dc 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -8,6 +8,7 @@ import { isUsernameValid, } from "../validator/validator"; import { + ACCOUNT_STATE, CALENDAR_MAX_FUTURE_BOOKING_DAYS, CALENDAR_MAX_SERVICE_DURATION, CALENDAR_MIN_EARLIEST_BOOKING_TIME, @@ -19,12 +20,14 @@ import { getUserSession, hashPassword, matchPassword, + newFeedbackId, newStoreId, newUserId, saveSession, } from "../utils/utils"; import Store from "../models/store"; import Session from "../models/session"; +import Feedback from "../models/feedback"; export async function SignUp(req: Request, res: Response) { try { @@ -180,6 +183,23 @@ export async function Login(req: Request, res: Response) { return res.status(400).send({ err: "invalid request" }); } + // check user state + + if (user.state === ACCOUNT_STATE.DELETED) { + // update user state back to active + + User.update( + { + state: ACCOUNT_STATE.ACTIVE, + }, + { + where: { + user_id: user.user_id, + }, + } + ); + } + // create session saveSession(req, res, user.user_id, user.username); } catch (error) { @@ -553,3 +573,72 @@ export async function DeleteUserProfileSession(req: Request, res: Response) { 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) { + 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 decodedPassword = decodeBase64(password); + + const match = await matchPassword(decodedPassword, user.password); + + if (!match) { + return res.status(400).send({ err: "invalid request" }); + } + + // set user state to deleted + + await User.update( + { + state: ACCOUNT_STATE.DELETED, + }, + { + 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}`, + }); + + res.status(200).send({ msg: "user deleted" }); + } catch (error) { + logger.error(error); + res.status(500).send({ err: "invalid request" }); + } +} diff --git a/src/models/feedback.ts b/src/models/feedback.ts new file mode 100644 index 0000000..1a456df --- /dev/null +++ b/src/models/feedback.ts @@ -0,0 +1,41 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface FeedbackAttributes { + feedback_id: string; + user_id: string; + feedback: string; +} + +class Feedback extends Model implements FeedbackAttributes { + declare feedback_id: string; + declare user_id: string; + declare feedback: string; +} + +Feedback.init( + { + // Model attributes are defined here + feedback_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + }, + feedback: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "feedback", + sequelize, // passing the `sequelize` instance is required + createdAt: "created_at", + updatedAt: "updated_at", + } +); + +export default Feedback; diff --git a/src/models/index.ts b/src/models/index.ts index 836a5ee..3633254 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ +import Feedback from "./feedback"; import Session from "./session"; import Store from "./store"; import StoreService from "./storeService"; @@ -14,6 +15,7 @@ function syncModels() { StoreServiceActivity.sync(); StoreServiceActivityUsers.sync(); Website.sync(); + Feedback.sync(); // UserGoogleTokens.sync(); not needed as it is created by the calendar backend } diff --git a/src/models/user.ts b/src/models/user.ts index 0dba72e..162a348 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -13,6 +13,7 @@ interface UserAttributes { calendar_min_earliest_booking_time?: number; calendar_using_primary_calendar?: boolean; language: string; + state?: number; // like active, deleted, etc analytics_enabled: boolean; } @@ -27,6 +28,7 @@ class User extends Model implements UserAttributes { declare calendar_min_earliest_booking_time: number; declare calendar_using_primary_calendar: boolean; declare language: string; + declare state: number; declare analytics_enabled: boolean; } @@ -74,6 +76,10 @@ User.init( type: DataTypes.STRING, allowNull: false, }, + state: { + type: DataTypes.INTEGER, + // allowNull defaults to true + }, analytics_enabled: { type: DataTypes.BOOLEAN, allowNull: false, diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 032bf9c..633f10f 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -34,5 +34,10 @@ router.delete( sessionProtection, userController.DeleteUserProfileSession ); +router.delete( + "/profile/delete", + sessionProtection, + userController.DeleteUserProfile +); export default router; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 89782e1..8cd4662 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,14 @@ export const USER_ANALYTICS_ENABLED_DEFAULT = true; export const VALID_LANGUAGE_CODES = ["en", "de"]; +export enum ACCOUNT_STATE { + ACTIVE = 0, // everything is fine + DELETED = 1, // account still exists but is marked as deleted, can be restored or will be deleted after a certain time +} + +export const FEEDBACK_MIN_LENGTH = 3; +export const FEEDBACK_MAX_LENGTH = 1024; + // TODO: outdated export const Roles = { // admin of the whole system independent of stores diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f65fd70..d41f25e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -45,6 +45,10 @@ export function newSessionId() { return uuidv4(); } +export function newFeedbackId() { + return uuidv4(); +} + export async function saveSession( req: Request, res: Response, diff --git a/src/validator/validator.ts b/src/validator/validator.ts index a4f1c42..7084ce7 100644 --- a/src/validator/validator.ts +++ b/src/validator/validator.ts @@ -17,6 +17,8 @@ import { STORE_SERVICE_ACTIVITY_DURATION_MAX, STORE_SERVICE_ACTIVITY_DURATION_MIN, VALID_LANGUAGE_CODES, + FEEDBACK_MAX_LENGTH, + FEEDBACK_MIN_LENGTH, } from "../utils/constants"; import User from "../models/user"; @@ -115,3 +117,10 @@ export function isStoreServiceActivityDurationValid( export function isLanguageCodeValid(languageCode: string) { return VALID_LANGUAGE_CODES.includes(languageCode); } + +export function isFeedbackValid(feedback: string) { + return ( + feedback.length >= FEEDBACK_MIN_LENGTH && + feedback.length <= FEEDBACK_MAX_LENGTH + ); +}