mirror of
https://github.com/tale/headplane.git
synced 2026-08-17 16:47:51 +00:00
feat: update to the v8 middleware api
This commit is contained in:
+16
-13
@@ -9,7 +9,7 @@ runs only on the Node process — never in the browser.
|
||||
app/server/
|
||||
├── app.ts ← The Headplane application (load context, RR listener)
|
||||
├── main.ts ← Production bootstrap (binds an http(s) server)
|
||||
├── context.ts ← createAppContext() — assembles the AppLoadContext
|
||||
├── context.ts ← createAppContext() — assembles the RouterContextProvider data
|
||||
├── result.ts ← Result<T, E> helper used across the server modules
|
||||
│
|
||||
├── config/ ← YAML config loading, schema, env-overrides, integrations
|
||||
@@ -26,9 +26,10 @@ There are two SSR entries; both are picked up by Vite via `vite.config.ts`.
|
||||
|
||||
### `app.ts` — the application module
|
||||
|
||||
Loads config → builds the `AppLoadContext` (via [`context.ts`](./context.ts))
|
||||
→ exports the React Router `RequestListener` as `default`, plus the
|
||||
resolved `config` as a named export.
|
||||
Loads config → builds the application context (via [`context.ts`](./context.ts))
|
||||
→ seeds React Router's `RouterContextProvider` with the named service contexts
|
||||
→ exports the React Router `RequestListener` as `default`, plus the resolved
|
||||
`config` as a named export.
|
||||
|
||||
This module has no opinions about how the server is hosted. It does not
|
||||
listen on a socket, doesn't compose static-asset serving, and doesn't
|
||||
@@ -71,17 +72,19 @@ process:
|
||||
- the (best-effort) parsed Headscale config (`hs`)
|
||||
- the integration adapter (`integration`)
|
||||
|
||||
The returned object is the `AppLoadContext` exposed to every React
|
||||
Router loader/action. The module also `declare module "react-router" { interface AppLoadContext extends AppContext {} }`
|
||||
so route handlers get full type inference on `context`.
|
||||
The returned object owns process-lifetime services, but route handlers consume
|
||||
those services through named React Router contexts such as `authContext`,
|
||||
`headscaleContext`, and `headscaleConfigContext`:
|
||||
|
||||
When a route needs the type, import it from `~/server/context`:
|
||||
When a route needs a service, import the matching context from
|
||||
`~/server/context`:
|
||||
|
||||
```ts
|
||||
import type { AppContext } from "~/server/context";
|
||||
import { authContext } from "~/server/context";
|
||||
|
||||
export async function loader({ context }: LoaderFunctionArgs<AppContext>) {
|
||||
// …
|
||||
export async function loader({ context, request }: Route.LoaderArgs) {
|
||||
const auth = context.get(authContext);
|
||||
const principal = await auth.require(request);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -105,7 +108,7 @@ file is loaded, not by runtime conditionals.
|
||||
that names a coherent concern, e.g. `metrics/`, `ratelimit/`).
|
||||
2. If it owns process-lifetime state (a connection pool, a service
|
||||
client, …), construct it in [`context.ts`](./context.ts) and add it
|
||||
to the returned object — this gives every route automatic access via
|
||||
`context.<name>`.
|
||||
to the returned object. Expose it through a named React Router context
|
||||
and seed that context in [`app.ts`](./app.ts)'s `getLoadContext`.
|
||||
3. If it's purely a helper (pure functions, type definitions), import
|
||||
it directly from the module that needs it.
|
||||
|
||||
+29
-2
@@ -12,6 +12,7 @@
|
||||
import { exit, versions } from "node:process";
|
||||
|
||||
import { createRequestListener } from "@react-router/node";
|
||||
import { RouterContextProvider } from "react-router";
|
||||
import * as build from "virtual:react-router/server-build";
|
||||
|
||||
import log from "~/utils/log";
|
||||
@@ -19,7 +20,20 @@ import log from "~/utils/log";
|
||||
import type { HeadplaneConfig } from "./config/config-schema";
|
||||
import { ConfigError } from "./config/error";
|
||||
import { loadConfig } from "./config/load";
|
||||
import { createAppContext } from "./context";
|
||||
import {
|
||||
agentsContext,
|
||||
appConfigContext,
|
||||
authContext,
|
||||
createAppContext,
|
||||
dbContext,
|
||||
headscaleApiKeyContext,
|
||||
headscaleConfigContext,
|
||||
headscaleContext,
|
||||
headscaleLiveStoreContext,
|
||||
integrationContext,
|
||||
oidcContext,
|
||||
requestApiContext,
|
||||
} from "./context";
|
||||
|
||||
log.info("server", "Running Node.js %s", versions.node);
|
||||
|
||||
@@ -65,6 +79,19 @@ export default createRequestListener({
|
||||
mode: import.meta.env.MODE,
|
||||
getLoadContext: (request, client) => {
|
||||
ctx.auth.registerRequestClientAddress(request, client.address);
|
||||
return ctx;
|
||||
|
||||
const routerContext = new RouterContextProvider();
|
||||
routerContext.set(agentsContext, ctx.agents);
|
||||
routerContext.set(appConfigContext, ctx.config);
|
||||
routerContext.set(authContext, ctx.auth);
|
||||
routerContext.set(dbContext, ctx.db);
|
||||
routerContext.set(headscaleContext, ctx.headscale);
|
||||
routerContext.set(headscaleApiKeyContext, ctx.headscaleApiKey);
|
||||
routerContext.set(headscaleConfigContext, ctx.hs);
|
||||
routerContext.set(headscaleLiveStoreContext, ctx.hsLive);
|
||||
routerContext.set(integrationContext, ctx.integration);
|
||||
routerContext.set(oidcContext, ctx.oidc);
|
||||
routerContext.set(requestApiContext, ctx.apiForRequest);
|
||||
return routerContext;
|
||||
},
|
||||
});
|
||||
|
||||
+13
-4
@@ -1,5 +1,7 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createContext } from "react-router";
|
||||
|
||||
import log from "~/utils/log";
|
||||
|
||||
import type { HeadplaneConfig } from "./config/config-schema";
|
||||
@@ -14,10 +16,17 @@ import { createOidcService, type OidcService } from "./oidc/provider";
|
||||
import { createAuthService, type Principal } from "./web/auth";
|
||||
|
||||
export type AppContext = Awaited<ReturnType<typeof createAppContext>>;
|
||||
|
||||
declare module "react-router" {
|
||||
interface AppLoadContext extends AppContext {}
|
||||
}
|
||||
export const agentsContext = createContext<AppContext["agents"]>();
|
||||
export const appConfigContext = createContext<AppContext["config"]>();
|
||||
export const authContext = createContext<AppContext["auth"]>();
|
||||
export const dbContext = createContext<AppContext["db"]>();
|
||||
export const headscaleContext = createContext<AppContext["headscale"]>();
|
||||
export const headscaleApiKeyContext = createContext<AppContext["headscaleApiKey"]>();
|
||||
export const headscaleConfigContext = createContext<AppContext["hs"]>();
|
||||
export const headscaleLiveStoreContext = createContext<AppContext["hsLive"]>();
|
||||
export const integrationContext = createContext<AppContext["integration"]>();
|
||||
export const oidcContext = createContext<AppContext["oidc"]>();
|
||||
export const requestApiContext = createContext<AppContext["apiForRequest"]>();
|
||||
|
||||
export async function createAppContext(config: HeadplaneConfig) {
|
||||
const db = await createDbClient(join(config.server.data_path, "hp_persist.db"));
|
||||
|
||||
Reference in New Issue
Block a user