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