swaf/src/auth/magic_link/MagicLinkController.ts

110 lines
4.0 KiB
TypeScript

import Controller from "../../Controller";
import {Request, Response} from "express";
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
import querystring from "querystring";
import Throttler from "../../Throttler";
import Mail, {MailTemplate} from "../../Mail";
import MagicLink from "../models/MagicLink";
import config from "config";
export default abstract class MagicLinkController extends Controller {
public static async sendMagicLink(sessionID: string, actionType: string, email: string, mailTemplate: MailTemplate, data: object, req: Request, res: Response): Promise<void> {
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionID, 0, 0);
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
let link = await MagicLink.bySessionID(sessionID, actionType);
if (!link) {
link = new MagicLink({
session_id: sessionID,
});
}
const token = await link.generateToken(email);
await link.save();
// Send email
await new Mail(mailTemplate, Object.assign(data, {
link: `${req.protocol}://${req.get('host') + Controller.route('magic_link')}?${querystring.stringify({
id: link.id,
token: token,
})}`,
})).send(email);
res.redirect(Controller.route('magic_link_lobby'));
}
protected readonly magicLinkWebsocketPath: string;
protected constructor(magicLinkWebsocketListener: MagicLinkWebSocketListener) {
super();
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
}
getRoutesPrefix(): string {
return '/magic';
}
routes(): void {
this.get('/lobby', this.getLobby, 'magic_link_lobby');
this.get('/link', this.getMagicLink, 'magic_link');
}
private async getLobby(req: Request, res: Response): Promise<void> {
const link = await MagicLink.bySessionID(req.sessionID!);
if (!link) {
throw new NotFoundHttpError('magic link', req.url);
}
if (!await link.isValid()) {
req.flash('error', 'This magic link has expired. Please try again.');
res.redirect(link.getOriginalURL());
return;
}
if (await link.isAuthorized()) {
await this.performAction(link, req, res);
return;
}
res.render('magic_link_lobby', {
email: link.getEmail(),
type: await link.getActionType(),
validUntil: link.getExpirationDate().getTime(),
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
});
}
private async getMagicLink(req: Request, res: Response): Promise<void> {
const id = parseInt(<string>req.query.id);
const token = <string>req.query.token;
if (!id || !token) throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
let success = true;
let err;
const magicLink = await MagicLink.getById<MagicLink>(id);
if (!magicLink) {
res.status(404);
err = `Couldn't find this magic link. Perhaps it has already expired.`;
success = false;
} else if (!await magicLink.isAuthorized()) {
err = await magicLink.verifyToken(token);
if (err === null) {
// Validation success, authenticate the user
magicLink.authorize();
await magicLink.save();
MagicLinkWebSocketListener.refreshMagicLink(magicLink.getSessionID());
}
}
res.render('magic_link', {
actionType: await magicLink?.getActionType(),
actionMessage: await magicLink?.getActionMessage(),
err: err,
success: success && err === null,
});
}
protected abstract async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void>;
}