swaf/src/auth/magic_link/MagicLinkAuthController.ts

156 lines
5.5 KiB
TypeScript

import {Request, Response} from "express";
import Controller from "../../Controller";
import MagicLink from "../models/MagicLink";
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_GUEST_MIDDLEWARE} from "../AuthComponent";
import {BadRequestError} from "../../HttpError";
import UserEmail from "../models/UserEmail";
import MagicLinkController from "./MagicLinkController";
import {MailTemplate} from "../../Mail";
import {AuthError} from "../AuthGuard";
import geoip from "geoip-lite";
export default abstract class AuthController extends Controller {
public static async checkAndAuth(req: Request, magicLink: MagicLink): Promise<void> {
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
if (!await magicLink.isValid()) throw new InvalidMagicLink();
// Auth
await req.authGuard.authenticateOrRegister(req.session!, magicLink);
}
protected readonly loginMagicLinkActionType: string = 'Login';
protected readonly registerMagicLinkActionType: string = 'Register';
private readonly magicLinkMailTemplate: MailTemplate;
protected constructor(magicLinkMailTemplate: MailTemplate) {
super();
this.magicLinkMailTemplate = magicLinkMailTemplate;
}
public getRoutesPrefix(): string {
return '/auth';
}
routes(): void {
this.get('/', this.getAuth, 'auth', REQUIRE_GUEST_MIDDLEWARE);
this.post('/', this.postAuth, 'auth', REQUIRE_GUEST_MIDDLEWARE);
this.get('/check', this.getCheckAuth, 'check_auth');
this.get('/logout', this.getLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE);
}
protected async getAuth(request: Request, response: Response): Promise<void> {
const registerEmail = request.flash('register_confirm_email');
const link = await MagicLink.bySessionID(request.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
if (link && await link.isValid()) {
response.redirect(Controller.route('magic_link_lobby'));
return;
}
response.render('auth', {
register_confirm_email: registerEmail.length > 0 ? registerEmail[0] : null,
});
}
protected async postAuth(req: Request, res: Response): Promise<void> {
const email = req.body.email;
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
let userEmail = await UserEmail.fromEmail(email);
let isRegistration = false;
if (!userEmail) {
isRegistration = true;
userEmail = new UserEmail({
email: email,
main: true,
});
await userEmail.validate(true);
}
if (!isRegistration || req.body.confirm_register === 'confirm') {
// Register (email link)
const geo = geoip.lookup(req.ip);
await MagicLinkController.sendMagicLink(
req.sessionID!,
isRegistration ? this.registerMagicLinkActionType : this.loginMagicLinkActionType,
Controller.route('auth'),
email,
this.magicLinkMailTemplate,
{
type: isRegistration ? 'register' : 'login',
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
req,
res
);
} else {
// Confirm registration req
req.flash('register_confirm_email', email);
res.redirect(Controller.route('auth'));
}
}
/**
* Check whether a magic link is authorized, and authenticate if yes
*/
protected async getCheckAuth(req: Request, res: Response): Promise<void> {
const magicLink = await MagicLink.bySessionID(req.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
if (!magicLink) {
res.format({
json: () => {
throw new BadRequestError('No magic link were found linked with that session.', 'Please retry once you have requested a magic link.', req.originalUrl);
},
default: () => {
req.flash('warning', 'No magic link found. Please try again.');
res.redirect(Controller.route('auth'));
},
});
return;
}
await AuthController.checkAndAuth(req, magicLink);
// Auth success
const username = req.models.user?.name;
res.format({
json: () => {
res.json({'status': 'success', 'message': `Welcome, ${username}!`});
},
default: () => {
req.flash('success', `Authentication success. Welcome, ${username}!`);
res.redirect('/');
},
});
return;
}
protected async getLogout(req: Request, res: Response): Promise<void> {
await req.authGuard.logout(req.session!);
req.flash('success', 'Successfully logged out.');
res.redirectBack('/');
}
}
export class BadOwnerMagicLink extends AuthError {
constructor() {
super(`This magic link doesn't belong to this session.`);
}
}
export class UnauthorizedMagicLink extends AuthError {
constructor() {
super(`This magic link is unauthorized.`);
}
}
export class InvalidMagicLink extends AuthError {
constructor() {
super(`This magic link is invalid.`);
}
}