feat: complete SSR CRM and refine auth UI
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.41",
|
||||
"version": "0.8.42",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1482,13 +1482,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
});
|
||||
const apiCtx = createContext(apiReq, apiUrl);
|
||||
apiCtx.locals = ctx.locals;
|
||||
apiCtx.user = ctx.user;
|
||||
apiCtx.tenant = ctx.tenant;
|
||||
apiCtx.tracer = ctx.tracer;
|
||||
apiCtx.ip = ctx.ip;
|
||||
apiCtx.lang = ctx.lang;
|
||||
apiCtx.t = ctx.t;
|
||||
apiCtx.params = ctx.params;
|
||||
apiCtx.cookies = ctx.cookies;
|
||||
apiCtx.session = ctx.session;
|
||||
apiCtx.localStorage = ctx.localStorage;
|
||||
|
||||
const apiRes = await handleApi(apiCtx);
|
||||
if (!apiRes.ok) {
|
||||
throw new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`);
|
||||
const contentType = apiRes.headers.get("content-type") ?? "";
|
||||
const data = contentType.includes("application/json")
|
||||
? await apiRes.json().catch(() => undefined)
|
||||
: await apiRes.text().catch(() => undefined);
|
||||
throw Object.assign(new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`), {
|
||||
status: apiRes.status,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
const contentType = apiRes.headers.get("content-type") ?? "";
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
test("in-process api failures preserve status and response data for server error sections", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-error-context-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "api"), { recursive: true });
|
||||
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
|
||||
writeFileSync(join(app, "api", "failure.ts"), "export const GET = () => null;\n");
|
||||
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
file.endsWith(`${join("api", "failure")}.ts`)
|
||||
? {
|
||||
GET: () => Response.json({ error: "denied", reason: "policy" }, { status: 403 }),
|
||||
}
|
||||
: {
|
||||
__wrnexusLoad: async (ctx: {
|
||||
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
|
||||
}) => {
|
||||
try {
|
||||
await ctx.__wrnexusCallApi("/api/failure", "GET");
|
||||
return { caught: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: {
|
||||
status: (error as { status?: unknown }).status,
|
||||
data: (error as { data?: unknown }).data,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
default: (ctx: { caught: unknown }) => `<pre>${JSON.stringify(ctx.caught)}</pre>`,
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
|
||||
const response = await handlers.fetch(new Request("https://example.test/probe"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
|
||||
expect(response?.status).toBe(200);
|
||||
expect(await response?.text()).toContain(
|
||||
JSON.stringify({ status: 403, data: { error: "denied", reason: "policy" } }),
|
||||
);
|
||||
});
|
||||
|
||||
test("in-process api calls inherit authenticated request context", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-auth-context-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "api"), { recursive: true });
|
||||
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
|
||||
writeFileSync(join(app, "api", "identity.ts"), "export const GET = () => null;\n");
|
||||
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
file.endsWith(`${join("api", "identity")}.ts`)
|
||||
? { GET: (ctx: { user?: unknown }) => Response.json({ user: ctx.user }) }
|
||||
: {
|
||||
__wrnexusLoad: async (ctx: {
|
||||
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
|
||||
}) => ({ identity: await ctx.__wrnexusCallApi("/api/identity", "GET") }),
|
||||
default: (ctx: { identity: unknown }) => `<pre>${JSON.stringify(ctx.identity)}</pre>`,
|
||||
},
|
||||
getMiddleware: async () => [
|
||||
(ctx, next) => {
|
||||
ctx.user = { id: "authenticated-user" };
|
||||
return next();
|
||||
},
|
||||
],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
|
||||
const response = await handlers.fetch(new Request("https://example.test/probe"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
|
||||
expect(response?.status).toBe(200);
|
||||
expect(await response?.text()).toContain(JSON.stringify({ user: { id: "authenticated-user" } }));
|
||||
});
|
||||
Reference in New Issue
Block a user