Improve approval mode component security, reliability and usage

This commit is contained in:
Alice Gaudon 2020-07-24 13:00:20 +02:00
parent 7a1656ea7f
commit 24de732167
4 changed files with 51 additions and 19 deletions

View File

@ -53,7 +53,7 @@ export default abstract class AuthGuard<P extends AuthProof> {
} }
if (user) { if (user) {
if (User.isApprovalMode()) { if (!user!.isApproved()) {
await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, { await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
username: user!.name, username: user!.name,
link: config.get<string>('base_url') + Controller.route('accounts-approval'), link: config.get<string>('base_url') + Controller.route('accounts-approval'),
@ -66,7 +66,9 @@ export default abstract class AuthGuard<P extends AuthProof> {
throw new UserAlreadyExistsAuthError(await proof.getEmail()); throw new UserAlreadyExistsAuthError(await proof.getEmail());
} }
if (!user.isApproved()) throw new PendingApprovalAuthError(); if (!user.isApproved()) {
throw new PendingApprovalAuthError();
}
session.auth_id = user.id; session.auth_id = user.id;
} }
@ -131,4 +133,4 @@ export class PendingApprovalAuthError extends AuthError {
constructor() { constructor() {
super(`User is not approved.`); super(`User is not approved.`);
} }
} }

View File

@ -3,9 +3,10 @@ import Controller from "../Controller";
import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "../auth/AuthComponent"; import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "../auth/AuthComponent";
import User from "../auth/models/User"; import User from "../auth/models/User";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {NotFoundHttpError} from "../HttpError"; import {BadRequestError, NotFoundHttpError} from "../HttpError";
import Mail from "../Mail"; import Mail from "../Mail";
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails"; import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
import UserEmail from "../auth/models/UserEmail";
export default class BackendController extends Controller { export default class BackendController extends Controller {
getRoutesPrefix(): string { getRoutesPrefix(): string {
@ -16,8 +17,8 @@ export default class BackendController extends Controller {
this.get('/', this.getIndex, 'backend', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE); this.get('/', this.getIndex, 'backend', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
if (User.isApprovalMode()) { if (User.isApprovalMode()) {
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE); 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/approve', 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); this.post('/accounts-approval/reject', this.postRejectAccount, 'reject-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
} }
} }
@ -36,9 +37,7 @@ export default class BackendController extends Controller {
} }
public async postApproveAccount(req: Request, res: Response): Promise<void> { public async postApproveAccount(req: Request, res: Response): Promise<void> {
const account = await User.select().where('id', req.params.id).with('mainEmail').first(); const {account, email} = await this.accountRequest(req);
if (!account) throw new NotFoundHttpError('User', req.url);
const email = await account.mainEmail.get();
account.approved = true; account.approved = true;
await account.save(); await account.save();
@ -53,9 +52,7 @@ export default class BackendController extends Controller {
} }
public async postRejectAccount(req: Request, res: Response): Promise<void> { public async postRejectAccount(req: Request, res: Response): Promise<void> {
const account = await User.select().where('id', req.params.id).with('mainEmail').first(); const {account, email} = await this.accountRequest(req);
if (!account) throw new NotFoundHttpError('User', req.url);
const email = await account.mainEmail.get();
await account.delete(); await account.delete();
@ -66,4 +63,19 @@ export default class BackendController extends Controller {
req.flash('success', `Account successfully deleted.`); req.flash('success', `Account successfully deleted.`);
res.redirectBack(Controller.route('accounts-approval')); res.redirectBack(Controller.route('accounts-approval'));
} }
private async accountRequest(req: Request): Promise<{
account: User,
email: UserEmail,
}> {
if (!req.body.user_id) throw new BadRequestError('Missing user_id field', 'Check your form', req.url);
const account = await User.select().where('id', req.body.user_id).with('mainEmail').first();
if (!account) throw new NotFoundHttpError('User', req.url);
const email = await account.mainEmail.get();
return {
account: account,
email: email!,
};
}
} }

View File

@ -25,12 +25,18 @@
<td>{{ user.created_at.toISOString() }}</td> <td>{{ user.created_at.toISOString() }}</td>
<td> <td>
<div class="max-content"> <div class="max-content">
<a href="{{ route('approve-account', user.id) }}" <form action="{{ route('approve-account') }}" method="POST">
class="button success"><i data-feather="check"></i> Approve</a> <input type="hidden" name="user_id" value="{{ user.id }}">
<button class="success"><i data-feather="check"></i> Approve</button>
{{ macros.csrf(getCSRFToken) }}
</form>
<a href="{{ route('reject-account', user.id) }}" <form action="{{ route('reject-account') }}" method="POST"
onclick="return confirm(`This will irrevocably delete the ${user.main_email} account.`)" data-confirm="This will irrevocably delete the {{ user.mainEmail.getOrFail().email }} account.">
class="button danger"><i data-feather="x"></i> Reject</a> <input type="hidden" name="user_id" value="{{ user.id }}">
<button class="danger"><i data-feather="check"></i> Reject</button>
{{ macros.csrf(getCSRFToken) }}
</form>
</div> </div>
</td> </td>
</tr> </tr>
@ -38,4 +44,16 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('form[data-confirm]').forEach(el => {
el.addEventListener('submit', e => {
if (!confirm(el.dataset['confirm'])) {
e.preventDefault();
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -8,11 +8,11 @@
</mj-text> </mj-text>
<mj-text> <mj-text>
A new user is pending review on {{ app.name }}. A new user is pending review on {{ app.name }}.
<br>
Username: {{ username }} Username: {{ username }}
</mj-text> </mj-text>
<mj-text><a href="{{ link | safe }}">Finalize my account registration</a></mj-text> <mj-text><a href="{{ link | safe }}">Go to account reviews</a></mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>
{% endblock %} {% endblock %}