Add auth utils parts
This commit is contained in:
parent
7db6c0e0c7
commit
ad20894565
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wms-core",
|
"name": "wms-core",
|
||||||
"version": "0.2.11",
|
"version": "0.2.16",
|
||||||
"description": "Node web framework",
|
"description": "Node web framework",
|
||||||
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
|
"repository": "git@gitlab.com:ArisuOngaku/wms-core.git",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"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 {Environment} from "nunjucks";
|
||||||
import Model from "../db/Model";
|
import Model from "../db/Model";
|
||||||
|
import AuthGuard from "../auth/AuthGuard";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
@ -7,6 +8,7 @@ declare global {
|
|||||||
env: Environment;
|
env: Environment;
|
||||||
models: { [p: string]: Model | null };
|
models: { [p: string]: Model | null };
|
||||||
modelCollections: { [p: string]: Model[] | null };
|
modelCollections: { [p: string]: Model[] | null };
|
||||||
|
authGuard: AuthGuard<any>;
|
||||||
|
|
||||||
flash(): { [key: string]: string[] };
|
flash(): { [key: string]: string[] };
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import CreateMigrationsTable from "../src/migrations/CreateMigrationsTable";
|
import CreateMigrationsTable from "../src/migrations/CreateMigrationsTable";
|
||||||
import CreateLogsTable from "../src/migrations/CreateLogsTable";
|
import CreateLogsTable from "../src/migrations/CreateLogsTable";
|
||||||
|
import CreateUsersAndUserEmailsTable from "../src/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||||
|
|
||||||
export const MIGRATIONS = [
|
export const MIGRATIONS = [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
CreateLogsTable,
|
CreateLogsTable,
|
||||||
|
CreateUsersAndUserEmailsTable,
|
||||||
];
|
];
|
Loading…
Reference in New Issue
Block a user