import AuthProof from "./AuthProof";
import MysqlConnectionManager from "../db/MysqlConnectionManager";
import User from "./models/User";
import {Connection} from "mysql";
import {Request} from "express";
import {PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE} from "../Mails";
import Mail from "../Mail";
import Controller from "../Controller";
import config from "config";
import Application from "../Application";
import NunjucksComponent from "../components/NunjucksComponent";
export default abstract class AuthGuard
> {
public constructor(
private readonly app: Application,
) {
}
protected abstract async getProofForSession(session: Express.Session): Promise
;
protected async getProofForRequest(_req: Request): Promise
{
return null;
}
public async getProof(req: Request): Promise
{
let proof = await this.isAuthenticatedViaRequest(req);
if (!proof && req.session) {
proof = await this.isAuthenticated(req.session);
}
return proof;
}
public async isAuthenticated(session: Express.Session): Promise
{
if (!session.is_authenticated) return null;
const proof = await this.getProofForSession(session);
if (!proof || !await proof.isValid() || !await proof.isAuthorized()) {
await proof?.revoke();
session.is_authenticated = false;
return null;
}
return proof;
}
public async isAuthenticatedViaRequest(req: Request): Promise
{
const proof = await this.getProofForRequest(req);
if (!proof || !await proof.isValid() || !await proof.isAuthorized()) {
await proof?.revoke();
return null;
}
return proof;
}
public async authenticateOrRegister(
session: Express.Session,
proof: P,
onLogin?: (user: User) => Promise,
beforeRegister?: (connection: Connection, user: User) => Promise,
afterRegister?: (connection: Connection, user: User) => Promise,
): Promise {
if (!await proof.isValid()) throw new InvalidAuthProofError();
if (!await proof.isAuthorized()) throw new UnauthorizedAuthProofError();
let user = await proof.getResource();
// Register if user doesn't exist
if (!user) {
const callbacks: RegisterCallback[] = [];
user = await MysqlConnectionManager.wrapTransaction(async connection => {
const user = User.create({});
if (beforeRegister) {
(await beforeRegister(connection, user)).forEach(c => callbacks.push(c));
}
await user.save(connection, c => callbacks.push(c));
if (afterRegister) {
(await afterRegister(connection, user)).forEach(c => callbacks.push(c));
}
return user;
});
for (const callback of callbacks) {
await callback();
}
if (!user.isApproved()) {
await new Mail(this.app.as(NunjucksComponent).getEnvironment(), PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
username: (await user.mainEmail.get())?.getOrFail('email'),
link: config.get('base_url') + Controller.route('accounts-approval'),
}).send(config.get('app.contact_email'));
}
}
// Don't login if user isn't approved
if (!user.isApproved()) {
throw new PendingApprovalAuthError();
}
// Login
session.is_authenticated = true;
if (onLogin) await onLogin(user);
return user;
}
}
export class AuthError extends Error {
}
export class AuthProofError extends AuthError {
}
export class InvalidAuthProofError extends AuthProofError {
public constructor() {
super('Invalid auth proof.');
}
}
export class UnauthorizedAuthProofError extends AuthProofError {
public constructor() {
super('Unauthorized auth proof.');
}
}
export class PendingApprovalAuthError extends AuthError {
public constructor() {
super(`User is not approved.`);
}
}
export type RegisterCallback = () => Promise;