refactor: extracted auth as middleware to be added manually to endpoints

This commit is contained in:
dswbx
2025-01-07 13:32:50 +01:00
parent 064bbba8aa
commit 7d3d1e811f
13 changed files with 211 additions and 178 deletions

View File

@@ -1,3 +1,4 @@
import type { CreateUserPayload } from "auth/AppAuth";
import { Event } from "core/events"; import { Event } from "core/events";
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
import { import {
@@ -68,6 +69,12 @@ export class App {
onFirstBoot: async () => { onFirstBoot: async () => {
console.log("[APP] first boot"); console.log("[APP] first boot");
this.trigger_first_boot = true; 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); this.modules.ctx().emgr.registerEvents(AppEvents);
@@ -87,9 +94,11 @@ export class App {
//console.log("syncing", syncResult); //console.log("syncing", syncResult);
} }
const { guard, server } = this.modules.ctx();
// load system controller // load system controller
this.modules.ctx().guard.registerPermissions(Object.values(SystemPermissions)); guard.registerPermissions(Object.values(SystemPermissions));
this.modules.server.route("/api/system", new SystemController(this).getController()); server.route("/api/system", new SystemController(this).getController());
// load plugins // load plugins
if (this.plugins.length > 0) { if (this.plugins.length > 0) {
@@ -99,8 +108,8 @@ export class App {
//console.log("emitting built", options); //console.log("emitting built", options);
await this.emgr.emit(new AppBuiltEvent({ app: this })); 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) { if (options?.save) {
await this.modules.save(); await this.modules.save();
@@ -158,6 +167,10 @@ export class App {
static create(config: CreateAppConfig) { static create(config: CreateAppConfig) {
return createApp(config); return createApp(config);
} }
async createUser(p: CreateUserPayload) {
return this.module.auth.createUser(p);
}
} }
export function createApp(config: CreateAppConfig = {}) { export function createApp(config: CreateAppConfig = {}) {

View File

@@ -1,6 +1,6 @@
import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth"; import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies"; 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 Static, secureRandomString, transformObject } from "core/utils";
import { type Entity, EntityIndex, type EntityManager } from "data"; import { type Entity, EntityIndex, type EntityManager } from "data";
import { type FieldSchema, entity, enumm, make, text } from "data/prototype"; import { type FieldSchema, entity, enumm, make, text } from "data/prototype";
@@ -17,6 +17,7 @@ declare module "core" {
} }
type AuthSchema = Static<typeof authConfigSchema>; type AuthSchema = Static<typeof authConfigSchema>;
export type CreateUserPayload = { email: string; password: string; [key: string]: any };
export class AppAuth extends Module<typeof authConfigSchema> { export class AppAuth extends Module<typeof authConfigSchema> {
private _authenticator?: Authenticator; private _authenticator?: Authenticator;
@@ -36,8 +37,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
return to; return to;
} }
get enabled() {
return this.config.enabled;
}
override async build() { override async build() {
if (!this.config.enabled) { if (!this.enabled) {
this.setBuilt(); this.setBuilt();
return; return;
} }
@@ -84,14 +89,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
return this._controller; return this._controller;
} }
getMiddleware() {
if (!this.config.enabled) {
return;
}
return new AuthController(this).getMiddleware;
}
getSchema() { getSchema() {
return authConfigSchema; return authConfigSchema;
} }
@@ -287,11 +284,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
} catch (e) {} } catch (e) {}
} }
async createUser({ async createUser({ email, password, ...additional }: CreateUserPayload): Promise<DB["users"]> {
email,
password,
...additional
}: { email: string; password: string; [key: string]: any }) {
const strategy = "password"; const strategy = "password";
const pw = this.authenticator.strategy(strategy) as PasswordStrategy; const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
const strategy_value = await pw.hash(password); const strategy_value = await pw.hash(password);

View File

@@ -1,42 +1,17 @@
import type { AppAuth } from "auth"; import type { AppAuth } from "auth";
import { type ClassController, isDebug } from "core"; import { Controller } from "modules/Controller";
import { Hono, type MiddlewareHandler } from "hono";
export class AuthController implements ClassController { export class AuthController extends Controller {
constructor(private auth: AppAuth) {} constructor(private auth: AppAuth) {
super();
}
get guard() { get guard() {
return this.auth.ctx.guard; return this.auth.ctx.guard;
} }
getMiddleware: MiddlewareHandler = async (c, next) => { override getController() {
// @todo: ONLY HOTFIX const hono = this.create();
// 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<any> {
const hono = new Hono();
const strategies = this.auth.authenticator.getStrategies(); const strategies = this.auth.authenticator.getStrategies();
for (const [name, strategy] of Object.entries(strategies)) { for (const [name, strategy] of Object.entries(strategies)) {

View File

@@ -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<ServerEnv>) {
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<ServerEnv>(async (c, next) => {
await resolveAuth(c.get("app"), c);
await next();
});
export const permission = (...permissions: Permission[]) =>
createMiddleware<ServerEnv>(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();
});

View File

@@ -35,9 +35,11 @@ async function action(action: "create" | "update", options: any) {
} }
async function create(app: App, 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 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({ const email = await $text({
message: "Enter email", message: "Enter email",
@@ -65,16 +67,11 @@ async function create(app: App, options: any) {
} }
try { try {
const mutator = app.modules.ctx().em.mutator(users_entity); const created = await app.createUser({
mutator.__unstable_toggleSystemEntityCreation(false);
const res = await mutator.insertOne({
email, email,
strategy: "password", password: await strategy.hash(password as string)
strategy_value: await strategy.hash(password as string) })
}); console.log("Created:", created);
mutator.__unstable_toggleSystemEntityCreation(true);
console.log("Created:", res.data);
} catch (e) { } catch (e) {
console.error("Error", e); console.error("Error", e);
} }

View File

@@ -1,32 +1,26 @@
import { type ClassController, isDebug, tbValidator as tb } from "core"; import { isDebug, tbValidator as tb } from "core";
import { StringEnum, Type, objectCleanEmpty, objectTransform } from "core/utils"; import { StringEnum, Type } from "core/utils";
import { import {
DataPermissions, DataPermissions,
type EntityData, type EntityData,
type EntityManager, type EntityManager,
FieldClassMap,
type MutatorResponse, type MutatorResponse,
PrimaryField,
type RepoQuery, type RepoQuery,
type RepositoryResponse, type RepositoryResponse,
TextField,
querySchema querySchema
} from "data"; } from "data";
import { Hono } from "hono";
import type { Handler } from "hono/types"; import type { Handler } from "hono/types";
import type { ModuleBuildContext } from "modules"; import type { ModuleBuildContext } from "modules";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions"; 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( constructor(
private readonly ctx: ModuleBuildContext, private readonly ctx: ModuleBuildContext,
private readonly config: AppDataConfig private readonly config: AppDataConfig
) { ) {
/*console.log( super();
"data controller",
this.em.entities.map((e) => e.name)
);*/
} }
get em(): EntityManager<any> { get em(): EntityManager<any> {
@@ -74,8 +68,9 @@ export class DataController implements ClassController {
} }
} }
getController(): Hono<any> { override getController() {
const hono = new Hono(); const hono = this.create();
const { permission } = this.middlewares;
const definedEntities = this.em.entities.map((e) => e.name); const definedEntities = this.em.entities.map((e) => e.name);
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" })) const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
.Decode(Number.parseInt) .Decode(Number.parseInt)
@@ -89,10 +84,7 @@ export class DataController implements ClassController {
return func; return func;
} }
hono.use("*", async (c, next) => { hono.use("*", permission(SystemPermissions.accessApi));
this.ctx.guard.throwUnlessGranted(SystemPermissions.accessApi);
await next();
});
// info // info
hono.get( hono.get(
@@ -104,9 +96,7 @@ export class DataController implements ClassController {
); );
// sync endpoint // sync endpoint
hono.get("/sync", async (c) => { hono.get("/sync", permission(DataPermissions.databaseSync), async (c) => {
this.guard.throwUnlessGranted(DataPermissions.databaseSync);
const force = c.req.query("force") === "1"; const force = c.req.query("force") === "1";
const drop = c.req.query("drop") === "1"; const drop = c.req.query("drop") === "1";
//console.log("force", force); //console.log("force", force);
@@ -126,10 +116,9 @@ export class DataController implements ClassController {
// fn: count // fn: count
.post( .post(
"/:entity/fn/count", "/:entity/fn/count",
permission(DataPermissions.entityRead),
tb("param", Type.Object({ entity: Type.String() })), tb("param", Type.Object({ entity: Type.String() })),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const { entity } = c.req.valid("param"); const { entity } = c.req.valid("param");
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -143,10 +132,9 @@ export class DataController implements ClassController {
// fn: exists // fn: exists
.post( .post(
"/:entity/fn/exists", "/:entity/fn/exists",
permission(DataPermissions.entityRead),
tb("param", Type.Object({ entity: Type.String() })), tb("param", Type.Object({ entity: Type.String() })),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const { entity } = c.req.valid("param"); const { entity } = c.req.valid("param");
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -163,8 +151,7 @@ export class DataController implements ClassController {
*/ */
hono hono
// read entity schema // read entity schema
.get("/schema.json", async (c) => { .get("/schema.json", permission(DataPermissions.entityRead), async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const $id = `${this.config.basepath}/schema.json`; const $id = `${this.config.basepath}/schema.json`;
const schemas = Object.fromEntries( const schemas = Object.fromEntries(
this.em.entities.map((e) => [ this.em.entities.map((e) => [
@@ -183,6 +170,7 @@ export class DataController implements ClassController {
// read schema // read schema
.get( .get(
"/schemas/:entity/:context?", "/schemas/:entity/:context?",
permission(DataPermissions.entityRead),
tb( tb(
"param", "param",
Type.Object({ Type.Object({
@@ -191,8 +179,6 @@ export class DataController implements ClassController {
}) })
), ),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
//console.log("request", c.req.raw); //console.log("request", c.req.raw);
const { entity, context } = c.req.param(); const { entity, context } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
@@ -216,11 +202,10 @@ export class DataController implements ClassController {
// read many // read many
.get( .get(
"/:entity", "/:entity",
permission(DataPermissions.entityRead),
tb("param", Type.Object({ entity: Type.String() })), tb("param", Type.Object({ entity: Type.String() })),
tb("query", querySchema), tb("query", querySchema),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
//console.log("request", c.req.raw); //console.log("request", c.req.raw);
const { entity } = c.req.param(); const { entity } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
@@ -238,6 +223,7 @@ export class DataController implements ClassController {
// read one // read one
.get( .get(
"/:entity/:id", "/:entity/:id",
permission(DataPermissions.entityRead),
tb( tb(
"param", "param",
Type.Object({ Type.Object({
@@ -246,11 +232,7 @@ export class DataController implements ClassController {
}) })
), ),
tb("query", querySchema), tb("query", querySchema),
/*zValidator("param", z.object({ entity: z.string(), id: z.coerce.number() })),
zValidator("query", repoQuerySchema),*/
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const { entity, id } = c.req.param(); const { entity, id } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -264,6 +246,7 @@ export class DataController implements ClassController {
// read many by reference // read many by reference
.get( .get(
"/:entity/:id/:reference", "/:entity/:id/:reference",
permission(DataPermissions.entityRead),
tb( tb(
"param", "param",
Type.Object({ Type.Object({
@@ -274,8 +257,6 @@ export class DataController implements ClassController {
), ),
tb("query", querySchema), tb("query", querySchema),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const { entity, id, reference } = c.req.param(); const { entity, id, reference } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -292,11 +273,10 @@ export class DataController implements ClassController {
// func query // func query
.post( .post(
"/:entity/query", "/:entity/query",
permission(DataPermissions.entityRead),
tb("param", Type.Object({ entity: Type.String() })), tb("param", Type.Object({ entity: Type.String() })),
tb("json", querySchema), tb("json", querySchema),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityRead);
const { entity } = c.req.param(); const { entity } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -314,9 +294,11 @@ export class DataController implements ClassController {
*/ */
// insert one // insert one
hono hono
.post("/:entity", tb("param", Type.Object({ entity: Type.String() })), async (c) => { .post(
this.guard.throwUnlessGranted(DataPermissions.entityCreate); "/:entity",
permission(DataPermissions.entityCreate),
tb("param", Type.Object({ entity: Type.String() })),
async (c) => {
const { entity } = c.req.param(); const { entity } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -325,14 +307,14 @@ export class DataController implements ClassController {
const result = await this.em.mutator(entity).insertOne(body); const result = await this.em.mutator(entity).insertOne(body);
return c.json(this.mutatorResult(result), 201); return c.json(this.mutatorResult(result), 201);
}) }
)
// update one // update one
.patch( .patch(
"/:entity/:id", "/:entity/:id",
permission(DataPermissions.entityUpdate),
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityUpdate);
const { entity, id } = c.req.param(); const { entity, id } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {
return c.notFound(); return c.notFound();
@@ -346,6 +328,8 @@ export class DataController implements ClassController {
// delete one // delete one
.delete( .delete(
"/:entity/:id", "/:entity/:id",
permission(DataPermissions.entityDelete),
tb("param", Type.Object({ entity: Type.String(), id: tbNumber })), tb("param", Type.Object({ entity: Type.String(), id: tbNumber })),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityDelete); this.guard.throwUnlessGranted(DataPermissions.entityDelete);
@@ -363,11 +347,10 @@ export class DataController implements ClassController {
// delete many // delete many
.delete( .delete(
"/:entity", "/:entity",
permission(DataPermissions.entityDelete),
tb("param", Type.Object({ entity: Type.String() })), tb("param", Type.Object({ entity: Type.String() })),
tb("json", querySchema.properties.where), tb("json", querySchema.properties.where),
async (c) => { async (c) => {
this.guard.throwUnlessGranted(DataPermissions.entityDelete);
//console.log("request", c.req.raw); //console.log("request", c.req.raw);
const { entity } = c.req.param(); const { entity } = c.req.param();
if (!this.entityExists(entity)) { if (!this.entityExists(entity)) {

View File

@@ -1,10 +1,10 @@
import { type ClassController, tbValidator as tb } from "core"; import { tbValidator as tb } from "core";
import { Type } from "core/utils"; import { Type } from "core/utils";
import { Hono } from "hono";
import { bodyLimit } from "hono/body-limit"; import { bodyLimit } from "hono/body-limit";
import type { StorageAdapter } from "media"; import type { StorageAdapter } from "media";
import { StorageEvents } from "media"; import { StorageEvents } from "media";
import { getRandomizedFilename } from "media"; import { getRandomizedFilename } from "media";
import { Controller } from "modules/Controller";
import type { AppMedia } from "../AppMedia"; import type { AppMedia } from "../AppMedia";
import { MediaField } from "../MediaField"; import { MediaField } from "../MediaField";
@@ -12,8 +12,10 @@ const booleanLike = Type.Transform(Type.String())
.Decode((v) => v === "1") .Decode((v) => v === "1")
.Encode((v) => (v ? "1" : "0")); .Encode((v) => (v ? "1" : "0"));
export class MediaController implements ClassController { export class MediaController extends Controller {
constructor(private readonly media: AppMedia) {} constructor(private readonly media: AppMedia) {
super();
}
private getStorageAdapter(): StorageAdapter { private getStorageAdapter(): StorageAdapter {
return this.getStorage().getAdapter(); return this.getStorage().getAdapter();
@@ -23,11 +25,11 @@ export class MediaController implements ClassController {
return this.media.storage; return this.media.storage;
} }
getController(): Hono<any> { override getController() {
// @todo: multiple providers? // @todo: multiple providers?
// @todo: implement range requests // @todo: implement range requests
const hono = new Hono(); const hono = this.create();
// get files list (temporary) // get files list (temporary)
hono.get("/files", async (c) => { hono.get("/files", async (c) => {

View File

@@ -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<ServerEnv> {
const server = Controller.createServer();
if (auth !== false) {
server.use(this.middlewares.auth);
}
return server;
}
static createServer(): Hono<ServerEnv> {
return new Hono<ServerEnv>();
}
getController(): Hono<ServerEnv> {
return this.create();
}
}

View File

@@ -1,3 +1,4 @@
import type { App } from "App";
import type { Guard } from "auth"; import type { Guard } from "auth";
import { SchemaObject } from "core"; import { SchemaObject } from "core";
import type { EventManager } from "core/events"; 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 { Connection, EntityManager } from "data";
import type { Hono } from "hono"; import type { Hono } from "hono";
export type ServerEnv = {
Variables: {
app: App;
auth_resolved: boolean;
html?: string;
};
};
export type ModuleBuildContext = { export type ModuleBuildContext = {
connection: Connection; connection: Connection;
server: Hono<any>; server: Hono<ServerEnv>;
em: EntityManager; em: EntityManager;
emgr: EventManager<any>; emgr: EventManager<any>;
guard: Guard; guard: Guard;
@@ -78,6 +87,10 @@ export abstract class Module<Schema extends TSchema = TSchema, ConfigSchema = St
return this._schema; return this._schema;
} }
getMiddleware() {
return undefined;
}
get ctx() { get ctx() {
if (!this._ctx) { if (!this._ctx) {
throw new Error("Context not set"); throw new Error("Context not set");

View File

@@ -1,3 +1,4 @@
import type { App } from "App";
import { Guard } from "auth"; import { Guard } from "auth";
import { BkndError, DebugLogger } from "core"; import { BkndError, DebugLogger } from "core";
import { EventManager } from "core/events"; import { EventManager } from "core/events";
@@ -33,7 +34,7 @@ import { AppAuth } from "../auth/AppAuth";
import { AppData } from "../data/AppData"; import { AppData } from "../data/AppData";
import { AppFlows } from "../flows/AppFlows"; import { AppFlows } from "../flows/AppFlows";
import { AppMedia } from "../media/AppMedia"; import { AppMedia } from "../media/AppMedia";
import type { Module, ModuleBuildContext } from "./Module"; import type { Module, ModuleBuildContext, ServerEnv } from "./Module";
export type { ModuleBuildContext }; export type { ModuleBuildContext };
@@ -79,6 +80,8 @@ export type ModuleManagerOptions = {
onFirstBoot?: () => Promise<void>; onFirstBoot?: () => Promise<void>;
// base path for the hono instance // base path for the hono instance
basePath?: string; basePath?: string;
// callback after server was created
onServerInit?: (server: Hono<ServerEnv>) => void;
// doesn't perform validity checks for given/fetched config // doesn't perform validity checks for given/fetched config
trustFetched?: boolean; trustFetched?: boolean;
// runs when initial config provided on a fresh database // runs when initial config provided on a fresh database
@@ -124,15 +127,12 @@ export class ModuleManager {
__em!: EntityManager<T_INTERNAL_EM>; __em!: EntityManager<T_INTERNAL_EM>;
// ctx for modules // ctx for modules
em!: EntityManager; em!: EntityManager;
server!: Hono; server!: Hono<ServerEnv>;
emgr!: EventManager; emgr!: EventManager;
guard!: Guard; guard!: Guard;
private _version: number = 0; private _version: number = 0;
private _built = false; private _built = false;
private _fetched = false;
// @todo: keep? not doing anything with it
private readonly _booted_with?: "provided" | "partial"; private readonly _booted_with?: "provided" | "partial";
private logger = new DebugLogger(false); private logger = new DebugLogger(false);
@@ -204,10 +204,13 @@ export class ModuleManager {
} }
private rebuildServer() { private rebuildServer() {
this.server = new Hono(); this.server = new Hono<ServerEnv>();
if (this.options?.basePath) { if (this.options?.basePath) {
this.server = this.server.basePath(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 // @todo: this is a current workaround, controllers must be reworked
objectEach(this.modules, (module) => { objectEach(this.modules, (module) => {

View File

@@ -1,11 +1,11 @@
/** @jsxImportSource hono/jsx */ /** @jsxImportSource hono/jsx */
import type { App } from "App"; import type { App } from "App";
import { type ClassController, isDebug } from "core"; import { isDebug } from "core";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { Hono } from "hono";
import { html } from "hono/html"; import { html } from "hono/html";
import { Fragment } from "hono/jsx"; import { Fragment } from "hono/jsx";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions"; import * as SystemPermissions from "modules/permissions";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->"; const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -17,11 +17,13 @@ export type AdminControllerOptions = {
forceDev?: boolean | { mainPath: string }; forceDev?: boolean | { mainPath: string };
}; };
export class AdminController implements ClassController { export class AdminController extends Controller {
constructor( constructor(
private readonly app: App, private readonly app: App,
private options: AdminControllerOptions = {} private options: AdminControllerOptions = {}
) {} ) {
super();
}
get ctx() { get ctx() {
return this.app.modules.ctx(); return this.app.modules.ctx();
@@ -32,19 +34,16 @@ export class AdminController implements ClassController {
} }
private withBasePath(route: string = "") { private withBasePath(route: string = "") {
return (this.basepath + route).replace(/\/+$/, "/"); return (this.basepath + route).replace(/(?<!:)\/+/g, "/");
} }
getController(): Hono<any> { override getController() {
const hono = this.create().basePath(this.withBasePath());
const auth = this.app.module.auth; const auth = this.app.module.auth;
const configs = this.app.modules.configs(); const configs = this.app.modules.configs();
// if auth is not enabled, authenticator is undefined // if auth is not enabled, authenticator is undefined
const auth_enabled = configs.auth.enabled; const auth_enabled = configs.auth.enabled;
const hono = new Hono<{
Variables: {
html: string;
};
}>().basePath(this.withBasePath());
const authRoutes = { const authRoutes = {
root: "/", root: "/",
success: configs.auth.cookie.pathSuccess ?? "/", success: configs.auth.cookie.pathSuccess ?? "/",
@@ -80,8 +79,7 @@ export class AdminController implements ClassController {
return c.redirect(authRoutes.success); return c.redirect(authRoutes.success);
} }
const html = c.get("html"); return c.html(c.get("html")!);
return c.html(html);
}); });
hono.get(authRoutes.logout, async (c) => { hono.get(authRoutes.logout, async (c) => {
@@ -96,8 +94,7 @@ export class AdminController implements ClassController {
return c.redirect(authRoutes.login); return c.redirect(authRoutes.login);
} }
const html = c.get("html"); return c.html(c.get("html")!);
return c.html(html);
}); });
return hono; return hono;

View File

@@ -1,10 +1,11 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import type { App } from "App"; import type { App } from "App";
import type { ClassController } from "core";
import { tbValidator as tb } from "core"; import { tbValidator as tb } from "core";
import { StringEnum, Type, TypeInvalidError } from "core/utils"; 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 { import {
MODULE_NAMES, MODULE_NAMES,
type ModuleConfigs, type ModuleConfigs,
@@ -27,21 +28,20 @@ export type ConfigUpdateResponse<Key extends ModuleKey = ModuleKey> =
| ConfigUpdate<Key> | ConfigUpdate<Key>
| { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any }; | { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any };
export class SystemController implements ClassController { export class SystemController extends Controller {
constructor(private readonly app: App) {} constructor(private readonly app: App) {
super();
}
get ctx() { get ctx() {
return this.app.modules.ctx(); return this.app.modules.ctx();
} }
private registerConfigController(client: Hono<any>): void { private registerConfigController(client: Hono<any>): void {
const hono = new Hono(); const hono = this.create();
const { permission } = this.middlewares;
/*hono.use("*", async (c, next) => { hono.use(permission(SystemPermissions.configRead));
//this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead);
console.log("perm?", this.ctx.guard.hasPermission(SystemPermissions.configRead));
return next();
});*/
hono.get( hono.get(
"/:module?", "/:module?",
@@ -57,7 +57,6 @@ export class SystemController implements ClassController {
const { secrets } = c.req.valid("query"); const { secrets } = c.req.valid("query");
const { module } = c.req.valid("param"); const { module } = c.req.valid("param");
this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead);
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets); secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets);
const config = this.app.toJSON(secrets); const config = this.app.toJSON(secrets);
@@ -96,6 +95,7 @@ export class SystemController implements ClassController {
hono.post( hono.post(
"/set/:module", "/set/:module",
permission(SystemPermissions.configWrite),
tb( tb(
"query", "query",
Type.Object({ Type.Object({
@@ -107,8 +107,6 @@ export class SystemController implements ClassController {
const { force } = c.req.valid("query"); const { force } = c.req.valid("query");
const value = await c.req.json(); const value = await c.req.json();
this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite);
return await handleConfigUpdateResponse(c, async () => { return await handleConfigUpdateResponse(c, async () => {
// you must explicitly set force to override existing values // you must explicitly set force to override existing values
// because omitted values gets removed // 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) // @todo: require auth (admin)
const module = c.req.param("module") as any; const module = c.req.param("module") as any;
const value = await c.req.json(); const value = await c.req.json();
const path = c.req.param("path") as string; const path = c.req.param("path") as string;
this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite);
const moduleConfig = this.app.mutateConfig(module); const moduleConfig = this.app.mutateConfig(module);
if (moduleConfig.has(path)) { if (moduleConfig.has(path)) {
return c.json({ success: false, path, error: "Path already exists" }, { status: 400 }); 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) // @todo: require auth (admin)
const module = c.req.param("module") as any; const module = c.req.param("module") as any;
const value = await c.req.json(); const value = await c.req.json();
const path = c.req.param("path"); const path = c.req.param("path");
this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite);
return await handleConfigUpdateResponse(c, async () => { return await handleConfigUpdateResponse(c, async () => {
await this.app.mutateConfig(module).patch(path, value); await this.app.mutateConfig(module).patch(path, value);
return { 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) // @todo: require auth (admin)
const module = c.req.param("module") as any; const module = c.req.param("module") as any;
const value = await c.req.json(); const value = await c.req.json();
const path = c.req.param("path"); const path = c.req.param("path");
this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite);
return await handleConfigUpdateResponse(c, async () => { return await handleConfigUpdateResponse(c, async () => {
await this.app.mutateConfig(module).overwrite(path, value); await this.app.mutateConfig(module).overwrite(path, value);
return { 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) // @todo: require auth (admin)
const module = c.req.param("module") as any; const module = c.req.param("module") as any;
const path = c.req.param("path")!; const path = c.req.param("path")!;
this.ctx.guard.throwUnlessGranted(SystemPermissions.configWrite);
return await handleConfigUpdateResponse(c, async () => { return await handleConfigUpdateResponse(c, async () => {
await this.app.mutateConfig(module).remove(path); await this.app.mutateConfig(module).remove(path);
return { return {
@@ -211,13 +201,15 @@ export class SystemController implements ClassController {
client.route("/config", hono); client.route("/config", hono);
} }
getController(): Hono { override getController() {
const hono = new Hono(); const hono = this.create();
const { permission } = this.middlewares;
this.registerConfigController(hono); this.registerConfigController(hono);
hono.get( hono.get(
"/schema/:module?", "/schema/:module?",
permission(SystemPermissions.schemaRead),
tb( tb(
"query", "query",
Type.Object({ Type.Object({
@@ -228,7 +220,7 @@ export class SystemController implements ClassController {
async (c) => { async (c) => {
const module = c.req.param("module") as ModuleKey | undefined; const module = c.req.param("module") as ModuleKey | undefined;
const { config, secrets } = c.req.valid("query"); const { config, secrets } = c.req.valid("query");
this.ctx.guard.throwUnlessGranted(SystemPermissions.schemaRead);
config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead); config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead);
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets); secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets);
@@ -300,8 +292,7 @@ export class SystemController implements ClassController {
return c.json({ return c.json({
version: this.app.version(), version: this.app.version(),
test: 2, test: 2,
// @ts-ignore app: c.get("app").version()
app: !!c.var.app
}); });
}); });

View File

@@ -12,7 +12,9 @@ export function AuthIndex() {
config: { roles, strategies, entity_name, enabled } config: { roles, strategies, entity_name, enabled }
} = useBkndAuth(); } = useBkndAuth();
const users_entity = entity_name; 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 usersTotal = $q.data?.count ?? 0;
const rolesTotal = Object.keys(roles ?? {}).length ?? 0; const rolesTotal = Object.keys(roles ?? {}).length ?? 0;
const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0; const strategiesTotal = Object.keys(strategies ?? {}).length ?? 0;