Add auth utils parts
This commit is contained in:
parent
7db6c0e0c7
commit
ad20894565
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wms-core",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.16",
|
||||
"description": "Node web framework",
|
||||
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
|
52
src/auth/AuthComponent.ts
Normal file
52
src/auth/AuthComponent.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import ApplicationComponent from "../ApplicationComponent";
|
||||
import {Express, NextFunction, Request, Response, Router} from "express";
|
||||
import AuthGuard from "./AuthGuard";
|
||||
import Controller from "../Controller";
|
||||
import {ForbiddenHttpError} from "../HttpError";
|
||||
|
||||
export default class AuthComponent extends ApplicationComponent<void> {
|
||||
private readonly authGuard: AuthGuard<any>;
|
||||
|
||||
public constructor(authGuard: AuthGuard<any>) {
|
||||
super();
|
||||
this.authGuard = authGuard;
|
||||
}
|
||||
|
||||
public async start(app: Express, router: Router): Promise<void> {
|
||||
router.use(async (req, res, next) => {
|
||||
req.authGuard = this.authGuard;
|
||||
res.locals.user = await req.authGuard.getUserForSession(req.session!);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const REQUIRE_AUTH_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
if (!await req.authGuard.isAuthenticated(req.session!)) {
|
||||
req.flash('error', `You must be logged in to access ${req.url}.`);
|
||||
res.redirect(Controller.route('auth') || '/');
|
||||
return;
|
||||
}
|
||||
|
||||
req.models.user = await req.authGuard.getUserForSession(req.session!);
|
||||
next();
|
||||
};
|
||||
export const REQUIRE_GUEST_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
if (await req.authGuard.isAuthenticated(req.session!)) {
|
||||
res.redirectBack('/');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
export const REQUIRE_ADMIN_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
if (!req.models.user || !req.models.user.isAdmin()) {
|
||||
throw new ForbiddenHttpError('secret tool', req.url);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
80
src/auth/AuthGuard.ts
Normal file
80
src/auth/AuthGuard.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import AuthProof from "./AuthProof";
|
||||
import MysqlConnectionManager from "../db/MysqlConnectionManager";
|
||||
import User from "./models/User";
|
||||
import UserEmail from "./models/UserEmail";
|
||||
|
||||
export default abstract class AuthGuard<P extends AuthProof> {
|
||||
public abstract async getProofForSession(sessionID: string): Promise<P | null>;
|
||||
|
||||
public async getUserForSession(session: Express.Session): Promise<User | null> {
|
||||
if (!await this.isAuthenticated(session)) return null;
|
||||
return await User.getById<User>(session.auth_id);
|
||||
}
|
||||
|
||||
public async authenticateOrRegister(session: Express.Session, proof: P): Promise<void> {
|
||||
if (!await proof.isAuthorized()) {
|
||||
throw new AuthError('Invalid argument: cannot authenticate with an unauthorized proof.');
|
||||
}
|
||||
|
||||
let user = await proof.getUser();
|
||||
if (!user) { // Register
|
||||
const callbacks: (() => Promise<void>)[] = [];
|
||||
|
||||
await MysqlConnectionManager.wrapTransaction(async connection => {
|
||||
const email = await proof.getEmail();
|
||||
user = new User({
|
||||
name: email.split('@')[0],
|
||||
});
|
||||
await user.save(connection, c => callbacks.push(c));
|
||||
|
||||
const userEmail = new UserEmail({
|
||||
user_id: user.id,
|
||||
email: email,
|
||||
main: true,
|
||||
});
|
||||
await userEmail.save(connection, c => callbacks.push(c));
|
||||
});
|
||||
|
||||
for (const callback of callbacks) {
|
||||
await callback();
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Unable to register user.');
|
||||
}
|
||||
}
|
||||
|
||||
session.auth_id = user.id;
|
||||
}
|
||||
|
||||
public async isAuthenticated(session: Express.Session): Promise<boolean> {
|
||||
return await this.checkCurrentSessionProofValidity(session);
|
||||
}
|
||||
|
||||
public async logout(session: Express.Session): Promise<void> {
|
||||
const proof = await this.getProofForSession(session.id);
|
||||
if (proof) {
|
||||
await proof.revoke();
|
||||
}
|
||||
session.auth_id = undefined;
|
||||
}
|
||||
|
||||
private async checkCurrentSessionProofValidity(session: Express.Session): Promise<boolean> {
|
||||
if (typeof session.auth_id !== 'number') return false;
|
||||
|
||||
const proof = await this.getProofForSession(session.id);
|
||||
|
||||
if (!proof || !await proof.isValid() || !await proof.isAuthorized() || !await proof.isOwnedBy(session.auth_id)) {
|
||||
await this.logout(session);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
15
src/auth/AuthProof.ts
Normal file
15
src/auth/AuthProof.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import User from "./models/User";
|
||||
|
||||
export default interface AuthProof {
|
||||
isValid(): Promise<boolean>;
|
||||
|
||||
isAuthorized(): Promise<boolean>;
|
||||
|
||||
isOwnedBy(userId: number): Promise<boolean>;
|
||||
|
||||
getUser(): Promise<User | null>;
|
||||
|
||||
getEmail(): Promise<string>;
|
||||
|
||||
revoke(): Promise<void>;
|
||||
}
|
29
src/auth/migrations/CreateUsersAndUserEmailsTable.ts
Normal file
29
src/auth/migrations/CreateUsersAndUserEmailsTable.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import Migration from "../../db/Migration";
|
||||
import {query} from "../../db/MysqlConnectionManager";
|
||||
|
||||
export default class CreateUsersAndUserEmailsTable extends Migration {
|
||||
async install(): Promise<void> {
|
||||
await query('CREATE TABLE users(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'name VARCHAR(64),' +
|
||||
'is_admin BOOLEAN NOT NULL DEFAULT false,' +
|
||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'updated_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'PRIMARY KEY(id)' +
|
||||
')');
|
||||
await query('CREATE TABLE user_emails(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT,' +
|
||||
'email VARCHAR(254) UNIQUE NOT NULL,' +
|
||||
'main BOOLEAN NOT NULL,' +
|
||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'PRIMARY KEY(id),' +
|
||||
'FOREIGN KEY user_fk (user_id) REFERENCES users (id) ON DELETE CASCADE' +
|
||||
')');
|
||||
}
|
||||
|
||||
async rollback(): Promise<void> {
|
||||
await query('DROP TABLE user_emails');
|
||||
await query('DROP TABLE users');
|
||||
}
|
||||
}
|
27
src/auth/models/User.ts
Normal file
27
src/auth/models/User.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import UserEmail from "./UserEmail";
|
||||
import Model from "../../db/Model";
|
||||
import Validator from "../../db/Validator";
|
||||
|
||||
export default class User extends Model {
|
||||
public static async fromEmail(email: string): Promise<User | null> {
|
||||
const users = await this.models<User>(this.select().where('id', UserEmail.select('user_id').where('email', email).first()).first());
|
||||
return users.length > 0 ? users[0] : null;
|
||||
}
|
||||
|
||||
public name?: string;
|
||||
private is_admin?: boolean;
|
||||
public created_at?: Date;
|
||||
public updated_at?: Date;
|
||||
|
||||
protected defineProperties(): void {
|
||||
this.defineProperty<string>('name', new Validator().acceptUndefined().between(3, 64));
|
||||
this.defineProperty<boolean>('is_admin', new Validator());
|
||||
this.defineProperty<Date>('created_at');
|
||||
this.defineProperty<Date>('updated_at');
|
||||
}
|
||||
|
||||
public isAdmin(): boolean {
|
||||
// @ts-ignore
|
||||
return this.is_admin === true || this.is_admin === 1;
|
||||
}
|
||||
}
|
62
src/auth/models/UserEmail.ts
Normal file
62
src/auth/models/UserEmail.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import User from "./User";
|
||||
import {Connection} from "mysql";
|
||||
import Model, {EMAIL_REGEX, ModelCache} from "../../db/Model";
|
||||
import Validator from "../../db/Validator";
|
||||
import {query} from "../../db/MysqlConnectionManager";
|
||||
|
||||
export default class UserEmail extends Model {
|
||||
public static async fromEmail(email: any): Promise<UserEmail | null> {
|
||||
const emails = await this.models<UserEmail>(this.select().where('email', email).first());
|
||||
return emails.length > 0 ? emails[0] : null;
|
||||
}
|
||||
|
||||
public user_id?: number;
|
||||
public email?: string;
|
||||
private main?: boolean;
|
||||
public created_at?: Date;
|
||||
|
||||
private wasSetToMain: boolean = false;
|
||||
|
||||
constructor(data: any) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
protected defineProperties(): void {
|
||||
this.defineProperty<number>('user_id', new Validator().acceptUndefined().exists(User, 'id'));
|
||||
this.defineProperty<string>('email', new Validator().defined().regexp(EMAIL_REGEX).unique(this));
|
||||
this.defineProperty<boolean>('main', new Validator().defined());
|
||||
this.defineProperty<Date>('created_at', new Validator());
|
||||
}
|
||||
|
||||
async beforeSave(exists: boolean, connection: Connection) {
|
||||
if (this.wasSetToMain) {
|
||||
await query(`UPDATE ${this.table} SET main=false WHERE user_id=${this.user_id}`, null, connection);
|
||||
}
|
||||
}
|
||||
|
||||
protected async afterSave(): Promise<void> {
|
||||
if (this.wasSetToMain) {
|
||||
this.wasSetToMain = false;
|
||||
const emails = ModelCache.all(this.table);
|
||||
if (emails) {
|
||||
for (const id in emails) {
|
||||
const otherEmail = emails[id];
|
||||
if (otherEmail.id !== this.id && otherEmail.user_id === this.user_id) {
|
||||
otherEmail.main = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isMain(): boolean {
|
||||
return !!this.main;
|
||||
}
|
||||
|
||||
public setMain() {
|
||||
if (!this.isMain()) {
|
||||
this.main = true;
|
||||
this.wasSetToMain = true;
|
||||
}
|
||||
}
|
||||
}
|
2
src/types/Express.d.ts
vendored
2
src/types/Express.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
import {Environment} from "nunjucks";
|
||||
import Model from "../db/Model";
|
||||
import AuthGuard from "../auth/AuthGuard";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
@ -7,6 +8,7 @@ declare global {
|
||||
env: Environment;
|
||||
models: { [p: string]: Model | null };
|
||||
modelCollections: { [p: string]: Model[] | null };
|
||||
authGuard: AuthGuard<any>;
|
||||
|
||||
flash(): { [key: string]: string[] };
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import CreateMigrationsTable from "../src/migrations/CreateMigrationsTable";
|
||||
import CreateLogsTable from "../src/migrations/CreateLogsTable";
|
||||
import CreateUsersAndUserEmailsTable from "../src/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||
|
||||
export const MIGRATIONS = [
|
||||
CreateMigrationsTable,
|
||||
CreateLogsTable,
|
||||
CreateUsersAndUserEmailsTable,
|
||||
];
|
Loading…
Reference in New Issue
Block a user