swaf/src/auth/magic_link/MagicLinkAuthMethod.ts

128 lines
4.5 KiB
TypeScript

import {Request, Response} from "express";
import {Session} from "express-session";
import geoip from "geoip-lite";
import Application from "../../Application.js";
import {route} from "../../common/Routing.js";
import ModelFactory from "../../db/ModelFactory.js";
import {WhereTest} from "../../db/ModelQuery.js";
import Validator, {EMAIL_REGEX} from "../../db/Validator.js";
import MailTemplate from "../../mail/MailTemplate.js";
import AuthMethod from "../AuthMethod.js";
import MagicLink from "../models/MagicLink.js";
import User from "../models/User.js";
import UserEmail from "../models/UserEmail.js";
import UserNameComponent from "../models/UserNameComponent.js";
import AuthMagicLinkActionType from "./AuthMagicLinkActionType.js";
import MagicLinkController from "./MagicLinkController.js";
export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
public constructor(
protected readonly app: Application,
protected readonly magicLinkMailTemplate: MailTemplate,
) {
}
public getName(): string {
return 'magic_link';
}
public getWeightForRequest(req: Request): number {
return !req.body.identifier || !EMAIL_REGEX.test(req.body.identifier) ?
0 :
1;
}
public async findUserByIdentifier(identifier: string): Promise<User | null> {
return (await UserEmail.select()
.with('user.mainEmail')
.where('email', identifier)
.first())?.user.getOrFail() || null;
}
public async getProofsForSession(session: Session): Promise<MagicLink[]> {
return await MagicLink.select()
.where('session_id', session.id)
.where('action_type', [AuthMagicLinkActionType.LOGIN, AuthMagicLinkActionType.REGISTER], WhereTest.IN)
.get();
}
public async interruptAuth(req: Request, res: Response): Promise<boolean> {
const pendingLink = await MagicLink.select()
.where('session_id', req.getSession().id)
.where('used', 0)
.first();
if (pendingLink) {
if (await pendingLink.isValid()) {
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: req.getIntendedUrl() || pendingLink.original_url || undefined,
}));
return true;
} else {
await pendingLink.delete();
}
}
return false;
}
public async attemptLogin(req: Request, res: Response, user: User): Promise<void> {
const userEmail = user.mainEmail.getOrFail();
if (!userEmail) throw new Error('No main email for user ' + user.id);
await this.auth(req, res, false, userEmail.getOrFail('email'));
}
public async attemptRegister(req: Request, res: Response, identifier: string): Promise<void> {
const userEmail = UserEmail.create({
email: identifier,
main: true,
});
await userEmail.validate(true);
await this.auth(req, res, true, identifier);
}
private async auth(req: Request, res: Response, isRegistration: boolean, email: string): Promise<void> {
const geo = geoip.lookup(req.ip);
const actionType = isRegistration ? AuthMagicLinkActionType.REGISTER : AuthMagicLinkActionType.LOGIN;
if (isRegistration) {
const usernameValidator = new Validator();
if (ModelFactory.get(User).hasComponent(UserNameComponent)) usernameValidator.defined();
await Validator.validate({
email: new Validator().defined().unique(UserEmail, 'email'),
name: usernameValidator,
}, {
email: email,
name: req.body.name,
});
}
req.getSession().wantsSessionPersistence = !!req.body.persist_session || isRegistration;
await MagicLinkController.sendMagicLink(
this.app,
req.getSession().id,
actionType,
route('auth', undefined, {
redirect_uri: req.getIntendedUrl() || undefined,
}),
email,
this.magicLinkMailTemplate,
{
type: actionType,
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
{
username: req.body.name,
},
);
res.redirect(route('magic_link_lobby', undefined, {
redirect_uri: req.getIntendedUrl(),
}));
}
}