mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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>
|
<title>BKND</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/ui/main.tsx"></script>
|
<script type="module" src="/src/ui/main.tsx"></script>
|
||||||
</body>
|
</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"],
|
"entry": ["src/index.ts", "src/ui/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
|
||||||
"minify": true,
|
"minify": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"external": ["bun:test"],
|
"external": ["bun:test", "bknd/dist/manifest.json"],
|
||||||
"sourcemap": true,
|
"sourcemap": true,
|
||||||
"metafile": true,
|
"metafile": true,
|
||||||
"platform": "browser",
|
"platform": "browser",
|
||||||
@@ -109,6 +109,9 @@
|
|||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
},
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -165,9 +168,15 @@
|
|||||||
"import": "./dist/adapter/bun/index.js",
|
"import": "./dist/adapter/bun/index.js",
|
||||||
"require": "./dist/adapter/bun/index.cjs"
|
"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/static/manifest.json": "./dist/static/.vite/manifest.json",
|
||||||
"./dist/styles.css": "./dist/styles.css",
|
"./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": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type Modules
|
type Modules
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
|
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
|
||||||
import { SystemController } from "modules/server/SystemController";
|
import { SystemController } from "modules/server/SystemController";
|
||||||
|
|
||||||
export type AppPlugin<DB> = (app: App<DB>) => void;
|
export type AppPlugin<DB> = (app: App<DB>) => void;
|
||||||
@@ -58,7 +59,7 @@ export class App<DB = any> {
|
|||||||
static create(config: CreateAppConfig) {
|
static create(config: CreateAppConfig) {
|
||||||
let connection: Connection | undefined = undefined;
|
let connection: Connection | undefined = undefined;
|
||||||
|
|
||||||
if (config.connection instanceof Connection) {
|
if (Connection.isConnection(config.connection)) {
|
||||||
connection = config.connection;
|
connection = config.connection;
|
||||||
} else if (typeof config.connection === "object") {
|
} else if (typeof config.connection === "object") {
|
||||||
switch (config.connection.type) {
|
switch (config.connection.type) {
|
||||||
@@ -66,6 +67,8 @@ export class App<DB = any> {
|
|||||||
connection = new LibsqlConnection(config.connection.config);
|
connection = new LibsqlConnection(config.connection.config);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown connection of type ${typeof config.connection} given.`);
|
||||||
}
|
}
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error("Invalid connection");
|
throw new Error("Invalid connection");
|
||||||
@@ -79,7 +82,6 @@ export class App<DB = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async build(options?: { sync?: boolean; drop?: boolean; save?: boolean }) {
|
async build(options?: { sync?: boolean; drop?: boolean; save?: boolean }) {
|
||||||
//console.log("building");
|
|
||||||
await this.modules.build();
|
await this.modules.build();
|
||||||
|
|
||||||
if (options?.sync) {
|
if (options?.sync) {
|
||||||
@@ -136,6 +138,12 @@ export class App<DB = any> {
|
|||||||
return this.modules.version();
|
return this.modules.version();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
|
// register admin
|
||||||
|
this.modules.server.route("/", new AdminController(this, config).getController());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(secrets?: boolean) {
|
toJSON(secrets?: boolean) {
|
||||||
return this.modules.toJSON(secrets);
|
return this.modules.toJSON(secrets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
import { App, type CreateAppConfig } from "bknd";
|
||||||
|
import { LibsqlConnection } from "bknd/data";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
|
||||||
let app: App;
|
async function getConnection(conn?: CreateAppConfig["connection"]) {
|
||||||
export function serve(config: CreateAppConfig, distPath?: string) {
|
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");
|
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
|
||||||
|
let app: App;
|
||||||
|
|
||||||
return async (req: Request) => {
|
return async (req: Request) => {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = App.create(config);
|
const connection = await getConnection(_config.connection);
|
||||||
|
app = App.create({
|
||||||
|
..._config,
|
||||||
|
connection
|
||||||
|
});
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.on(
|
||||||
"app-built",
|
"app-built",
|
||||||
@@ -20,7 +42,7 @@ export function serve(config: CreateAppConfig, distPath?: string) {
|
|||||||
root
|
root
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
app.module?.server?.setAdminHtml(await readFile(root + "/index.html", "utf-8"));
|
app.registerAdminController();
|
||||||
},
|
},
|
||||||
"sync"
|
"sync"
|
||||||
);
|
);
|
||||||
@@ -28,6 +50,6 @@ export function serve(config: CreateAppConfig, distPath?: string) {
|
|||||||
await app.build();
|
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"
|
"sync"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
if (config?.setAdminHtml !== false) {
|
if (config.setAdminHtml) {
|
||||||
app.module.server.setAdminHtml(html);
|
app.registerAdminController({ html });
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -147,6 +146,7 @@ async function getCached(
|
|||||||
await cache.delete(key);
|
await cache.delete(key);
|
||||||
return c.json({ message: "Cache cleared" });
|
return c.json({ message: "Cache cleared" });
|
||||||
});
|
});
|
||||||
|
app.registerAdminController({ html });
|
||||||
|
|
||||||
config.onBuilt!(app);
|
config.onBuilt!(app);
|
||||||
},
|
},
|
||||||
@@ -163,13 +163,13 @@ async function getCached(
|
|||||||
);
|
);
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
if (!cachedConfig) {
|
|
||||||
saveConfig(app.toJSON(true));
|
if (config.setAdminHtml) {
|
||||||
|
app.registerAdminController({ html });
|
||||||
}
|
}
|
||||||
|
|
||||||
//addAssetsRoute(app, manifest);
|
if (!cachedConfig) {
|
||||||
if (config?.setAdminHtml !== false) {
|
saveConfig(app.toJSON(true));
|
||||||
app.module.server.setAdminHtml(html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
@@ -212,10 +212,6 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
colo: context.colo
|
colo: context.colo
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options?.setAdminHtml !== false) {
|
|
||||||
app.module.server.setAdminHtml(options.html);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sync"
|
"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",
|
"app-built",
|
||||||
async () => {
|
async () => {
|
||||||
await config.onBuilt?.(app);
|
await config.onBuilt?.(app);
|
||||||
app.module.server.setAdminHtml(html);
|
app.registerAdminController();
|
||||||
app.module.server.client.get("/assets/!*", serveStatic({ root: "./" }));
|
app.module.server.client.get("/assets/!*", serveStatic({ root: "./" }));
|
||||||
},
|
},
|
||||||
"sync"
|
"sync"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class AuthApi extends ModuleApi<AuthApiOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async strategies() {
|
async strategies() {
|
||||||
return this.get<{ strategies: AppAuthSchema["strategies"] }>(["strategies"]);
|
return this.get<Pick<AppAuthSchema, "strategies" | "basepath">>(["strategies"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {}
|
async logout() {}
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
import type { AppAuth } from "auth";
|
import type { AppAuth } from "auth";
|
||||||
import type { ClassController } from "core";
|
import type { ClassController } from "core";
|
||||||
import { Hono, type MiddlewareHandler } from "hono";
|
import { Hono, type MiddlewareHandler } from "hono";
|
||||||
|
import * as SystemPermissions from "modules/permissions";
|
||||||
|
|
||||||
export class AuthController implements ClassController {
|
export class AuthController implements ClassController {
|
||||||
constructor(private auth: AppAuth) {}
|
constructor(private auth: AppAuth) {}
|
||||||
|
|
||||||
getMiddleware: MiddlewareHandler = async (c, next) => {
|
get guard() {
|
||||||
// @todo: consider adding app name to the payload, because user is not refetched
|
return this.auth.ctx.guard;
|
||||||
|
}
|
||||||
|
|
||||||
//try {
|
getMiddleware: MiddlewareHandler = async (c, next) => {
|
||||||
|
let token: string | undefined;
|
||||||
if (c.req.raw.headers.has("Authorization")) {
|
if (c.req.raw.headers.has("Authorization")) {
|
||||||
const bearerHeader = String(c.req.header("Authorization"));
|
const bearerHeader = String(c.req.header("Authorization"));
|
||||||
const token = bearerHeader.replace("Bearer ", "");
|
token = bearerHeader.replace("Bearer ", "");
|
||||||
const verified = await this.auth.authenticator.verify(token);
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
// @todo: don't extract user from token, but from the database or cache
|
// @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());
|
this.auth.ctx.guard.setUserContext(this.auth.authenticator.getUser());
|
||||||
/*console.log("jwt verified?", {
|
|
||||||
verified,
|
|
||||||
auth: this.auth.authenticator.isUserLoggedIn()
|
|
||||||
});*/
|
|
||||||
} else {
|
} else {
|
||||||
this.auth.authenticator.__setUserNull();
|
this.auth.authenticator.__setUserNull();
|
||||||
}
|
}
|
||||||
/* } catch (e) {
|
|
||||||
this.auth.authenticator.__setUserNull();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
@@ -49,7 +47,8 @@ export class AuthController implements ClassController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
hono.get("/strategies", async (c) => {
|
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;
|
return hono;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export type GuardConfig = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
export class Guard {
|
export class Guard {
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
user?: GuardUserContext;
|
user?: GuardUserContext;
|
||||||
@@ -96,12 +98,12 @@ export class Guard {
|
|||||||
if (this.user && typeof this.user.role === "string") {
|
if (this.user && typeof this.user.role === "string") {
|
||||||
const role = this.roles?.find((role) => role.name === this.user?.role);
|
const role = this.roles?.find((role) => role.name === this.user?.role);
|
||||||
if (role) {
|
if (role) {
|
||||||
console.log("guard: role found", this.user.role);
|
debug && console.log("guard: role found", this.user.role);
|
||||||
return 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();
|
return this.getDefaultRole();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +111,14 @@ export class Guard {
|
|||||||
return this.roles?.find((role) => role.is_default);
|
return this.roles?.find((role) => role.is_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isEnabled() {
|
||||||
|
return this.config?.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
hasPermission(permission: Permission): boolean;
|
hasPermission(permission: Permission): boolean;
|
||||||
hasPermission(name: string): boolean;
|
hasPermission(name: string): boolean;
|
||||||
hasPermission(permissionOrName: Permission | string): boolean {
|
hasPermission(permissionOrName: Permission | string): boolean {
|
||||||
if (this.config?.enabled !== true) {
|
if (!this.isEnabled()) {
|
||||||
//console.log("guard not enabled, allowing");
|
//console.log("guard not enabled, allowing");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -126,10 +132,10 @@ export class Guard {
|
|||||||
const role = this.getUserRole();
|
const role = this.getUserRole();
|
||||||
|
|
||||||
if (!role) {
|
if (!role) {
|
||||||
console.log("guard: role not found, denying");
|
debug && console.log("guard: role not found, denying");
|
||||||
return false;
|
return false;
|
||||||
} else if (role.implicit_allow === true) {
|
} else if (role.implicit_allow === true) {
|
||||||
console.log("guard: role implicit allow, allowing");
|
debug && console.log("guard: role implicit allow, allowing");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +143,12 @@ export class Guard {
|
|||||||
(rolePermission) => rolePermission.permission.name === name
|
(rolePermission) => rolePermission.permission.name === name
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("guard: rolePermission, allowing?", {
|
debug &&
|
||||||
permission: name,
|
console.log("guard: rolePermission, allowing?", {
|
||||||
role: role.name,
|
permission: name,
|
||||||
allowing: !!rolePermission
|
role: role.name,
|
||||||
});
|
allowing: !!rolePermission
|
||||||
|
});
|
||||||
return !!rolePermission;
|
return !!rolePermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const config: CliCommand = (program) => {
|
|||||||
.description("get default config")
|
.description("get default config")
|
||||||
.option("--pretty", "pretty print")
|
.option("--pretty", "pretty print")
|
||||||
.action((options) => {
|
.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 type { Config } from "@libsql/client/node";
|
||||||
import { App } from "App";
|
import { App } from "App";
|
||||||
import type { BkndConfig } from "adapter";
|
import type { BkndConfig } from "adapter";
|
||||||
|
import type { CliCommand } from "cli/types";
|
||||||
import { Option } from "commander";
|
import { Option } from "commander";
|
||||||
import type { Connection } from "data";
|
import type { Connection } from "data";
|
||||||
import type { CliCommand } from "../../types";
|
|
||||||
import {
|
import {
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
type Platform,
|
type Platform,
|
||||||
@@ -48,14 +48,13 @@ type MakeAppConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function makeApp(config: MakeAppConfig) {
|
async function makeApp(config: MakeAppConfig) {
|
||||||
const html = await getHtml();
|
|
||||||
const app = new App(config.connection);
|
const app = new App(config.connection);
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.on(
|
||||||
"app-built",
|
"app-built",
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, config.server?.platform ?? "node");
|
await attachServeStatic(app, config.server?.platform ?? "node");
|
||||||
app.module.server.setAdminHtml(html);
|
app.registerAdminController({ html: await getHtml() });
|
||||||
|
|
||||||
if (config.onBuilt) {
|
if (config.onBuilt) {
|
||||||
await config.onBuilt(app);
|
await config.onBuilt(app);
|
||||||
@@ -70,14 +69,13 @@ async function makeApp(config: MakeAppConfig) {
|
|||||||
|
|
||||||
export async function makeConfigApp(config: BkndConfig, platform?: Platform) {
|
export async function makeConfigApp(config: BkndConfig, platform?: Platform) {
|
||||||
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
const appConfig = typeof config.app === "function" ? config.app(process.env) : config.app;
|
||||||
const html = await getHtml();
|
|
||||||
const app = App.create(appConfig);
|
const app = App.create(appConfig);
|
||||||
|
|
||||||
app.emgr.on(
|
app.emgr.on(
|
||||||
"app-built",
|
"app-built",
|
||||||
async () => {
|
async () => {
|
||||||
await attachServeStatic(app, platform ?? "node");
|
await attachServeStatic(app, platform ?? "node");
|
||||||
app.module.server.setAdminHtml(html);
|
app.registerAdminController({ html: await getHtml() });
|
||||||
|
|
||||||
if (config.onBuilt) {
|
if (config.onBuilt) {
|
||||||
await config.onBuilt(app);
|
await config.onBuilt(app);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const schema: CliCommand = (program) => {
|
|||||||
.description("get schema")
|
.description("get schema")
|
||||||
.option("--pretty", "pretty print")
|
.option("--pretty", "pretty print")
|
||||||
.action((options) => {
|
.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 { 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 { AppData } from "../AppData";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import { type AppDataConfig, FIELDS } from "../data-schema";
|
import { type AppDataConfig, FIELDS } from "../data-schema";
|
||||||
|
|
||||||
export class DataController implements ClassController {
|
export class DataController implements ClassController {
|
||||||
@@ -89,12 +89,10 @@ export class DataController implements ClassController {
|
|||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add timing
|
hono.use("*", async (c, next) => {
|
||||||
/*hono.use("*", async (c, next) => {
|
this.ctx.guard.throwUnlessGranted(SystemPermissions.api);
|
||||||
startTime(c, "data");
|
|
||||||
await next();
|
await next();
|
||||||
endTime(c, "data");
|
});
|
||||||
});*/
|
|
||||||
|
|
||||||
// info
|
// info
|
||||||
hono.get(
|
hono.get(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type DbFunctions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export abstract class Connection {
|
export abstract class Connection {
|
||||||
|
cls = "bknd:connection";
|
||||||
kysely: Kysely<any>;
|
kysely: Kysely<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -52,6 +53,15 @@ export abstract class Connection {
|
|||||||
this.kysely = kysely;
|
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 {
|
getIntrospector(): ConnectionIntrospector {
|
||||||
return this.kysely.introspection as ConnectionIntrospector;
|
return this.kysely.introspection as ConnectionIntrospector;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class EntityManager<DB> {
|
|||||||
relations.forEach((relation) => this.addRelation(relation));
|
relations.forEach((relation) => this.addRelation(relation));
|
||||||
indices.forEach((index) => this.addIndex(index));
|
indices.forEach((index) => this.addIndex(index));
|
||||||
|
|
||||||
if (!(connection instanceof Connection)) {
|
if (!Connection.isConnection(connection)) {
|
||||||
throw new UnableToConnectException("");
|
throw new UnableToConnectException("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,19 +425,19 @@ export class ModuleManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultSchema(pretty = false) {
|
export function getDefaultSchema() {
|
||||||
const schema = {
|
const schema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
...transformObject(MODULES, (module) => module.prototype.getSchema())
|
...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) => {
|
const config = transformObject(MODULES, (module) => {
|
||||||
return Default(module.prototype.getSchema(), {});
|
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";
|
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 configRead = new Permission("system.config.read");
|
||||||
export const configReadSecrets = new Permission("system.config.read.secrets");
|
export const configReadSecrets = new Permission("system.config.read.secrets");
|
||||||
export const configWrite = new Permission("system.config.write");
|
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 { cors } from "hono/cors";
|
||||||
import { timing } from "hono/timing";
|
import { timing } from "hono/timing";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
|
import * as SystemPermissions from "modules/permissions";
|
||||||
|
|
||||||
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
||||||
export const serverConfigSchema = Type.Object(
|
export const serverConfigSchema = Type.Object(
|
||||||
@@ -49,7 +50,7 @@ export type AppServerConfig = Static<typeof serverConfigSchema>;
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
export class AppServer extends Module<typeof serverConfigSchema> {
|
export class AppServer extends Module<typeof serverConfigSchema> {
|
||||||
private admin_html?: string;
|
//private admin_html?: string;
|
||||||
|
|
||||||
override getRestrictedPaths() {
|
override getRestrictedPaths() {
|
||||||
return [];
|
return [];
|
||||||
@@ -64,12 +65,6 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async build() {
|
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(
|
this.client.use(
|
||||||
"*",
|
"*",
|
||||||
cors({
|
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) => {
|
this.client.onError((err, c) => {
|
||||||
//throw err;
|
//throw err;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -124,18 +107,31 @@ export class AppServer extends Module<typeof serverConfigSchema> {
|
|||||||
this.setBuilt();
|
this.setBuilt();
|
||||||
}
|
}
|
||||||
|
|
||||||
setAdminHtml(html: string) {
|
/*setAdminHtml(html: string) {
|
||||||
this.admin_html = html;
|
this.admin_html = html;
|
||||||
const basepath = (String(this.config.admin.basepath) + "/").replace(/\/+$/, "/");
|
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) => {
|
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!);
|
return c.html(this.admin_html!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAdminHtml() {
|
getAdminHtml() {
|
||||||
return this.admin_html;
|
return this.admin_html;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
override toJSON(secrets?: boolean) {
|
override toJSON(secrets?: boolean) {
|
||||||
return this.config;
|
return this.config;
|
||||||
|
|||||||
@@ -98,7 +98,12 @@ export class SystemController implements ClassController {
|
|||||||
// 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
|
||||||
if (force === true) {
|
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 {
|
} else {
|
||||||
await this.app.mutateConfig(module).patch("", value);
|
await this.app.mutateConfig(module).patch("", value);
|
||||||
}
|
}
|
||||||
@@ -287,7 +292,7 @@ export class SystemController implements ClassController {
|
|||||||
|
|
||||||
hono.get("/openapi.json", async (c) => {
|
hono.get("/openapi.json", async (c) => {
|
||||||
//const config = this.app.toJSON();
|
//const config = this.app.toJSON();
|
||||||
const config = JSON.parse(getDefaultConfig() as any);
|
const config = getDefaultConfig();
|
||||||
return c.json(generateOpenAPI(config));
|
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 type { ModuleConfigs, ModuleSchemas } from "../../modules";
|
||||||
import { useClient } from "./ClientProvider";
|
import { useClient } from "./ClientProvider";
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
@@ -22,18 +24,47 @@ export function BkndProvider({
|
|||||||
children
|
children
|
||||||
}: { includeSecrets?: boolean; children: any }) {
|
}: { includeSecrets?: boolean; children: any }) {
|
||||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
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();
|
const client = useClient();
|
||||||
|
|
||||||
async function fetchSchema(_includeSecrets: boolean = false) {
|
async function fetchSchema(_includeSecrets: boolean = false) {
|
||||||
if (withSecrets) return;
|
if (withSecrets) return;
|
||||||
const { body } = await client.api.system.readSchema({
|
const { body, res } = await client.api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets
|
secrets: _includeSecrets
|
||||||
});
|
});
|
||||||
console.log("--schema fetched", body);
|
|
||||||
setSchema(body as any);
|
if (!res.ok) {
|
||||||
setWithSecrets(_includeSecrets);
|
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() {
|
async function requireSecrets() {
|
||||||
@@ -46,8 +77,8 @@ export function BkndProvider({
|
|||||||
fetchSchema(includeSecrets);
|
fetchSchema(includeSecrets);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!schema?.schema) return null;
|
if (!fetched || !schema) return null;
|
||||||
const app = new AppReduced(schema.config as any);
|
const app = new AppReduced(schema?.config as any);
|
||||||
|
|
||||||
const actions = getSchemaActions({ client, setSchema });
|
const actions = getSchemaActions({ client, setSchema });
|
||||||
|
|
||||||
@@ -64,20 +95,3 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
|
|||||||
|
|
||||||
return ctx;
|
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 }): {
|
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
|
||||||
strategies: AppAuthSchema["strategies"];
|
export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthStrategyData> & {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
} => {
|
} => {
|
||||||
const [strategies, setStrategies] = useState<AppAuthSchema["strategies"]>();
|
const [data, setData] = useState<AuthStrategyData>();
|
||||||
const ctxBaseUrl = useBaseUrl();
|
const ctxBaseUrl = useBaseUrl();
|
||||||
const api = new Api({
|
const api = new Api({
|
||||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl,
|
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl,
|
||||||
@@ -98,10 +98,10 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): {
|
|||||||
const res = await api.auth.strategies();
|
const res = await api.auth.strategies();
|
||||||
console.log("res", res);
|
console.log("res", res);
|
||||||
if (res.res.ok) {
|
if (res.res.ok) {
|
||||||
setStrategies(res.body.strategies);
|
setData(res.body);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [options?.baseUrl]);
|
}, [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();
|
return this.api.getAuthState();
|
||||||
},
|
},
|
||||||
verify: async () => {
|
verify: async () => {
|
||||||
console.log("verifiying");
|
try {
|
||||||
const res = await this.api.auth.me();
|
//console.log("verifiying");
|
||||||
console.log("verifying result", res);
|
const res = await this.api.auth.me();
|
||||||
if (!res.res.ok) {
|
//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.markAuthVerified(false);
|
||||||
this.api.updateToken(undefined);
|
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
|
* Reduced version of the App class for frontend use
|
||||||
|
* @todo: remove this class
|
||||||
*/
|
*/
|
||||||
export class AppReduced {
|
export class AppReduced {
|
||||||
// @todo: change to record
|
// @todo: change to record
|
||||||
@@ -16,7 +17,7 @@ export class AppReduced {
|
|||||||
private _flows: Flow[] = [];
|
private _flows: Flow[] = [];
|
||||||
|
|
||||||
constructor(protected appJson: AppType) {
|
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]) => {
|
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
||||||
return AppData.constructEntity(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);
|
//console.log("search", token, "/api/auth/google?redirect=" + window.location.href);
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const { strategies, loading } = useAuthStrategies();
|
const { strategies, basepath, loading } = useAuthStrategies();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,7 +92,7 @@ export function AuthLogin() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
onClick={() => {
|
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)}
|
Continue with {ucFirstAllSnakeToPascalWithSpaces(oauth.name)}
|
||||||
|
|||||||
@@ -35,5 +35,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./env.d.ts"],
|
"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({
|
await build({
|
||||||
...baseConfig("bun")
|
...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 { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { createClient } from "@libsql/client/node";
|
import { createClient } from "@libsql/client/node";
|
||||||
import { App, type BkndConfig, type CreateAppConfig } from "./src";
|
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 { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter";
|
||||||
import { registries } from "./src/modules/registries";
|
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", {
|
registries.media.add("local", {
|
||||||
cls: StorageLocalAdapter,
|
cls: StorageLocalAdapter,
|
||||||
schema: StorageLocalAdapter.prototype.getSchema()
|
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: {
|
app: {
|
||||||
connection
|
connection
|
||||||
},
|
},
|
||||||
setAdminHtml: true
|
setAdminHtml: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user