diff --git a/config/default.ts b/config/default.ts index 083afae..bd9ad07 100644 --- a/config/default.ts +++ b/config/default.ts @@ -34,5 +34,6 @@ export default { }, view: { cache: false - } + }, + approval_mode: false, }; \ No newline at end of file diff --git a/package.json b/package.json index d235354..7287b95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wms-core", - "version": "0.8.12", + "version": "0.9.3", "description": "Node web framework", "repository": "git@gitlab.com:ArisuOngaku/wms-core.git", "author": "Alice Gaudon ", diff --git a/src/auth/AuthComponent.ts b/src/auth/AuthComponent.ts index 1df2bd4..3e5d8dd 100644 --- a/src/auth/AuthComponent.ts +++ b/src/auth/AuthComponent.ts @@ -30,7 +30,7 @@ export const REQUIRE_REQUEST_AUTH_MIDDLEWARE = async (req: Request, res: Respons res.redirect(Controller.route('auth') || '/'); return; } - + req.models.user = await req.authGuard.getUserForRequest(req); next(); }; @@ -59,7 +59,7 @@ export const REQUIRE_GUEST_MIDDLEWARE = async (req: Request, res: Response, next next(); }; export const REQUIRE_ADMIN_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise => { - if (!req.models.user || !req.models.user.isAdmin()) { + if (!req.models.user || !req.models.user.is_admin) { throw new ForbiddenHttpError('secret tool', req.url); } diff --git a/src/auth/AuthGuard.ts b/src/auth/AuthGuard.ts index 0d4c327..23f0a07 100644 --- a/src/auth/AuthGuard.ts +++ b/src/auth/AuthGuard.ts @@ -55,6 +55,8 @@ export default abstract class AuthGuard

{ throw new UserAlreadyExistsAuthError(await proof.getEmail()); } + if (!user.isApproved()) throw new PendingApprovalAuthError(); + session.auth_id = user.id; } @@ -112,4 +114,10 @@ export class UserAlreadyExistsAuthError extends AuthError { super(`User with email ${userEmail} already exists.`); this.email = userEmail; } +} + +export class PendingApprovalAuthError extends AuthError { + constructor() { + super(`User is not approved.`); + } } \ No newline at end of file diff --git a/src/auth/magic_link/MagicLinkAuthController.ts b/src/auth/magic_link/MagicLinkAuthController.ts index 0d7d655..5e00d93 100644 --- a/src/auth/magic_link/MagicLinkAuthController.ts +++ b/src/auth/magic_link/MagicLinkAuthController.ts @@ -6,11 +6,11 @@ import {BadRequestError} from "../../HttpError"; import UserEmail from "../models/UserEmail"; import MagicLinkController from "./MagicLinkController"; import {MailTemplate} from "../../Mail"; -import {AuthError} from "../AuthGuard"; +import {AuthError, PendingApprovalAuthError} from "../AuthGuard"; import geoip from "geoip-lite"; -export default abstract class AuthController extends Controller { +export default abstract class MagicLinkAuthController extends Controller { public static async checkAndAuth(req: Request, magicLink: MagicLink): Promise { if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink(); if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink(); @@ -114,7 +114,27 @@ export default abstract class AuthController extends Controller { return; } - await AuthController.checkAndAuth(req, magicLink); + try { + await MagicLinkAuthController.checkAndAuth(req, magicLink); + } catch (e) { + if (e instanceof PendingApprovalAuthError) { + res.format({ + json: () => { + res.json({ + 'status': 'warning', + 'message': `Your account is pending review. You'll receive an email once you're approved.` + }); + }, + default: () => { + req.flash('warning', `Your account is pending review. You'll receive an email once you're approved.`); + res.redirectBack('/'); + } + }); + return; + } else { + throw e; + } + } // Auth success const username = req.models.user?.name; diff --git a/src/auth/migrations/AddApprovedFieldToUsersTable.ts b/src/auth/migrations/AddApprovedFieldToUsersTable.ts new file mode 100644 index 0000000..c90d78d --- /dev/null +++ b/src/auth/migrations/AddApprovedFieldToUsersTable.ts @@ -0,0 +1,12 @@ +import Migration from "../../db/Migration"; +import {Connection} from "mysql"; + +export default class AddApprovedFieldToUsersTable extends Migration { + public async install(connection: Connection): Promise { + await this.query('ALTER TABLE users ADD COLUMN approved BOOLEAN NOT NULL DEFAULT 0', connection); + } + + public async rollback(connection: Connection): Promise { + await this.query('ALTER TABLE users DROP COLUMN approved', connection); + } +} \ No newline at end of file diff --git a/src/auth/models/User.ts b/src/auth/models/User.ts index 1a2dbec..3447890 100644 --- a/src/auth/models/User.ts +++ b/src/auth/models/User.ts @@ -1,6 +1,9 @@ import UserEmail from "./UserEmail"; import Model from "../../db/Model"; import Validator from "../../db/Validator"; +import MysqlConnectionManager from "../../db/MysqlConnectionManager"; +import AddApprovedFieldToUsersTable from "../migrations/AddApprovedFieldToUsersTable"; +import config from "config"; export default class User extends Model { public static async fromEmail(email: string): Promise { @@ -8,20 +11,47 @@ export default class User extends Model { return users.length > 0 ? users[0] : null; } + public static async countAccountsToApprove(): Promise { + if (!this.isApprovalMode()) return 0; + return (await this.select('COUNT(*) as c').where('approved', false).execute()) + .results[0]['c']; + } + + public static async getUsersToApprove(): Promise { + if (!this.isApprovalMode()) return []; + return await this.models(this.select('users.*', 'ue.email as main_email') + .where('approved', false) + .leftJoin('user_emails as ue').on('ue.user_id', 'users.id') + .where('ue.main', '1')); + } + + public static isApprovalMode(): boolean { + return config.get('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable); + } + + public static async getAdminAccounts(): Promise { + return await this.models(this.select('users.*', '') + .where('is_admin', true) + .leftJoin('user_emails as ue').on('ue.user_id', 'users.id') + .where('ue.main', true)); + } + public name?: string; - private is_admin?: boolean; + public approved: boolean = false; + public is_admin: boolean = false; public created_at?: Date; public updated_at?: Date; protected defineProperties(): void { this.defineProperty('name', new Validator().acceptUndefined().between(3, 64)); - this.defineProperty('is_admin', new Validator()); + if (User.isApprovalMode()) this.defineProperty('approved', new Validator().defined()); + this.defineProperty('is_admin', new Validator().defined()); + this.defineProperty('created_at'); this.defineProperty('updated_at'); } - public isAdmin(): boolean { - // @ts-ignore - return this.is_admin === true || this.is_admin === 1; + public isApproved(): boolean { + return !User.isApprovalMode() || this.approved!; } } \ No newline at end of file diff --git a/src/db/MysqlConnectionManager.ts b/src/db/MysqlConnectionManager.ts index 3866ec3..7ca30ec 100644 --- a/src/db/MysqlConnectionManager.ts +++ b/src/db/MysqlConnectionManager.ts @@ -32,6 +32,13 @@ export default class MysqlConnectionManager { this.migrations.push(migration(this.migrations.length + 1)); } + public static hasMigration(migration: Type) { + for (const m of this.migrations) { + if (m.constructor === migration) return true; + } + return false; + } + public static async prepare(runMigrations: boolean = true) { if (config.get('mysql.create_database_automatically') === true) { const dbName = config.get('mysql.database'); diff --git a/src/types/Express.d.ts b/src/types/Express.d.ts index f75ccb9..006e76c 100644 --- a/src/types/Express.d.ts +++ b/src/types/Express.d.ts @@ -2,12 +2,16 @@ import {Environment} from "nunjucks"; import Model from "../db/Model"; import AuthGuard from "../auth/AuthGuard"; import {Files} from "formidable"; +import User from "../auth/models/User"; declare global { namespace Express { export interface Request { env: Environment; - models: { [p: string]: Model | null }; + models: { + user?: User | null, + [p: string]: Model | null | undefined, + }; modelCollections: { [p: string]: Model[] | null }; authGuard: AuthGuard; files: Files;