From 7d3d1e811fcd3529387da87c510cd467c60b0fe9 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 7 Jan 2025 13:32:50 +0100 Subject: [PATCH] refactor: extracted auth as middleware to be added manually to endpoints --- app/src/App.ts | 23 ++++-- app/src/auth/AppAuth.ts | 23 ++---- app/src/auth/api/AuthController.ts | 39 ++-------- app/src/auth/middlewares.ts | 38 +++++++++ app/src/cli/commands/user.ts | 21 +++-- app/src/data/api/DataController.ts | 89 +++++++++------------- app/src/media/api/MediaController.ts | 16 ++-- app/src/modules/Controller.ts | 26 +++++++ app/src/modules/Module.ts | 15 +++- app/src/modules/ModuleManager.ts | 17 +++-- app/src/modules/server/AdminController.tsx | 27 +++---- app/src/modules/server/SystemController.ts | 51 +++++-------- app/src/ui/routes/auth/auth.index.tsx | 4 +- 13 files changed, 211 insertions(+), 178 deletions(-) create mode 100644 app/src/auth/middlewares.ts create mode 100644 app/src/modules/Controller.ts diff --git a/app/src/App.ts b/app/src/App.ts index 9c856da..9faa1d4 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -1,3 +1,4 @@ +import type { CreateUserPayload } from "auth/AppAuth"; import { Event } from "core/events"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; import { @@ -68,6 +69,12 @@ export class App { onFirstBoot: async () => { console.log("[APP] first boot"); this.trigger_first_boot = true; + }, + onServerInit: async (server) => { + server.use(async (c, next) => { + c.set("app", this); + await next(); + }) } }); this.modules.ctx().emgr.registerEvents(AppEvents); @@ -87,9 +94,11 @@ export class App { //console.log("syncing", syncResult); } + const { guard, server } = this.modules.ctx(); + // load system controller - this.modules.ctx().guard.registerPermissions(Object.values(SystemPermissions)); - this.modules.server.route("/api/system", new SystemController(this).getController()); + guard.registerPermissions(Object.values(SystemPermissions)); + server.route("/api/system", new SystemController(this).getController()); // load plugins if (this.plugins.length > 0) { @@ -99,8 +108,8 @@ export class App { //console.log("emitting built", options); await this.emgr.emit(new AppBuiltEvent({ app: this })); - // not found on any not registered api route - this.modules.server.all("/api/*", async (c) => c.notFound()); + + server.all("/api/*", async (c) => c.notFound()); if (options?.save) { await this.modules.save(); @@ -158,6 +167,10 @@ export class App { static create(config: CreateAppConfig) { return createApp(config); } + + async createUser(p: CreateUserPayload) { + return this.module.auth.createUser(p); + } } export function createApp(config: CreateAppConfig = {}) { @@ -181,4 +194,4 @@ export function createApp(config: CreateAppConfig = {}) { } return new App(connection, config.initialConfig, config.plugins, config.options); -} +} \ No newline at end of file diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 48b6ada..c8557da 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -1,6 +1,6 @@ import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth"; import type { PasswordStrategy } from "auth/authenticate/strategies"; -import { Exception, type PrimaryFieldType } from "core"; +import { type DB, Exception, type PrimaryFieldType } from "core"; import { type Static, secureRandomString, transformObject } from "core/utils"; import { type Entity, EntityIndex, type EntityManager } from "data"; import { type FieldSchema, entity, enumm, make, text } from "data/prototype"; @@ -17,6 +17,7 @@ declare module "core" { } type AuthSchema = Static; +export type CreateUserPayload = { email: string; password: string; [key: string]: any }; export class AppAuth extends Module { private _authenticator?: Authenticator; @@ -36,8 +37,12 @@ export class AppAuth extends Module { return to; } + get enabled() { + return this.config.enabled; + } + override async build() { - if (!this.config.enabled) { + if (!this.enabled) { this.setBuilt(); return; } @@ -84,14 +89,6 @@ export class AppAuth extends Module { return this._controller; } - getMiddleware() { - if (!this.config.enabled) { - return; - } - - return new AuthController(this).getMiddleware; - } - getSchema() { return authConfigSchema; } @@ -287,11 +284,7 @@ export class AppAuth extends Module { } catch (e) {} } - async createUser({ - email, - password, - ...additional - }: { email: string; password: string; [key: string]: any }) { + async createUser({ email, password, ...additional }: CreateUserPayload): Promise { const strategy = "password"; const pw = this.authenticator.strategy(strategy) as PasswordStrategy; const strategy_value = await pw.hash(password); diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 9afd302..0291461 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -1,42 +1,17 @@ import type { AppAuth } from "auth"; -import { type ClassController, isDebug } from "core"; -import { Hono, type MiddlewareHandler } from "hono"; +import { Controller } from "modules/Controller"; -export class AuthController implements ClassController { - constructor(private auth: AppAuth) {} +export class AuthController extends Controller { + constructor(private auth: AppAuth) { + super(); + } get guard() { return this.auth.ctx.guard; } - getMiddleware: MiddlewareHandler = async (c, next) => { - // @todo: ONLY HOTFIX - // middlewares are added for all routes are registered. But we need to make sure that - // only HTML/JSON routes are adding a cookie to the response. Config updates might - // also use an extension "syntax", e.g. /api/system/patch/data/entities.posts - // This middleware should be extracted and added by each Controller individually, - // but it requires access to the auth secret. - // Note: This doesn't mean endpoints aren't protected, just the cookie is not set. - const url = new URL(c.req.url); - const last = url.pathname.split("/")?.pop(); - const ext = last?.includes(".") ? last.split(".")?.pop() : undefined; - if ( - !this.auth.authenticator.isJsonRequest(c) && - ["GET", "HEAD", "OPTIONS"].includes(c.req.method) && - ext && - ["js", "css", "png", "jpg", "jpeg", "svg", "ico"].includes(ext) - ) { - isDebug() && console.log("Skipping auth", { ext }, url.pathname); - } else { - const user = await this.auth.authenticator.resolveAuthFromRequest(c); - this.auth.ctx.guard.setUserContext(user); - } - - await next(); - }; - - getController(): Hono { - const hono = new Hono(); + override getController() { + const hono = this.create(); const strategies = this.auth.authenticator.getStrategies(); for (const [name, strategy] of Object.entries(strategies)) { diff --git a/app/src/auth/middlewares.ts b/app/src/auth/middlewares.ts new file mode 100644 index 0000000..d378dc4 --- /dev/null +++ b/app/src/auth/middlewares.ts @@ -0,0 +1,38 @@ +import type { Permission } from "core"; +import type { Context } from "hono"; +import { createMiddleware } from "hono/factory"; +import type { ServerEnv } from "modules/Module"; + +async function resolveAuth(app: ServerEnv["Variables"]["app"], c: Context) { + const resolved = c.get("auth_resolved") ?? false; + if (resolved) { + return; + } + if (!app.module.auth.enabled) { + return; + } + + const authenticator = app.module.auth.authenticator; + const guard = app.modules.ctx().guard; + + guard.setUserContext(await authenticator.resolveAuthFromRequest(c)); +} + +export const auth = createMiddleware(async (c, next) => { + await resolveAuth(c.get("app"), c); + await next(); +}); + +export const permission = (...permissions: Permission[]) => + createMiddleware(async (c, next) => { + const app = c.get("app"); + await resolveAuth(app, c); + + const p = Array.isArray(permissions) ? permissions : [permissions]; + const guard = app.modules.ctx().guard; + for (const permission of p) { + guard.throwUnlessGranted(permission); + } + + await next(); + }); diff --git a/app/src/cli/commands/user.ts b/app/src/cli/commands/user.ts index 0883c67..235fd4c 100644 --- a/app/src/cli/commands/user.ts +++ b/app/src/cli/commands/user.ts @@ -35,9 +35,11 @@ async function action(action: "create" | "update", options: any) { } async function create(app: App, options: any) { - const config = app.module.auth.toJSON(true); const strategy = app.module.auth.authenticator.strategy("password") as PasswordStrategy; - const users_entity = config.entity_name as "users"; + + if (!strategy) { + throw new Error("Password strategy not configured"); + } const email = await $text({ message: "Enter email", @@ -65,16 +67,11 @@ async function create(app: App, options: any) { } try { - const mutator = app.modules.ctx().em.mutator(users_entity); - mutator.__unstable_toggleSystemEntityCreation(false); - const res = await mutator.insertOne({ + const created = await app.createUser({ email, - strategy: "password", - strategy_value: await strategy.hash(password as string) - }); - mutator.__unstable_toggleSystemEntityCreation(true); - - console.log("Created:", res.data); + password: await strategy.hash(password as string) + }) + console.log("Created:", created); } catch (e) { console.error("Error", e); } @@ -141,4 +138,4 @@ async function update(app: App, options: any) { } catch (e) { console.error("Error", e); } -} +} \ No newline at end of file diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 68a5417..b94b7b0 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -1,32 +1,26 @@ -import { type ClassController, isDebug, tbValidator as tb } from "core"; -import { StringEnum, Type, objectCleanEmpty, objectTransform } from "core/utils"; +import { isDebug, tbValidator as tb } from "core"; +import { StringEnum, Type } from "core/utils"; import { DataPermissions, type EntityData, type EntityManager, - FieldClassMap, type MutatorResponse, - PrimaryField, type RepoQuery, type RepositoryResponse, - TextField, querySchema } from "data"; -import { Hono } from "hono"; import type { Handler } from "hono/types"; import type { ModuleBuildContext } from "modules"; +import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; -import { type AppDataConfig, FIELDS } from "../data-schema"; +import type { AppDataConfig } from "../data-schema"; -export class DataController implements ClassController { +export class DataController extends Controller { constructor( private readonly ctx: ModuleBuildContext, private readonly config: AppDataConfig ) { - /*console.log( - "data controller", - this.em.entities.map((e) => e.name) - );*/ + super(); } get em(): EntityManager { @@ -74,8 +68,9 @@ export class DataController implements ClassController { } } - getController(): Hono { - const hono = new Hono(); + override getController() { + const hono = this.create(); + const { permission } = this.middlewares; const definedEntities = this.em.entities.map((e) => e.name); const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" })) .Decode(Number.parseInt) @@ -89,10 +84,7 @@ export class DataController implements ClassController { return func; } - hono.use("*", async (c, next) => { - this.ctx.guard.throwUnlessGranted(SystemPermissions.accessApi); - await next(); - }); + hono.use("*", permission(SystemPermissions.accessApi)); // info hono.get( @@ -104,9 +96,7 @@ export class DataController implements ClassController { ); // sync endpoint - hono.get("/sync", async (c) => { - this.guard.throwUnlessGranted(DataPermissions.databaseSync); - + hono.get("/sync", permission(DataPermissions.databaseSync), async (c) => { const force = c.req.query("force") === "1"; const drop = c.req.query("drop") === "1"; //console.log("force", force); @@ -126,10 +116,9 @@ export class DataController implements ClassController { // fn: count .post( "/:entity/fn/count", + permission(DataPermissions.entityRead), tb("param", Type.Object({ entity: Type.String() })), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return c.notFound(); @@ -143,10 +132,9 @@ export class DataController implements ClassController { // fn: exists .post( "/:entity/fn/exists", + permission(DataPermissions.entityRead), tb("param", Type.Object({ entity: Type.String() })), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - const { entity } = c.req.valid("param"); if (!this.entityExists(entity)) { return c.notFound(); @@ -163,8 +151,7 @@ export class DataController implements ClassController { */ hono // read entity schema - .get("/schema.json", async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); + .get("/schema.json", permission(DataPermissions.entityRead), async (c) => { const $id = `${this.config.basepath}/schema.json`; const schemas = Object.fromEntries( this.em.entities.map((e) => [ @@ -183,6 +170,7 @@ export class DataController implements ClassController { // read schema .get( "/schemas/:entity/:context?", + permission(DataPermissions.entityRead), tb( "param", Type.Object({ @@ -191,8 +179,6 @@ export class DataController implements ClassController { }) ), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - //console.log("request", c.req.raw); const { entity, context } = c.req.param(); if (!this.entityExists(entity)) { @@ -216,11 +202,10 @@ export class DataController implements ClassController { // read many .get( "/:entity", + permission(DataPermissions.entityRead), tb("param", Type.Object({ entity: Type.String() })), tb("query", querySchema), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - //console.log("request", c.req.raw); const { entity } = c.req.param(); if (!this.entityExists(entity)) { @@ -238,6 +223,7 @@ export class DataController implements ClassController { // read one .get( "/:entity/:id", + permission(DataPermissions.entityRead), tb( "param", Type.Object({ @@ -246,11 +232,7 @@ export class DataController implements ClassController { }) ), tb("query", querySchema), - /*zValidator("param", z.object({ entity: z.string(), id: z.coerce.number() })), - zValidator("query", repoQuerySchema),*/ async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - const { entity, id } = c.req.param(); if (!this.entityExists(entity)) { return c.notFound(); @@ -264,6 +246,7 @@ export class DataController implements ClassController { // read many by reference .get( "/:entity/:id/:reference", + permission(DataPermissions.entityRead), tb( "param", Type.Object({ @@ -274,8 +257,6 @@ export class DataController implements ClassController { ), tb("query", querySchema), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - const { entity, id, reference } = c.req.param(); if (!this.entityExists(entity)) { return c.notFound(); @@ -292,11 +273,10 @@ export class DataController implements ClassController { // func query .post( "/:entity/query", + permission(DataPermissions.entityRead), tb("param", Type.Object({ entity: Type.String() })), tb("json", querySchema), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityRead); - const { entity } = c.req.param(); if (!this.entityExists(entity)) { return c.notFound(); @@ -314,25 +294,27 @@ export class DataController implements ClassController { */ // insert one hono - .post("/:entity", tb("param", Type.Object({ entity: Type.String() })), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityCreate); + .post( + "/:entity", + permission(DataPermissions.entityCreate), + tb("param", Type.Object({ entity: Type.String() })), + async (c) => { + const { entity } = c.req.param(); + if (!this.entityExists(entity)) { + return c.notFound(); + } + const body = (await c.req.json()) as EntityData; + const result = await this.em.mutator(entity).insertOne(body); - const { entity } = c.req.param(); - if (!this.entityExists(entity)) { - return c.notFound(); + return c.json(this.mutatorResult(result), 201); } - const body = (await c.req.json()) as EntityData; - const result = await this.em.mutator(entity).insertOne(body); - - return c.json(this.mutatorResult(result), 201); - }) + ) // update one .patch( "/:entity/:id", + permission(DataPermissions.entityUpdate), tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityUpdate); - const { entity, id } = c.req.param(); if (!this.entityExists(entity)) { return c.notFound(); @@ -346,6 +328,8 @@ export class DataController implements ClassController { // delete one .delete( "/:entity/:id", + + permission(DataPermissions.entityDelete), tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), async (c) => { this.guard.throwUnlessGranted(DataPermissions.entityDelete); @@ -363,11 +347,10 @@ export class DataController implements ClassController { // delete many .delete( "/:entity", + permission(DataPermissions.entityDelete), tb("param", Type.Object({ entity: Type.String() })), tb("json", querySchema.properties.where), async (c) => { - this.guard.throwUnlessGranted(DataPermissions.entityDelete); - //console.log("request", c.req.raw); const { entity } = c.req.param(); if (!this.entityExists(entity)) { diff --git a/app/src/media/api/MediaController.ts b/app/src/media/api/MediaController.ts index 2a1a304..fcbbddc 100644 --- a/app/src/media/api/MediaController.ts +++ b/app/src/media/api/MediaController.ts @@ -1,10 +1,10 @@ -import { type ClassController, tbValidator as tb } from "core"; +import { tbValidator as tb } from "core"; import { Type } from "core/utils"; -import { Hono } from "hono"; import { bodyLimit } from "hono/body-limit"; import type { StorageAdapter } from "media"; import { StorageEvents } from "media"; import { getRandomizedFilename } from "media"; +import { Controller } from "modules/Controller"; import type { AppMedia } from "../AppMedia"; import { MediaField } from "../MediaField"; @@ -12,8 +12,10 @@ const booleanLike = Type.Transform(Type.String()) .Decode((v) => v === "1") .Encode((v) => (v ? "1" : "0")); -export class MediaController implements ClassController { - constructor(private readonly media: AppMedia) {} +export class MediaController extends Controller { + constructor(private readonly media: AppMedia) { + super(); + } private getStorageAdapter(): StorageAdapter { return this.getStorage().getAdapter(); @@ -23,11 +25,11 @@ export class MediaController implements ClassController { return this.media.storage; } - getController(): Hono { + override getController() { // @todo: multiple providers? // @todo: implement range requests - const hono = new Hono(); + const hono = this.create(); // get files list (temporary) hono.get("/files", async (c) => { @@ -190,4 +192,4 @@ export class MediaController implements ClassController { return hono; } -} +} \ No newline at end of file diff --git a/app/src/modules/Controller.ts b/app/src/modules/Controller.ts new file mode 100644 index 0000000..ebd31c0 --- /dev/null +++ b/app/src/modules/Controller.ts @@ -0,0 +1,26 @@ +import { auth, permission } from "auth/middlewares"; +import { Hono } from "hono"; +import type { ServerEnv } from "modules/Module"; + +export class Controller { + protected middlewares = { + auth, + permission + } + + protected create({ auth }: { auth?: boolean } = {}): Hono { + const server = Controller.createServer(); + if (auth !== false) { + server.use(this.middlewares.auth); + } + return server; + } + + static createServer(): Hono { + return new Hono(); + } + + getController(): Hono { + return this.create(); + } +} \ No newline at end of file diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 32f098c..6a86d3e 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -1,3 +1,4 @@ +import type { App } from "App"; import type { Guard } from "auth"; import { SchemaObject } from "core"; import type { EventManager } from "core/events"; @@ -5,9 +6,17 @@ import type { Static, TSchema } from "core/utils"; import type { Connection, EntityManager } from "data"; import type { Hono } from "hono"; +export type ServerEnv = { + Variables: { + app: App; + auth_resolved: boolean; + html?: string; + }; +}; + export type ModuleBuildContext = { connection: Connection; - server: Hono; + server: Hono; em: EntityManager; emgr: EventManager; guard: Guard; @@ -78,6 +87,10 @@ export abstract class Module Promise; // base path for the hono instance basePath?: string; + // callback after server was created + onServerInit?: (server: Hono) => void; // doesn't perform validity checks for given/fetched config trustFetched?: boolean; // runs when initial config provided on a fresh database @@ -124,15 +127,12 @@ export class ModuleManager { __em!: EntityManager; // ctx for modules em!: EntityManager; - server!: Hono; + server!: Hono; emgr!: EventManager; guard!: Guard; private _version: number = 0; private _built = false; - private _fetched = false; - - // @todo: keep? not doing anything with it private readonly _booted_with?: "provided" | "partial"; private logger = new DebugLogger(false); @@ -204,10 +204,13 @@ export class ModuleManager { } private rebuildServer() { - this.server = new Hono(); + this.server = new Hono(); if (this.options?.basePath) { this.server = this.server.basePath(this.options.basePath); } + if (this.options?.onServerInit) { + this.options.onServerInit(this.server); + } // @todo: this is a current workaround, controllers must be reworked objectEach(this.modules, (module) => { @@ -547,4 +550,4 @@ export function getDefaultConfig(): ModuleConfigs { }); return config as any; -} +} \ No newline at end of file diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index f573132..1da3dee 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -1,11 +1,11 @@ /** @jsxImportSource hono/jsx */ import type { App } from "App"; -import { type ClassController, isDebug } from "core"; +import { isDebug } from "core"; import { addFlashMessage } from "core/server/flash"; -import { Hono } from "hono"; import { html } from "hono/html"; import { Fragment } from "hono/jsx"; +import { Controller } from "modules/Controller"; import * as SystemPermissions from "modules/permissions"; const htmlBkndContextReplace = ""; @@ -17,11 +17,13 @@ export type AdminControllerOptions = { forceDev?: boolean | { mainPath: string }; }; -export class AdminController implements ClassController { +export class AdminController extends Controller { constructor( private readonly app: App, private options: AdminControllerOptions = {} - ) {} + ) { + super(); + } get ctx() { return this.app.modules.ctx(); @@ -32,19 +34,16 @@ export class AdminController implements ClassController { } private withBasePath(route: string = "") { - return (this.basepath + route).replace(/\/+$/, "/"); + return (this.basepath + route).replace(/(? { + override getController() { + const hono = this.create().basePath(this.withBasePath()); const auth = this.app.module.auth; const configs = this.app.modules.configs(); // if auth is not enabled, authenticator is undefined const auth_enabled = configs.auth.enabled; - const hono = new Hono<{ - Variables: { - html: string; - }; - }>().basePath(this.withBasePath()); + const authRoutes = { root: "/", success: configs.auth.cookie.pathSuccess ?? "/", @@ -80,8 +79,7 @@ export class AdminController implements ClassController { return c.redirect(authRoutes.success); } - const html = c.get("html"); - return c.html(html); + return c.html(c.get("html")!); }); hono.get(authRoutes.logout, async (c) => { @@ -96,8 +94,7 @@ export class AdminController implements ClassController { return c.redirect(authRoutes.login); } - const html = c.get("html"); - return c.html(html); + return c.html(c.get("html")!); }); return hono; diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index a9fb8d3..e7810b3 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -1,10 +1,11 @@ /// import type { App } from "App"; -import type { ClassController } from "core"; import { tbValidator as tb } from "core"; import { StringEnum, Type, TypeInvalidError } from "core/utils"; -import { type Context, Hono } from "hono"; +import type { Context, Hono } from "hono"; +import { Controller } from "modules/Controller"; + import { MODULE_NAMES, type ModuleConfigs, @@ -27,21 +28,20 @@ export type ConfigUpdateResponse = | ConfigUpdate | { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any }; -export class SystemController implements ClassController { - constructor(private readonly app: App) {} +export class SystemController extends Controller { + constructor(private readonly app: App) { + super(); + } get ctx() { return this.app.modules.ctx(); } private registerConfigController(client: Hono): void { - const hono = new Hono(); + const hono = this.create(); + const { permission } = this.middlewares; - /*hono.use("*", async (c, next) => { - //this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead); - console.log("perm?", this.ctx.guard.hasPermission(SystemPermissions.configRead)); - return next(); - });*/ + hono.use(permission(SystemPermissions.configRead)); hono.get( "/:module?", @@ -57,7 +57,6 @@ export class SystemController implements ClassController { const { secrets } = c.req.valid("query"); const { module } = c.req.valid("param"); - this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead); secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets); const config = this.app.toJSON(secrets); @@ -96,6 +95,7 @@ export class SystemController implements ClassController { hono.post( "/set/:module", + permission(SystemPermissions.configWrite), tb( "query", Type.Object({ @@ -107,8 +107,6 @@ export class SystemController implements ClassController { const { force } = c.req.valid("query"); const value = await c.req.json(); - this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite); - return await handleConfigUpdateResponse(c, async () => { // you must explicitly set force to override existing values // because omitted values gets removed @@ -131,14 +129,12 @@ export class SystemController implements ClassController { } ); - hono.post("/add/:module/:path", async (c) => { + hono.post("/add/:module/:path", permission(SystemPermissions.configWrite), async (c) => { // @todo: require auth (admin) const module = c.req.param("module") as any; const value = await c.req.json(); const path = c.req.param("path") as string; - this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite); - const moduleConfig = this.app.mutateConfig(module); if (moduleConfig.has(path)) { return c.json({ success: false, path, error: "Path already exists" }, { status: 400 }); @@ -155,14 +151,12 @@ export class SystemController implements ClassController { }); }); - hono.patch("/patch/:module/:path", async (c) => { + hono.patch("/patch/:module/:path", permission(SystemPermissions.configWrite), async (c) => { // @todo: require auth (admin) const module = c.req.param("module") as any; const value = await c.req.json(); const path = c.req.param("path"); - this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite); - return await handleConfigUpdateResponse(c, async () => { await this.app.mutateConfig(module).patch(path, value); return { @@ -173,14 +167,12 @@ export class SystemController implements ClassController { }); }); - hono.put("/overwrite/:module/:path", async (c) => { + hono.put("/overwrite/:module/:path", permission(SystemPermissions.configWrite), async (c) => { // @todo: require auth (admin) const module = c.req.param("module") as any; const value = await c.req.json(); const path = c.req.param("path"); - this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite); - return await handleConfigUpdateResponse(c, async () => { await this.app.mutateConfig(module).overwrite(path, value); return { @@ -191,13 +183,11 @@ export class SystemController implements ClassController { }); }); - hono.delete("/remove/:module/:path", async (c) => { + hono.delete("/remove/:module/:path", permission(SystemPermissions.configWrite), async (c) => { // @todo: require auth (admin) const module = c.req.param("module") as any; const path = c.req.param("path")!; - this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite); - return await handleConfigUpdateResponse(c, async () => { await this.app.mutateConfig(module).remove(path); return { @@ -211,13 +201,15 @@ export class SystemController implements ClassController { client.route("/config", hono); } - getController(): Hono { - const hono = new Hono(); + override getController() { + const hono = this.create(); + const { permission } = this.middlewares; this.registerConfigController(hono); hono.get( "/schema/:module?", + permission(SystemPermissions.schemaRead), tb( "query", Type.Object({ @@ -228,7 +220,7 @@ export class SystemController implements ClassController { async (c) => { const module = c.req.param("module") as ModuleKey | undefined; const { config, secrets } = c.req.valid("query"); - this.ctx.guard.throwUnlessGranted(SystemPermissions.schemaRead); + config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead); secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets); @@ -300,8 +292,7 @@ export class SystemController implements ClassController { return c.json({ version: this.app.version(), test: 2, - // @ts-ignore - app: !!c.var.app + app: c.get("app").version() }); }); diff --git a/app/src/ui/routes/auth/auth.index.tsx b/app/src/ui/routes/auth/auth.index.tsx index 21eace5..ea2f7f4 100644 --- a/app/src/ui/routes/auth/auth.index.tsx +++ b/app/src/ui/routes/auth/auth.index.tsx @@ -12,7 +12,9 @@ export function AuthIndex() { config: { roles, strategies, entity_name, enabled } } = useBkndAuth(); const users_entity = entity_name; - const $q = useApiQuery((api) => api.data.count(users_entity)); + const $q = useApiQuery((api) => api.data.count(users_entity), { + enabled + }); const usersTotal = $q.data?.count ?? 0; const rolesTotal = Object.keys(roles ?? {}).length ?? 0; const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;