mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
reworked html serving, added new permissions for api/auth, updated adapters
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
<title>BKND</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/ui/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<DB> = (app: App<DB>) => void;
|
||||
@@ -58,7 +59,7 @@ export class App<DB = any> {
|
||||
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<DB = any> {
|
||||
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<DB = any> {
|
||||
}
|
||||
|
||||
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<DB = any> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<CreateAppConfig> = {}, 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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
82
app/src/adapter/node/index.ts
Normal file
82
app/src/adapter/node/index.ts
Normal file
@@ -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<typeof honoServe>[1];
|
||||
};
|
||||
|
||||
export function serve(_config: Partial<CreateAppConfig> = {}, 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
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -34,7 +34,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
||||
}
|
||||
|
||||
async strategies() {
|
||||
return this.get<{ strategies: AppAuthSchema["strategies"] }>(["strategies"]);
|
||||
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
||||
}
|
||||
|
||||
async logout() {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -42,6 +42,7 @@ export type DbFunctions = {
|
||||
};
|
||||
|
||||
export abstract class Connection {
|
||||
cls = "bknd:connection";
|
||||
kysely: Kysely<any>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class EntityManager<DB> {
|
||||
relations.forEach((relation) => this.addRelation(relation));
|
||||
indices.forEach((index) => this.addIndex(index));
|
||||
|
||||
if (!(connection instanceof Connection)) {
|
||||
if (!Connection.isConnection(connection)) {
|
||||
throw new UnableToConnectException("");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
104
app/src/modules/server/AdminController.tsx
Normal file
104
app/src/modules/server/AdminController.tsx
Normal file
@@ -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(
|
||||
<html lang="en" class={configs.server.admin.color_scheme ?? "light"}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<title>BKND</title>
|
||||
{isProd ? (
|
||||
<Fragment>
|
||||
<script type="module" CrossOrigin src={script} />
|
||||
{css.map((c) => (
|
||||
<link rel="stylesheet" CrossOrigin href={c} key={c} />
|
||||
))}
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: I know what I do here :) */}
|
||||
<script type="module" dangerouslySetInnerHTML={{ __html: viteInject }} />
|
||||
<script type="module" src="/@vite/client" />
|
||||
</Fragment>
|
||||
)}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" />
|
||||
{!isProd && <script type="module" src="/src/ui/main.tsx" />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
});
|
||||
|
||||
return hono;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<typeof serverConfigSchema>;
|
||||
}*/
|
||||
|
||||
export class AppServer extends Module<typeof serverConfigSchema> {
|
||||
private admin_html?: string;
|
||||
//private admin_html?: string;
|
||||
|
||||
override getRestrictedPaths() {
|
||||
return [];
|
||||
@@ -64,12 +65,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
||||
}
|
||||
|
||||
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<typeof serverConfigSchema> {
|
||||
})
|
||||
);
|
||||
|
||||
/*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<typeof serverConfigSchema> {
|
||||
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;
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
|
||||
@@ -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<boolean>(includeSecrets);
|
||||
const [schema, setSchema] = useState<BkndContext>();
|
||||
const [schema, setSchema] =
|
||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
|
||||
const [fetched, setFetched] = useState(false);
|
||||
const errorShown = useRef<boolean>();
|
||||
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<Key extends keyof ModuleSchemas> = {
|
||||
version: number;
|
||||
schema: ModuleSchemas[Key];
|
||||
config: ModuleConfigs[Key];
|
||||
};
|
||||
|
||||
export function useSchemaFor<Key extends keyof ModuleConfigs>(module: Key): UseSchemaForType<Key> {
|
||||
//const app = useApp();
|
||||
const { version, schema, config } = useSchema();
|
||||
return {
|
||||
version,
|
||||
schema: schema[module],
|
||||
config: config[module]
|
||||
};
|
||||
}*/
|
||||
|
||||
@@ -82,11 +82,11 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useAuthStrategies = (options?: { baseUrl?: string }): {
|
||||
strategies: AppAuthSchema["strategies"];
|
||||
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
|
||||
export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthStrategyData> & {
|
||||
loading: boolean;
|
||||
} => {
|
||||
const [strategies, setStrategies] = useState<AppAuthSchema["strategies"]>();
|
||||
const [data, setData] = useState<AuthStrategyData>();
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AppType = ReturnType<App["toJSON"]>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -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<string | null>(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)}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"],
|
||||
"exclude": ["node_modules", "dist/**/*", "../examples/bun"]
|
||||
"exclude": ["node_modules", "./dist/**/*", "../examples/bun"]
|
||||
}
|
||||
|
||||
@@ -46,3 +46,9 @@ await build({
|
||||
await build({
|
||||
...baseConfig("bun")
|
||||
});
|
||||
|
||||
await build({
|
||||
...baseConfig("node"),
|
||||
format: ["esm", "cjs"],
|
||||
platform: "node"
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
"<head>",
|
||||
`<script type="module">
|
||||
import RefreshRuntime from "/@react-refresh"
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
<script type="module" src="/@vite/client"></script>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user