Finish promoting email views and add backend controller

This commit is contained in:
Alice Gaudon 2020-07-20 17:32:32 +02:00
parent 6618e874e0
commit f127abbc74
6 changed files with 169 additions and 50 deletions

17
src/Mails.ts Normal file
View File

@ -0,0 +1,17 @@
import config from "config";
import {MailTemplate} from "./Mail";
export const MAGIC_LINK_MAIL = new MailTemplate(
'magic_link',
data => data.type === 'register' ? 'Registration' : 'Login magic link'
);
export const ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
'account_review_notice',
data => `Your account was ${data.approved ? 'approved' : 'rejected'}.`
);
export const PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
'pending_account_review',
() => 'A new account is pending review on ' + config.get<string>('domain'),
);

View File

@ -4,6 +4,10 @@ import User from "./models/User";
import UserEmail from "./models/UserEmail"; import UserEmail from "./models/UserEmail";
import {Connection} from "mysql"; import {Connection} from "mysql";
import {Request} from "express"; import {Request} from "express";
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
import Mail from "../Mail";
import Controller from "../Controller";
import config from "config";
export default abstract class AuthGuard<P extends AuthProof> { export default abstract class AuthGuard<P extends AuthProof> {
public abstract async getProofForSession(session: Express.Session): Promise<P | null>; public abstract async getProofForSession(session: Express.Session): Promise<P | null>;
@ -48,7 +52,14 @@ export default abstract class AuthGuard<P extends AuthProof> {
await callback(); await callback();
} }
if (!user) { if (user) {
if (User.isApprovalMode()) {
await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
username: user!.name,
link: Controller.route('accounts-approval'),
}).send(config.get<string>('app.contact_email'));
}
} else {
throw new Error('Unable to register user.'); throw new Error('Unable to register user.');
} }
} else if (registerCallback) { } else if (registerCallback) {

View File

@ -0,0 +1,69 @@
import config from "config";
import Controller from "../Controller";
import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "../auth/AuthComponent";
import User from "../auth/models/User";
import {Request, Response} from "express";
import {NotFoundHttpError} from "../HttpError";
import Mail from "../Mail";
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
export default class BackendController extends Controller {
getRoutesPrefix(): string {
return '/backend';
}
routes(): void {
this.get('/', this.getIndex, 'backend', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
if (User.isApprovalMode()) {
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
this.post('/accounts-approval/approve/:id', this.postApproveAccount, 'approve-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
this.post('/accounts-approval/reject/:id', this.postRejectAccount, 'reject-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
}
}
public async getIndex(req: Request, res: Response): Promise<void> {
res.render('backend/index', {
approval_mode: User.isApprovalMode(),
accounts_to_approve: User.isApprovalMode() ? await User.select().count() : 0,
});
}
public async getAccountApproval(req: Request, res: Response): Promise<void> {
const accounts = await User.select().where('approved', 0).with('mainEmail').get();
res.render('backend/accounts_approval', {
accounts: User.isApprovalMode() ? accounts : 0,
});
}
public async postApproveAccount(req: Request, res: Response): Promise<void> {
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
if (!account) throw new NotFoundHttpError('User', req.url);
const email = await account.mainEmail.get();
account.approved = true;
await account.save();
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
approved: true,
link: config.get<string>('base_url') + Controller.route('auth'),
}).send(email!.email!);
req.flash('success', `Account successfully approved.`);
res.redirectBack(Controller.route('accounts-approval'));
}
public async postRejectAccount(req: Request, res: Response): Promise<void> {
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
if (!account) throw new NotFoundHttpError('User', req.url);
const email = await account.mainEmail.get();
await account.delete();
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
approved: false,
}).send(email!.email!);
req.flash('success', `Account successfully deleted.`);
res.redirectBack(Controller.route('accounts-approval'));
}
}

View File

@ -0,0 +1,41 @@
{% extends 'layouts/base.njk' %}
{% set title = app.name + ' - Review accounts' %}
{% block body %}
<h1>Accounts pending review</h1>
<div class="panel">
<table class="data-table">
<thead>
<tr>
<th class="shrink-col">#</th>
<th>Name</th>
<th>Main email</th>
<th>Registered at</th>
<th class="shrink-col">Action</th>
</tr>
</thead>
<tbody>
{% for user in accounts %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.mainEmail.getOrFail().email }}</td>
<td>{{ user.created_at.toISOString() }}</td>
<td>
<div class="max-content">
<a href="{{ route('approve-account', user.id) }}"
class="button success"><i data-feather="check"></i> Approve</a>
<a href="{{ route('reject-account', user.id) }}"
onclick="return confirm(`This will irrevocably delete the ${user.main_email} account.`)"
class="button danger"><i data-feather="x"></i> Reject</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

21
views/backend/index.njk Normal file
View File

@ -0,0 +1,21 @@
{% extends 'layouts/base.njk' %}
{% set title = app.name + ' - Backend' %}
{% block body %}
<h1>App administration</h1>
<div class="container">
<div class="panel">
<nav>
<ul>
{% if approval_mode %}
<li>
<a href="{{ route('accounts-approval') }}">Accounts approval ({{ accounts_to_approve }})</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endblock %}

View File

@ -4,63 +4,23 @@
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-text mj-class="title"> <mj-text mj-class="title">
{% if type == 'register' %} New user account on {{ app.name }}
Register an account on {{ app.name }}
{% else %}
Log in to {{ app.name }}
{% endif %}
</mj-text> </mj-text>
<mj-text> <mj-text>
{% if type == 'register' %} A new user is pending review on {{ app.name }}.
Someone has requested an account registration for <strong>{{ mail_to }}</strong>. If it was not you,
please ignore this message. Username: {{ username }}
{% else %}
Someone is attempting to log in to your account <strong>{{ mail_to }}</strong>.
{% endif %}
</mj-text> </mj-text>
{% if type == 'register' %} <mj-text><a href="{{ link | safe }}">Finalize my account registration</a></mj-text>
<mj-button href="{{ link | safe }}">Finalize my account registration</mj-button>
{% else %}
<mj-text>If it is not you, <strong>DO NOT CLICK ON THIS BUTTON</strong>.</mj-text>
{% endif %}
</mj-column> </mj-column>
</mj-section> </mj-section>
{% if type == 'login' %}
<mj-section background-color="#1b252d">
<mj-column>
<mj-text mj-class="important-line" padding-bottom="0px">
IP: <strong>{{ ip }}</strong>
</mj-text>
<mj-text mj-class="important-line">
Location: <strong>{{ geo }}</strong>
</mj-text>
</mj-column>
<mj-column>
<mj-button href="{{ link | safe }}" padding="20px 0" background-color="#caa200">
Authorize log in
</mj-button>
</mj-column>
</mj-section>
{% endif %}
{% endblock %} {% endblock %}
{% block text %} {% block text %}
{% if type == 'register' %} Hi!
Hi! A new user is pending review on {{ app.name }}.
Someone requested an account registration for {{ mail_to }}. If it was not you,
please ignore this message.
To finalize your account registration, please follow this link: {{ link|safe }} Username: {{ username }}
{% else %} To review this account, please follow this link: {{ link|safe }}
Hi!
Someone is attempting to log in to your account {{ mail_to }}.
If it is not you, DO NOT FOLLOW THIS LINK.
IP: {{ ip }}
Location: {{ geo }}
To authorize this log in, please follow this link: {{ link|safe }}
{% endif %}
{% endblock %} {% endblock %}