188 lines
7.2 KiB
TypeScript
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.`);
|
|
}
|
|
}
|