Add optional user approval mode

This commit is contained in:
Alice Gaudon 2020-06-16 11:12:58 +02:00
parent 0a5fcfa7a4
commit c8157b7bb0
9 changed files with 95 additions and 13 deletions

View File

@ -34,5 +34,6 @@ export default {
},
view: {
cache: false
}
},
approval_mode: false,
};

View File

@ -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 <alice@gaudon.pro>",

View File

@ -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<void> => {
if (!req.models.user || !req.models.user.isAdmin()) {
if (!req.models.user || !req.models.user.is_admin) {
throw new ForbiddenHttpError('secret tool', req.url);
}

View File

@ -55,6 +55,8 @@ export default abstract class AuthGuard<P extends AuthProof> {
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.`);
}
}

View File

@ -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<void> {
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;

View File

@ -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<void> {
await this.query('ALTER TABLE users ADD COLUMN approved BOOLEAN NOT NULL DEFAULT 0', connection);
}
public async rollback(connection: Connection): Promise<void> {
await this.query('ALTER TABLE users DROP COLUMN approved', connection);
}
}

View File

@ -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<User | null> {
@ -8,20 +11,47 @@ export default class User extends Model {
return users.length > 0 ? users[0] : null;
}
public static async countAccountsToApprove(): Promise<number> {
if (!this.isApprovalMode()) return 0;
return (await this.select('COUNT(*) as c').where('approved', false).execute())
.results[0]['c'];
}
public static async getUsersToApprove(): Promise<User[]> {
if (!this.isApprovalMode()) return [];
return await this.models<User>(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<boolean>('approval_mode') && MysqlConnectionManager.hasMigration(AddApprovedFieldToUsersTable);
}
public static async getAdminAccounts(): Promise<User[]> {
return await this.models<User>(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<string>('name', new Validator().acceptUndefined().between(3, 64));
this.defineProperty<boolean>('is_admin', new Validator());
if (User.isApprovalMode()) this.defineProperty<boolean>('approved', new Validator().defined());
this.defineProperty<boolean>('is_admin', new Validator().defined());
this.defineProperty<Date>('created_at');
this.defineProperty<Date>('updated_at');
}
public isAdmin(): boolean {
// @ts-ignore
return this.is_admin === true || this.is_admin === 1;
public isApproved(): boolean {
return !User.isApprovalMode() || this.approved!;
}
}

View File

@ -32,6 +32,13 @@ export default class MysqlConnectionManager {
this.migrations.push(migration(this.migrations.length + 1));
}
public static hasMigration(migration: Type<Migration>) {
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');

View File

@ -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<any>;
files: Files;