diff --git a/app/package.json b/app/package.json index 95d2a495..dccc461e 100644 --- a/app/package.json +++ b/app/package.json @@ -60,8 +60,7 @@ "bcryptjs": "^3.0.2", "dayjs": "^1.11.13", "fast-xml-parser": "^5.0.8", - "hono": "^4.7.11", - "json-schema-form-react": "^0.0.2", + "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", "kysely": "^0.27.6", @@ -100,7 +99,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.2.2", + "jsonv-ts": "^0.2.3", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", diff --git a/app/src/adapter/index.ts b/app/src/adapter/index.ts index 9e74dd01..ebab187d 100644 --- a/app/src/adapter/index.ts +++ b/app/src/adapter/index.ts @@ -70,7 +70,7 @@ export async function createAdapterApp = Record< await addFlashMessage(c, String(error), "error"); - const referer = this.getSafeUrl(c, opts?.redirect ?? c.req.header("Referer") ?? "/"); + const referer = this.getSafeUrl(c, c.req.header("Referer") ?? "/"); return c.redirect(referer); } diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index f8a295c4..d8955b8c 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -1,5 +1,5 @@ import type { PrimaryFieldType } from "core"; -import { $console } from "core/utils"; +import { $console, isPlainObject } from "core/utils"; import { isDebug } from "core/env"; import { encodeSearch } from "core/utils/reqres"; import type { ApiFetcher } from "Api"; @@ -95,7 +95,11 @@ export abstract class ModuleApi { private renderFallback() { if (this.props.fallback) { - return typeof this.props.fallback === "function" - ? this.props.fallback({ error: this.state.error!, resetError: this.resetError }) - : this.props.fallback; + return typeof this.props.fallback === "function" ? ( + this.props.fallback({ error: this.state.error!, resetError: this.resetError }) + ) : ( + {this.props.fallback} + ); } - return Error; + return Error1; } override render() { diff --git a/app/src/ui/components/form/native-form/NativeForm.tsx b/app/src/ui/components/form/native-form/NativeForm.tsx index 15b6d5cd..17cc6497 100644 --- a/app/src/ui/components/form/native-form/NativeForm.tsx +++ b/app/src/ui/components/form/native-form/NativeForm.tsx @@ -6,7 +6,6 @@ import { useRef, useState, } from "react"; -import { useEvent } from "ui/hooks/use-event"; import { type CleanOptions, type InputElement, diff --git a/app/src/ui/elements/auth/AuthForm.tsx b/app/src/ui/elements/auth/AuthForm.tsx index 4f22b170..0865317f 100644 --- a/app/src/ui/elements/auth/AuthForm.tsx +++ b/app/src/ui/elements/auth/AuthForm.tsx @@ -1,20 +1,11 @@ import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema"; import clsx from "clsx"; -import { Form } from "json-schema-form-react"; +import { NativeForm } from "ui/components/form/native-form/NativeForm"; import { transform } from "lodash-es"; import type { ComponentPropsWithoutRef } from "react"; import { Button } from "ui/components/buttons/Button"; import { Group, Input, Password, Label } from "ui/components/form/Formy/components"; import { SocialLink } from "./SocialLink"; -import type { Validator } from "json-schema-form-react"; -import { s } from "bknd/core"; -import type { ErrorDetail } from "jsonv-ts"; - -class JsonvTsValidator implements Validator { - async validate(schema: s.Schema, data: any) { - return schema.validate(data).errors; - } -} export type LoginFormProps = Omit, "onSubmit" | "action"> & { className?: string; @@ -25,16 +16,6 @@ export type LoginFormProps = Omit, "onSubmit" | buttonLabel?: string; }; -const validator = new JsonvTsValidator(); -const schema = s.strictObject({ - email: s.string({ - pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$", - }), - password: s.string({ - minLength: 8, // @todo: this should be configurable - }), -}); - export function AuthForm({ formData, className, @@ -79,38 +60,31 @@ export function AuthForm({ )} -
- {({ errors, submitting }) => ( - <> - - - - - - - - + + + + + + + + - - - )} -
+ + ); } diff --git a/app/src/ui/routes/data/forms/entity.fields.form.tsx b/app/src/ui/routes/data/forms/entity.fields.form.tsx index 60be4c8e..ee8bf76e 100644 --- a/app/src/ui/routes/data/forms/entity.fields.form.tsx +++ b/app/src/ui/routes/data/forms/entity.fields.form.tsx @@ -23,6 +23,7 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec import type { TPrimaryFieldFormat } from "data/fields/PrimaryField"; import { s, stringIdentifier } from "bknd/core"; import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; +import ErrorBoundary from "ui/components/display/ErrorBoundary"; const fieldsSchemaObject = originalFieldsSchemaObject; const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject)); @@ -453,19 +454,17 @@ function EntityField({
- { - setValue(`${prefix}.config`, { - ...getValues([`fields.${index}.config`])[0], - ...value, - }); - }} - /> + + { + setValue(`${prefix}.config`, { + ...getValues([`fields.${index}.config`])[0], + ...value, + }); + }} + /> +
@@ -490,3 +489,25 @@ function EntityField({ ); } + +const SpecificForm = ({ + field, + onChange, +}: { + field: FieldArrayWithId; + onChange: (value: any) => void; +}) => { + const type = field.field.type; + const specificData = omit(field.field.config, commonProps); + + return ( + + ); +}; diff --git a/bun.lock b/bun.lock index a002cc6e..5264eeef 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ }, "app": { "name": "bknd", - "version": "0.15.0", + "version": "0.16.0-rc.0", "bin": "./dist/cli/index.js", "dependencies": { "@cfworker/json-schema": "^4.1.1", @@ -32,8 +32,7 @@ "bcryptjs": "^3.0.2", "dayjs": "^1.11.13", "fast-xml-parser": "^5.0.8", - "hono": "^4.7.11", - "json-schema-form-react": "^0.0.2", + "hono": "4.8.3", "json-schema-library": "10.0.0-rc7", "json-schema-to-ts": "^3.1.1", "kysely": "^0.27.6", @@ -72,7 +71,7 @@ "dotenv": "^16.4.7", "jotai": "^2.12.2", "jsdom": "^26.0.0", - "jsonv-ts": "^0.2.2", + "jsonv-ts": "^0.2.3", "kysely-d1": "^0.3.0", "kysely-generic-sqlite": "^1.2.1", "libsql-stateless-easy": "^1.8.0", @@ -124,7 +123,6 @@ "version": "0.5.1", "devDependencies": { "@types/bun": "latest", - "bknd": "workspace:*", "tsdx": "^0.14.1", "typescript": "^5.0.0", }, @@ -2208,7 +2206,7 @@ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], - "hono": ["hono@4.7.11", "", {}, "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ=="], + "hono": ["hono@4.8.3", "", {}, "sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ=="], "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], @@ -2478,8 +2476,6 @@ "json-schema-compare": ["json-schema-compare@0.2.2", "", { "dependencies": { "lodash": "^4.17.4" } }, "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ=="], - "json-schema-form-react": ["json-schema-form-react@0.0.2", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-AoLuYpmKmFitc5eZPXKnPmVpel1VVhMTKBz/U5/esLrd74P3uxN2JNVKI/AI2lwLq6K0u9PBqX2pmpVGcTwzaw=="], - "json-schema-library": ["json-schema-library@10.0.0-rc7", "", { "dependencies": { "@sagold/json-pointer": "^6.0.1", "@sagold/json-query": "^6.2.0", "deepmerge": "^4.3.1", "fast-copy": "^3.0.2", "fast-deep-equal": "^3.1.3", "smtp-address-parser": "1.0.10", "valid-url": "^1.0.9" } }, "sha512-q9DMhftVyO8Xa8cfupS5Kx5Uv1A9OJvyRn8DVDMATQv8bITq18cdZimWilwjHIuf2Mzphy67bSJU9gEFno7BLw=="], "json-schema-merge-allof": ["json-schema-merge-allof@0.8.1", "", { "dependencies": { "compute-lcm": "^1.1.2", "json-schema-compare": "^0.2.2", "lodash": "^4.17.20" } }, "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w=="], @@ -2500,7 +2496,7 @@ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "jsonv-ts": ["jsonv-ts@0.2.2", "", { "optionalDependencies": { "hono": "4.7.11" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-g4kUDYYPBb32Nfbv3R55Se6G4CAv4UOXOPJIEPqw8XX+0SSy44T/AREkwFfGz9GwYuX9UxcvOILbgac2nshL4A=="], + "jsonv-ts": ["jsonv-ts@0.2.3", "", { "optionalDependencies": { "hono": "*" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-WvRQ/Eu9DNgohr/WbfzMS1kmvAJVDJWOYcImwRMee2RXNfRGcXgP8PDk0k1gaeb4OPzvJnFrbFNY2tZq3BvXEQ=="], "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], diff --git a/examples/waku/.gitignore b/examples/waku/.gitignore new file mode 100644 index 00000000..ad583432 --- /dev/null +++ b/examples/waku/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.env* +*.tsbuildinfo +.cache +.DS_Store +*.pem diff --git a/examples/waku/bknd.config.ts b/examples/waku/bknd.config.ts new file mode 100644 index 00000000..28ecf039 --- /dev/null +++ b/examples/waku/bknd.config.ts @@ -0,0 +1,62 @@ +import { registerLocalMediaAdapter } from "bknd/adapter/node"; +import type { BkndConfig } from "bknd/adapter"; +import { boolean, em, entity, text } from "bknd/data"; +import { secureRandomString } from "bknd/utils"; + +// since we're running in node, we can register the local media adapter +const local = registerLocalMediaAdapter(); + +const schema = em({ + todos: entity("todos", { + title: text(), + done: boolean(), + }), +}); + +// register your schema to get automatic type completion +type Database = (typeof schema)["DB"]; +declare module "bknd/core" { + interface DB extends Database {} +} + +export default { + // we can use any libsql config, and if omitted, uses in-memory + connection: { + url: process.env.DB_URL ?? "file:data.db", + }, + // an initial config is only applied if the database is empty + initialConfig: { + data: schema.toJSON(), + // we're enabling auth ... + auth: { + enabled: true, + jwt: { + issuer: "bknd-waku-example", + secret: secureRandomString(64), + }, + }, + // ... and media + media: { + enabled: true, + adapter: local({ + path: "./public/uploads", + }), + }, + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + // create some entries + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); + + // and create a user + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, +} as const satisfies BkndConfig<{ DB_URL?: string }>; diff --git a/examples/waku/package.json b/examples/waku/package.json new file mode 100644 index 00000000..1ce31e49 --- /dev/null +++ b/examples/waku/package.json @@ -0,0 +1,26 @@ +{ + "name": "waku", + "version": "0.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "waku dev", + "build": "waku build", + "start": "waku start" + }, + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "react-server-dom-webpack": "19.0.0", + "waku": "0.23.3", + "bknd": "file:../../app" + }, + "devDependencies": { + "@tailwindcss/postcss": "4.1.10", + "@types/react": "19.1.8", + "@types/react-dom": "19.1.6", + "postcss": "8.5.6", + "tailwindcss": "4.1.10", + "typescript": "5.8.3" + } +} diff --git a/examples/waku/postcss.config.js b/examples/waku/postcss.config.js new file mode 100644 index 00000000..a34a3d56 --- /dev/null +++ b/examples/waku/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/examples/waku/public/images/favicon.png b/examples/waku/public/images/favicon.png new file mode 100644 index 00000000..cd90d790 Binary files /dev/null and b/examples/waku/public/images/favicon.png differ diff --git a/examples/waku/public/robots.txt b/examples/waku/public/robots.txt new file mode 100644 index 00000000..b4d27bb5 --- /dev/null +++ b/examples/waku/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /RSC/ \ No newline at end of file diff --git a/examples/waku/src/bknd.ts b/examples/waku/src/bknd.ts new file mode 100644 index 00000000..577d4b22 --- /dev/null +++ b/examples/waku/src/bknd.ts @@ -0,0 +1,22 @@ +import { createFrameworkApp } from "bknd/adapter"; +import config from "../bknd.config"; + +export async function getApp() { + return await createFrameworkApp(config, process.env, { + force: import.meta.env && !import.meta.env.PROD, + }); +} + +export async function getApi(opts?: { + headers?: Headers | any; + verify?: boolean; +}) { + const app = await getApp(); + if (opts?.verify && opts.headers) { + const api = app.getApi({ headers: opts.headers }); + await api.verifyAuth(); + return api; + } + + return app.getApi(); +} diff --git a/examples/waku/src/components/counter.tsx b/examples/waku/src/components/counter.tsx new file mode 100644 index 00000000..0e540b8b --- /dev/null +++ b/examples/waku/src/components/counter.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { useState } from 'react'; + +export const Counter = () => { + const [count, setCount] = useState(0); + + const handleIncrement = () => setCount((c) => c + 1); + + return ( +
+
Count: {count}
+ +
+ ); +}; diff --git a/examples/waku/src/components/footer.tsx b/examples/waku/src/components/footer.tsx new file mode 100644 index 00000000..8cfd9c89 --- /dev/null +++ b/examples/waku/src/components/footer.tsx @@ -0,0 +1,18 @@ +export const Footer = () => { + return ( + + ); +}; diff --git a/examples/waku/src/components/header.tsx b/examples/waku/src/components/header.tsx new file mode 100644 index 00000000..1b03ba54 --- /dev/null +++ b/examples/waku/src/components/header.tsx @@ -0,0 +1,11 @@ +import { Link } from 'waku'; + +export const Header = () => { + return ( +
+

+ Waku starter +

+
+ ); +}; diff --git a/examples/waku/src/lib/waku/client.tsx b/examples/waku/src/lib/waku/client.tsx new file mode 100644 index 00000000..2d889c97 --- /dev/null +++ b/examples/waku/src/lib/waku/client.tsx @@ -0,0 +1,20 @@ +"use client"; + +import * as React from "react"; + +export function BrowserOnly(props: React.SuspenseProps) { + const hydrated = useHydrated(); + if (!hydrated) { + return props.fallback; + } + return ; +} + +const noopStore = () => () => {}; + +const useHydrated = () => + React.useSyncExternalStore( + noopStore, + () => true, + () => false + ); diff --git a/examples/waku/src/lib/waku/server.ts b/examples/waku/src/lib/waku/server.ts new file mode 100644 index 00000000..608aa26e --- /dev/null +++ b/examples/waku/src/lib/waku/server.ts @@ -0,0 +1,26 @@ +"use server"; + +import { getContext } from "waku/middleware/context"; +import { getApi } from "../../bknd"; + +export { unstable_rerenderRoute as rerender } from "waku/router/server"; + +export function context() { + return getContext(); +} + +export function handlerReq() { + return getContext().req; +} + +export function headers() { + const context = getContext(); + return new Headers(context.req.headers); +} + +export async function getUserApi(opts?: { verify?: boolean }) { + return await getApi({ + headers: headers(), + verify: !!opts?.verify, + }); +} diff --git a/examples/waku/src/pages.gen.ts b/examples/waku/src/pages.gen.ts new file mode 100644 index 00000000..a3c9fb7f --- /dev/null +++ b/examples/waku/src/pages.gen.ts @@ -0,0 +1,30 @@ +// deno-fmt-ignore-file +// biome-ignore format: generated types do not need formatting +// prettier-ignore +import type { PathsForPages, GetConfigResponse } from 'waku/router'; + +// prettier-ignore +import type { getConfig as File_About_getConfig } from './pages/about'; +// prettier-ignore +import type { getConfig as File_AdminAdmin_getConfig } from './pages/admin/[...admin]'; +// prettier-ignore +import type { getConfig as File_Index_getConfig } from './pages/index'; + +// prettier-ignore +type Page = +| ({ path: '/about' } & GetConfigResponse) +| ({ path: '/admin/[...admin]' } & GetConfigResponse) +| { path: '/admin'; render: 'dynamic' } +| ({ path: '/' } & GetConfigResponse) +| { path: '/login'; render: 'dynamic' } +| { path: '/test'; render: 'dynamic' }; + +// prettier-ignore +declare module 'waku/router' { + interface RouteConfig { + paths: PathsForPages; + } + interface CreatePagesConfig { + pages: Page; + } +} diff --git a/examples/waku/src/pages/_layout.tsx b/examples/waku/src/pages/_layout.tsx new file mode 100644 index 00000000..2e3c7e93 --- /dev/null +++ b/examples/waku/src/pages/_layout.tsx @@ -0,0 +1,28 @@ +import "../styles.css"; + +import type { ReactNode } from "react"; + +import { ClientProvider } from "bknd/client"; + +type RootLayoutProps = { children: ReactNode }; + +export default async function RootLayout({ children }: RootLayoutProps) { + const data = await getData(); + + return children; +} + +const getData = async () => { + const data = { + description: "An internet website!", + icon: "/images/favicon.png", + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: "static", + } as const; +}; diff --git a/examples/waku/src/pages/about.tsx b/examples/waku/src/pages/about.tsx new file mode 100644 index 00000000..15d4c90e --- /dev/null +++ b/examples/waku/src/pages/about.tsx @@ -0,0 +1,32 @@ +import { Link } from 'waku'; + +export default async function AboutPage() { + const data = await getData(); + + return ( +
+ {data.title} +

{data.headline}

+

{data.body}

+ + Return home + +
+ ); +} + +const getData = async () => { + const data = { + title: 'About', + headline: 'About Waku', + body: 'The minimal React framework', + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: 'static', + } as const; +}; diff --git a/examples/waku/src/pages/admin/[...admin].tsx b/examples/waku/src/pages/admin/[...admin].tsx new file mode 100644 index 00000000..136182bb --- /dev/null +++ b/examples/waku/src/pages/admin/[...admin].tsx @@ -0,0 +1,32 @@ +/** + * See https://github.com/wakujs/waku/issues/1499 + */ + +import { Suspense, lazy } from "react"; +import { getUserApi } from "../../lib/waku/server"; + +const AdminComponent = lazy(() => import("./_components/AdminLoader")); + +export default async function HomePage() { + const api = await getUserApi({ verify: true }); + + // @ts-ignore + const styles = await import("bknd/dist/styles.css?inline").then((m) => m.default); + return ( + <> + + + + + + ); +} + +// Enable dynamic server rendering. +// Static rendering is possible if you want to render at build time. +// The Hono context will not be available. +export const getConfig = async () => { + return { + render: "dynamic", + } as const; +}; diff --git a/examples/waku/src/pages/admin/_components/AdminImpl.tsx b/examples/waku/src/pages/admin/_components/AdminImpl.tsx new file mode 100644 index 00000000..e1c9afd0 --- /dev/null +++ b/examples/waku/src/pages/admin/_components/AdminImpl.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Admin, type BkndAdminProps } from "bknd/ui"; + +export const AdminImpl = (props: BkndAdminProps) => { + if (typeof window === "undefined") { + return null; + } + + return ( + + ); +}; + +export default AdminImpl; diff --git a/examples/waku/src/pages/admin/_components/AdminLoader.tsx b/examples/waku/src/pages/admin/_components/AdminLoader.tsx new file mode 100644 index 00000000..6b2e9a74 --- /dev/null +++ b/examples/waku/src/pages/admin/_components/AdminLoader.tsx @@ -0,0 +1,18 @@ +"use client"; + +import type { BkndAdminProps } from "bknd/ui"; +import { lazy } from "react"; +import { BrowserOnly } from "../../../lib/waku/client"; + +const AdminImpl = import.meta.env.SSR ? undefined : lazy(() => import("./AdminImpl")); + +export const AdminLoader = (props: BkndAdminProps) => { + return ( + + {/* @ts-expect-error */} + + + ); +}; + +export default AdminLoader; diff --git a/examples/waku/src/pages/admin/index.tsx b/examples/waku/src/pages/admin/index.tsx new file mode 100644 index 00000000..7d32f6fe --- /dev/null +++ b/examples/waku/src/pages/admin/index.tsx @@ -0,0 +1,6 @@ +import { unstable_redirect as redirect } from "waku/router/server"; + +export default async function AdminIndex() { + await new Promise((resolve) => setTimeout(resolve, 100)); + redirect("/admin/data"); +} diff --git a/examples/waku/src/pages/api/[...api].ts b/examples/waku/src/pages/api/[...api].ts new file mode 100644 index 00000000..eaf8bb35 --- /dev/null +++ b/examples/waku/src/pages/api/[...api].ts @@ -0,0 +1,5 @@ +import { getApp } from "../../bknd"; + +export default async function handler(request: Request) { + return (await getApp()).fetch(request); +} diff --git a/examples/waku/src/pages/index.tsx b/examples/waku/src/pages/index.tsx new file mode 100644 index 00000000..553d23bc --- /dev/null +++ b/examples/waku/src/pages/index.tsx @@ -0,0 +1,70 @@ +import { Link } from "waku"; + +import { Counter } from "../components/counter"; +import { rerender, getUserApi } from "../lib/waku/server"; + +async function toggleTodo(todo: any, path: string) { + "use server"; + const api = await getUserApi(); + await api.data.updateOne("todos", todo.id, { + done: !todo.done, + }); + rerender(path); +} + +export default async function HomePage({ path }: any) { + const api = await getUserApi({ verify: true }); + const todos = await api.data.readMany("todos"); + const user = api.getUser(); + const data = await getData(); + + return ( +
+ {data.title} +

{data.headline}

+

{data.body}

+
    + {todos?.map((todo) => ( +
  • + {todo.title} {todo.done ? "✅" : "❌"} +
    + +
    +
  • + ))} +
+ + + About page + + {user ? ( + + Logout ({user.email}) + + ) : ( + + Login + + )} + + Admin + +
+ ); +} + +const getData = async () => { + const data = { + title: "Waku", + headline: "Waku", + body: "Hello world!", + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: "dynamic", + } as const; +}; diff --git a/examples/waku/src/pages/login.tsx b/examples/waku/src/pages/login.tsx new file mode 100644 index 00000000..db41ac0a --- /dev/null +++ b/examples/waku/src/pages/login.tsx @@ -0,0 +1,9 @@ +export default function LoginPage() { + return ( +
+ + + +
+ ); +} diff --git a/examples/waku/src/pages/test.tsx b/examples/waku/src/pages/test.tsx new file mode 100644 index 00000000..d74ce923 --- /dev/null +++ b/examples/waku/src/pages/test.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect } from "react"; + +export default function Test() { + useEffect(() => { + bridge(); + }, []); + return null; +} + +async function bridge() { + const aud = new URLSearchParams(location.search).get("aud") || ""; + // 1. Verify the user still has an auth cookie + const me = await fetch("/api/auth/me", { credentials: "include" }); + console.log("sso-bridge:me", me); + if (!me.ok) { + console.log("sso-bridge:no session"); + parent.postMessage({ type: "NOSESSION" }, aud); + } else { + console.log("sso-bridge:session"); + + // 2. Get short-lived JWT (internal endpoint, same origin) + const res = await fetch("/api/issue-jwt", { + credentials: "include", + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ + aud, + }), + }); + console.log("sso-bridge:res", res); + const { jwt, exp } = (await res.json()) as any; // exp = unix timestamp seconds + console.log("sso-bridge:jwt", { jwt, exp }); + + // 3. Send token up + parent.postMessage({ type: "JWT", jwt, exp }, aud); + + // 4. Listen for refresh requests + window.addEventListener("message", async (ev) => { + console.log("sso-bridge:message", ev); + if (ev.origin !== aud) return; + if (ev.data !== "REFRESH") return; + console.log("sso-bridge:message:refresh"); + + const r = await fetch("/api/issue-jwt", { + credentials: "include", + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ aud: ev.origin }), + }); + console.log("sso-bridge:message:r", r); + const { jwt, exp } = (await r.json()) as any; + console.log("sso-bridge:message:jwt", { jwt, exp }); + parent.postMessage({ type: "JWT", jwt, exp }, ev.origin); + }); + } +} diff --git a/examples/waku/src/styles.css b/examples/waku/src/styles.css new file mode 100644 index 00000000..f1d3c37c --- /dev/null +++ b/examples/waku/src/styles.css @@ -0,0 +1,3 @@ +@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap') +layer(base); +@import 'tailwindcss'; diff --git a/examples/waku/tsconfig.json b/examples/waku/tsconfig.json new file mode 100644 index 00000000..3b26664b --- /dev/null +++ b/examples/waku/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "strict": true, + "target": "esnext", + "noEmit": true, + "isolatedModules": true, + "moduleDetection": "force", + "downlevelIteration": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "jsx": "react-jsx" + } +}