Add optional user approval mode
This commit is contained in:
parent
0a5fcfa7a4
commit
c8157b7bb0
@ -34,5 +34,6 @@ export default {
|
||||
},
|
||||
view: {
|
||||
cache: false
|
||||
}
|
||||
},
|
||||
approval_mode: false,
|
||||
};
|
@ -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>",
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.`);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
12
src/auth/migrations/AddApprovedFieldToUsersTable.ts
Normal file
12
src/auth/migrations/AddApprovedFieldToUsersTable.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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!;
|
||||
}
|
||||
}
|
@ -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');
|
||||
|
6
src/types/Express.d.ts
vendored
6
src/types/Express.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user