parent
cdac3c5f68
commit
562431449b
@ -22,3 +22,8 @@ export const ADD_EMAIL_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
|||||||
'add_email',
|
'add_email',
|
||||||
(data) => `Add ${data.email} address to your ${config.get<string>('app.name')} account.`,
|
(data) => `Add ${data.email} address to your ${config.get<string>('app.name')} account.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const REMOVE_PASSWORD_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||||
|
'remove_password',
|
||||||
|
() => `Remove password from your ${config.get<string>('app.name')} account.`,
|
||||||
|
);
|
||||||
|
@ -10,15 +10,16 @@ import ModelFactory from "../db/ModelFactory";
|
|||||||
import UserEmail from "./models/UserEmail";
|
import UserEmail from "./models/UserEmail";
|
||||||
import MagicLinkController from "./magic_link/MagicLinkController";
|
import MagicLinkController from "./magic_link/MagicLinkController";
|
||||||
import {MailTemplate} from "../mail/Mail";
|
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";
|
import AuthMagicLinkActionType from "./magic_link/AuthMagicLinkActionType";
|
||||||
|
|
||||||
export default class AccountController extends Controller {
|
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();
|
super();
|
||||||
this.addEmailMailTemplate = addEmailMailTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRoutesPrefix(): string {
|
public getRoutesPrefix(): string {
|
||||||
@ -30,6 +31,7 @@ export default class AccountController extends Controller {
|
|||||||
|
|
||||||
if (ModelFactory.get(User).hasComponent(UserPasswordComponent)) {
|
if (ModelFactory.get(User).hasComponent(UserPasswordComponent)) {
|
||||||
this.post('/change-password', this.postChangePassword, 'change-password', RequireAuthMiddleware);
|
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('/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<void> {
|
protected async getAccount(req: Request, res: Response): Promise<void> {
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
|
|
||||||
|
const passwordComponent = user.asOptional(UserPasswordComponent);
|
||||||
res.render('auth/account', {
|
res.render('auth/account', {
|
||||||
main_email: await user.mainEmail.get(),
|
main_email: await user.mainEmail.get(),
|
||||||
emails: await user.emails.get(),
|
emails: await user.emails.get(),
|
||||||
display_email_warning: config.get('app.display_email_warning'),
|
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'));
|
res.redirect(Controller.route('account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async postRemovePassword(req: Request, res: Response): Promise<void> {
|
||||||
|
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<void> {
|
protected async addEmail(req: Request, res: Response): Promise<void> {
|
||||||
await Validator.validate({
|
await Validator.validate({
|
||||||
|
@ -2,4 +2,5 @@ export default {
|
|||||||
LOGIN: 'login',
|
LOGIN: 'login',
|
||||||
REGISTER: 'register',
|
REGISTER: 'register',
|
||||||
ADD_EMAIL: 'add_email',
|
ADD_EMAIL: 'add_email',
|
||||||
|
REMOVE_PASSWORD: 'remove_password',
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ import {QueryVariable} from "../../db/MysqlConnectionManager";
|
|||||||
import UserNameComponent from "../models/UserNameComponent";
|
import UserNameComponent from "../models/UserNameComponent";
|
||||||
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
import MagicLinkUserNameComponent from "../models/MagicLinkUserNameComponent";
|
||||||
import {logger} from "../../Logger";
|
import {logger} from "../../Logger";
|
||||||
|
import UserPasswordComponent from "../password/UserPasswordComponent";
|
||||||
|
|
||||||
export default class MagicLinkController<A extends Application> extends Controller {
|
export default class MagicLinkController<A extends Application> extends Controller {
|
||||||
public static async sendMagicLink(
|
public static async sendMagicLink(
|
||||||
@ -233,6 +234,29 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
res.redirect(Controller.route('account'));
|
res.redirect(Controller.route('account'));
|
||||||
break;
|
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:
|
default:
|
||||||
logger.warn('Unknown magic link action type ' + magicLink.action_type);
|
logger.warn('Unknown magic link action type ' + magicLink.action_type);
|
||||||
break;
|
break;
|
||||||
|
@ -6,7 +6,7 @@ import Validator from "../../db/Validator";
|
|||||||
export default class UserPasswordComponent extends ModelComponent<User> {
|
export default class UserPasswordComponent extends ModelComponent<User> {
|
||||||
public static readonly PASSWORD_MIN_LENGTH = 12;
|
public static readonly PASSWORD_MIN_LENGTH = 12;
|
||||||
|
|
||||||
private password?: string = undefined;
|
private password?: string | null = undefined;
|
||||||
|
|
||||||
public init(): void {
|
public init(): void {
|
||||||
this.setValidation('password').acceptUndefined().maxLength(128);
|
this.setValidation('password').acceptUndefined().maxLength(128);
|
||||||
@ -36,4 +36,8 @@ export default class UserPasswordComponent extends ModelComponent<User> {
|
|||||||
public hasPassword(): boolean {
|
public hasPassword(): boolean {
|
||||||
return typeof this.password === 'string';
|
return typeof this.password === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removePassword(): void {
|
||||||
|
this.password = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,21 +22,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="panel">
|
{% if has_password_component %}
|
||||||
<h2><i data-feather="key"></i> {% if has_password %}Change{% else %}Set{% endif %} password</h2>
|
{% include './password_panel.njk' %}
|
||||||
|
{% endif %}
|
||||||
<form action="{{ route('change-password') }}" method="POST">
|
|
||||||
{% 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') }}
|
|
||||||
|
|
||||||
<button type="submit"><i data-feather="save"></i> Save</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2 id="emails"><i data-feather="shield"></i> Email addresses</h2>
|
<h2 id="emails"><i data-feather="shield"></i> Email addresses</h2>
|
||||||
|
45
views/auth/password_panel.njk
Normal file
45
views/auth/password_panel.njk
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="key"></i> {% if has_password %}Change{% else %}Set{% endif %} password</h2>
|
||||||
|
|
||||||
|
<form action="{{ route('change-password') }}" method="POST" id="change-password-form">
|
||||||
|
{% if has_password %}
|
||||||
|
{{ macros.field(_locals, 'password', 'current_password', null, 'Current password') }}
|
||||||
|
<p><a href="javascript: void(0);" class="switch-form-link">Forgot your password?</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{{ macros.field(_locals, 'password', 'new_password', null, 'New password') }}
|
||||||
|
{{ macros.field(_locals, 'password', 'new_password_confirmation', null, 'New password confirmation') }}
|
||||||
|
|
||||||
|
<button type="submit"><i data-feather="save"></i> Save</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if has_password %}
|
||||||
|
<form action="{{ route('remove-password') }}" method="POST" id="remove-password-form" class="hidden">
|
||||||
|
<p><a href="javascript: void(0);" class="switch-form-link">Go back</a></p>
|
||||||
|
|
||||||
|
<button type="submit" class="danger"><i data-feather="trash"></i> Remove password</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const changePasswordForm = document.getElementById('change-password-form');
|
||||||
|
const removePasswordLink = changePasswordForm.querySelector('a.switch-form-link');
|
||||||
|
const removePasswordForm = document.getElementById('remove-password-form');
|
||||||
|
const changePasswordLink = removePasswordForm.querySelector('a.switch-form-link');
|
||||||
|
|
||||||
|
removePasswordLink.addEventListener('click', () => {
|
||||||
|
changePasswordForm.classList.add('hidden');
|
||||||
|
removePasswordForm.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
changePasswordLink.addEventListener('click', () => {
|
||||||
|
removePasswordForm.classList.add('hidden');
|
||||||
|
changePasswordForm.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
27
views/mails/remove_password.mjml.njk
Normal file
27
views/mails/remove_password.mjml.njk
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends 'mails/base_layout.mjml.njk' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="title">
|
||||||
|
Remove your password on your {{ app.name }} account
|
||||||
|
</mj-text>
|
||||||
|
<mj-text>
|
||||||
|
Someone wants to remove your password from your account.
|
||||||
|
<br><br>
|
||||||
|
<strong>Do not click on this if this is not you!</strong>
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<mj-button href="{{ link | safe }}">
|
||||||
|
Remove my {{ app.name }} password
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% 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 %}
|
Loading…
Reference in New Issue
Block a user