Add optional user approval mode
This commit is contained in:
parent
0a5fcfa7a4
commit
c8157b7bb0
@ -34,5 +34,6 @@ export default {
|
|||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
cache: false
|
cache: false
|
||||||
}
|
},
|
||||||
|
approval_mode: false,
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wms-core",
|
"name": "wms-core",
|
||||||
"version": "0.8.12",
|
"version": "0.9.3",
|
||||||
"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>",
|
||||||
|
@ -30,7 +30,7 @@ export const REQUIRE_REQUEST_AUTH_MIDDLEWARE = async (req: Request, res: Respons
|
|||||||
res.redirect(Controller.route('auth') || '/');
|
res.redirect(Controller.route('auth') || '/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
req.models.user = await req.authGuard.getUserForRequest(req);
|
req.models.user = await req.authGuard.getUserForRequest(req);
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
@ -59,7 +59,7 @@ export const REQUIRE_GUEST_MIDDLEWARE = async (req: Request, res: Response, next
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
export const REQUIRE_ADMIN_MIDDLEWARE = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
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);
|
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());
|
throw new UserAlreadyExistsAuthError(await proof.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.isApproved()) throw new PendingApprovalAuthError();
|
||||||
|
|
||||||
session.auth_id = user.id;
|
session.auth_id = user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,4 +114,10 @@ export class UserAlreadyExistsAuthError extends AuthError {
|
|||||||
super(`User with email ${userEmail} already exists.`);
|
super(`User with email ${userEmail} already exists.`);
|
||||||
this.email = userEmail;
|
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 UserEmail from "../models/UserEmail";
|
||||||
import MagicLinkController from "./MagicLinkController";
|
import MagicLinkController from "./MagicLinkController";
|
||||||
import {MailTemplate} from "../../Mail";
|
import {MailTemplate} from "../../Mail";
|
||||||
import {AuthError} from "../AuthGuard";
|
import {AuthError, PendingApprovalAuthError} from "../AuthGuard";
|
||||||
import geoip from "geoip-lite";
|
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> {
|
public static async checkAndAuth(req: Request, magicLink: MagicLink): Promise<void> {
|
||||||
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
|
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
|
||||||
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
|
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
|
||||||
@ -114,7 +114,27 @@ export default abstract class AuthController extends Controller {
|
|||||||
return;
|
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
|
// Auth success
|
||||||
const username = req.models.user?.name;
|
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 UserEmail from "./UserEmail";
|
||||||
import Model from "../../db/Model";
|
import Model from "../../db/Model";
|
||||||
import Validator from "../../db/Validator";
|
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 {
|
export default class User extends Model {
|
||||||
public static async fromEmail(email: string): Promise<User | null> {
|
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;
|
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;
|
public name?: string;
|
||||||
private is_admin?: boolean;
|
public approved: boolean = false;
|
||||||
|
public is_admin: boolean = false;
|
||||||
public created_at?: Date;
|
public created_at?: Date;
|
||||||
public updated_at?: Date;
|
public updated_at?: Date;
|
||||||
|
|
||||||
protected defineProperties(): void {
|
protected defineProperties(): void {
|
||||||
this.defineProperty<string>('name', new Validator().acceptUndefined().between(3, 64));
|
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>('created_at');
|
||||||
this.defineProperty<Date>('updated_at');
|
this.defineProperty<Date>('updated_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAdmin(): boolean {
|
public isApproved(): boolean {
|
||||||
// @ts-ignore
|
return !User.isApprovalMode() || this.approved!;
|
||||||
return this.is_admin === true || this.is_admin === 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,6 +32,13 @@ export default class MysqlConnectionManager {
|
|||||||
this.migrations.push(migration(this.migrations.length + 1));
|
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) {
|
public static async prepare(runMigrations: boolean = true) {
|
||||||
if (config.get('mysql.create_database_automatically') === true) {
|
if (config.get('mysql.create_database_automatically') === true) {
|
||||||
const dbName = config.get('mysql.database');
|
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 Model from "../db/Model";
|
||||||
import AuthGuard from "../auth/AuthGuard";
|
import AuthGuard from "../auth/AuthGuard";
|
||||||
import {Files} from "formidable";
|
import {Files} from "formidable";
|
||||||
|
import User from "../auth/models/User";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
env: Environment;
|
env: Environment;
|
||||||
models: { [p: string]: Model | null };
|
models: {
|
||||||
|
user?: User | null,
|
||||||
|
[p: string]: Model | null | undefined,
|
||||||
|
};
|
||||||
modelCollections: { [p: string]: Model[] | null };
|
modelCollections: { [p: string]: Model[] | null };
|
||||||
authGuard: AuthGuard<any>;
|
authGuard: AuthGuard<any>;
|
||||||
files: Files;
|
files: Files;
|
||||||
|
Loading…
Reference in New Issue
Block a user