swaf/src/auth/magic_link/MagicLinkAuthController.ts

188 lines
7.2 KiB
TypeScript

import {NextFunction, Request, Response} from "express";
import Controller from "../../Controller";
import MagicLink from "../models/MagicLink";
import {BadRequestError} from "../../HttpError";
import UserEmail from "../models/UserEmail";
import MagicLinkController from "./MagicLinkController";
import {MailTemplate} from "../../Mail";
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "../AuthGuard";
import geoip from "geoip-lite";
import AuthController from "../AuthController";
import RedirectBackComponent from "../../components/RedirectBackComponent";
import {AuthMiddleware} from "../AuthComponent";
import User from "../models/User";
export default abstract class MagicLinkAuthController extends AuthController {
public static async checkAndAuth(req: Request, res: Response, magicLink: MagicLink): Promise<User | null> {
const session = req.getSession();
if (magicLink.getOrFail('session_id') !== session.id) throw new BadOwnerMagicLink();
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
if (!await magicLink.isValid()) throw new InvalidMagicLink();
// Auth
try {
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
session, magicLink, undefined, undefined, async (connection, user) => {
const callbacks: RegisterCallback[] = [];
const userEmail = UserEmail.create({
user_id: user.id,
email: magicLink.getOrFail('email'),
});
await userEmail.save(connection, c => callbacks.push(c));
user.main_email_id = userEmail.id;
await user.save(connection, c => callbacks.push(c));
return callbacks;
});
} catch (e) {
if (e instanceof PendingApprovalAuthError) {
res.format({
json: () => {
res.json({
'status': 'warning',
'message': `Your account is pending review. You'll receive an email once you're approved.`,
});
},
html: () => {
req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`);
res.redirect('/');
},
});
return null;
} else {
throw e;
}
}
}
protected readonly loginMagicLinkActionType: string = 'Login';
protected readonly registerMagicLinkActionType: string = 'Register';
private readonly magicLinkMailTemplate: MailTemplate;
protected constructor(magicLinkMailTemplate: MailTemplate) {
super();
this.magicLinkMailTemplate = magicLinkMailTemplate;
}
protected async getAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
const link = await MagicLink.bySessionId(req.getSession().id,
[this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
if (link && await link.isValid()) {
res.redirect(Controller.route('magic_link_lobby', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || undefined,
}));
return;
}
await super.getAuth(req, res, next);
}
protected async postAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
const email = req.body.email;
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
let userEmail = await UserEmail.select().where('email', email).first();
let isRegistration = false;
if (!userEmail) {
isRegistration = true;
userEmail = UserEmail.create({
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(
this.getApp(),
req.getSession().id,
isRegistration ? this.registerMagicLinkActionType : this.loginMagicLinkActionType,
Controller.route('auth', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || undefined,
}),
email,
this.magicLinkMailTemplate,
{
type: isRegistration ? 'register' : 'login',
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
);
res.redirect(Controller.route('magic_link_lobby', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || RedirectBackComponent.getPreviousURL(req),
}));
} else {
// Confirm registration req
req.flash('register_confirm_email', email);
res.redirect(Controller.route('auth', undefined, {
redirect_uri: req.query.redirect_uri?.toString() || undefined,
}));
}
}
/**
* Check whether a magic link is authorized, and authenticate if yes
*/
protected async getCheckAuth(req: Request, res: Response, _next: NextFunction): Promise<void> {
const magicLink = await MagicLink.bySessionId(req.getSession().id,
[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;
}
const user = await MagicLinkAuthController.checkAndAuth(req, res, magicLink);
if (user) {
// Auth success
const username = user.name;
res.format({
json: () => {
res.json({'status': 'success', 'message': `Welcome, ${username}!`});
},
default: () => {
req.flash('success', `Authentication success. Welcome, ${username}!`);
res.redirect('/');
},
});
}
}
}
export class BadOwnerMagicLink extends AuthError {
public constructor() {
super(`This magic link doesn't belong to this session.`);
}
}
export class UnauthorizedMagicLink extends AuthError {
public constructor() {
super(`This magic link is unauthorized.`);
}
}
export class InvalidMagicLink extends AuthError {
public constructor() {
super(`This magic link is invalid.`);
}
}