accounts: enable adding/removing recovery email addresses
This commit is contained in:
parent
933d8f1cbb
commit
41fd867193
|
@ -1 +1,7 @@
|
|||
@import "layout";
|
||||
@import "layout";
|
||||
|
||||
td.actions {
|
||||
form {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -507,6 +507,10 @@ button, .button {
|
|||
|
||||
&.warning {
|
||||
background-color: $warningColor;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($warningColor, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.error, &.danger {
|
||||
|
|
|
@ -25,7 +25,6 @@ import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable";
|
|||
import CreateLogsTable from "wms-core/migrations/CreateLogsTable";
|
||||
import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||
import AddPasswordToUsers from "./migrations/AddPasswordToUsers";
|
||||
import CreateUsernamesTable from "./migrations/AddNameToUsers";
|
||||
import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable";
|
||||
import MailController from "wms-core/auth/MailController";
|
||||
import MagicLinkController from "./controllers/MagicLinkController";
|
||||
|
@ -52,7 +51,6 @@ export default class App extends Application {
|
|||
CreateLogsTable,
|
||||
CreateUsersAndUserEmailsTable,
|
||||
AddPasswordToUsers,
|
||||
CreateUsernamesTable,
|
||||
CreateMagicLinksTable,
|
||||
AddApprovedFieldToUsersTable,
|
||||
FixUserMainEmailRelation,
|
||||
|
|
|
@ -2,9 +2,9 @@ import ApplicationComponent from "wms-core/ApplicationComponent";
|
|||
import {Express} from "express";
|
||||
import ldap, {InvalidCredentialsError, Server} from "ldapjs";
|
||||
import Logger from "wms-core/Logger";
|
||||
import Username from "./models/Username";
|
||||
import {PasswordAuthProof} from "./models/UserPasswordComponent";
|
||||
import UserPasswordComponent from "./models/UserPasswordComponent";
|
||||
import Throttler from "wms-core/Throttler";
|
||||
import User from "wms-core/auth/models/User";
|
||||
|
||||
export default class LDAPServerComponent extends ApplicationComponent<void> {
|
||||
private server?: Server;
|
||||
|
@ -35,12 +35,11 @@ export default class LDAPServerComponent extends ApplicationComponent<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const user = await Username.getUserFromUsername(username);
|
||||
const user = await User.select().where('name', username).first();
|
||||
if (user) {
|
||||
const email = await user.mainEmail.get();
|
||||
if (email) {
|
||||
const authProof = new PasswordAuthProof(email.email!);
|
||||
if (await authProof.authorize(req.credentials)) {
|
||||
if (await user.as(UserPasswordComponent).verifyPassword(req.credentials)) {
|
||||
Logger.debug('Success');
|
||||
res.end();
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import {MailTemplate} from "wms-core/Mail";
|
||||
|
||||
export const ADD_RECOVERY_EMAIL_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||
'add_recovery_email',
|
||||
(data) => 'Add ' + data.email + ' as you recovery email.',
|
||||
);
|
|
@ -1,11 +1,20 @@
|
|||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import {ADD_RECOVERY_EMAIL_MAIL_TEMPLATE} from "../Mails";
|
||||
import Validator, {InvalidFormatValidationError, ValidationBag} from "wms-core/db/Validator";
|
||||
import {EMAIL_REGEX} from "wms-core/db/Model";
|
||||
import MagicLinkController from "./MagicLinkController";
|
||||
import {MagicLinkActionType} from "./MagicLinkActionType";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "wms-core/HttpError";
|
||||
|
||||
export default class AccountController extends Controller {
|
||||
routes(): void {
|
||||
this.get('/account', this.getAccount, 'account', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/add-recovery-email', this.addRecoveryEmail, 'add-recovery-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/set-main-email', this.postSetMainEmail, 'set-main-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/remove-email', this.postRemoveEmail, 'remove-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
}
|
||||
|
||||
protected async getAccount(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
|
@ -15,7 +24,69 @@ export default class AccountController extends Controller {
|
|||
}
|
||||
|
||||
protected async addRecoveryEmail(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
req.flash('warn', 'Not implemented');
|
||||
res.redirectBack();
|
||||
await this.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 bag = new ValidationBag();
|
||||
const error = new InvalidFormatValidationError('You already have this email.');
|
||||
error.thingName = 'email';
|
||||
bag.addMessage(error);
|
||||
throw bag;
|
||||
}
|
||||
|
||||
await MagicLinkController.sendMagicLink(
|
||||
req.sessionID!,
|
||||
MagicLinkActionType.ADD_RECOVERY_EMAIL,
|
||||
Controller.route('account'),
|
||||
email,
|
||||
ADD_RECOVERY_EMAIL_MAIL_TEMPLATE,
|
||||
{}
|
||||
);
|
||||
|
||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
||||
redirect_uri: Controller.route('account'),
|
||||
}));
|
||||
}
|
||||
|
||||
protected async postSetMainEmail(req: Request, res: Response): Promise<void> {
|
||||
if (!req.body.id)
|
||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
||||
|
||||
const userEmail = await UserEmail.getById(req.body.id);
|
||||
if (!userEmail)
|
||||
throw new NotFoundHttpError('email', req.url);
|
||||
if (userEmail.user_id !== req.models.user!.id)
|
||||
throw new ForbiddenHttpError('email', req.url);
|
||||
if (userEmail.id === req.models.user!.main_email_id)
|
||||
throw new BadRequestError('This address is already your main address', 'Try refreshing the account page.', req.url);
|
||||
|
||||
req.models.user!.main_email_id = userEmail.id;
|
||||
await req.models.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<void> {
|
||||
if (!req.body.id)
|
||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
||||
|
||||
const userEmail = await UserEmail.getById(req.body.id);
|
||||
if (!userEmail)
|
||||
throw new NotFoundHttpError('email', req.url);
|
||||
if (userEmail.user_id !== req.models.user!.id)
|
||||
throw new ForbiddenHttpError('email', req.url);
|
||||
if (userEmail.id === req.models.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'));
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_GUEST_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import {EMAIL_REGEX} from "wms-core/db/Model";
|
||||
import {PasswordAuthProof} from "../models/UserPassword";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import Username, {USERNAME_REGEXP} from "../models/Username";
|
||||
import Validator, {InvalidFormatValidationError, ValidationBag} from "wms-core/db/Validator";
|
||||
import UserPasswordComponent, {PasswordAuthProof} from "../models/UserPasswordComponent";
|
||||
import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent";
|
||||
import _AuthController from "wms-core/auth/AuthController";
|
||||
import {ServerError} from "wms-core/HttpError";
|
||||
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "wms-core/auth/AuthGuard";
|
||||
import User from "wms-core/auth/models/User";
|
||||
|
||||
export default class AuthController extends _AuthController {
|
||||
routes(): void {
|
||||
|
@ -24,20 +24,38 @@ export default class AuthController extends _AuthController {
|
|||
|
||||
protected async postLogin(req: Request, res: Response): Promise<void> {
|
||||
await this.validate({
|
||||
email: new Validator().defined().regexp(EMAIL_REGEX),
|
||||
username: new Validator().defined().exists(User, 'name'),
|
||||
password: new Validator().acceptUndefined(),
|
||||
}, req.body);
|
||||
|
||||
const passwordAuthProof = new PasswordAuthProof(req.body.email);
|
||||
const user = await passwordAuthProof.getUser();
|
||||
const user = await User.select()
|
||||
.where('name', req.body.username)
|
||||
.first();
|
||||
if (!user) {
|
||||
req.flash('error', 'Unknown email address');
|
||||
res.redirect(Controller.route('login'));
|
||||
return;
|
||||
const bag = new ValidationBag();
|
||||
const err = new InvalidFormatValidationError('Unknown email address.');
|
||||
err.thingName = 'email';
|
||||
bag.addMessage(err)
|
||||
throw bag;
|
||||
}
|
||||
|
||||
await passwordAuthProof.authorize(req.body.password, req.session);
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof);
|
||||
const passwordAuthProof = PasswordAuthProof.createProofForLogin(req.session!);
|
||||
passwordAuthProof.setResource(user);
|
||||
|
||||
await passwordAuthProof.authorize(req.body.password);
|
||||
try {
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
const bag = new ValidationBag();
|
||||
const err = new InvalidFormatValidationError('Invalid password.');
|
||||
err.thingName = 'password';
|
||||
bag.addMessage(err)
|
||||
throw bag;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
req.flash('success', `Welcome, ${user.name}.`);
|
||||
res.redirect(Controller.route('home'));
|
||||
|
@ -48,53 +66,41 @@ export default class AuthController extends _AuthController {
|
|||
}
|
||||
|
||||
protected async postRegister(req: Request, res: Response): Promise<void> {
|
||||
const validationMap: any = {
|
||||
username: new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(Username),
|
||||
await this.validate({
|
||||
username: new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(User, 'name'),
|
||||
password: new Validator().defined().minLength(8),
|
||||
password_confirmation: new Validator().defined().sameAs('password', req.body.password),
|
||||
terms: new Validator().defined(),
|
||||
};
|
||||
}, req.body);
|
||||
|
||||
let email: string;
|
||||
if (req.body.create_email) {
|
||||
validationMap['domain'] = new Validator().defined().regexp(/^(toot\.party)$/);
|
||||
validationMap['recovery_email'] = new Validator().acceptUndefined(true).regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
email = req.body.email = req.body.username + '@' + req.body.domain;
|
||||
validationMap['email'] = new Validator().defined().regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
} else {
|
||||
validationMap['recovery_email'] = new Validator().defined().regexp(EMAIL_REGEX).unique(UserEmail, 'email');
|
||||
email = req.body.recovery_email;
|
||||
}
|
||||
await this.validate(validationMap, req.body);
|
||||
const passwordAuthProof = PasswordAuthProof.createAuthorizedProofForRegistration(req.session!);
|
||||
try {
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof, undefined, async (connection, user) => {
|
||||
passwordAuthProof.setResource(user);
|
||||
|
||||
const passwordAuthProof = new PasswordAuthProof(email, false);
|
||||
const userPassword = await passwordAuthProof.register(req.body.password);
|
||||
await passwordAuthProof.authorize(req.body.password_confirmation, req.session);
|
||||
await req.authGuard.authenticateOrRegister(req.session!, passwordAuthProof, async (connection, userID) => {
|
||||
const callbacks: (() => Promise<void>)[] = [];
|
||||
const callbacks: RegisterCallback[] = [];
|
||||
|
||||
// Password
|
||||
await userPassword.setUser(userID);
|
||||
await userPassword.save(connection, c => callbacks.push(c));
|
||||
// Password
|
||||
await user.as(UserPasswordComponent).setPassword(req.body.password);
|
||||
|
||||
// Username
|
||||
await new Username({user_id: userID, username: req.body.username}).save(connection, c => callbacks.push(c));
|
||||
// Username
|
||||
user.as(UserNameComponent).name = req.body.username;
|
||||
|
||||
// Email
|
||||
if (req.body.create_email && req.body.recovery_email) {
|
||||
await new UserEmail({
|
||||
user_id: userID,
|
||||
email: req.body.recovery_email,
|
||||
main: false,
|
||||
}).save(connection, c => callbacks.push(c));
|
||||
return callbacks;
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof PendingApprovalAuthError) {
|
||||
req.flash('info', `Your account was successfully created and is pending review from an administrator.`);
|
||||
res.redirect(Controller.route('home'));
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return callbacks;
|
||||
});
|
||||
const user = (await passwordAuthProof.getResource())!;
|
||||
|
||||
const user = (await passwordAuthProof.getUser())!;
|
||||
|
||||
req.flash('success', `Your account was successfully created! Welcome, ${user.name}.`);
|
||||
req.flash('success', `Your account was successfully created! Welcome, ${user.as(UserNameComponent).name}.`);
|
||||
res.redirect(Controller.route('home'));
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {MagicLinkActionType} from "./MagicLinkActionType";
|
|||
import Controller from "wms-core/Controller";
|
||||
import {BadOwnerMagicLink} from "wms-core/auth/magic_link/MagicLinkAuthController";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
|
||||
export default class MagicLinkController extends _MagicLinkController {
|
||||
constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener) {
|
||||
|
@ -16,16 +17,32 @@ export default class MagicLinkController extends _MagicLinkController {
|
|||
switch (magicLink.getActionType()) {
|
||||
case MagicLinkActionType.ADD_RECOVERY_EMAIL:
|
||||
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
|
||||
await magicLink.delete();
|
||||
|
||||
const user = await req.authGuard.getUserForSession(req.session!);
|
||||
if (!user || !(await magicLink.isOwnedBy(user.id!))) throw new BadOwnerMagicLink();
|
||||
const proof = await req.authGuard.getProofForSession(req.session!);
|
||||
const user = await proof?.getResource();
|
||||
if (!user) break;
|
||||
|
||||
let userEmail;
|
||||
await (userEmail = new UserEmail({
|
||||
const email = await magicLink.getEmail();
|
||||
|
||||
// Existing email
|
||||
if (await UserEmail.select().with('user').where('email', email).first()) {
|
||||
req.flash('error', 'An account already exists with this email address. Please first remove it there before adding it here.');
|
||||
res.redirect(Controller.route('account'));
|
||||
break;
|
||||
}
|
||||
|
||||
const userEmail = ModelFactory.get(UserEmail).make({
|
||||
user_id: user.id,
|
||||
email: await magicLink.getEmail(),
|
||||
email: email,
|
||||
main: false,
|
||||
})).save();
|
||||
});
|
||||
await userEmail.save();
|
||||
|
||||
if (!user.main_email_id) {
|
||||
user.main_email_id = userEmail.id;
|
||||
await user.save();
|
||||
}
|
||||
|
||||
req.flash('success', `Recovery email ${userEmail.email} successfully added.`);
|
||||
res.redirect(Controller.route('account'));
|
||||
|
|
|
@ -4,7 +4,7 @@ import User from "wms-core/auth/models/User";
|
|||
import UserNameComponent from "../models/UserNameComponent";
|
||||
import {Connection} from "mysql";
|
||||
|
||||
export default class CreateUserPasswordsTable extends Migration {
|
||||
export default class AddNameToUsers extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`, connection);
|
||||
|
|
|
@ -4,10 +4,11 @@ import ModelComponent from "wms-core/db/ModelComponent";
|
|||
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
||||
|
||||
export default class UserNameComponent extends ModelComponent<User> {
|
||||
public name!: string;
|
||||
public name?: string = undefined;
|
||||
|
||||
public init(): void {
|
||||
this.setValidation('username').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
|
||||
super.init();
|
||||
this.setValidation('name').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ export default class UserPasswordComponent extends ModelComponent<User> {
|
|||
}
|
||||
|
||||
public init(): void {
|
||||
super.init();
|
||||
this.setValidation('password').acceptUndefined().maxLength(128);
|
||||
}
|
||||
|
||||
|
@ -46,15 +47,19 @@ export class PasswordAuthProof implements AuthProof<User> {
|
|||
return proofForSession;
|
||||
}
|
||||
|
||||
public static createProofForLogin(session: Express.Session): PasswordAuthProof {
|
||||
return new PasswordAuthProof(session);
|
||||
}
|
||||
|
||||
private readonly session: Express.Session;
|
||||
private userID: number | null;
|
||||
private authorized: boolean;
|
||||
private userPassword: UserPasswordComponent | null = null;
|
||||
|
||||
public constructor(session: Express.Session) {
|
||||
private constructor(session: Express.Session) {
|
||||
this.session = session;
|
||||
this.authorized = session.auth_password_proof.authorized || false;
|
||||
this.userID = session.auth_password_proof.userID || null;
|
||||
this.authorized = session.auth_password_proof?.authorized || false;
|
||||
this.userID = session.auth_password_proof?.userID || null;
|
||||
}
|
||||
|
||||
public async getResource(): Promise<User | null> {
|
||||
|
@ -71,7 +76,7 @@ export class PasswordAuthProof implements AuthProof<User> {
|
|||
}
|
||||
|
||||
public async isValid(): Promise<boolean> {
|
||||
return await this.getUserPassword() !== null;
|
||||
return await this.isAuthorized() || typeof this.userID === 'number';
|
||||
}
|
||||
|
||||
public async revoke(): Promise<void> {
|
||||
|
|
|
@ -12,13 +12,56 @@
|
|||
<section class="sub-panel">
|
||||
<h2>Email addresses</h2>
|
||||
|
||||
{% for email in emails %}
|
||||
{% if email.main %}
|
||||
<p>Main email: {{ email.email }}</p>
|
||||
{% else %}
|
||||
<p>Recovery email: {{ email.email }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for email in emails %}
|
||||
{% if email.id == user.main_email_id %}
|
||||
<tr>
|
||||
<td>Main</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for email in emails %}
|
||||
{% if email.id != user.main_email_id %}
|
||||
<tr>
|
||||
<td>Secondary</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('set-main-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
|
||||
<button class="warning"
|
||||
onclick="return confirm('Are you sure you want to set {{ email.email }} as your main address?');">
|
||||
<i data-feather="refresh-ccw"></i> Set as main address
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
|
||||
<form action="{{ route('remove-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ email.email }}?');">
|
||||
<i data-feather="trash"></i> Remove
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="{{ route('add-recovery-email') }}" method="POST">
|
||||
{{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<div class="container">
|
||||
<div class="panel center">
|
||||
<form action="{{ route('auth') }}" method="POST">
|
||||
{{ macros.field(_locals, 'email', 'email', null, 'Your email address', null, 'required') }}
|
||||
{{ macros.field(_locals, 'text', 'username', null, 'Your username', null, 'required') }}
|
||||
{{ macros.field(_locals, 'password', 'password', null, 'Your password', null, 'required') }}
|
||||
|
||||
<button type="submit"><i data-feather="log-in"></i> Login</button>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{% extends 'mails/base_layout.mjml.njk' %}
|
||||
|
||||
{% block body %}
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="title">
|
||||
Add this email as recovery for {{ app.name }}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
Someone wants to add <strong>{{ mail_to }}</strong> as a recovery email to their account.
|
||||
<br><br>
|
||||
<strong>Do not click on this if this is not you!</strong>
|
||||
</mj-text>
|
||||
|
||||
<mj-button href="{{ link | safe }}">
|
||||
Add as recovery email
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
|
||||
{% block text %}
|
||||
Hi!
|
||||
Someone wants to add {{ mail_to }} as a recovery email to their account.
|
||||
|
||||
To add it as a recovery email, please follow this link: {{ link|safe }}
|
||||
{% endblock %}
|
|
@ -17,23 +17,6 @@
|
|||
{{ macros.field(_locals, 'text', 'username', null, 'Choose your username', 'This cannot be changed later.', 'pattern="[0-9a-z_-]+" required') }}
|
||||
</section>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Email</h2>
|
||||
{{ macros.field(_locals, 'checkbox', 'create_email', null, 'Create an email address') }}
|
||||
|
||||
<div class="inline-fields">
|
||||
<span id="email_username">@</span>
|
||||
{{ macros.field(_locals, 'select', 'domain', null, 'Choose your domain', null, 'disabled', ['toot.party']) }}
|
||||
</div>
|
||||
{{ macros.fieldError(_locals, 'email') }}
|
||||
<div class="hint"><i data-feather="info"></i> You won't be able to change this again.</div>
|
||||
</section>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Recovery email</h2>
|
||||
{{ macros.field(_locals, 'email', 'recovery_email', null, 'Your email address', 'Optional') }}
|
||||
</section>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Password</h2>
|
||||
{{ macros.field(_locals, 'password', 'password', null, 'Choose a password', null, 'required') }}
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -8023,9 +8023,9 @@ redent@^1.0.0:
|
|||
strip-indent "^1.0.1"
|
||||
|
||||
redis-commands@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.toot.party/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785"
|
||||
integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==
|
||||
version "1.6.0"
|
||||
resolved "https://registry.toot.party/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23"
|
||||
integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==
|
||||
|
||||
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||
version "1.2.0"
|
||||
|
@ -9891,9 +9891,9 @@ widest-line@^3.1.0:
|
|||
string-width "^4.0.0"
|
||||
|
||||
wms-core@^0:
|
||||
version "0.19.5"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.5.tgz#e366f449a6c9c5bdbd2fe8aa8e653cdffe821af7"
|
||||
integrity sha512-ftXCHxbZlBQpLjXO1xpabX5+lcYYgw3stX/vPQuR1r0XLmFG5kn3U3QhlYAWx51dyOqqWDidP8bJ5EHBT9SByQ==
|
||||
version "0.19.14"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.14.tgz#9c64954f3ccfd0b1287cdbf3f5c646725db55319"
|
||||
integrity sha512-fwVjNNoKO779ZCVVGinhfOwVMPpg48DoJcz+g+QLhJgqmYqrsdULT1oZiSntE+1cgII1pWouahyGRywRJyOtQA==
|
||||
dependencies:
|
||||
argon2 "^0.26.2"
|
||||
compression "^1.7.4"
|
||||
|
|
Loading…
Reference in New Issue