2020-05-09 23:19:47 +02:00
|
|
|
import Controller from "../../Controller";
|
|
|
|
import {Request, Response} from "express";
|
|
|
|
import MagicLinkWebSocketListener from "./MagicLinkWebSocketListener";
|
|
|
|
import {BadRequestError, NotFoundHttpError} from "../../HttpError";
|
|
|
|
import Throttler from "../../Throttler";
|
|
|
|
import Mail, {MailTemplate} from "../../Mail";
|
|
|
|
import MagicLink from "../models/MagicLink";
|
|
|
|
import config from "config";
|
2020-09-25 23:42:15 +02:00
|
|
|
import Application from "../../Application";
|
|
|
|
import {ParsedUrlQueryInput} from "querystring";
|
2020-11-03 10:29:36 +01:00
|
|
|
import NunjucksComponent from "../../components/NunjucksComponent";
|
2020-09-25 23:42:15 +02:00
|
|
|
|
|
|
|
export default abstract class MagicLinkController<A extends Application> extends Controller {
|
|
|
|
public static async sendMagicLink(
|
2020-11-03 10:29:36 +01:00
|
|
|
app: Application,
|
2020-09-25 23:42:15 +02:00
|
|
|
sessionId: string,
|
|
|
|
actionType: string,
|
|
|
|
original_url: string,
|
|
|
|
email: string,
|
|
|
|
mailTemplate: MailTemplate,
|
|
|
|
data: ParsedUrlQueryInput,
|
|
|
|
): Promise<void> {
|
|
|
|
Throttler.throttle('magic_link', 2, MagicLink.validityPeriod(), sessionId, 0, 0);
|
2020-05-09 23:19:47 +02:00
|
|
|
Throttler.throttle('magic_link', 1, MagicLink.validityPeriod(), email, 0, 0);
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
const link = await MagicLink.bySessionId(sessionId, actionType) ||
|
2020-07-27 10:52:39 +02:00
|
|
|
MagicLink.create({
|
2020-09-25 23:42:15 +02:00
|
|
|
session_id: sessionId,
|
2020-05-09 23:37:12 +02:00
|
|
|
action_type: actionType,
|
|
|
|
original_url: original_url,
|
2020-05-09 23:19:47 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const token = await link.generateToken(email);
|
|
|
|
await link.save();
|
|
|
|
|
|
|
|
// Send email
|
2020-11-03 10:29:36 +01:00
|
|
|
await new Mail(app.as(NunjucksComponent).getEnvironment(), mailTemplate, Object.assign(data, {
|
2020-07-15 11:42:49 +02:00
|
|
|
link: `${config.get<string>('base_url')}${Controller.route('magic_link', undefined, {
|
2020-05-09 23:19:47 +02:00
|
|
|
id: link.id,
|
|
|
|
token: token,
|
|
|
|
})}`,
|
|
|
|
})).send(email);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected readonly magicLinkWebsocketPath: string;
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
protected constructor(magicLinkWebsocketListener: MagicLinkWebSocketListener<A>) {
|
2020-05-09 23:19:47 +02:00
|
|
|
super();
|
|
|
|
this.magicLinkWebsocketPath = magicLinkWebsocketListener.path();
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
public getRoutesPrefix(): string {
|
2020-05-09 23:19:47 +02:00
|
|
|
return '/magic';
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
public routes(): void {
|
2020-05-09 23:19:47 +02:00
|
|
|
this.get('/lobby', this.getLobby, 'magic_link_lobby');
|
|
|
|
this.get('/link', this.getMagicLink, 'magic_link');
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
protected async getLobby(req: Request, res: Response): Promise<void> {
|
2020-09-25 23:42:15 +02:00
|
|
|
const link = await MagicLink.bySessionId(req.getSession().id);
|
2020-05-09 23:19:47 +02:00
|
|
|
if (!link) {
|
|
|
|
throw new NotFoundHttpError('magic link', req.url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!await link.isValid()) {
|
|
|
|
req.flash('error', 'This magic link has expired. Please try again.');
|
2020-09-25 23:42:15 +02:00
|
|
|
res.redirect(link.getOrFail('original_url'));
|
2020-05-09 23:19:47 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (await link.isAuthorized()) {
|
|
|
|
await this.performAction(link, req, res);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
res.render('magic_link_lobby', {
|
2020-09-25 23:42:15 +02:00
|
|
|
email: link.getOrFail('email'),
|
|
|
|
type: link.getOrFail('action_type'),
|
2020-05-09 23:19:47 +02:00
|
|
|
validUntil: link.getExpirationDate().getTime(),
|
|
|
|
websocketUrl: config.get<string>('public_websocket_url') + this.magicLinkWebsocketPath,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-25 10:28:50 +02:00
|
|
|
protected async getMagicLink(req: Request, res: Response): Promise<void> {
|
2020-05-09 23:19:47 +02:00
|
|
|
const id = parseInt(<string>req.query.id);
|
|
|
|
const token = <string>req.query.token;
|
2020-09-25 23:42:15 +02:00
|
|
|
if (!id || !token)
|
|
|
|
throw new BadRequestError('Need parameters id, token.', 'Please try again.', req.originalUrl);
|
2020-05-09 23:19:47 +02:00
|
|
|
|
|
|
|
let success = true;
|
|
|
|
let err;
|
2020-06-27 14:36:50 +02:00
|
|
|
const magicLink = await MagicLink.getById<MagicLink>(id);
|
2020-05-09 23:19:47 +02:00
|
|
|
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();
|
2020-09-25 23:42:15 +02:00
|
|
|
this.getApp().as<MagicLinkWebSocketListener<A>>(MagicLinkWebSocketListener)
|
|
|
|
.refreshMagicLink(magicLink.getOrFail('session_id'));
|
2020-05-09 23:19:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
res.render('magic_link', {
|
2020-05-10 00:26:15 +02:00
|
|
|
magicLink: magicLink,
|
2020-05-09 23:19:47 +02:00
|
|
|
err: err,
|
|
|
|
success: success && err === null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
protected abstract async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void>;
|
2020-09-25 23:42:15 +02:00
|
|
|
}
|