minutework 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/claude-local/skills/README.md +2 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
- package/assets/templates/next-tenant-app/README.md +26 -138
- package/assets/templates/next-tenant-app/package.json +1 -0
- package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
- package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
- package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
- package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
- package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
- package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
- package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
- package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
- package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
- package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
- package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
- package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
- package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
- package/assets/templates/next-tenant-app/template.json +3 -3
- package/assets/templates/next-tenant-app/template.schema.json +1 -0
- package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
- package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
- package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
- package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
- package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
- package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
- package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
- package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
- package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
- package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
- package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
- package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
- package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
- package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
- package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
- package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
import "server-only";
|
|
2
|
-
|
|
3
1
|
import type { Metadata } from "next";
|
|
4
2
|
|
|
5
|
-
import { env } from "@/lib/platform/env.server";
|
|
6
|
-
|
|
7
|
-
export function isPublicContentDisabled() {
|
|
8
|
-
return env.MW_PUBLIC_CONTENT_SOURCE === "none";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function buildDisabledPublicMetadata(): Metadata {
|
|
12
|
-
return {
|
|
13
|
-
robots: {
|
|
14
|
-
index: false,
|
|
15
|
-
follow: false,
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
3
|
export function resolvePublicMetadataBase() {
|
|
21
|
-
|
|
4
|
+
const value = process.env.MW_PUBLIC_BASE_URL || "";
|
|
5
|
+
if (!value) {
|
|
22
6
|
return null;
|
|
23
7
|
}
|
|
24
|
-
|
|
25
|
-
return new URL(
|
|
26
|
-
env.MW_PUBLIC_BASE_URL.endsWith("/")
|
|
27
|
-
? env.MW_PUBLIC_BASE_URL
|
|
28
|
-
: `${env.MW_PUBLIC_BASE_URL}/`,
|
|
29
|
-
);
|
|
8
|
+
return new URL(value.endsWith("/") ? value : `${value}/`);
|
|
30
9
|
}
|
|
31
10
|
|
|
32
11
|
export function resolvePublicSiteUrl(pathname = "/") {
|
|
@@ -41,11 +20,8 @@ export function buildPublicMetadata(input: {
|
|
|
41
20
|
description: string;
|
|
42
21
|
path: string;
|
|
43
22
|
siteName?: string;
|
|
44
|
-
type?: "website" | "article";
|
|
45
|
-
publishedTime?: string | null;
|
|
46
23
|
}): Metadata {
|
|
47
24
|
const canonicalUrl = resolvePublicSiteUrl(input.path)?.toString();
|
|
48
|
-
|
|
49
25
|
return {
|
|
50
26
|
title: input.title,
|
|
51
27
|
description: input.description,
|
|
@@ -59,10 +35,9 @@ export function buildPublicMetadata(input: {
|
|
|
59
35
|
openGraph: {
|
|
60
36
|
title: input.title,
|
|
61
37
|
description: input.description,
|
|
62
|
-
siteName: input.siteName ?? env.MW_TEMPLATE_APP_NAME,
|
|
63
|
-
type:
|
|
38
|
+
siteName: input.siteName ?? process.env.MW_TEMPLATE_APP_NAME ?? "Tenant App",
|
|
39
|
+
type: "website",
|
|
64
40
|
...(canonicalUrl ? { url: canonicalUrl } : {}),
|
|
65
|
-
...(input.publishedTime ? { publishedTime: input.publishedTime } : {}),
|
|
66
41
|
},
|
|
67
42
|
twitter: {
|
|
68
43
|
card: "summary_large_image",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// MinuteWork substrate. Thin layer - do not put product UI/logic here.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createIdempotencyKey,
|
|
7
|
+
createMinuteWorkClient,
|
|
8
|
+
type IdempotencyKey,
|
|
9
|
+
} from "@minutework/web-auth";
|
|
10
|
+
|
|
11
|
+
export const mwAppId =
|
|
12
|
+
process.env.NEXT_PUBLIC_MW_APP_ID?.trim() || "tenant.app";
|
|
13
|
+
|
|
14
|
+
export const mw = createMinuteWorkClient({
|
|
15
|
+
appId: mwAppId,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export { createIdempotencyKey, type IdempotencyKey };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createIdempotencyKey } from "@/mw/client";
|
|
4
|
+
import { createTemplateMockMinuteWorkClient } from "@/mw/mock";
|
|
5
|
+
|
|
6
|
+
describe("MinuteWork SDK mock substrate", () => {
|
|
7
|
+
it("supports the template manifest query and action without platform BFF routes", async () => {
|
|
8
|
+
const client = createTemplateMockMinuteWorkClient();
|
|
9
|
+
await client.auth.verifyEmail({ token: "mock-verification-token" });
|
|
10
|
+
|
|
11
|
+
const queryResult = await client.query<{ count: number }>("demo.list", {});
|
|
12
|
+
const actionResult = await client.action<{ created: boolean }>(
|
|
13
|
+
"demo.create",
|
|
14
|
+
{ title: "Created" },
|
|
15
|
+
{ idempotencyKey: createIdempotencyKey("test") },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(queryResult.count).toBe(1);
|
|
19
|
+
expect(actionResult.created).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// MinuteWork substrate. Thin layer - do not put product UI/logic here.
|
|
4
|
+
|
|
5
|
+
import { createMockMinuteWorkClient } from "@minutework/web-auth/mock";
|
|
6
|
+
|
|
7
|
+
export function createTemplateMockMinuteWorkClient() {
|
|
8
|
+
return createMockMinuteWorkClient({
|
|
9
|
+
appId: "tenant.app",
|
|
10
|
+
verified: true,
|
|
11
|
+
queryHandlers: {
|
|
12
|
+
"demo.list": () => ({
|
|
13
|
+
data: [
|
|
14
|
+
{
|
|
15
|
+
id: "demo-record-1",
|
|
16
|
+
display_label: "Demo record",
|
|
17
|
+
data: { title: "Demo record", status: "ready" },
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
count: 1,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
actionHandlers: {
|
|
24
|
+
"demo.create": (payload, options) => ({
|
|
25
|
+
data: {
|
|
26
|
+
id: "demo-created",
|
|
27
|
+
display_label: String(payload.title || "Created record"),
|
|
28
|
+
data: payload,
|
|
29
|
+
},
|
|
30
|
+
created: true,
|
|
31
|
+
idempotency_key: options.idempotencyKey,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// MinuteWork substrate. Thin layer - do not put product UI/logic here.
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
import { MinuteWorkProvider } from "@minutework/web-auth/react";
|
|
8
|
+
|
|
9
|
+
import { mw } from "@/mw/client";
|
|
10
|
+
|
|
11
|
+
export function MinuteWorkSdkProvider({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<MinuteWorkProvider client={mw} loadSessionOnMount>
|
|
14
|
+
{children}
|
|
15
|
+
</MinuteWorkProvider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"template_id": "next-tenant-app",
|
|
3
3
|
"template_kind": "combined_web",
|
|
4
|
-
"template_profile": "
|
|
4
|
+
"template_profile": "tenant_web_auth_sdk",
|
|
5
5
|
"template_bundle_ref": "runtime/builder/templates/next-tenant-app",
|
|
6
6
|
"template_version": "0.3.0",
|
|
7
7
|
"materialize": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"reference/mwv3-dj6-docs/runtime_compute_isolation_and_sandboxing_contract.md"
|
|
21
21
|
],
|
|
22
22
|
"example_features": {
|
|
23
|
-
"
|
|
24
|
-
"default_enabled":
|
|
23
|
+
"manifest_sdk_demo": {
|
|
24
|
+
"default_enabled": true
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -15,13 +15,12 @@ const requiredPaths = [
|
|
|
15
15
|
"src/app/login/page.tsx",
|
|
16
16
|
"src/app/app/layout.tsx",
|
|
17
17
|
"src/app/app/page.tsx",
|
|
18
|
-
"src/app/app/
|
|
18
|
+
"src/app/app/demo/page.tsx",
|
|
19
19
|
"src/app/robots.ts",
|
|
20
20
|
"src/app/sitemap.ts",
|
|
21
|
-
"src/
|
|
22
|
-
"src/
|
|
23
|
-
"src/
|
|
24
|
-
"src/lib/content/adapter.server.ts",
|
|
21
|
+
"src/mw/client.ts",
|
|
22
|
+
"src/mw/provider.tsx",
|
|
23
|
+
"src/mw/mock.ts",
|
|
25
24
|
];
|
|
26
25
|
|
|
27
26
|
const missingPaths = requiredPaths.filter(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minutework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "MinuteWork CLI for workspace scaffolding, local preview workflows, and hosted preview deploys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"jiti": "^2.6.1",
|
|
26
26
|
"zod": "^4.3.6",
|
|
27
27
|
"@minutework/platform-config": "0.1.2",
|
|
28
|
-
"@minutework/schema-compiler": "0.1.
|
|
28
|
+
"@minutework/schema-compiler": "0.1.5"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^24.9.1",
|
|
@@ -356,8 +356,10 @@ export declare const SchemaStatusSchema: z.ZodObject<{
|
|
|
356
356
|
input_schema_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
357
357
|
output_schema_key: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
358
358
|
primitive: z.ZodOptional<z.ZodString>;
|
|
359
|
+
required_roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
359
360
|
schema_key: z.ZodString;
|
|
360
361
|
version: z.ZodLiteral<"ActionManifestV1">;
|
|
362
|
+
web_customer_exposed: z.ZodOptional<z.ZodBoolean>;
|
|
361
363
|
}, z.core.$strict>>;
|
|
362
364
|
appManifests: z.ZodArray<z.ZodObject<{
|
|
363
365
|
actions: z.ZodArray<z.ZodString>;
|
|
@@ -416,8 +418,10 @@ export declare const SchemaStatusSchema: z.ZodObject<{
|
|
|
416
418
|
detail: "detail";
|
|
417
419
|
list: "list";
|
|
418
420
|
}>, z.ZodLiteral<"connector">]>;
|
|
421
|
+
required_roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
419
422
|
schema_key: z.ZodString;
|
|
420
423
|
version: z.ZodLiteral<"QueryManifestV1">;
|
|
424
|
+
web_customer_exposed: z.ZodOptional<z.ZodBoolean>;
|
|
421
425
|
}, z.core.$strict>>;
|
|
422
426
|
releaseMetadata: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
|
|
423
427
|
routeManifests: z.ZodArray<z.ZodObject<{
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { Metadata } from "next";
|
|
2
|
-
import { notFound } from "next/navigation";
|
|
3
|
-
|
|
4
|
-
import { ContentStructureRenderer } from "@/features/public-shell/components/section-renderer";
|
|
5
|
-
import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
|
|
6
|
-
import { getPageByPath, getSiteConfig, listPages } from "@/lib/content/adapter.server";
|
|
7
|
-
import {
|
|
8
|
-
buildDisabledPublicMetadata,
|
|
9
|
-
buildPublicMetadata,
|
|
10
|
-
isPublicContentDisabled,
|
|
11
|
-
} from "@/lib/public-site";
|
|
12
|
-
|
|
13
|
-
type PageParams = { path: string[] };
|
|
14
|
-
|
|
15
|
-
export async function generateStaticParams(): Promise<PageParams[]> {
|
|
16
|
-
if (isPublicContentDisabled()) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const pages = await listPages("published");
|
|
21
|
-
return pages
|
|
22
|
-
.filter((p) => p.path !== "/")
|
|
23
|
-
.map((p) => ({
|
|
24
|
-
path: p.path.replace(/^\//, "").split("/").filter(Boolean),
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function generateMetadata({
|
|
29
|
-
params,
|
|
30
|
-
}: {
|
|
31
|
-
params: Promise<PageParams>;
|
|
32
|
-
}): Promise<Metadata> {
|
|
33
|
-
if (isPublicContentDisabled()) {
|
|
34
|
-
return buildDisabledPublicMetadata();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const { path: pathSegments } = await params;
|
|
38
|
-
const pagePath = `/${pathSegments.join("/")}`;
|
|
39
|
-
const page = await getPageByPath(pagePath);
|
|
40
|
-
|
|
41
|
-
if (!page) return {};
|
|
42
|
-
|
|
43
|
-
const seo = page.seo_metadata;
|
|
44
|
-
return buildPublicMetadata({
|
|
45
|
-
title: seo?.title ?? page.title,
|
|
46
|
-
description: seo?.description ?? page.title,
|
|
47
|
-
path: pagePath,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default async function CmsPage({
|
|
52
|
-
params,
|
|
53
|
-
}: {
|
|
54
|
-
params: Promise<PageParams>;
|
|
55
|
-
}) {
|
|
56
|
-
if (isPublicContentDisabled()) {
|
|
57
|
-
notFound();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { path: pathSegments } = await params;
|
|
61
|
-
const pagePath = `/${pathSegments.join("/")}`;
|
|
62
|
-
const [siteConfig, page] = await Promise.all([
|
|
63
|
-
getSiteConfig(),
|
|
64
|
-
getPageByPath(pagePath),
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
if (!page || page.status === "draft") {
|
|
68
|
-
notFound();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const sections = page.content_structure?.sections ?? [];
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<PublicSiteShell siteConfig={siteConfig} activeHref={pagePath}>
|
|
75
|
-
<div className="space-y-8">
|
|
76
|
-
<div className="space-y-3">
|
|
77
|
-
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
78
|
-
{page.title}
|
|
79
|
-
</h1>
|
|
80
|
-
</div>
|
|
81
|
-
{sections.length > 0 ? (
|
|
82
|
-
<ContentStructureRenderer sections={sections} />
|
|
83
|
-
) : (
|
|
84
|
-
<p className="text-muted-foreground">This page has no content yet.</p>
|
|
85
|
-
)}
|
|
86
|
-
</div>
|
|
87
|
-
</PublicSiteShell>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const fetchSessionContext = vi.fn();
|
|
4
|
-
const resetSessionContext = vi.fn();
|
|
5
|
-
const updateSessionContext = vi.fn();
|
|
6
|
-
const readPlatformAuthState = vi.fn();
|
|
7
|
-
const syncPlatformAuthStateToResponse = vi.fn();
|
|
8
|
-
|
|
9
|
-
vi.mock("@/lib/platform/client.server", () => ({
|
|
10
|
-
fetchSessionContext,
|
|
11
|
-
resetSessionContext,
|
|
12
|
-
updateSessionContext,
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock("@/lib/platform/session.server", () => ({
|
|
16
|
-
readPlatformAuthState,
|
|
17
|
-
syncPlatformAuthStateToResponse,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
describe("auth context route", () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("proxies valid tenant context updates through the BFF", async () => {
|
|
26
|
-
const authState = { sessionId: "session-1", csrfToken: "csrf-1" };
|
|
27
|
-
const payload = {
|
|
28
|
-
authenticated: true as const,
|
|
29
|
-
user: {
|
|
30
|
-
id: "user-1",
|
|
31
|
-
username: "demo-user",
|
|
32
|
-
email: "demo@example.com",
|
|
33
|
-
},
|
|
34
|
-
active_tenant_id: "tenant-2",
|
|
35
|
-
active_tenant: {
|
|
36
|
-
tenant_id: "tenant-2",
|
|
37
|
-
tenant_slug: "beta",
|
|
38
|
-
tenant_name: "Beta",
|
|
39
|
-
role: "member",
|
|
40
|
-
},
|
|
41
|
-
memberships: [
|
|
42
|
-
{
|
|
43
|
-
tenant_id: "tenant-2",
|
|
44
|
-
tenant_slug: "beta",
|
|
45
|
-
tenant_name: "Beta",
|
|
46
|
-
role: "member",
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
readPlatformAuthState.mockResolvedValue(authState);
|
|
52
|
-
updateSessionContext.mockResolvedValue({
|
|
53
|
-
data: payload,
|
|
54
|
-
authState: authState,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const route = await import("./route");
|
|
58
|
-
const response = await route.PUT(
|
|
59
|
-
new Request("http://localhost/api/auth/context", {
|
|
60
|
-
method: "PUT",
|
|
61
|
-
headers: { "content-type": "application/json" },
|
|
62
|
-
body: JSON.stringify({ tenant_id: "tenant-2" }),
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(updateSessionContext).toHaveBeenCalledWith(authState, {
|
|
67
|
-
tenant_id: "tenant-2",
|
|
68
|
-
});
|
|
69
|
-
expect(syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
|
|
70
|
-
response,
|
|
71
|
-
authState,
|
|
72
|
-
authState,
|
|
73
|
-
);
|
|
74
|
-
expect(response.status).toBe(200);
|
|
75
|
-
await expect(response.json()).resolves.toEqual(payload);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("returns 401 when the browser has no stored platform session", async () => {
|
|
79
|
-
readPlatformAuthState.mockResolvedValue({
|
|
80
|
-
sessionId: null,
|
|
81
|
-
csrfToken: "csrf-1",
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const route = await import("./route");
|
|
85
|
-
const response = await route.DELETE();
|
|
86
|
-
|
|
87
|
-
expect(resetSessionContext).not.toHaveBeenCalled();
|
|
88
|
-
expect(response.status).toBe(401);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
fetchSessionContext,
|
|
5
|
-
resetSessionContext,
|
|
6
|
-
updateSessionContext,
|
|
7
|
-
} from "@/lib/platform/client.server";
|
|
8
|
-
import { tenantSessionContextRequestSchema } from "@/lib/platform/contracts";
|
|
9
|
-
import { toPlatformErrorResponse } from "@/lib/platform/route-response";
|
|
10
|
-
import {
|
|
11
|
-
readPlatformAuthState,
|
|
12
|
-
syncPlatformAuthStateToResponse,
|
|
13
|
-
} from "@/lib/platform/session.server";
|
|
14
|
-
|
|
15
|
-
function unauthorizedResponse() {
|
|
16
|
-
return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function GET() {
|
|
20
|
-
const authState = await readPlatformAuthState();
|
|
21
|
-
|
|
22
|
-
if (!authState.sessionId) {
|
|
23
|
-
return unauthorizedResponse();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const result = await fetchSessionContext(authState);
|
|
28
|
-
const response = NextResponse.json(result.data, { status: 200 });
|
|
29
|
-
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
30
|
-
return response;
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return toPlatformErrorResponse(error, "Unable to load tenant context.", authState);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function PUT(request: Request) {
|
|
37
|
-
const authState = await readPlatformAuthState();
|
|
38
|
-
|
|
39
|
-
if (!authState.sessionId) {
|
|
40
|
-
return unauthorizedResponse();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const body = await request.json().catch(() => null);
|
|
44
|
-
const parsed = tenantSessionContextRequestSchema.safeParse(body);
|
|
45
|
-
|
|
46
|
-
if (!parsed.success) {
|
|
47
|
-
return NextResponse.json(
|
|
48
|
-
{ detail: "Invalid tenant context payload." },
|
|
49
|
-
{ status: 400 },
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const result = await updateSessionContext(authState, parsed.data);
|
|
55
|
-
const response = NextResponse.json(result.data, { status: 200 });
|
|
56
|
-
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
57
|
-
return response;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
return toPlatformErrorResponse(error, "Unable to update tenant context.", authState);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function DELETE() {
|
|
64
|
-
const authState = await readPlatformAuthState();
|
|
65
|
-
|
|
66
|
-
if (!authState.sessionId) {
|
|
67
|
-
return unauthorizedResponse();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const result = await resetSessionContext(authState);
|
|
72
|
-
const response = NextResponse.json(result.data, { status: 200 });
|
|
73
|
-
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
74
|
-
return response;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
return toPlatformErrorResponse(error, "Unable to reset tenant context.", authState);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
|
|
3
|
-
import { loginWithPassword } from "@/lib/platform/client.server";
|
|
4
|
-
import { loginRequestSchema } from "@/lib/platform/contracts";
|
|
5
|
-
import { toPlatformErrorResponse } from "@/lib/platform/route-response";
|
|
6
|
-
import {
|
|
7
|
-
readPlatformAuthState,
|
|
8
|
-
syncPlatformAuthStateToResponse,
|
|
9
|
-
} from "@/lib/platform/session.server";
|
|
10
|
-
|
|
11
|
-
export async function POST(request: Request) {
|
|
12
|
-
const authState = await readPlatformAuthState();
|
|
13
|
-
const body = await request.json().catch(() => null);
|
|
14
|
-
const parsed = loginRequestSchema.safeParse(body);
|
|
15
|
-
|
|
16
|
-
if (!parsed.success) {
|
|
17
|
-
return NextResponse.json(
|
|
18
|
-
{ detail: "Invalid login payload." },
|
|
19
|
-
{ status: 400 },
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const result = await loginWithPassword(authState, parsed.data);
|
|
25
|
-
const response = NextResponse.json({ authenticated: true }, { status: 200 });
|
|
26
|
-
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
27
|
-
return response;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
return toPlatformErrorResponse(error, "Unable to sign in.", authState);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
|
|
3
|
-
import { logoutPlatformSession } from "@/lib/platform/client.server";
|
|
4
|
-
import {
|
|
5
|
-
clearPlatformSessionFromResponse,
|
|
6
|
-
readPlatformAuthState,
|
|
7
|
-
} from "@/lib/platform/session.server";
|
|
8
|
-
|
|
9
|
-
export async function POST() {
|
|
10
|
-
const authState = await readPlatformAuthState();
|
|
11
|
-
await logoutPlatformSession(authState).catch(() => null);
|
|
12
|
-
|
|
13
|
-
const response = new NextResponse(null, { status: 204 });
|
|
14
|
-
clearPlatformSessionFromResponse(response);
|
|
15
|
-
return response;
|
|
16
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const changePassword = vi.fn();
|
|
4
|
-
const readPlatformAuthState = vi.fn();
|
|
5
|
-
const syncPlatformAuthStateToResponse = vi.fn();
|
|
6
|
-
|
|
7
|
-
vi.mock("@/lib/platform/client.server", () => ({
|
|
8
|
-
changePassword,
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock("@/lib/platform/session.server", () => ({
|
|
12
|
-
readPlatformAuthState,
|
|
13
|
-
syncPlatformAuthStateToResponse,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe("password change route", () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("rejects invalid password payloads before proxying upstream", async () => {
|
|
22
|
-
readPlatformAuthState.mockResolvedValue({
|
|
23
|
-
sessionId: "session-1",
|
|
24
|
-
csrfToken: "csrf-1",
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const route = await import("./route");
|
|
28
|
-
const response = await route.POST(
|
|
29
|
-
new Request("http://localhost/api/auth/password-change", {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers: { "content-type": "application/json" },
|
|
32
|
-
body: JSON.stringify({
|
|
33
|
-
current_password: "old",
|
|
34
|
-
new_password: "new-password",
|
|
35
|
-
confirm_password: "different-password",
|
|
36
|
-
}),
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
expect(changePassword).not.toHaveBeenCalled();
|
|
41
|
-
expect(response.status).toBe(400);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("proxies valid password changes through the BFF", async () => {
|
|
45
|
-
const authState = { sessionId: "session-1", csrfToken: "csrf-1" };
|
|
46
|
-
|
|
47
|
-
readPlatformAuthState.mockResolvedValue(authState);
|
|
48
|
-
changePassword.mockResolvedValue({
|
|
49
|
-
data: { has_password: true },
|
|
50
|
-
authState,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const route = await import("./route");
|
|
54
|
-
const response = await route.POST(
|
|
55
|
-
new Request("http://localhost/api/auth/password-change", {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers: { "content-type": "application/json" },
|
|
58
|
-
body: JSON.stringify({
|
|
59
|
-
current_password: "old-password",
|
|
60
|
-
new_password: "new-password",
|
|
61
|
-
confirm_password: "new-password",
|
|
62
|
-
}),
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(changePassword).toHaveBeenCalledWith(authState, {
|
|
67
|
-
current_password: "old-password",
|
|
68
|
-
new_password: "new-password",
|
|
69
|
-
confirm_password: "new-password",
|
|
70
|
-
});
|
|
71
|
-
expect(syncPlatformAuthStateToResponse).toHaveBeenCalledWith(
|
|
72
|
-
response,
|
|
73
|
-
authState,
|
|
74
|
-
authState,
|
|
75
|
-
);
|
|
76
|
-
expect(response.status).toBe(200);
|
|
77
|
-
await expect(response.json()).resolves.toEqual({ has_password: true });
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
|
|
3
|
-
import { changePassword } from "@/lib/platform/client.server";
|
|
4
|
-
import { passwordChangeRequestSchema } from "@/lib/platform/contracts";
|
|
5
|
-
import { toPlatformErrorResponse } from "@/lib/platform/route-response";
|
|
6
|
-
import {
|
|
7
|
-
readPlatformAuthState,
|
|
8
|
-
syncPlatformAuthStateToResponse,
|
|
9
|
-
} from "@/lib/platform/session.server";
|
|
10
|
-
|
|
11
|
-
export async function POST(request: Request) {
|
|
12
|
-
const authState = await readPlatformAuthState();
|
|
13
|
-
|
|
14
|
-
if (!authState.sessionId) {
|
|
15
|
-
return NextResponse.json({ detail: "Authentication required." }, { status: 401 });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const body = await request.json().catch(() => null);
|
|
19
|
-
const parsed = passwordChangeRequestSchema.safeParse(body);
|
|
20
|
-
|
|
21
|
-
if (!parsed.success) {
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{ detail: "Invalid password change payload." },
|
|
24
|
-
{ status: 400 },
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const result = await changePassword(authState, parsed.data);
|
|
30
|
-
const response = NextResponse.json(result.data, { status: 200 });
|
|
31
|
-
syncPlatformAuthStateToResponse(response, authState, result.authState);
|
|
32
|
-
return response;
|
|
33
|
-
} catch (error) {
|
|
34
|
-
return toPlatformErrorResponse(
|
|
35
|
-
error,
|
|
36
|
-
"Unable to change password.",
|
|
37
|
-
authState,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|