import config from "config"; import {Request, Response} from "express"; import Time from "../common/Time.js"; import Controller from "../Controller.js"; import ModelFactory from "../db/ModelFactory.js"; import Validator, {EMAIL_REGEX, InvalidFormatValidationError} from "../db/Validator.js"; import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "../HttpError.js"; import MailTemplate from "../mail/MailTemplate.js"; import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails.js"; import {RequireAuthMiddleware} from "./AuthComponent.js"; import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType.js"; import MagicLinkController from "./magic_link/MagicLinkController.js"; import User from "./models/User.js"; import UserEmail from "./models/UserEmail.js"; import UserNameComponent from "./models/UserNameComponent.js"; import UserPasswordComponent from "./password/UserPasswordComponent.js"; export default class AccountController extends Controller { public constructor( private readonly addEmailMailTemplate: MailTemplate = ADD_EMAIL_MAIL_TEMPLATE, private readonly removePasswordMailTemplate: MailTemplate = REMOVE_PASSWORD_MAIL_TEMPLATE, ) { super(); } public getRoutesPrefix(): string { return '/account'; } public routes(): void { this.get('/', this.getAccount, 'account', RequireAuthMiddleware); if (ModelFactory.get(User).hasComponent(UserNameComponent)) { this.post('/change-name', this.postChangeName, 'change-name', RequireAuthMiddleware); } if (ModelFactory.get(User).hasComponent(UserPasswordComponent)) { this.post('/change-password', this.postChangePassword, 'change-password', RequireAuthMiddleware); this.post('/remove-password', this.postRemovePassword, 'remove-password', RequireAuthMiddleware); } this.post('/add-email', this.addEmail, 'add-email', RequireAuthMiddleware); this.post('/set-main-email', this.postSetMainEmail, 'set-main-email', RequireAuthMiddleware); this.post('/remove-email', this.postRemoveEmail, 'remove-email', RequireAuthMiddleware); } protected async getAccount(req: Request, res: Response): Promise { const user = req.as(RequireAuthMiddleware).getUser(); const passwordComponent = user.asOptional(UserPasswordComponent); const nameComponent = user.asOptional(UserNameComponent); const nameChangeWaitPeriod = config.get('auth.name_change_wait_period'); const nameChangedAt = nameComponent?.getNameChangedAt()?.getTime() || Date.now(); const nameChangeRemainingTime = new Date(nameChangedAt + nameChangeWaitPeriod); res.render('auth/account/account', { main_email: await user.mainEmail.get(), emails: await user.emails.get(), display_email_warning: config.get('app.display_email_warning'), has_password_component: !!passwordComponent, has_password: passwordComponent?.hasPassword(), has_name_component: !!nameComponent, name_change_wait_period: Time.humanizeDuration(nameChangeWaitPeriod, false, true), can_change_name: nameComponent?.canChangeName(), can_change_name_in: Time.humanizeTimeTo(nameChangeRemainingTime), }); } protected async postChangeName(req: Request, res: Response): Promise { await Validator.validate({ 'name': new Validator().defined(), }, req.body); const user = req.as(RequireAuthMiddleware).getUser(); const userNameComponent = user.as(UserNameComponent); if (!userNameComponent.setName(req.body.name)) { const nameChangedAt = userNameComponent.getNameChangedAt()?.getTime() || Date.now(); const nameChangeWaitPeriod = config.get('auth.name_change_wait_period'); req.flash('error', `Your can't change your name until ${new Date(nameChangedAt + nameChangeWaitPeriod)}.`); res.redirect(Controller.route('account')); return; } await user.save(); req.flash('success', `Your name was successfully changed to ${req.body.name}.`); res.redirect(Controller.route('account')); } protected async postChangePassword(req: Request, res: Response): Promise { const validationMap = { 'new_password': new Validator().defined(), 'new_password_confirmation': new Validator().sameAs('new_password', req.body.new_password), }; await Validator.validate(validationMap, req.body); const user = req.as(RequireAuthMiddleware).getUser(); const passwordComponent = user.as(UserPasswordComponent); if (passwordComponent.hasPassword() && !await passwordComponent.verifyPassword(req.body.current_password)) { req.flash('error', 'Invalid current password.'); res.redirect(Controller.route('account')); return; } await passwordComponent.setPassword(req.body.new_password, 'new_password'); await user.save(); req.flash('success', 'Password changed successfully.'); res.redirect(Controller.route('account')); } protected async postRemovePassword(req: Request, res: Response): Promise { const user = req.as(RequireAuthMiddleware).getUser(); const mainEmail = await user.mainEmail.get(); if (!mainEmail || !mainEmail.email) { req.flash('error', 'You can\'t remove your password without adding an email address first.'); res.redirect(Controller.route('account')); return; } await MagicLinkController.sendMagicLink( this.getApp(), req.getSession().id, AuthMagicLinkActionType.REMOVE_PASSWORD, Controller.route('account'), mainEmail.email, this.removePasswordMailTemplate, {}, ); res.redirect(Controller.route('magic_link_lobby', undefined, { redirect_uri: Controller.route('account'), })); } protected async addEmail(req: Request, res: Response): Promise { await Validator.validate({ email: new Validator().defined().regexp(EMAIL_REGEX), }, req.body); const email = req.body.email; // Existing email if (await UserEmail.select().where('email', email).first()) { const error = new InvalidFormatValidationError('You already have this email.'); error.thingName = 'email'; throw error; } await MagicLinkController.sendMagicLink( this.getApp(), req.getSession().id, AuthMagicLinkActionType.ADD_EMAIL, Controller.route('account'), email, this.addEmailMailTemplate, { email: email, }, ); res.redirect(Controller.route('magic_link_lobby', undefined, { redirect_uri: Controller.route('account'), })); } protected async postSetMainEmail(req: Request, res: Response): Promise { if (!req.body.id) throw new BadRequestError('Missing id field', 'Check form parameters.', req.url); const user = req.as(RequireAuthMiddleware).getUser(); const userEmail = await UserEmail.getById(req.body.id); if (!userEmail) throw new NotFoundHttpError('email', req.url); if (userEmail.user_id !== user.id) throw new ForbiddenHttpError('email', req.url); if (userEmail.id === user.main_email_id) throw new BadRequestError('This address is already your main address', 'Try refreshing the account page.', req.url); user.main_email_id = userEmail.id; await user.save(); req.flash('success', 'This email was successfully set as your main address.'); res.redirect(Controller.route('account')); } protected async postRemoveEmail(req: Request, res: Response): Promise { if (!req.body.id) throw new BadRequestError('Missing id field', 'Check form parameters.', req.url); const user = req.as(RequireAuthMiddleware).getUser(); const userEmail = await UserEmail.getById(req.body.id); if (!userEmail) throw new NotFoundHttpError('email', req.url); if (userEmail.user_id !== user.id) throw new ForbiddenHttpError('email', req.url); if (userEmail.id === user.main_email_id) throw new BadRequestError('Cannot remove main email address', 'Try refreshing the account page.', req.url); await userEmail.delete(); req.flash('success', 'This email was successfully removed from your account.'); res.redirect(Controller.route('account')); } }