diff --git a/app/index.html b/app/index.html
index 4807db2..2efd6e2 100644
--- a/app/index.html
+++ b/app/index.html
@@ -6,7 +6,6 @@
BKND
-
diff --git a/app/package.json b/app/package.json
index a324e8d..39c65cc 100644
--- a/app/package.json
+++ b/app/package.json
@@ -92,7 +92,7 @@
"entry": ["src/index.ts", "src/ui/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
"minify": true,
"outDir": "dist",
- "external": ["bun:test"],
+ "external": ["bun:test", "bknd/dist/manifest.json"],
"sourcemap": true,
"metafile": true,
"platform": "browser",
@@ -109,6 +109,9 @@
"react": ">=18",
"react-dom": ">=18"
},
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
@@ -165,9 +168,15 @@
"import": "./dist/adapter/bun/index.js",
"require": "./dist/adapter/bun/index.cjs"
},
+ "./adapter/node": {
+ "types": "./dist/adapter/node/index.d.ts",
+ "import": "./dist/adapter/node/index.js",
+ "require": "./dist/adapter/node/index.cjs"
+ },
"./dist/static/manifest.json": "./dist/static/.vite/manifest.json",
"./dist/styles.css": "./dist/styles.css",
- "./dist/index.html": "./dist/static/index.html"
+ "./dist/index.html": "./dist/static/index.html",
+ "./dist/manifest.json": "./dist/static/.vite/manifest.json"
},
"files": [
"dist",
diff --git a/app/src/App.ts b/app/src/App.ts
index 4d7a432..a617a0a 100644
--- a/app/src/App.ts
+++ b/app/src/App.ts
@@ -7,6 +7,7 @@ import {
type Modules
} from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions";
+import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
export type AppPlugin = (app: App) => void;
@@ -58,7 +59,7 @@ export class App {
static create(config: CreateAppConfig) {
let connection: Connection | undefined = undefined;
- if (config.connection instanceof Connection) {
+ if (Connection.isConnection(config.connection)) {
connection = config.connection;
} else if (typeof config.connection === "object") {
switch (config.connection.type) {
@@ -66,6 +67,8 @@ export class App {
connection = new LibsqlConnection(config.connection.config);
break;
}
+ } else {
+ throw new Error(`Unknown connection of type ${typeof config.connection} given.`);
}
if (!connection) {
throw new Error("Invalid connection");
@@ -79,7 +82,6 @@ export class App {
}
async build(options?: { sync?: boolean; drop?: boolean; save?: boolean }) {
- //console.log("building");
await this.modules.build();
if (options?.sync) {
@@ -136,6 +138,12 @@ export class App {
return this.modules.version();
}
+ registerAdminController(config?: AdminControllerOptions) {
+ // register admin
+ this.modules.server.route("/", new AdminController(this, config).getController());
+ return this;
+ }
+
toJSON(secrets?: boolean) {
return this.modules.toJSON(secrets);
}
diff --git a/app/src/adapter/bun/bun.adapter.ts b/app/src/adapter/bun/bun.adapter.ts
index 9c276ce..fefe6ef 100644
--- a/app/src/adapter/bun/bun.adapter.ts
+++ b/app/src/adapter/bun/bun.adapter.ts
@@ -1,15 +1,37 @@
-import { readFile } from "node:fs/promises";
import path from "node:path";
import { App, type CreateAppConfig } from "bknd";
+import { LibsqlConnection } from "bknd/data";
import { serveStatic } from "hono/bun";
-let app: App;
-export function serve(config: CreateAppConfig, distPath?: string) {
+async function getConnection(conn?: CreateAppConfig["connection"]) {
+ if (conn) {
+ if (LibsqlConnection.isConnection(conn)) {
+ return conn;
+ }
+
+ return new LibsqlConnection(conn.config);
+ }
+
+ const createClient = await import("@libsql/client/node").then((m) => m.createClient);
+ if (!createClient) {
+ throw new Error('libsql client not found, you need to install "@libsql/client/node"');
+ }
+
+ console.log("Using in-memory database");
+ return new LibsqlConnection(createClient({ url: ":memory:" }));
+}
+
+export function serve(_config: Partial = {}, distPath?: string) {
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
+ let app: App;
return async (req: Request) => {
if (!app) {
- app = App.create(config);
+ const connection = await getConnection(_config.connection);
+ app = App.create({
+ ..._config,
+ connection
+ });
app.emgr.on(
"app-built",
@@ -20,7 +42,7 @@ export function serve(config: CreateAppConfig, distPath?: string) {
root
})
);
- app.module?.server?.setAdminHtml(await readFile(root + "/index.html", "utf-8"));
+ app.registerAdminController();
},
"sync"
);
@@ -28,6 +50,6 @@ export function serve(config: CreateAppConfig, distPath?: string) {
await app.build();
}
- return app.modules.server.fetch(req);
+ return app.fetch(req);
};
}
diff --git a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
index 8798bbe..a7cb1a4 100644
--- a/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
+++ b/app/src/adapter/cloudflare/cloudflare-workers.adapter.ts
@@ -113,11 +113,10 @@ async function getFresh(config: BkndConfig, { env, html }: Context) {
"sync"
);
}
-
await app.build();
- if (config?.setAdminHtml !== false) {
- app.module.server.setAdminHtml(html);
+ if (config.setAdminHtml) {
+ app.registerAdminController({ html });
}
return app;
@@ -147,6 +146,7 @@ async function getCached(
await cache.delete(key);
return c.json({ message: "Cache cleared" });
});
+ app.registerAdminController({ html });
config.onBuilt!(app);
},
@@ -163,13 +163,13 @@ async function getCached(
);
await app.build();
- if (!cachedConfig) {
- saveConfig(app.toJSON(true));
+
+ if (config.setAdminHtml) {
+ app.registerAdminController({ html });
}
- //addAssetsRoute(app, manifest);
- if (config?.setAdminHtml !== false) {
- app.module.server.setAdminHtml(html);
+ if (!cachedConfig) {
+ saveConfig(app.toJSON(true));
}
return app;
@@ -212,10 +212,6 @@ export class DurableBkndApp extends DurableObject {
colo: context.colo
});
});
-
- if (options?.setAdminHtml !== false) {
- app.module.server.setAdminHtml(options.html);
- }
},
"sync"
);
diff --git a/app/src/adapter/node/index.ts b/app/src/adapter/node/index.ts
new file mode 100644
index 0000000..ef19c09
--- /dev/null
+++ b/app/src/adapter/node/index.ts
@@ -0,0 +1,82 @@
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+import { serve as honoServe } from "@hono/node-server";
+import { serveStatic } from "@hono/node-server/serve-static";
+import { App, type CreateAppConfig } from "bknd";
+import { LibsqlConnection } from "bknd/data";
+import type { Manifest } from "vite";
+
+async function getConnection(conn?: CreateAppConfig["connection"]) {
+ if (conn) {
+ if (LibsqlConnection.isConnection(conn)) {
+ return conn;
+ }
+
+ return new LibsqlConnection(conn.config);
+ }
+
+ const createClient = await import("@libsql/client/node").then((m) => m.createClient);
+ if (!createClient) {
+ throw new Error('libsql client not found, you need to install "@libsql/client/node"');
+ }
+
+ console.log("Using in-memory database");
+ return new LibsqlConnection(createClient({ url: ":memory:" }));
+}
+
+export type NodeAdapterOptions = {
+ relativeDistPath?: string;
+ viteManifest?: Manifest;
+ port?: number;
+ hostname?: string;
+ listener?: Parameters[1];
+};
+
+export function serve(_config: Partial = {}, options: NodeAdapterOptions = {}) {
+ const root = path.relative(
+ process.cwd(),
+ path.resolve(options.relativeDistPath ?? "./node_modules/bknd/dist", "static")
+ );
+ let app: App;
+
+ honoServe(
+ {
+ port: options.port ?? 1337,
+ hostname: options.hostname,
+ fetch: async (req: Request) => {
+ if (!app) {
+ const connection = await getConnection(_config.connection);
+ app = App.create({
+ ..._config,
+ connection
+ });
+
+ const viteManifest =
+ options.viteManifest ??
+ JSON.parse(await readFile(path.resolve(root, ".vite/manifest.json"), "utf-8"));
+
+ app.emgr.on(
+ "app-built",
+ async () => {
+ app.modules.server.get(
+ "/assets/*",
+ serveStatic({
+ root
+ })
+ );
+ app.registerAdminController({
+ viteManifest
+ });
+ },
+ "sync"
+ );
+
+ await app.build();
+ }
+
+ return app.fetch(req);
+ }
+ },
+ options.listener
+ );
+}
diff --git a/app/src/adapter/vite/vite.adapter.ts b/app/src/adapter/vite/vite.adapter.ts
index 16b5393..6d7b3cf 100644
--- a/app/src/adapter/vite/vite.adapter.ts
+++ b/app/src/adapter/vite/vite.adapter.ts
@@ -31,7 +31,7 @@ function setAppBuildListener(app: App, config: BkndConfig, html: string) {
"app-built",
async () => {
await config.onBuilt?.(app);
- app.module.server.setAdminHtml(html);
+ app.registerAdminController();
app.module.server.client.get("/assets/!*", serveStatic({ root: "./" }));
},
"sync"
diff --git a/app/src/auth/api/AuthApi.ts b/app/src/auth/api/AuthApi.ts
index 6b6ef0d..df7ffb0 100644
--- a/app/src/auth/api/AuthApi.ts
+++ b/app/src/auth/api/AuthApi.ts
@@ -34,7 +34,7 @@ export class AuthApi extends ModuleApi {
}
async strategies() {
- return this.get<{ strategies: AppAuthSchema["strategies"] }>(["strategies"]);
+ return this.get>(["strategies"]);
}
async logout() {}
diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts
index b9eb02f..a616bd0 100644
--- a/app/src/auth/api/AuthController.ts
+++ b/app/src/auth/api/AuthController.ts
@@ -1,31 +1,29 @@
import type { AppAuth } from "auth";
import type { ClassController } from "core";
import { Hono, type MiddlewareHandler } from "hono";
+import * as SystemPermissions from "modules/permissions";
export class AuthController implements ClassController {
constructor(private auth: AppAuth) {}
- getMiddleware: MiddlewareHandler = async (c, next) => {
- // @todo: consider adding app name to the payload, because user is not refetched
+ get guard() {
+ return this.auth.ctx.guard;
+ }
- //try {
+ getMiddleware: MiddlewareHandler = async (c, next) => {
+ let token: string | undefined;
if (c.req.raw.headers.has("Authorization")) {
const bearerHeader = String(c.req.header("Authorization"));
- const token = bearerHeader.replace("Bearer ", "");
- const verified = await this.auth.authenticator.verify(token);
+ token = bearerHeader.replace("Bearer ", "");
+ }
+ if (token) {
// @todo: don't extract user from token, but from the database or cache
+ await this.auth.authenticator.verify(token);
this.auth.ctx.guard.setUserContext(this.auth.authenticator.getUser());
- /*console.log("jwt verified?", {
- verified,
- auth: this.auth.authenticator.isUserLoggedIn()
- });*/
} else {
this.auth.authenticator.__setUserNull();
}
- /* } catch (e) {
- this.auth.authenticator.__setUserNull();
- }*/
await next();
};
@@ -49,7 +47,8 @@ export class AuthController implements ClassController {
});
hono.get("/strategies", async (c) => {
- return c.json({ strategies: this.auth.toJSON(false).strategies });
+ const { strategies, basepath } = this.auth.toJSON(false);
+ return c.json({ strategies, basepath });
});
return hono;
diff --git a/app/src/auth/authorize/Guard.ts b/app/src/auth/authorize/Guard.ts
index caae555..f40348d 100644
--- a/app/src/auth/authorize/Guard.ts
+++ b/app/src/auth/authorize/Guard.ts
@@ -11,6 +11,8 @@ export type GuardConfig = {
enabled?: boolean;
};
+const debug = false;
+
export class Guard {
permissions: Permission[];
user?: GuardUserContext;
@@ -96,12 +98,12 @@ export class Guard {
if (this.user && typeof this.user.role === "string") {
const role = this.roles?.find((role) => role.name === this.user?.role);
if (role) {
- console.log("guard: role found", this.user.role);
+ debug && console.log("guard: role found", this.user.role);
return role;
}
}
- console.log("guard: role not found", this.user, this.user?.role);
+ debug && console.log("guard: role not found", this.user, this.user?.role);
return this.getDefaultRole();
}
@@ -109,10 +111,14 @@ export class Guard {
return this.roles?.find((role) => role.is_default);
}
+ isEnabled() {
+ return this.config?.enabled === true;
+ }
+
hasPermission(permission: Permission): boolean;
hasPermission(name: string): boolean;
hasPermission(permissionOrName: Permission | string): boolean {
- if (this.config?.enabled !== true) {
+ if (!this.isEnabled()) {
//console.log("guard not enabled, allowing");
return true;
}
@@ -126,10 +132,10 @@ export class Guard {
const role = this.getUserRole();
if (!role) {
- console.log("guard: role not found, denying");
+ debug && console.log("guard: role not found, denying");
return false;
} else if (role.implicit_allow === true) {
- console.log("guard: role implicit allow, allowing");
+ debug && console.log("guard: role implicit allow, allowing");
return true;
}
@@ -137,11 +143,12 @@ export class Guard {
(rolePermission) => rolePermission.permission.name === name
);
- console.log("guard: rolePermission, allowing?", {
- permission: name,
- role: role.name,
- allowing: !!rolePermission
- });
+ debug &&
+ console.log("guard: rolePermission, allowing?", {
+ permission: name,
+ role: role.name,
+ allowing: !!rolePermission
+ });
return !!rolePermission;
}
diff --git a/app/src/cli/commands/config.ts b/app/src/cli/commands/config.ts
index 461ee78..3d853ab 100644
--- a/app/src/cli/commands/config.ts
+++ b/app/src/cli/commands/config.ts
@@ -7,6 +7,7 @@ export const config: CliCommand = (program) => {
.description("get default config")
.option("--pretty", "pretty print")
.action((options) => {
- console.log(getDefaultConfig(options.pretty));
+ const config = getDefaultConfig();
+ console.log(options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config));
});
};
diff --git a/app/src/cli/commands/run/run.ts b/app/src/cli/commands/run/run.ts
index 14730f0..dbb5cad 100644
--- a/app/src/cli/commands/run/run.ts
+++ b/app/src/cli/commands/run/run.ts
@@ -1,9 +1,9 @@
import type { Config } from "@libsql/client/node";
import { App } from "App";
import type { BkndConfig } from "adapter";
+import type { CliCommand } from "cli/types";
import { Option } from "commander";
import type { Connection } from "data";
-import type { CliCommand } from "../../types";
import {
PLATFORMS,
type Platform,
@@ -48,14 +48,13 @@ type MakeAppConfig = {
};
async function makeApp(config: MakeAppConfig) {
- const html = await getHtml();
const app = new App(config.connection);
app.emgr.on(
"app-built",
async () => {
await attachServeStatic(app, config.server?.platform ?? "node");
- app.module.server.setAdminHtml(html);
+ app.registerAdminController({ html: await getHtml() });
if (config.onBuilt) {
await config.onBuilt(app);
@@ -70,14 +69,13 @@ async function makeApp(config: MakeAppConfig) {
export async function makeConfigApp(config: BkndConfig, platform?: Platform) {
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
- const html = await getHtml();
const app = App.create(appConfig);
app.emgr.on(
"app-built",
async () => {
await attachServeStatic(app, platform ?? "node");
- app.module.server.setAdminHtml(html);
+ app.registerAdminController({ html: await getHtml() });
if (config.onBuilt) {
await config.onBuilt(app);
diff --git a/app/src/cli/commands/schema.ts b/app/src/cli/commands/schema.ts
index 8c59d7e..13c1c1e 100644
--- a/app/src/cli/commands/schema.ts
+++ b/app/src/cli/commands/schema.ts
@@ -7,6 +7,7 @@ export const schema: CliCommand = (program) => {
.description("get schema")
.option("--pretty", "pretty print")
.action((options) => {
- console.log(getDefaultSchema(options.pretty));
+ const schema = getDefaultSchema();
+ console.log(options.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema));
});
};
diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts
index 418e8ae..3585b16 100644
--- a/app/src/data/api/DataController.ts
+++ b/app/src/data/api/DataController.ts
@@ -15,7 +15,7 @@ import {
import { Hono } from "hono";
import type { Handler } from "hono/types";
import type { ModuleBuildContext } from "modules";
-import { AppData } from "../AppData";
+import * as SystemPermissions from "modules/permissions";
import { type AppDataConfig, FIELDS } from "../data-schema";
export class DataController implements ClassController {
@@ -89,12 +89,10 @@ export class DataController implements ClassController {
return func;
}
- // add timing
- /*hono.use("*", async (c, next) => {
- startTime(c, "data");
+ hono.use("*", async (c, next) => {
+ this.ctx.guard.throwUnlessGranted(SystemPermissions.api);
await next();
- endTime(c, "data");
- });*/
+ });
// info
hono.get(
diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts
index b3f7e10..bc97ff0 100644
--- a/app/src/data/connection/Connection.ts
+++ b/app/src/data/connection/Connection.ts
@@ -42,6 +42,7 @@ export type DbFunctions = {
};
export abstract class Connection {
+ cls = "bknd:connection";
kysely: Kysely;
constructor(
@@ -52,6 +53,15 @@ export abstract class Connection {
this.kysely = kysely;
}
+ /**
+ * This is a helper function to manage Connection classes
+ * coming from different places
+ * @param conn
+ */
+ static isConnection(conn: any): conn is Connection {
+ return conn?.cls === "bknd:connection";
+ }
+
getIntrospector(): ConnectionIntrospector {
return this.kysely.introspection as ConnectionIntrospector;
}
diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts
index 353d3a9..674d7e2 100644
--- a/app/src/data/entities/EntityManager.ts
+++ b/app/src/data/entities/EntityManager.ts
@@ -36,7 +36,7 @@ export class EntityManager {
relations.forEach((relation) => this.addRelation(relation));
indices.forEach((index) => this.addIndex(index));
- if (!(connection instanceof Connection)) {
+ if (!Connection.isConnection(connection)) {
throw new UnableToConnectException("");
}
diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts
index 85c6ea5..51a1768 100644
--- a/app/src/modules/ModuleManager.ts
+++ b/app/src/modules/ModuleManager.ts
@@ -425,19 +425,19 @@ export class ModuleManager {
}
}
-export function getDefaultSchema(pretty = false) {
+export function getDefaultSchema() {
const schema = {
type: "object",
...transformObject(MODULES, (module) => module.prototype.getSchema())
};
- return JSON.stringify(schema, null, pretty ? 2 : undefined);
+ return schema as any;
}
-export function getDefaultConfig(pretty = false): ModuleConfigs {
+export function getDefaultConfig(): ModuleConfigs {
const config = transformObject(MODULES, (module) => {
return Default(module.prototype.getSchema(), {});
});
- return JSON.stringify(config, null, pretty ? 2 : undefined) as any;
+ return config as any;
}
diff --git a/app/src/modules/permissions/index.ts b/app/src/modules/permissions/index.ts
index 8b9cb9b..820b3ec 100644
--- a/app/src/modules/permissions/index.ts
+++ b/app/src/modules/permissions/index.ts
@@ -1,5 +1,7 @@
import { Permission } from "core";
+export const admin = new Permission("system.admin");
+export const api = new Permission("system.api");
export const configRead = new Permission("system.config.read");
export const configReadSecrets = new Permission("system.config.read.secrets");
export const configWrite = new Permission("system.config.write");
diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx
new file mode 100644
index 0000000..e600d9d
--- /dev/null
+++ b/app/src/modules/server/AdminController.tsx
@@ -0,0 +1,104 @@
+/** @jsxImportSource hono/jsx */
+
+import type { App } from "App";
+import { type ClassController, isDebug } from "core";
+import { Hono } from "hono";
+import { html, raw } from "hono/html";
+import { Fragment } from "hono/jsx";
+import * as SystemPermissions from "modules/permissions";
+import type { Manifest } from "vite";
+
+const viteInject = `
+import RefreshRuntime from "/@react-refresh"
+RefreshRuntime.injectIntoGlobalHook(window)
+window.$RefreshReg$ = () => {}
+window.$RefreshSig$ = () => (type) => type
+window.__vite_plugin_react_preamble_installed__ = true
+`;
+
+export type AdminControllerOptions = {
+ html?: string;
+ viteManifest?: Manifest;
+};
+
+export class AdminController implements ClassController {
+ constructor(
+ private readonly app: App,
+ private options: AdminControllerOptions = {}
+ ) {}
+
+ get ctx() {
+ return this.app.modules.ctx();
+ }
+
+ getController(): Hono {
+ const hono = new Hono();
+ const configs = this.app.modules.configs();
+ const basepath = (String(configs.server.admin.basepath) + "/").replace(/\/+$/, "/");
+
+ this.ctx.server.get(basepath + "*", async (c) => {
+ if (this.options.html) {
+ return c.html(this.options.html);
+ }
+
+ // @todo: implement guard redirect once cookie sessions arrive
+
+ const isProd = !isDebug();
+ let script: string | undefined;
+ let css: string[] = [];
+
+ if (isProd) {
+ const manifest: Manifest = this.options.viteManifest
+ ? this.options.viteManifest
+ : isProd
+ ? // @ts-ignore cases issues when building types
+ await import("bknd/dist/manifest.json", { assert: { type: "json" } }).then(
+ (m) => m.default
+ )
+ : {};
+ //console.log("manifest", manifest, manifest["index.html"]);
+ const entry = Object.values(manifest).find((f: any) => f.isEntry === true);
+ if (!entry) {
+ // do something smart
+ return;
+ }
+
+ script = "/" + entry.file;
+ css = entry.css?.map((c: string) => "/" + c) ?? [];
+ }
+
+ return c.html(
+
+
+
+
+ BKND
+ {isProd ? (
+
+
+ {css.map((c) => (
+
+ ))}
+
+ ) : (
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: I know what I do here :) */}
+
+
+
+ )}
+
+
+
+ {!isProd && }
+
+
+ );
+ });
+
+ return hono;
+ }
+}
diff --git a/app/src/modules/server/AppController.ts b/app/src/modules/server/AppController.ts
deleted file mode 100644
index 2562814..0000000
--- a/app/src/modules/server/AppController.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import type { ClassController } from "core";
-import { SimpleRenderer } from "core";
-import { FetchTask, Flow, LogTask } from "flows";
-import { Hono } from "hono";
-import { endTime, startTime } from "hono/timing";
-import type { App } from "../../App";
-
-export class AppController implements ClassController {
- constructor(
- private readonly app: App,
- private config: any = {}
- ) {}
-
- getController(): Hono {
- const hono = new Hono();
-
- // @todo: add test endpoints
-
- hono
- .get("/config", (c) => {
- return c.json(this.app.toJSON());
- })
- .get("/ping", (c) => {
- //console.log("c", c);
- try {
- // @ts-ignore @todo: fix with env
- const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
- const cf = {
- colo: context.colo,
- city: context.city,
- postal: context.postalCode,
- region: context.region,
- regionCode: context.regionCode,
- continent: context.continent,
- country: context.country,
- eu: context.isEUCountry,
- lat: context.latitude,
- lng: context.longitude,
- timezone: context.timezone
- };
- return c.json({ pong: true, cf, another: 6 });
- } catch (e) {
- return c.json({ pong: true, cf: null });
- }
- });
-
- // test endpoints
- if (this.config?.registerTest) {
- hono.get("/test/kv", async (c) => {
- // @ts-ignore
- const cache = c.env!.CACHE as KVNamespace;
- startTime(c, "kv-get");
- const value: any = await cache.get("count");
- endTime(c, "kv-get");
- console.log("value", value);
- startTime(c, "kv-put");
- if (!value) {
- await cache.put("count", "1");
- } else {
- await cache.put("count", (Number(value) + 1).toString());
- }
- endTime(c, "kv-put");
-
- let cf: any = {};
- // @ts-ignore
- if ("cf" in c.req.raw) {
- cf = {
- // @ts-ignore
- colo: c.req.raw.cf?.colo
- };
- }
-
- return c.json({ pong: true, value, cf });
- });
-
- hono.get("/test/flow", async (c) => {
- const first = new LogTask("Task 0");
- const second = new LogTask("Task 1");
- const third = new LogTask("Task 2", { delay: 250 });
- const fourth = new FetchTask("Fetch Something", {
- url: "https://jsonplaceholder.typicode.com/todos/1"
- });
- const fifth = new LogTask("Task 4"); // without connection
-
- const flow = new Flow("flow", [first, second, third, fourth, fifth]);
- flow.task(first).asInputFor(second);
- flow.task(first).asInputFor(third);
- flow.task(fourth).asOutputFor(third);
-
- flow.setRespondingTask(fourth);
-
- const execution = flow.createExecution();
- await execution.start();
-
- const results = flow.tasks.map((t) => t.toJSON());
-
- return c.json({ results, response: execution.getResponse() });
- });
-
- hono.get("/test/template", async (c) => {
- const renderer = new SimpleRenderer({ var: 123 });
- const template = "Variable: {{ var }}";
-
- return c.text(await renderer.render(template));
- });
- }
-
- return hono;
- }
-}
diff --git a/app/src/modules/server/AppServer.ts b/app/src/modules/server/AppServer.ts
index 9df8e02..3ce7774 100644
--- a/app/src/modules/server/AppServer.ts
+++ b/app/src/modules/server/AppServer.ts
@@ -4,6 +4,7 @@ import { Hono } from "hono";
import { cors } from "hono/cors";
import { timing } from "hono/timing";
import { Module } from "modules/Module";
+import * as SystemPermissions from "modules/permissions";
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
export const serverConfigSchema = Type.Object(
@@ -49,7 +50,7 @@ export type AppServerConfig = Static;
}*/
export class AppServer extends Module {
- private admin_html?: string;
+ //private admin_html?: string;
override getRestrictedPaths() {
return [];
@@ -64,12 +65,6 @@ export class AppServer extends Module {
}
override async build() {
- //this.client.use(timing());
-
- /*this.client.use("*", async (c, next) => {
- console.log(`[${c.req.method}] ${c.req.url}`);
- await next();
- });*/
this.client.use(
"*",
cors({
@@ -79,18 +74,6 @@ export class AppServer extends Module {
})
);
- /*this.client.use(async (c, next) => {
- c.res.headers.set("X-Powered-By", "BKND");
- try {
- c.res.headers.set("X-Colo", c.req.raw.cf.colo);
- } catch (e) {}
- await next();
- });
- this.client.use(async (c, next) => {
- console.log(`[${c.req.method}] ${c.req.url}`);
- await next();
- });*/
-
this.client.onError((err, c) => {
//throw err;
console.error(err);
@@ -124,18 +107,31 @@ export class AppServer extends Module {
this.setBuilt();
}
- setAdminHtml(html: string) {
+ /*setAdminHtml(html: string) {
this.admin_html = html;
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
+ const allowed_prefix = basepath + "auth";
+ const login_path = basepath + "auth/login";
+
this.client.get(basepath + "*", async (c, next) => {
+ const path = new URL(c.req.url).pathname;
+ if (!path.startsWith(allowed_prefix)) {
+ console.log("guard check permissions");
+ try {
+ this.ctx.guard.throwUnlessGranted(SystemPermissions.admin);
+ } catch (e) {
+ return c.redirect(login_path);
+ }
+ }
+
return c.html(this.admin_html!);
});
}
getAdminHtml() {
return this.admin_html;
- }
+ }*/
override toJSON(secrets?: boolean) {
return this.config;
diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts
index d425a22..b3e0949 100644
--- a/app/src/modules/server/SystemController.ts
+++ b/app/src/modules/server/SystemController.ts
@@ -98,7 +98,12 @@ export class SystemController implements ClassController {
// you must explicitly set force to override existing values
// because omitted values gets removed
if (force === true) {
- await this.app.mutateConfig(module).set(value);
+ // force overwrite defined keys
+ const newConfig = {
+ ...this.app.module[module].config,
+ ...value
+ };
+ await this.app.mutateConfig(module).set(newConfig);
} else {
await this.app.mutateConfig(module).patch("", value);
}
@@ -287,7 +292,7 @@ export class SystemController implements ClassController {
hono.get("/openapi.json", async (c) => {
//const config = this.app.toJSON();
- const config = JSON.parse(getDefaultConfig() as any);
+ const config = getDefaultConfig();
return c.json(generateOpenAPI(config));
});
diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx
index 7cd99c2..cacdd6a 100644
--- a/app/src/ui/client/BkndProvider.tsx
+++ b/app/src/ui/client/BkndProvider.tsx
@@ -1,4 +1,6 @@
-import { createContext, useContext, useEffect, useRef, useState } from "react";
+import { notifications } from "@mantine/notifications";
+import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
+import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
import { useClient } from "./ClientProvider";
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
@@ -22,18 +24,47 @@ export function BkndProvider({
children
}: { includeSecrets?: boolean; children: any }) {
const [withSecrets, setWithSecrets] = useState(includeSecrets);
- const [schema, setSchema] = useState();
+ const [schema, setSchema] =
+ useState>();
+ const [fetched, setFetched] = useState(false);
+ const errorShown = useRef();
const client = useClient();
async function fetchSchema(_includeSecrets: boolean = false) {
if (withSecrets) return;
- const { body } = await client.api.system.readSchema({
+ const { body, res } = await client.api.system.readSchema({
config: true,
secrets: _includeSecrets
});
- console.log("--schema fetched", body);
- setSchema(body as any);
- setWithSecrets(_includeSecrets);
+
+ if (!res.ok) {
+ if (errorShown.current) return;
+ errorShown.current = true;
+ notifications.show({
+ title: "Failed to fetch schema",
+ // @ts-ignore
+ message: body.error,
+ color: "red",
+ position: "top-right",
+ autoClose: false,
+ withCloseButton: true
+ });
+ }
+
+ const schema = res.ok
+ ? body
+ : ({
+ version: 0,
+ schema: getDefaultSchema(),
+ config: getDefaultConfig(),
+ permissions: []
+ } as any);
+
+ startTransition(() => {
+ setSchema(schema);
+ setWithSecrets(_includeSecrets);
+ setFetched(true);
+ });
}
async function requireSecrets() {
@@ -46,8 +77,8 @@ export function BkndProvider({
fetchSchema(includeSecrets);
}, []);
- if (!schema?.schema) return null;
- const app = new AppReduced(schema.config as any);
+ if (!fetched || !schema) return null;
+ const app = new AppReduced(schema?.config as any);
const actions = getSchemaActions({ client, setSchema });
@@ -64,20 +95,3 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
return ctx;
}
-
-/*
-type UseSchemaForType = {
- version: number;
- schema: ModuleSchemas[Key];
- config: ModuleConfigs[Key];
-};
-
-export function useSchemaFor(module: Key): UseSchemaForType {
- //const app = useApp();
- const { version, schema, config } = useSchema();
- return {
- version,
- schema: schema[module],
- config: config[module]
- };
-}*/
diff --git a/app/src/ui/client/schema/auth/use-auth.ts b/app/src/ui/client/schema/auth/use-auth.ts
index 42276da..12f01b4 100644
--- a/app/src/ui/client/schema/auth/use-auth.ts
+++ b/app/src/ui/client/schema/auth/use-auth.ts
@@ -82,11 +82,11 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
};
};
-export const useAuthStrategies = (options?: { baseUrl?: string }): {
- strategies: AppAuthSchema["strategies"];
+type AuthStrategyData = Pick;
+export const useAuthStrategies = (options?: { baseUrl?: string }): Partial & {
loading: boolean;
} => {
- const [strategies, setStrategies] = useState();
+ const [data, setData] = useState();
const ctxBaseUrl = useBaseUrl();
const api = new Api({
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl,
@@ -98,10 +98,10 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): {
const res = await api.auth.strategies();
console.log("res", res);
if (res.res.ok) {
- setStrategies(res.body.strategies);
+ setData(res.body);
}
})();
}, [options?.baseUrl]);
- return { strategies, loading: !strategies };
+ return { strategies: data?.strategies, basepath: data?.basepath, loading: !data };
};
diff --git a/app/src/ui/client/utils/AppQueryClient.ts b/app/src/ui/client/utils/AppQueryClient.ts
index 76b5896..b5aa3c0 100644
--- a/app/src/ui/client/utils/AppQueryClient.ts
+++ b/app/src/ui/client/utils/AppQueryClient.ts
@@ -50,14 +50,18 @@ export class AppQueryClient {
return this.api.getAuthState();
},
verify: async () => {
- console.log("verifiying");
- const res = await this.api.auth.me();
- console.log("verifying result", res);
- if (!res.res.ok) {
+ try {
+ //console.log("verifiying");
+ const res = await this.api.auth.me();
+ //console.log("verifying result", res);
+ if (!res.res.ok || !res.body.user) {
+ throw new Error();
+ }
+
+ this.api.markAuthVerified(true);
+ } catch (e) {
this.api.markAuthVerified(false);
this.api.updateToken(undefined);
- } else {
- this.api.markAuthVerified(true);
}
}
};
diff --git a/app/src/ui/client/utils/AppReduced.ts b/app/src/ui/client/utils/AppReduced.ts
index 3c95934..0abc27b 100644
--- a/app/src/ui/client/utils/AppReduced.ts
+++ b/app/src/ui/client/utils/AppReduced.ts
@@ -8,6 +8,7 @@ export type AppType = ReturnType;
/**
* Reduced version of the App class for frontend use
+ * @todo: remove this class
*/
export class AppReduced {
// @todo: change to record
@@ -16,7 +17,7 @@ export class AppReduced {
private _flows: Flow[] = [];
constructor(protected appJson: AppType) {
- console.log("received appjson", appJson);
+ //console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return AppData.constructEntity(name, entity);
diff --git a/app/src/ui/routes/auth/auth.login.tsx b/app/src/ui/routes/auth/auth.login.tsx
index 93dc36d..414397b 100644
--- a/app/src/ui/routes/auth/auth.login.tsx
+++ b/app/src/ui/routes/auth/auth.login.tsx
@@ -25,7 +25,7 @@ export function AuthLogin() {
//console.log("search", token, "/api/auth/google?redirect=" + window.location.href);
const auth = useAuth();
- const { strategies, loading } = useAuthStrategies();
+ const { strategies, basepath, loading } = useAuthStrategies();
const [error, setError] = useState(null);
useEffect(() => {
@@ -92,7 +92,7 @@ export function AuthLogin() {
variant="outline"
className="justify-center"
onClick={() => {
- window.location.href = `/api/auth/${name}/login?redirect=${window.location.href}`;
+ window.location.href = `${basepath}/${name}/login?redirect=${window.location.href}`;
}}
>
Continue with {ucFirstAllSnakeToPascalWithSpaces(oauth.name)}
diff --git a/app/tsconfig.json b/app/tsconfig.json
index bdefd64..305b414 100644
--- a/app/tsconfig.json
+++ b/app/tsconfig.json
@@ -35,5 +35,5 @@
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"],
- "exclude": ["node_modules", "dist/**/*", "../examples/bun"]
+ "exclude": ["node_modules", "./dist/**/*", "../examples/bun"]
}
diff --git a/app/tsup.adapters.ts b/app/tsup.adapters.ts
index 81f0db3..a97712c 100644
--- a/app/tsup.adapters.ts
+++ b/app/tsup.adapters.ts
@@ -46,3 +46,9 @@ await build({
await build({
...baseConfig("bun")
});
+
+await build({
+ ...baseConfig("node"),
+ format: ["esm", "cjs"],
+ platform: "node"
+});
diff --git a/app/vite.dev.ts b/app/vite.dev.ts
index 4186b6e..641ec59 100644
--- a/app/vite.dev.ts
+++ b/app/vite.dev.ts
@@ -1,4 +1,3 @@
-import { readFile } from "node:fs/promises";
import { serveStatic } from "@hono/node-server/serve-static";
import { createClient } from "@libsql/client/node";
import { App, type BkndConfig, type CreateAppConfig } from "./src";
@@ -6,61 +5,6 @@ import { LibsqlConnection } from "./src/data";
import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
import { registries } from "./src/modules/registries";
-async function getHtml() {
- return readFile("index.html", "utf8");
-}
-function addViteScripts(html: string) {
- return html.replace(
- "",
- `
-
-`
- );
-}
-
-function createApp(config: BkndConfig, env: any) {
- const create_config = typeof config.app === "function" ? config.app(env) : config.app;
- return App.create(create_config as CreateAppConfig);
-}
-
-function setAppBuildListener(app: App, config: BkndConfig, html: string) {
- app.emgr.on(
- "app-built",
- async () => {
- await config.onBuilt?.(app as any);
- app.module.server.setAdminHtml(html);
- app.module.server.client.get("/assets/!*", serveStatic({ root: "./" }));
- },
- "sync"
- );
-}
-
-export async function serveFresh(config: BkndConfig, _html?: string) {
- let html = _html;
- if (!html) {
- html = await getHtml();
- }
-
- html = addViteScripts(html);
-
- return {
- async fetch(request: Request, env: any) {
- const app = createApp(config, env);
-
- setAppBuildListener(app, config, html);
- await app.build();
-
- return app.fetch(request, env);
- }
- };
-}
-
registries.media.add("local", {
cls: StorageLocalAdapter,
schema: StorageLocalAdapter.prototype.getSchema()
@@ -72,11 +16,35 @@ const connection = new LibsqlConnection(
})
);
-const app = await serveFresh({
+function createApp(config: BkndConfig, env: any) {
+ const create_config = typeof config.app === "function" ? config.app(env) : config.app;
+ return App.create(create_config as CreateAppConfig);
+}
+
+export async function serveFresh(config: BkndConfig) {
+ return {
+ async fetch(request: Request, env: any) {
+ const app = createApp(config, env);
+
+ app.emgr.on(
+ "app-built",
+ async () => {
+ await config.onBuilt?.(app as any);
+ app.registerAdminController();
+ app.module.server.client.get("/assets/*", serveStatic({ root: "./" }));
+ },
+ "sync"
+ );
+ await app.build();
+
+ return app.fetch(request, env);
+ }
+ };
+}
+
+export default await serveFresh({
app: {
connection
},
setAdminHtml: true
});
-
-export default app;