130 lines
3.9 KiB
TypeScript
130 lines
3.9 KiB
TypeScript
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";
|
|
|
|
export default abstract class AuthGuard<P extends AuthProof<User>> {
|
|
protected abstract async getProofForSession(session: Express.Session): Promise<P | null>;
|
|
|
|
protected async getProofForRequest(req: Request): Promise<P | null> {
|
|
return null;
|
|
}
|
|
|
|
public async getProof(req: Request): Promise<P | null> {
|
|
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<P | null> {
|
|
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<P | null> {
|
|
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<void>,
|
|
onRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>
|
|
): Promise<void> {
|
|
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[] = [];
|
|
|
|
await MysqlConnectionManager.wrapTransaction(async connection => {
|
|
user = User.create({});
|
|
if (onRegister) {
|
|
(await onRegister(connection, user)).forEach(c => callbacks.push(c));
|
|
}
|
|
await user.save(connection, c => callbacks.push(c));
|
|
});
|
|
|
|
for (const callback of callbacks) {
|
|
await callback();
|
|
}
|
|
|
|
if (user) {
|
|
if (!user!.isApproved()) {
|
|
await new Mail(PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE, {
|
|
username: user!.name,
|
|
link: config.get<string>('base_url') + Controller.route('accounts-approval'),
|
|
}).send(config.get<string>('app.contact_email'));
|
|
}
|
|
} else {
|
|
throw new Error('Unable to register user.');
|
|
}
|
|
}
|
|
|
|
// Don't login if user isn't approved
|
|
if (!user.isApproved()) {
|
|
throw new PendingApprovalAuthError();
|
|
}
|
|
|
|
// Login
|
|
session.is_authenticated = true;
|
|
if (onLogin) await onLogin(user);
|
|
}
|
|
|
|
}
|
|
|
|
export class AuthError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
export class AuthProofError extends AuthError {
|
|
}
|
|
|
|
export class InvalidAuthProofError extends AuthProofError {
|
|
constructor() {
|
|
super('Invalid auth proof.');
|
|
}
|
|
}
|
|
|
|
export class UnauthorizedAuthProofError extends AuthProofError {
|
|
constructor() {
|
|
super('Unauthorized auth proof.');
|
|
}
|
|
}
|
|
|
|
export class PendingApprovalAuthError extends AuthError {
|
|
constructor() {
|
|
super(`User is not approved.`);
|
|
}
|
|
}
|
|
|
|
export type RegisterCallback = () => Promise<void>;
|