Compare commits

...

24 Commits

Author SHA1 Message Date
dswbx a4079804c2 release 0.2.2 2024-12-04 14:49:55 +01:00
dswbx 2bd4b720bb Merge pull request #18 from bknd-io/17-cannot-find-module-node_moduleslodash-esget
fix: lodash-es/* imports causing crash on astro
2024-12-04 14:49:19 +01:00
dswbx f3619bee26 fix lodash-es/* imports causing crash on astro 2024-12-04 14:47:52 +01:00
dswbx c54455870c fix: docs logo 2024-12-03 19:34:14 +01:00
dswbx 20477a655e update docs to navigate to web on logo click 2024-12-03 19:30:16 +01:00
dswbx 1497470d33 bump version for patch release 0.2.1 2024-12-03 17:21:06 +01:00
dswbx abe38fe69d Merge pull request #12 from bknd-io/chores/remove-deps
Remove unusued deps and fixing auth tests
2024-12-03 17:18:18 +01:00
dswbx 665c41f051 Merge pull request #16 from bknd-io/chores/export-data-prototype
export data prototype
2024-12-03 17:17:53 +01:00
dswbx 0184f47a41 added prototype to the list of exports from "bknd/data" 2024-12-03 17:15:53 +01:00
dswbx 5374afc9c8 remove the quick theme switcher if admin settings are overridden 2024-12-03 17:09:27 +01:00
dswbx d6978f9873 fix auth tests 2024-12-03 09:16:00 +01:00
dswbx 3c5bd95988 removed unused deps 2024-12-03 09:15:39 +01:00
dswbx 91470d530f bump version for release 0.2.0 2024-12-01 09:02:32 +01:00
dswbx 0c8f3cd22a Merge pull request #9 from bknd-io/feat/astro-adapter-2
improved astro adapter (serving api) + added documentation
2024-12-01 09:00:40 +01:00
dswbx feeb13c053 improved astro adapter (serving api) + added documentation 2024-12-01 08:58:08 +01:00
dswbx b55fdd7516 fix invalid discord invite + added missing updates from astro adapter 2024-11-30 07:20:17 +01:00
dswbx 894a90fca9 Merge pull request #5 from bknd-io/feat/astro-adapter
Feat: Astro Adapter
2024-11-29 21:23:26 +01:00
dswbx 582dbd4272 added minimal astro adapter + improved the example 2024-11-29 21:21:59 +01:00
dswbx 6eb0d2242f Merge remote-tracking branch 'origin/main' into feat/astro-adapter 2024-11-29 20:14:31 +01:00
dswbx 4693be5615 added db for astro 2024-11-28 11:22:02 +01:00
dswbx bdc6eb55bf added bknd admin config override to make it easier to include <Admin /> 2024-11-28 11:15:07 +01:00
dswbx 54b38401d8 added fully working astro example 2024-11-28 10:22:18 +01:00
dswbx 49c2a7b4db Merge remote-tracking branch 'origin/main' into feat/astro-adapter 2024-11-28 10:12:37 +01:00
dswbx 6718419d41 prepare admin for astro: inject clientprovider props directly from admin component 2024-11-28 10:04:14 +01:00
52 changed files with 769 additions and 104 deletions
+1
View File
@@ -17,6 +17,7 @@ packages/media/.env
**/*/vite.config.ts.timestamp* **/*/vite.config.ts.timestamp*
.history .history
**/*/.db/* **/*/.db/*
**/*/*.db
**/*/*.db-shm **/*/*.db-shm
**/*/*.db-wal **/*/*.db-wal
.npmrc .npmrc
+7 -1
View File
@@ -39,7 +39,10 @@ describe("AppAuth", () => {
test("creates user on register", async () => { test("creates user on register", async () => {
const auth = new AppAuth( const auth = new AppAuth(
{ {
enabled: true enabled: true,
jwt: {
secret: "123456"
}
}, },
ctx ctx
); );
@@ -57,6 +60,9 @@ describe("AppAuth", () => {
disableConsoleLog(); disableConsoleLog();
const res = await app.request("/password/register", { const res = await app.request("/password/register", {
method: "POST", method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ body: JSON.stringify({
email: "some@body.com", email: "some@body.com",
password: "123456" password: "123456"
+5
View File
@@ -179,3 +179,8 @@ await tsup.build({
platform: "node", platform: "node",
format: ["esm", "cjs"] format: ["esm", "cjs"]
}); });
await tsup.build({
...baseConfig("astro"),
format: ["esm", "cjs"]
});
+7 -5
View File
@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.1.1", "version": "0.2.2",
"scripts": { "scripts": {
"build:all": "bun run build && bun run build:cli", "build:all": "bun run build && bun run build:cli",
"dev": "vite", "dev": "vite",
@@ -45,7 +45,6 @@
"@uiw/react-codemirror": "^4.23.6", "@uiw/react-codemirror": "^4.23.6",
"@xyflow/react": "^12.3.2", "@xyflow/react": "^12.3.2",
"aws4fetch": "^1.0.18", "aws4fetch": "^1.0.18",
"codemirror-lang-liquid": "^1.0.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0", "fast-xml-parser": "^4.4.0",
"hono": "^4.6.12", "hono": "^4.6.12",
@@ -57,12 +56,10 @@
"react-hook-form": "^7.53.1", "react-hook-form": "^7.53.1",
"react-icons": "5.2.1", "react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1", "react-json-view-lite": "^2.0.1",
"reactflow": "^11.11.4",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"wouter": "^3.3.5", "wouter": "^3.3.5",
"zod": "^3.23.8", "zod": "^3.23.8"
"zod-to-json-schema": "^3.23.2"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.613.0", "@aws-sdk/client-s3": "^3.613.0",
@@ -160,6 +157,11 @@
"import": "./dist/adapter/node/index.js", "import": "./dist/adapter/node/index.js",
"require": "./dist/adapter/node/index.cjs" "require": "./dist/adapter/node/index.cjs"
}, },
"./adapter/astro": {
"types": "./dist/adapter/astro/index.d.ts",
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs"
},
"./dist/styles.css": "./dist/ui/main.css", "./dist/styles.css": "./dist/ui/main.css",
"./dist/manifest.json": "./dist/static/manifest.json" "./dist/manifest.json": "./dist/static/manifest.json"
}, },
+31
View File
@@ -0,0 +1,31 @@
import { Api, type ApiOptions } from "bknd";
import { App, type CreateAppConfig } from "bknd";
type TAstro = {
request: Request;
};
export type Options = {
mode?: "static" | "dynamic";
} & Omit<ApiOptions, "host"> & {
host?: string;
};
export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
return new Api({
host: new URL(Astro.request.url).origin,
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
});
}
let app: App;
export function serve(config: CreateAppConfig) {
return async (args: TAstro) => {
if (!app) {
app = App.create(config);
await app.build();
}
return app.fetch(args.request);
};
}
+1
View File
@@ -0,0 +1 @@
export * from "./astro.adapter";
+3 -7
View File
@@ -1,4 +1,5 @@
import { withApi } from "bknd/adapter/nextjs"; import { withApi } from "bknd/adapter/nextjs";
import type { BkndAdminProps } from "bknd/ui";
import type { InferGetServerSidePropsType } from "next"; import type { InferGetServerSidePropsType } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@@ -10,15 +11,10 @@ export const getServerSideProps = withApi(async (context) => {
}; };
}); });
export function adminPage() { export function adminPage(adminProps?: BkndAdminProps) {
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { ssr: false }); const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { ssr: false });
const ClientProvider = dynamic(() => import("bknd/ui").then((mod) => mod.ClientProvider));
return (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { return (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
if (typeof document === "undefined") return null; if (typeof document === "undefined") return null;
return ( return <Admin withProvider={{ user: props.user }} {...adminProps} />;
<ClientProvider user={props.user}>
<Admin />
</ClientProvider>
);
}; };
} }
+3 -2
View File
@@ -1,6 +1,7 @@
import type { BkndAdminProps } from "bknd/ui";
import { Suspense, lazy, useEffect, useState } from "react"; import { Suspense, lazy, useEffect, useState } from "react";
export function adminPage() { export function adminPage(props?: BkndAdminProps) {
const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin }))); const Admin = lazy(() => import("bknd/ui").then((mod) => ({ default: mod.Admin })));
return () => { return () => {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
@@ -12,7 +13,7 @@ export function adminPage() {
return ( return (
<Suspense> <Suspense>
<Admin /> <Admin {...props} />
</Suspense> </Suspense>
); );
}; };
+12 -1
View File
@@ -11,10 +11,21 @@ export class AuthController implements ClassController {
getMiddleware: MiddlewareHandler = async (c, next) => { getMiddleware: MiddlewareHandler = async (c, next) => {
// @todo: ONLY HOTFIX // @todo: ONLY HOTFIX
// middlewares are added for all routes are registered. But we need to make sure that
// only HTML/JSON routes are adding a cookie to the response. Config updates might
// also use an extension "syntax", e.g. /api/system/patch/data/entities.posts
// This middleware should be extracted and added by each Controller individually,
// but it requires access to the auth secret.
// Note: This doesn't mean endpoints aren't protected, just the cookie is not set.
const url = new URL(c.req.url); const url = new URL(c.req.url);
const last = url.pathname.split("/")?.pop(); const last = url.pathname.split("/")?.pop();
const ext = last?.includes(".") ? last.split(".")?.pop() : undefined; const ext = last?.includes(".") ? last.split(".")?.pop() : undefined;
if (ext) { if (
!this.auth.authenticator.isJsonRequest(c) &&
["GET", "HEAD", "OPTIONS"].includes(c.req.method) &&
ext &&
["js", "css", "png", "jpg", "jpeg", "svg", "ico"].includes(ext)
) {
isDebug() && console.log("Skipping auth", { ext }, url.pathname); isDebug() && console.log("Skipping auth", { ext }, url.pathname);
} else { } else {
const user = await this.auth.authenticator.resolveAuthFromRequest(c); const user = await this.auth.authenticator.resolveAuthFromRequest(c);
+8 -2
View File
@@ -11,7 +11,7 @@ import {
} from "core/utils"; } from "core/utils";
import type { Context, Hono } from "hono"; import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
import { decode, sign, verify } from "hono/jwt"; import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie"; import type { CookieOptions } from "hono/utils/cookie";
import { omit } from "lodash-es"; import { omit } from "lodash-es";
@@ -177,7 +177,12 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
payload.exp = Math.floor(Date.now() / 1000) + this.config.jwt.expires; payload.exp = Math.floor(Date.now() / 1000) + this.config.jwt.expires;
} }
return sign(payload, this.config.jwt?.secret ?? "", this.config.jwt?.alg ?? "HS256"); const secret = this.config.jwt.secret;
if (!secret || secret.length === 0) {
throw new Error("Cannot sign JWT without a secret");
}
return sign(payload, secret, this.config.jwt?.alg ?? "HS256");
} }
async verify(jwt: string): Promise<boolean> { async verify(jwt: string): Promise<boolean> {
@@ -249,6 +254,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
} }
} }
// @todo: move this to a server helper
isJsonRequest(c: Context): boolean { isJsonRequest(c: Context): boolean {
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded"; //return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
return c.req.header("Content-Type") === "application/json"; return c.req.header("Content-Type") === "application/json";
+2 -4
View File
@@ -1,4 +1,4 @@
import { type Client, type InStatement, createClient } from "@libsql/client/web"; import { type Client, type Config, type InStatement, createClient } from "@libsql/client/web";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely"; import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin"; import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
@@ -8,9 +8,7 @@ import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector"; import { SqliteIntrospector } from "./SqliteIntrospector";
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const; export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
export type LibSqlCredentials = { export type LibSqlCredentials = Config & {
url: string;
authToken?: string;
protocol?: (typeof LIBSQL_PROTOCOLS)[number]; protocol?: (typeof LIBSQL_PROTOCOLS)[number];
}; };
+1
View File
@@ -4,6 +4,7 @@ export * from "./fields";
export * from "./entities"; export * from "./entities";
export * from "./relations"; export * from "./relations";
export * from "./schema/SchemaManager"; export * from "./schema/SchemaManager";
export * from "./prototype";
export { export {
type RepoQuery, type RepoQuery,
+1 -1
View File
@@ -8,5 +8,5 @@ export {
type ModuleSchemas type ModuleSchemas
} from "modules/ModuleManager"; } from "modules/ModuleManager";
export * from "./adapter"; export type * from "./adapter";
export { Api, type ApiOptions } from "./Api"; export { Api, type ApiOptions } from "./Api";
+16 -6
View File
@@ -1,26 +1,36 @@
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import type { ModuleConfigs } from "modules";
import React from "react"; import React from "react";
import { FlashMessage } from "ui/modules/server/FlashMessage"; import { FlashMessage } from "ui/modules/server/FlashMessage";
import { BkndProvider, ClientProvider, useBknd } from "./client"; import { BkndProvider, ClientProvider, type ClientProviderProps, useBknd } from "./client";
import { createMantineTheme } from "./lib/mantine/theme"; import { createMantineTheme } from "./lib/mantine/theme";
import { BkndModalsProvider } from "./modals"; import { BkndModalsProvider } from "./modals";
import { Routes } from "./routes"; import { Routes } from "./routes";
export type BkndAdminProps = { export type BkndAdminProps = {
baseUrl?: string; baseUrl?: string;
withProvider?: boolean; withProvider?: boolean | ClientProviderProps;
// @todo: add admin config override config?: ModuleConfigs["server"]["admin"];
}; };
export default function Admin({ baseUrl: baseUrlOverride, withProvider = false }: BkndAdminProps) { export default function Admin({
baseUrl: baseUrlOverride,
withProvider = false,
config
}: BkndAdminProps) {
const Component = ( const Component = (
<BkndProvider> <BkndProvider adminOverride={config}>
<AdminInternal /> <AdminInternal />
</BkndProvider> </BkndProvider>
); );
return withProvider ? ( return withProvider ? (
<ClientProvider baseUrl={baseUrlOverride}>{Component}</ClientProvider> <ClientProvider
baseUrl={baseUrlOverride}
{...(typeof withProvider === "object" ? withProvider : {})}
>
{Component}
</ClientProvider>
) : ( ) : (
Component Component
); );
+11 -2
View File
@@ -14,6 +14,7 @@ type BkndContext = {
requireSecrets: () => Promise<void>; requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>; actions: ReturnType<typeof getSchemaActions>;
app: AppReduced; app: AppReduced;
adminOverride?: ModuleConfigs["server"]["admin"];
}; };
const BkndContext = createContext<BkndContext>(undefined!); const BkndContext = createContext<BkndContext>(undefined!);
@@ -21,8 +22,9 @@ export type { TSchemaActions };
export function BkndProvider({ export function BkndProvider({
includeSecrets = false, includeSecrets = false,
adminOverride,
children children
}: { includeSecrets?: boolean; children: any }) { }: { includeSecrets?: boolean; children: any } & Pick<BkndContext, "adminOverride">) {
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets); const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
const [schema, setSchema] = const [schema, setSchema] =
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>(); useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
@@ -64,6 +66,13 @@ export function BkndProvider({
permissions: [] permissions: []
} as any); } as any);
if (adminOverride) {
schema.config.server.admin = {
...schema.config.server.admin,
...adminOverride
};
}
startTransition(() => { startTransition(() => {
setSchema(schema); setSchema(schema);
setWithSecrets(_includeSecrets); setWithSecrets(_includeSecrets);
@@ -86,7 +95,7 @@ export function BkndProvider({
const actions = getSchemaActions({ client, setSchema, reloadSchema }); const actions = getSchemaActions({ client, setSchema, reloadSchema });
return ( return (
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}> <BkndContext.Provider value={{ ...schema, actions, requireSecrets, app, adminOverride }}>
{children} {children}
</BkndContext.Provider> </BkndContext.Provider>
); );
+7 -5
View File
@@ -17,11 +17,13 @@ export const queryClient = new QueryClient({
} }
}); });
export const ClientProvider = ({ export type ClientProviderProps = {
children, children?: any;
baseUrl, baseUrl?: string;
user user?: TApiUser | null | undefined;
}: { children?: any; baseUrl?: string; user?: TApiUser | null }) => { };
export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => {
const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null); const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
const winCtx = useBkndWindowContext(); const winCtx = useBkndWindowContext();
+1 -1
View File
@@ -1,4 +1,4 @@
export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider"; export { ClientProvider, type ClientProviderProps, useClient, useBaseUrl } from "./ClientProvider";
export { BkndProvider, useBknd } from "./BkndProvider"; export { BkndProvider, useBknd } from "./BkndProvider";
export { useAuth } from "./schema/auth/use-auth"; export { useAuth } from "./schema/auth/use-auth";
+2 -2
View File
@@ -1,11 +1,11 @@
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type AlertProps = ComponentPropsWithoutRef<"div"> & { export type AlertProps = ComponentPropsWithoutRef<"div"> & {
className?: string; className?: string;
visible?: boolean; visible?: boolean;
title?: string; title?: string;
message?: string; message?: ReactNode | string;
}; };
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) => const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) =>
@@ -12,7 +12,7 @@ import type {
ValidatorType ValidatorType
} from "@rjsf/utils"; } from "@rjsf/utils";
import { toErrorSchema } from "@rjsf/utils"; import { toErrorSchema } from "@rjsf/utils";
import get from "lodash-es/get"; import { get } from "lodash-es";
function removeUndefinedKeys(obj: any): any { function removeUndefinedKeys(obj: any): any {
if (!obj) return obj; if (!obj) return obj;
@@ -14,9 +14,7 @@ import {
getWidget, getWidget,
mergeSchemas mergeSchemas
} from "@rjsf/utils"; } from "@rjsf/utils";
import get from "lodash-es/get"; import { get, isEmpty, omit } from "lodash-es";
import isEmpty from "lodash-es/isEmpty";
import omit from "lodash-es/omit";
import { Component } from "react"; import { Component } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Label } from "../templates/FieldTemplate"; import { Label } from "../templates/FieldTemplate";
+1 -1
View File
@@ -1,4 +1,4 @@
export { default as Admin } from "./Admin"; export { default as Admin, type BkndAdminProps } from "./Admin";
export { Button } from "./components/buttons/Button"; export { Button } from "./components/buttons/Button";
export { Context } from "./components/Context"; export { Context } from "./components/Context";
export { export {
+4 -1
View File
@@ -146,6 +146,7 @@ export function Header({ hasSidebar = true }) {
} }
function UserMenu() { function UserMenu() {
const { adminOverride } = useBknd();
const auth = useAuth(); const auth = useAuth();
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const { logout_route } = useBkndWindowContext(); const { logout_route } = useBkndWindowContext();
@@ -170,7 +171,9 @@ function UserMenu() {
items.push({ label: `Logout ${auth.user.email}`, onClick: handleLogout, icon: IconKeyOff }); items.push({ label: `Logout ${auth.user.email}`, onClick: handleLogout, icon: IconKeyOff });
} }
items.push(() => <UserMenuThemeToggler />); if (!adminOverride) {
items.push(() => <UserMenuThemeToggler />);
}
return ( return (
<> <>
@@ -135,10 +135,10 @@ type FormInputElement = HTMLInputElement | HTMLTextAreaElement;
function EntityFormField({ fieldApi, field, action, data, ...props }: EntityFormFieldProps) { function EntityFormField({ fieldApi, field, action, data, ...props }: EntityFormFieldProps) {
const handleUpdate = useEvent((e: React.ChangeEvent<FormInputElement> | any) => { const handleUpdate = useEvent((e: React.ChangeEvent<FormInputElement> | any) => {
if (typeof e === "object" && "target" in e) { if (typeof e === "object" && "target" in e) {
console.log("handleUpdate", e.target.value); //console.log("handleUpdate", e.target.value);
fieldApi.handleChange(e.target.value); fieldApi.handleChange(e.target.value);
} else { } else {
console.log("handleUpdate-", e); //console.log("handleUpdate-", e);
fieldApi.handleChange(e); fieldApi.handleChange(e);
} }
}); });
@@ -234,7 +234,7 @@ export function Setting<Schema extends TObject = any>({
<Breadcrumbs path={path} /> <Breadcrumbs path={path} />
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable key={path.join("-")}> <AppShell.Scrollable key={path.join("-")}>
{typeof showAlert === "string" && <Alert.Warning message={showAlert} />} {typeof showAlert !== "undefined" && <Alert.Warning message={showAlert} />}
<div className="flex flex-col flex-grow p-3 gap-3"> <div className="flex flex-col flex-grow p-3 gap-3">
<JsonSchemaForm <JsonSchemaForm
+2 -36
View File
@@ -12,6 +12,7 @@ import { Setting } from "./components/Setting";
import { AuthSettings } from "./routes/auth.settings"; import { AuthSettings } from "./routes/auth.settings";
import { DataSettings } from "./routes/data.settings"; import { DataSettings } from "./routes/data.settings";
import { FlowsSettings } from "./routes/flows.settings"; import { FlowsSettings } from "./routes/flows.settings";
import { ServerSettings } from "./routes/server.settings";
function SettingsSidebar() { function SettingsSidebar() {
const { version, schema } = useBknd(); const { version, schema } = useBknd();
@@ -39,36 +40,6 @@ function SettingsSidebar() {
))} ))}
</nav> </nav>
</div> </div>
{/*<button
onClick={() =>
modals.openContextModal({
modal: "test",
title: "Test Modal",
innerProps: { modalBody: "This is a test modal" }
})
}
>
modal
</button>
<button
onClick={() =>
bkndModals.open(bkndModals.ids.test, { modalBody: "test" }, { title: "what" })
}
>
modal2
</button>
<button onClick={() => bkndModals.open("test", { modalBody: "test" })}>modal</button>
<button
onClick={() =>
bkndModals.open("debug", {
data: {
one: { what: 1 }
}
})
}
>
debug
</button>*/}
</AppShell.Scrollable> </AppShell.Scrollable>
</AppShell.Sidebar> </AppShell.Sidebar>
); );
@@ -145,12 +116,7 @@ const SettingRoutesRoutes = () => {
return ( return (
<> <>
<FallbackRoutes <ServerSettings schema={schema.server} config={config.server} />
module="server"
schema={schema}
config={config}
uiSchema={uiSchema.server}
/>
<DataSettings schema={schema.data} config={config.data} /> <DataSettings schema={schema.data} config={config.data} />
<AuthSettings schema={schema.auth} config={config.auth} /> <AuthSettings schema={schema.auth} config={config.auth} />
<FallbackRoutes module="media" schema={schema} config={config} uiSchema={uiSchema.media} /> <FallbackRoutes module="media" schema={schema} config={config} uiSchema={uiSchema.media} />
@@ -0,0 +1,55 @@
import { cloneDeep } from "lodash-es";
import { useBknd } from "ui";
import { Setting } from "ui/routes/settings/components/Setting";
import { Route } from "wouter";
const uiSchema = {
cors: {
allow_methods: {
"ui:widget": "checkboxes"
},
allow_headers: {
"ui:options": {
orderable: false
}
}
}
};
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
const { app, adminOverride } = useBknd();
const { basepath } = app.getAdminConfig();
const _schema = cloneDeep(_unsafe_copy);
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const schema = _schema;
if (adminOverride) {
schema.properties.admin.readOnly = true;
}
return (
<Route path="/server" nest>
<Route
path="/"
component={() => (
<Setting
options={{
showAlert: () => {
if (adminOverride) {
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
}
return;
}
}}
schema={schema}
uiSchema={uiSchema}
config={config}
prefix={`${prefix}/server`}
path={["server"]}
/>
)}
nest
/>
</Route>
);
};
BIN
View File
Binary file not shown.
+120
View File
@@ -0,0 +1,120 @@
---
title: 'Astro'
description: 'Run bknd inside Astro'
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
Install bknd as a dependency:
<InstallBknd />
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
npx astro add react
```
You also need to make sure to set the output to `hybrid` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
```
<Note>
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI
statically using Astro's middleware. In case you're interested in this, feel free to reach
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new).
</Note>
## Serve the API
Create a new catch-all route at `src/pages/api/[...api].ts`:
```ts src/pages/api/[...api].ts
import { serve } from "bknd/adapter/astro";
export const prerender = false;
export const ALL = serve({
connection: {
type: "libsql",
config: {
url: "http://127.0.0.1:8080"
}
}
});
```
For more information about the connection object, refer to the [Setup](/setup) guide. In the
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
## Enabling the Admin UI
Create a new catch-all route at `src/pages/admin/[...admin].astro`:
```jsx src/pages/admin/[...admin].astro
---
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
export const prerender = false;
---
<html>
<body>
<Admin
withProvider={{ user }}
config={{ basepath: "/admin", color_scheme: "dark" }}
client:load
/>
</body>
</html>
```
## Example usage of the API
You use the API in both static and SSR pages. Just note that on static pages, authentication
might not work as expected, because Cookies are not available in the static context.
Here is an example of using the API in static context:
```jsx
---
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro);
const { data } = await api.data.readMany("todos");
---
<ul>
{data.map((todo) => (
<li>{todo.title}</li>
))}
</ul>
```
On SSR pages, you can also access the authenticated user:
```jsx
---
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
const { data } = await api.data.readMany("todos");
export const prerender = false;
---
{user
? <p>Logged in as <b>{user.email}</b>.</p>
: <p>Not authenticated.</p>}
<ul>
{data.map((todo) => (
<li>{todo.title}</li>
))}
</ul>
```
Check the [astro example](https://github.com/bknd-io/bknd/tree/main/examples/astro) for more implementation details.
+3 -1
View File
@@ -38,7 +38,9 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs";
import "bknd/dist/styles.css"; import "bknd/dist/styles.css";
export { getServerSideProps }; export { getServerSideProps };
export default adminPage(); export default adminPage({
config: { basepath: "/admin" }
});
``` ```
## Example usage of the API in pages dir ## Example usage of the API in pages dir
+3 -1
View File
@@ -81,7 +81,9 @@ Create a new splat route file at `app/routes/admin.$.tsx`:
import { adminPage } from "bknd/adapter/remix"; import { adminPage } from "bknd/adapter/remix";
import "bknd/dist/styles.css"; import "bknd/dist/styles.css";
export default adminPage(); export default adminPage({
config: { basepath: "/admin" }
});
``` ```
## Example usage of the API ## Example usage of the API
+9
View File
@@ -46,6 +46,15 @@ in the future, so stay tuned!
</div>} </div>}
href="/integration/remix" href="/integration/remix"
/> />
<Card
title="Astro"
icon={<div className="text-primary-light">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/><path fill="currentColor" d="M9.24 19.035c-.901-.826-1.164-2.561-.789-3.819c.65.793 1.552 1.044 2.486 1.186c1.44.218 2.856.137 4.195-.524c.153-.076.295-.177.462-.278c.126.365.159.734.115 1.11c-.107.915-.56 1.622-1.283 2.158c-.289.215-.594.406-.892.608c-.916.622-1.164 1.35-.82 2.41l.034.114a2.4 2.4 0 0 1-1.07-.918a2.6 2.6 0 0 1-.412-1.401c-.003-.248-.003-.497-.036-.74c-.081-.595-.36-.86-.883-.876a1.034 1.034 0 0 0-1.075.843q-.013.058-.033.126M4.1 15.007s2.666-1.303 5.34-1.303l2.016-6.26c.075-.304.296-.51.544-.51c.25 0 .47.206.545.51l2.016 6.26c3.167 0 5.34 1.303 5.34 1.303L15.363 2.602c-.13-.366-.35-.602-.645-.602H9.283c-.296 0-.506.236-.645.602c-.01.024-4.538 12.405-4.538 12.405"/>
</svg>
</div>}
href="/integration/astro"
/>
<Card <Card
title="Cloudflare" title="Cloudflare"
icon={<div className="text-primary-light"> icon={<div className="text-primary-light">
+5 -15
View File
@@ -3,7 +3,8 @@
"name": "bknd", "name": "bknd",
"logo": { "logo": {
"dark": "/_assets/logo/bknd_logo_white.svg", "dark": "/_assets/logo/bknd_logo_white.svg",
"light": "/_assets/logo/bknd_logo_black.svg" "light": "/_assets/logo/bknd_logo_black.svg",
"href": "https://bknd.io"
}, },
"theme": "prism", "theme": "prism",
"layout": "sidenav", "layout": "sidenav",
@@ -22,8 +23,8 @@
}, },
"topbarLinks": [ "topbarLinks": [
{ {
"name": "Support", "name": "Discord",
"url": "mailto:hi@help.bknd.io" "url": "https://discord.gg/952SFk8Tb8"
} }
], ],
"topbarCtaButton": { "topbarCtaButton": {
@@ -44,24 +45,12 @@
} }
], ],
"anchors": [ "anchors": [
{
"name": "Web",
"icon": "globe",
"iconType": "solid",
"url": "https://bknd.io"
},
{ {
"name": "Docs", "name": "Docs",
"icon": "book-open-cover", "icon": "book-open-cover",
"iconType": "solid", "iconType": "solid",
"url": "/" "url": "/"
}, },
{
"name": "Discord",
"icon": "discord",
"iconType": "solid",
"url": "https://discord.gg/YAHnKXr5"
},
{ {
"name": "GitHub", "name": "GitHub",
"icon": "github", "icon": "github",
@@ -114,6 +103,7 @@
"integration/bun", "integration/bun",
"integration/vite", "integration/vite",
"integration/express", "integration/express",
"integration/astro",
"integration/nodejs", "integration/nodejs",
"integration/deno", "integration/deno",
"integration/browser" "integration/browser"
+1 -1
View File
@@ -5,6 +5,6 @@
"dev": "mintlify dev" "dev": "mintlify dev"
}, },
"devDependencies": { "devDependencies": {
"mintlify": "^4.0.269" "mintlify": "^4.0.285"
} }
} }
+5
View File
@@ -0,0 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1732785435939
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="astro/client" />
+24
View File
@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
+47
View File
@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
+10
View File
@@ -0,0 +1,10 @@
// @ts-check
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
output: "hybrid",
integrations: [react()]
});
+25
View File
@@ -0,0 +1,25 @@
{
"name": "astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"db": "turso dev --db-file test.db",
"db:check": "sqlite3 test.db \"PRAGMA wal_checkpoint(FULL);\"",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/react": "^3.6.3",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^4.16.16",
"bknd": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.7.2"
}
}
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

+73
View File
@@ -0,0 +1,73 @@
---
interface Props {
title: string;
body: string;
done?: boolean;
}
const { done, title, body } = Astro.props;
---
<li class="link-card" data-done={done ? 1 : undefined}>
<div class="inner">
<div class="check">
<span>{done ? "✅" : "🔘"}</span>
</div>
<h2>
{title}
</h2>
<p>
{body}
</p>
</div>
</li>
<style>
.link-card {
list-style: none;
display: flex;
padding: 1px;
background-color: #23262d;
background-image: none;
background-size: 400%;
border-radius: 7px;
background-position: 100%;
transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
}
.link-card[data-done] {
background-color: #0c3e29;
}
.link-card > .inner {
position: relative;
width: 100%;
text-decoration: none;
line-height: 1.4;
padding: calc(1.5rem - 1px);
border-radius: 8px;
color: white;
background-color: #23262d;
opacity: 0.8;
}
.inner .check {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}
h2 {
margin: 0;
font-size: 1.25rem;
transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
p {
margin-top: 0.5rem;
margin-bottom: 0;
}
.link-card:is(:hover, :focus-within) {
background-position: 0;
background-image: var(--accent-gradient);
}
.link-card:is(:hover, :focus-within) h2 {
color: rgb(var(--accent-light));
}
</style>
+1
View File
@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />
+137
View File
@@ -0,0 +1,137 @@
---
import Card from "../components/Card.astro";
interface Props {
title: string;
}
const { title } = Astro.props;
const path = new URL(Astro.request.url).pathname;
const items = [
{ href: "/", text: "Home (static)" },
{ href: "/ssr", text: "SSR (with auth)" },
{ href: "/admin", text: "Admin" }
];
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<nav>
{items.map((item) => (
<a href={item.href} data-active={path === item.href}>{item.text}</a>
))}
</nav>
<main>
<div class="center">
<h1>bknd + <span class="text-gradient">Astro</span></h1>
<slot name="context" />
</div>
<slot />
</main>
</body>
</html>
<style is:global>
:root {
--accent: 136, 58, 234;
--accent-light: 224, 204, 250;
--accent-dark: 49, 10, 101;
--accent-gradient: linear-gradient(
45deg,
rgb(var(--accent)),
rgb(var(--accent-light)) 30%,
white 60%
);
}
html {
font-family: system-ui, sans-serif;
background: #13151a;
}
code {
font-family:
Menlo,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
}
a, a:visited {
color: white;
font-weight: bold;
}
nav {
margin: auto;
padding: 1rem;
width: 800px;
color: white;
display: flex;
flex-direction: row;
justify-content: center;
gap: 1rem;
}
nav a {
color: white;
text-decoration: none;
background-image: none;
}
nav a[data-active],
nav a:hover {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
main {
margin: auto;
padding: 1rem;
width: 800px;
max-width: calc(100% - 2rem);
color: white;
font-size: 20px;
line-height: 1.6;
}
main .center {
text-align: center;
margin-bottom: 2rem;
p {
opacity: 50%;
font-size: 1rem;
}
}
.astro-a {
position: absolute;
top: -32px;
left: 50%;
transform: translatex(-50%);
width: 220px;
height: auto;
z-index: -1;
}
h1 {
font-size: 4rem;
font-weight: 700;
line-height: 1;
text-align: center;
margin-bottom: 0.4em;
}
.text-gradient {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
</style>
@@ -0,0 +1,21 @@
---
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro";
const api = getApi(Astro, { mode: "dynamic" });
const user = api.getUser();
export const prerender = false;
---
<html>
<body>
<Admin
withProvider={{ user }}
config={{ basepath: "/admin", color_scheme: "dark" }}
client:load
/>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
import { serve } from "bknd/adapter/astro";
export const prerender = false;
export const ALL = serve({
connection: {
type: "libsql",
config: {
url: "http://127.0.0.1:8080"
}
}
});
+29
View File
@@ -0,0 +1,29 @@
---
import { getApi } from "bknd/adapter/astro";
import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro";
const api = getApi(Astro);
const { data } = await api.data.readMany("todos");
---
<Layout title="Welcome to Astro.">
<p slot="context">Static Rendering</p>
<ul role="list" class="link-card-grid">
{data.map((todo: any) => (
<Card
done={todo.done}
title={todo.title}
body={todo.description}
/>
))}
</ul>
</Layout>
<style>
.link-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
gap: 2rem;
padding: 0;
}
</style>
+35
View File
@@ -0,0 +1,35 @@
---
import { getApi } from "bknd/adapter/astro";
import Card from "../components/Card.astro";
import Layout from "../layouts/Layout.astro";
const api = getApi(Astro, { mode: "dynamic" });
const { data } = await api.data.readMany("todos");
const user = api.getUser();
export const prerender = false;
---
<Layout title="Welcome to Astro.">
<p slot="context">Server Side Rendering</p>
<ul role="list" class="link-card-grid">
{data.map((todo: any) => (
<Card
done={todo.done}
title={todo.title}
body={todo.description}
/>
))}
</ul>
<div class="center">
{user ? <p>Logged in as <b>{user?.email}</b>. <a href="/api/auth/logout">Logout</a></p> : <p>Not authenticated. <a href="/admin/auth/login">Sign in</a></p>}
</div>
</Layout>
<style>
.link-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
gap: 2rem;
padding: 0;
}
</style>
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}
@@ -2,4 +2,8 @@ import { adminPage, getServerSideProps } from "bknd/adapter/nextjs";
import "bknd/dist/styles.css"; import "bknd/dist/styles.css";
export { getServerSideProps }; export { getServerSideProps };
export default adminPage(); export default adminPage({
config: {
basepath: "/admin"
}
});
Binary file not shown.
+5 -1
View File
@@ -1,4 +1,8 @@
import { adminPage } from "bknd/adapter/remix"; import { adminPage } from "bknd/adapter/remix";
import "bknd/dist/styles.css"; import "bknd/dist/styles.css";
export default adminPage(); export default adminPage({
config: {
basepath: "/admin"
}
});