From 562431449b67f7cd17f9e0c2f78430418805b77a Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 20 Feb 2021 20:18:03 +0100 Subject: [PATCH] Allow users to remove their password in case they forget it Closes #23 --- src/Mails.ts | 5 +++ src/auth/AccountController.ts | 39 +++++++++++++--- .../magic_link/AuthMagicLinkActionType.ts | 1 + src/auth/magic_link/MagicLinkController.ts | 24 ++++++++++ src/auth/password/UserPasswordComponent.ts | 6 ++- views/auth/account.njk | 18 ++------ views/auth/password_panel.njk | 45 +++++++++++++++++++ views/mails/remove_password.mjml.njk | 27 +++++++++++ 8 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 views/auth/password_panel.njk create mode 100644 views/mails/remove_password.mjml.njk diff --git a/src/Mails.ts b/src/Mails.ts index b1d619f..0208a9c 100644 --- a/src/Mails.ts +++ b/src/Mails.ts @@ -22,3 +22,8 @@ export const ADD_EMAIL_MAIL_TEMPLATE: MailTemplate = new MailTemplate( 'add_email', (data) => `Add ${data.email} address to your ${config.get('app.name')} account.`, ); + +export const REMOVE_PASSWORD_MAIL_TEMPLATE: MailTemplate = new MailTemplate( + 'remove_password', + () => `Remove password from your ${config.get('app.name')} account.`, +); diff --git a/src/auth/AccountController.ts b/src/auth/AccountController.ts index a68d0e4..851c0ac 100644 --- a/src/auth/AccountController.ts +++ b/src/auth/AccountController.ts @@ -10,15 +10,16 @@ import ModelFactory from "../db/ModelFactory"; import UserEmail from "./models/UserEmail"; import MagicLinkController from "./magic_link/MagicLinkController"; import {MailTemplate} from "../mail/Mail"; -import {ADD_EMAIL_MAIL_TEMPLATE} from "../Mails"; +import {ADD_EMAIL_MAIL_TEMPLATE, REMOVE_PASSWORD_MAIL_TEMPLATE} from "../Mails"; import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType"; export default class AccountController extends Controller { - private readonly addEmailMailTemplate: MailTemplate; - public constructor(addEmailMailTemplate: MailTemplate = ADD_EMAIL_MAIL_TEMPLATE) { + public constructor( + private readonly addEmailMailTemplate: MailTemplate = ADD_EMAIL_MAIL_TEMPLATE, + private readonly removePasswordMailTemplate: MailTemplate = REMOVE_PASSWORD_MAIL_TEMPLATE, + ) { super(); - this.addEmailMailTemplate = addEmailMailTemplate; } public getRoutesPrefix(): string { @@ -30,6 +31,7 @@ export default class AccountController extends Controller { 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); @@ -40,11 +42,13 @@ export default class AccountController extends Controller { protected async getAccount(req: Request, res: Response): Promise { const user = req.as(RequireAuthMiddleware).getUser(); + const passwordComponent = user.asOptional(UserPasswordComponent); res.render('auth/account', { main_email: await user.mainEmail.get(), emails: await user.emails.get(), display_email_warning: config.get('app.display_email_warning'), - has_password: user.asOptional(UserPasswordComponent)?.hasPassword(), + has_password_component: !!passwordComponent, + has_password: passwordComponent?.hasPassword(), }); } @@ -69,6 +73,31 @@ export default class AccountController extends Controller { 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({ diff --git a/src/auth/magic_link/AuthMagicLinkActionType.ts b/src/auth/magic_link/AuthMagicLinkActionType.ts index 7e907cf..33bb26b 100644 --- a/src/auth/magic_link/AuthMagicLinkActionType.ts +++ b/src/auth/magic_link/AuthMagicLinkActionType.ts @@ -2,4 +2,5 @@ export default { LOGIN: 'login', REGISTER: 'register', ADD_EMAIL: 'add_email', + REMOVE_PASSWORD: 'remove_password', }; diff --git a/src/auth/magic_link/MagicLinkController.ts b/src/auth/magic_link/MagicLinkController.ts index e10163f..2aca47c 100644 --- a/src/auth/magic_link/MagicLinkController.ts +++ b/src/auth/magic_link/MagicLinkController.ts @@ -18,6 +18,7 @@ import {QueryVariable} from "../../db/MysqlConnectionManager"; import UserNameComponent from "../models/UserNameComponent"; import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent"; import {logger} from "../../Logger"; +import UserPasswordComponent from "../password/UserPasswordComponent"; export default class MagicLinkController extends Controller { public static async sendMagicLink( @@ -233,6 +234,29 @@ export default class MagicLinkController extends Controll res.redirect(Controller.route('account')); break; } + + case AuthMagicLinkActionType.REMOVE_PASSWORD: { + const session = req.getSessionOptional(); + if (!session || magicLink.session_id !== session.id) throw new BadOwnerMagicLink(); + + await magicLink.delete(); + + const authGuard = this.getApp().as(AuthComponent).getAuthGuard(); + const proofs = await authGuard.getProofsForSession(session); + const user = await proofs[0]?.getResource(); + if (!user) return; + + const passwordComponent = user.asOptional(UserPasswordComponent); + if (passwordComponent) { + passwordComponent.removePassword(); + await user.save(); + } + + req.flash('success', `Password successfully removed.`); + res.redirect(Controller.route('account')); + break; + } + default: logger.warn('Unknown magic link action type ' + magicLink.action_type); break; diff --git a/src/auth/password/UserPasswordComponent.ts b/src/auth/password/UserPasswordComponent.ts index e1c25a7..a8bbc13 100644 --- a/src/auth/password/UserPasswordComponent.ts +++ b/src/auth/password/UserPasswordComponent.ts @@ -6,7 +6,7 @@ import Validator from "../../db/Validator"; export default class UserPasswordComponent extends ModelComponent { public static readonly PASSWORD_MIN_LENGTH = 12; - private password?: string = undefined; + private password?: string | null = undefined; public init(): void { this.setValidation('password').acceptUndefined().maxLength(128); @@ -36,4 +36,8 @@ export default class UserPasswordComponent extends ModelComponent { public hasPassword(): boolean { return typeof this.password === 'string'; } + + public removePassword(): void { + this.password = null; + } } diff --git a/views/auth/account.njk b/views/auth/account.njk index 77925ad..6f4e633 100644 --- a/views/auth/account.njk +++ b/views/auth/account.njk @@ -22,21 +22,9 @@ {% endif %} -
-

{% if has_password %}Change{% else %}Set{% endif %} password

- -
- {% if has_password %} - {{ macros.field(_locals, 'password', 'current_password', null, 'Current password') }} - {% endif %} - {{ macros.field(_locals, 'password', 'new_password', null, 'New password') }} - {{ macros.field(_locals, 'password', 'new_password_confirmation', null, 'New password confirmation') }} - - - - {{ macros.csrf(getCsrfToken) }} -
-
+ {% if has_password_component %} + {% include './password_panel.njk' %} + {% endif %}

Email addresses

diff --git a/views/auth/password_panel.njk b/views/auth/password_panel.njk new file mode 100644 index 0000000..fe5dbd1 --- /dev/null +++ b/views/auth/password_panel.njk @@ -0,0 +1,45 @@ +
+

{% if has_password %}Change{% else %}Set{% endif %} password

+ +
+ {% if has_password %} + {{ macros.field(_locals, 'password', 'current_password', null, 'Current password') }} +

Forgot your password?

+ {% endif %} + {{ macros.field(_locals, 'password', 'new_password', null, 'New password') }} + {{ macros.field(_locals, 'password', 'new_password_confirmation', null, 'New password confirmation') }} + + + + {{ macros.csrf(getCsrfToken) }} +
+ + {% if has_password %} + + + + {% endif %} +
diff --git a/views/mails/remove_password.mjml.njk b/views/mails/remove_password.mjml.njk new file mode 100644 index 0000000..5681303 --- /dev/null +++ b/views/mails/remove_password.mjml.njk @@ -0,0 +1,27 @@ +{% extends 'mails/base_layout.mjml.njk' %} + +{% block body %} + + + + Remove your password on your {{ app.name }} account + + + Someone wants to remove your password from your account. +

+ Do not click on this if this is not you! +
+ + + Remove my {{ app.name }} password + +
+
+{% endblock %} + +{% block text %} + Hi! + Someone wants to remove your password from your {{ app.name }} account. + + To confirm this action and remove your password, please follow this link: {{ link|safe }} +{% endblock %}