minutework 0.1.32 → 0.1.34
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 +24 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +19 -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 +37 -135
- 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,108 +1,81 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { startTransition, useEffect, useState } from "react";
|
|
4
3
|
import type { FormEvent } from "react";
|
|
4
|
+
import { startTransition, useState } from "react";
|
|
5
|
+
import { ArrowRight, MailCheck } from "lucide-react";
|
|
6
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
7
|
|
|
6
|
-
import { ArrowRight, ShieldCheck, TerminalSquare } from "lucide-react";
|
|
7
|
-
import { useRouter } from "next/navigation";
|
|
8
|
-
|
|
9
|
-
import { Button } from "@/design-system/primitives/button";
|
|
10
|
-
import { Input } from "@/design-system/primitives/input";
|
|
11
8
|
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
12
9
|
import { ThemeModeToggle } from "@/design-system/patterns/theme-mode-toggle";
|
|
10
|
+
import { Button } from "@/design-system/primitives/button";
|
|
11
|
+
import { Input } from "@/design-system/primitives/input";
|
|
13
12
|
import { appRoutes } from "@/lib/app-routes";
|
|
13
|
+
import { useMinuteWorkAuth } from "@minutework/web-auth/react";
|
|
14
|
+
|
|
15
|
+
type AuthMode = "login" | "signup";
|
|
14
16
|
|
|
15
|
-
export function LoginScreen({
|
|
16
|
-
appName,
|
|
17
|
-
operatorConsoleHref,
|
|
18
|
-
}: {
|
|
19
|
-
appName: string;
|
|
20
|
-
operatorConsoleHref: string;
|
|
21
|
-
}) {
|
|
17
|
+
export function LoginScreen({ appName }: { appName: string }) {
|
|
22
18
|
const router = useRouter();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
19
|
+
const searchParams = useSearchParams();
|
|
20
|
+
const { login, signup } = useMinuteWorkAuth();
|
|
21
|
+
const [mode, setMode] = useState<AuthMode>("login");
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [displayName, setDisplayName] = useState("");
|
|
24
|
+
const [password, setPassword] = useState("");
|
|
25
|
+
const [notice, setNotice] = useState("");
|
|
25
26
|
const [error, setError] = useState("");
|
|
26
27
|
const [submitting, setSubmitting] = useState(false);
|
|
27
28
|
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
void fetch("/api/auth/session", {
|
|
30
|
-
method: "GET",
|
|
31
|
-
cache: "no-store",
|
|
32
|
-
}).catch(() => null);
|
|
33
|
-
}, []);
|
|
34
|
-
|
|
35
29
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
36
30
|
event.preventDefault();
|
|
37
31
|
setSubmitting(true);
|
|
38
32
|
setError("");
|
|
33
|
+
setNotice("");
|
|
39
34
|
|
|
40
35
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
detail?: string;
|
|
50
|
-
};
|
|
51
|
-
setError(payload.detail || "Login failed.");
|
|
36
|
+
if (mode === "signup") {
|
|
37
|
+
await signup({
|
|
38
|
+
email,
|
|
39
|
+
password,
|
|
40
|
+
displayName,
|
|
41
|
+
claimRef: searchParams.get("claim_ref") || undefined,
|
|
42
|
+
});
|
|
43
|
+
setNotice("Check your email to finish verification.");
|
|
52
44
|
setSubmitting(false);
|
|
53
45
|
return;
|
|
54
46
|
}
|
|
55
47
|
|
|
48
|
+
await login({ email, password });
|
|
56
49
|
startTransition(() => {
|
|
57
50
|
router.replace(appRoutes.appHome);
|
|
58
51
|
router.refresh();
|
|
59
52
|
});
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
53
|
+
} catch (caught) {
|
|
54
|
+
const message =
|
|
55
|
+
caught instanceof Error ? caught.message : "Unable to complete auth.";
|
|
56
|
+
setError(message);
|
|
62
57
|
setSubmitting(false);
|
|
63
58
|
}
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
return (
|
|
67
62
|
<main className="min-h-screen bg-background text-foreground">
|
|
68
|
-
<div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<section className="max-w-2xl space-y-6">
|
|
63
|
+
<div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-8 px-6 py-8 lg:flex-row lg:items-center lg:justify-between">
|
|
64
|
+
<section className="max-w-2xl space-y-5">
|
|
65
|
+
<div className="flex justify-end lg:hidden">
|
|
66
|
+
<ThemeModeToggle className="max-w-xs" />
|
|
67
|
+
</div>
|
|
74
68
|
<div className="space-y-3">
|
|
75
69
|
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
76
|
-
|
|
70
|
+
{appName}
|
|
77
71
|
</p>
|
|
78
72
|
<h1 className="max-w-xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
|
|
79
|
-
|
|
73
|
+
Customer workspace
|
|
80
74
|
</h1>
|
|
81
75
|
<p className="max-w-xl text-base leading-7 text-muted-foreground sm:text-lg">
|
|
82
|
-
|
|
83
|
-
root while preserving a server-owned platform session BFF under
|
|
84
|
-
`/app`.
|
|
76
|
+
Use your customer account to continue.
|
|
85
77
|
</p>
|
|
86
78
|
</div>
|
|
87
|
-
|
|
88
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
89
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
|
|
90
|
-
<TerminalSquare className="size-5 text-primary" />
|
|
91
|
-
<p className="text-sm font-medium">Generic tenant shell</p>
|
|
92
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
93
|
-
Start with public storytelling at the root and authenticated
|
|
94
|
-
workspace routing under `/app` instead of a private-only home page.
|
|
95
|
-
</p>
|
|
96
|
-
</PanelFrame>
|
|
97
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
|
|
98
|
-
<ShieldCheck className="size-5 text-primary" />
|
|
99
|
-
<p className="text-sm font-medium">Server-owned session</p>
|
|
100
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
101
|
-
Session cookies stay on the server boundary while public pages
|
|
102
|
-
remain free of tenant-context reads and private BFF assumptions.
|
|
103
|
-
</p>
|
|
104
|
-
</PanelFrame>
|
|
105
|
-
</div>
|
|
106
79
|
</section>
|
|
107
80
|
|
|
108
81
|
<div className="flex w-full max-w-md flex-col gap-4 self-center lg:self-auto">
|
|
@@ -111,37 +84,69 @@ export function LoginScreen({
|
|
|
111
84
|
</div>
|
|
112
85
|
|
|
113
86
|
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6 shadow-2xl">
|
|
114
|
-
<div className="
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
87
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border p-1">
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
variant={mode === "login" ? "default" : "ghost"}
|
|
91
|
+
onClick={() => {
|
|
92
|
+
setMode("login");
|
|
93
|
+
setError("");
|
|
94
|
+
setNotice("");
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
Log in
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
type="button"
|
|
101
|
+
variant={mode === "signup" ? "default" : "ghost"}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
setMode("signup");
|
|
104
|
+
setError("");
|
|
105
|
+
setNotice("");
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Sign up
|
|
109
|
+
</Button>
|
|
125
110
|
</div>
|
|
126
111
|
|
|
127
112
|
<form className="space-y-5" onSubmit={handleSubmit}>
|
|
113
|
+
{mode === "signup" ? (
|
|
114
|
+
<div className="space-y-2">
|
|
115
|
+
<label
|
|
116
|
+
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
117
|
+
htmlFor="display-name"
|
|
118
|
+
>
|
|
119
|
+
Name
|
|
120
|
+
</label>
|
|
121
|
+
<Input
|
|
122
|
+
id="display-name"
|
|
123
|
+
autoComplete="name"
|
|
124
|
+
className="h-12 text-base"
|
|
125
|
+
value={displayName}
|
|
126
|
+
onChange={(event) => setDisplayName(event.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
) : null}
|
|
130
|
+
|
|
128
131
|
<div className="space-y-2">
|
|
129
132
|
<label
|
|
130
133
|
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
131
|
-
htmlFor="
|
|
134
|
+
htmlFor="email"
|
|
132
135
|
>
|
|
133
|
-
|
|
136
|
+
Email
|
|
134
137
|
</label>
|
|
135
138
|
<Input
|
|
136
|
-
id="
|
|
137
|
-
name="
|
|
138
|
-
|
|
139
|
+
id="email"
|
|
140
|
+
name="email"
|
|
141
|
+
type="email"
|
|
142
|
+
autoComplete="email"
|
|
139
143
|
className="h-12 text-base"
|
|
140
|
-
value={
|
|
144
|
+
value={email}
|
|
141
145
|
onChange={(event) => {
|
|
142
|
-
|
|
146
|
+
setEmail(event.target.value);
|
|
143
147
|
setError("");
|
|
144
148
|
}}
|
|
149
|
+
required
|
|
145
150
|
/>
|
|
146
151
|
</div>
|
|
147
152
|
|
|
@@ -156,40 +161,34 @@ export function LoginScreen({
|
|
|
156
161
|
id="password"
|
|
157
162
|
name="password"
|
|
158
163
|
type="password"
|
|
159
|
-
autoComplete="current-password"
|
|
164
|
+
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
160
165
|
className="h-12 text-base"
|
|
161
166
|
value={password}
|
|
162
167
|
onChange={(event) => {
|
|
163
168
|
setPassword(event.target.value);
|
|
164
169
|
setError("");
|
|
165
170
|
}}
|
|
171
|
+
required
|
|
166
172
|
/>
|
|
167
173
|
</div>
|
|
168
174
|
|
|
169
|
-
{
|
|
170
|
-
<
|
|
175
|
+
{notice ? (
|
|
176
|
+
<div className="flex items-start gap-2 rounded-md border border-border bg-muted/40 p-3 text-sm text-foreground">
|
|
177
|
+
<MailCheck className="mt-0.5 size-4 text-primary" />
|
|
178
|
+
<p>{notice}</p>
|
|
179
|
+
</div>
|
|
171
180
|
) : null}
|
|
181
|
+
{error ? <p className="text-sm leading-6 text-destructive">{error}</p> : null}
|
|
172
182
|
|
|
173
183
|
<Button
|
|
174
184
|
type="submit"
|
|
175
185
|
className="h-12 w-full text-sm font-semibold"
|
|
176
186
|
disabled={submitting}
|
|
177
187
|
>
|
|
178
|
-
{submitting ? "
|
|
188
|
+
{submitting ? "Working" : mode === "signup" ? "Create account" : "Log in"}
|
|
179
189
|
<ArrowRight className="size-4" />
|
|
180
190
|
</Button>
|
|
181
191
|
</form>
|
|
182
|
-
|
|
183
|
-
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
|
|
184
|
-
<p className="text-sm text-muted-foreground">
|
|
185
|
-
Need the operator console instead?
|
|
186
|
-
</p>
|
|
187
|
-
<Button asChild type="button" variant="outline">
|
|
188
|
-
<a href={operatorConsoleHref} target="_blank" rel="noreferrer">
|
|
189
|
-
Open /ops
|
|
190
|
-
</a>
|
|
191
|
-
</Button>
|
|
192
|
-
</div>
|
|
193
192
|
</PanelFrame>
|
|
194
193
|
</div>
|
|
195
194
|
</div>
|
package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import {
|
|
4
|
+
import { ArrowRight, Database, ShieldCheck, UserRound } from "lucide-react";
|
|
5
|
+
import type { TenantCustomerSession } from "@minutework/web-auth";
|
|
5
6
|
|
|
6
7
|
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
7
8
|
import { StatusBadge } from "@/design-system/patterns/status-badge";
|
|
8
9
|
import { Button } from "@/design-system/primitives/button";
|
|
9
10
|
import { appRoutes } from "@/lib/app-routes";
|
|
10
|
-
import type { AuthenticatedPlatformSession } from "@/lib/platform/contracts";
|
|
11
11
|
|
|
12
12
|
function DashboardMetric({
|
|
13
13
|
label,
|
|
@@ -28,40 +28,32 @@ function DashboardMetric({
|
|
|
28
28
|
|
|
29
29
|
export function TenantDashboard({
|
|
30
30
|
appName,
|
|
31
|
-
runtimeCommandExampleEnabled,
|
|
32
31
|
session,
|
|
33
32
|
}: {
|
|
34
33
|
appName: string;
|
|
35
|
-
|
|
36
|
-
session: AuthenticatedPlatformSession;
|
|
34
|
+
session: TenantCustomerSession;
|
|
37
35
|
}) {
|
|
38
36
|
return (
|
|
39
37
|
<div className="grid gap-6">
|
|
40
38
|
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6">
|
|
41
39
|
<div className="space-y-2">
|
|
42
|
-
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
43
|
-
Authenticated Workspace
|
|
44
|
-
</p>
|
|
45
40
|
<h2 className="text-3xl font-semibold tracking-tight">
|
|
46
|
-
{appName}
|
|
41
|
+
{appName} workspace
|
|
47
42
|
</h2>
|
|
48
43
|
<p className="max-w-3xl text-sm leading-7 text-muted-foreground">
|
|
49
|
-
|
|
50
|
-
sandbox. Public storytelling lives at the root, while the
|
|
51
|
-
authenticated shell, tenant context controls, password settings,
|
|
52
|
-
route gating, and BFF auth boundary remain real under `/app`.
|
|
44
|
+
Customer session state is loaded through the MinuteWork web auth SDK.
|
|
53
45
|
</p>
|
|
54
46
|
</div>
|
|
55
47
|
|
|
56
48
|
<div className="grid gap-3 md:grid-cols-3">
|
|
57
|
-
<DashboardMetric label="Signed-in user" value={session.user.username} />
|
|
58
49
|
<DashboardMetric
|
|
59
|
-
label="
|
|
60
|
-
value={session.
|
|
50
|
+
label="Customer"
|
|
51
|
+
value={session.user?.display_name || session.user?.email || "Customer"}
|
|
61
52
|
/>
|
|
53
|
+
<DashboardMetric label="Tenant" value={session.tenant.name} />
|
|
62
54
|
<DashboardMetric
|
|
63
|
-
label="
|
|
64
|
-
value={
|
|
55
|
+
label="Role"
|
|
56
|
+
value={session.customer_membership?.roles.join(", ") || "customer"}
|
|
65
57
|
/>
|
|
66
58
|
</div>
|
|
67
59
|
</PanelFrame>
|
|
@@ -71,85 +63,58 @@ export function TenantDashboard({
|
|
|
71
63
|
<div className="flex items-center gap-2">
|
|
72
64
|
<ShieldCheck className="size-5 text-primary" />
|
|
73
65
|
<h3 className="text-xl font-semibold tracking-tight">
|
|
74
|
-
|
|
66
|
+
Tenant web auth
|
|
75
67
|
</h3>
|
|
76
68
|
</div>
|
|
77
69
|
<div className="flex flex-wrap gap-3">
|
|
78
|
-
<StatusBadge tone="primary">
|
|
79
|
-
<StatusBadge tone="info">
|
|
80
|
-
<StatusBadge tone="success">CSRF
|
|
70
|
+
<StatusBadge tone="primary">tenant_web_auth_sdk</StatusBadge>
|
|
71
|
+
<StatusBadge tone="info">HttpOnly cookies</StatusBadge>
|
|
72
|
+
<StatusBadge tone="success">CSRF protected</StatusBadge>
|
|
81
73
|
</div>
|
|
82
74
|
<p className="text-sm leading-7 text-muted-foreground">
|
|
83
|
-
|
|
84
|
-
cookies and CSRF tokens remain server-owned inside route handlers.
|
|
85
|
-
Request-scoped tenant overrides stay scoped to feature actions unless
|
|
86
|
-
the explicit session-context APIs are used.
|
|
75
|
+
Customer sessions stay same-origin through /_mw.
|
|
87
76
|
</p>
|
|
88
77
|
</PanelFrame>
|
|
89
78
|
|
|
90
79
|
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
|
|
91
80
|
<div className="flex items-center gap-2">
|
|
92
|
-
<
|
|
81
|
+
<Database className="size-5 text-primary" />
|
|
93
82
|
<h3 className="text-xl font-semibold tracking-tight">
|
|
94
|
-
|
|
83
|
+
Manifest API
|
|
95
84
|
</h3>
|
|
96
85
|
</div>
|
|
97
86
|
<p className="text-sm leading-7 text-muted-foreground">
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Builder flows that writable root lives at
|
|
101
|
-
`BuilderWorkspace.sandbox_root/app/`; in CLI scaffolds the same
|
|
102
|
-
starter lands in `tenant-app/`. This source bundle stays governed
|
|
103
|
-
and read-only in normal authoring flows.
|
|
87
|
+
Query and action calls go through the SDK and are authorized as
|
|
88
|
+
`tenant_customer`.
|
|
104
89
|
</p>
|
|
90
|
+
<Button asChild className="w-fit">
|
|
91
|
+
<Link href={appRoutes.demo}>
|
|
92
|
+
Open demo
|
|
93
|
+
<ArrowRight className="size-4" />
|
|
94
|
+
</Link>
|
|
95
|
+
</Button>
|
|
105
96
|
</PanelFrame>
|
|
106
97
|
</section>
|
|
107
98
|
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
|
|
125
|
-
<div className="flex items-center gap-2">
|
|
126
|
-
<FolderKanban className="size-5 text-primary" />
|
|
127
|
-
<h3 className="text-xl font-semibold tracking-tight">
|
|
128
|
-
Optional example surface
|
|
129
|
-
</h3>
|
|
130
|
-
</div>
|
|
131
|
-
<p className="text-sm leading-7 text-muted-foreground">
|
|
132
|
-
The runtime-command example is bundled as optional sample code rather
|
|
133
|
-
than the template's default product identity.
|
|
134
|
-
</p>
|
|
135
|
-
{runtimeCommandExampleEnabled ? (
|
|
136
|
-
<Button asChild className="w-fit">
|
|
137
|
-
<Link href={appRoutes.runtimeCommandExample}>
|
|
138
|
-
Open runtime command example
|
|
139
|
-
</Link>
|
|
140
|
-
</Button>
|
|
141
|
-
) : (
|
|
142
|
-
<div className="space-y-2">
|
|
143
|
-
<StatusBadge tone="default">Disabled by default</StatusBadge>
|
|
144
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
145
|
-
Set `MW_ENABLE_RUNTIME_COMMAND_EXAMPLE=true` in the materialized
|
|
146
|
-
workspace to enable the example route and its matching gateway
|
|
147
|
-
proxy handlers.
|
|
148
|
-
</p>
|
|
149
|
-
</div>
|
|
99
|
+
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-4">
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
<UserRound className="size-5 text-primary" />
|
|
102
|
+
<h3 className="text-xl font-semibold tracking-tight">
|
|
103
|
+
Session payload
|
|
104
|
+
</h3>
|
|
105
|
+
</div>
|
|
106
|
+
<pre className="overflow-auto rounded-md bg-muted p-4 text-xs text-muted-foreground">
|
|
107
|
+
{JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
principal_kind: session.principal_kind,
|
|
110
|
+
tenant_id: session.tenant.id,
|
|
111
|
+
customer_membership_id: session.customer_membership?.id,
|
|
112
|
+
},
|
|
113
|
+
null,
|
|
114
|
+
2,
|
|
150
115
|
)}
|
|
151
|
-
</
|
|
152
|
-
</
|
|
116
|
+
</pre>
|
|
117
|
+
</PanelFrame>
|
|
153
118
|
</div>
|
|
154
119
|
);
|
|
155
120
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Database, Play, RefreshCcw } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
7
|
+
import { Button } from "@/design-system/primitives/button";
|
|
8
|
+
import { Input } from "@/design-system/primitives/input";
|
|
9
|
+
import { createIdempotencyKey, mw } from "@/mw/client";
|
|
10
|
+
|
|
11
|
+
type DemoResult = {
|
|
12
|
+
data?: unknown;
|
|
13
|
+
count?: number;
|
|
14
|
+
created?: boolean;
|
|
15
|
+
idempotency_key?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function ManifestDemo() {
|
|
19
|
+
const [title, setTitle] = useState("Demo record");
|
|
20
|
+
const [result, setResult] = useState<DemoResult | null>(null);
|
|
21
|
+
const [error, setError] = useState("");
|
|
22
|
+
const [pending, setPending] = useState(false);
|
|
23
|
+
|
|
24
|
+
async function runQuery() {
|
|
25
|
+
setPending(true);
|
|
26
|
+
setError("");
|
|
27
|
+
try {
|
|
28
|
+
setResult(await mw.query<DemoResult>("demo.list", { limit: 10 }));
|
|
29
|
+
} catch (caught) {
|
|
30
|
+
setError(caught instanceof Error ? caught.message : "Query failed.");
|
|
31
|
+
} finally {
|
|
32
|
+
setPending(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runAction() {
|
|
37
|
+
setPending(true);
|
|
38
|
+
setError("");
|
|
39
|
+
try {
|
|
40
|
+
setResult(
|
|
41
|
+
await mw.action<DemoResult>(
|
|
42
|
+
"demo.create",
|
|
43
|
+
{ title, status: "new" },
|
|
44
|
+
{ idempotencyKey: createIdempotencyKey("demo-create") },
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
} catch (caught) {
|
|
48
|
+
setError(caught instanceof Error ? caught.message : "Action failed.");
|
|
49
|
+
} finally {
|
|
50
|
+
setPending(false);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="grid gap-6">
|
|
56
|
+
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<Database className="size-5 text-primary" />
|
|
59
|
+
<h2 className="text-2xl font-semibold tracking-tight">
|
|
60
|
+
Manifest query and action
|
|
61
|
+
</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="grid gap-3 md:grid-cols-[1fr,auto,auto]">
|
|
64
|
+
<Input
|
|
65
|
+
value={title}
|
|
66
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
67
|
+
className="h-11"
|
|
68
|
+
aria-label="Demo title"
|
|
69
|
+
/>
|
|
70
|
+
<Button type="button" variant="outline" onClick={runQuery} disabled={pending}>
|
|
71
|
+
<RefreshCcw className="size-4" />
|
|
72
|
+
Query
|
|
73
|
+
</Button>
|
|
74
|
+
<Button type="button" onClick={runAction} disabled={pending}>
|
|
75
|
+
<Play className="size-4" />
|
|
76
|
+
Action
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
80
|
+
</PanelFrame>
|
|
81
|
+
|
|
82
|
+
<PanelFrame tone="raised" radius="xl" padding="lg">
|
|
83
|
+
<pre className="min-h-40 overflow-auto rounded-md bg-muted p-4 text-xs text-muted-foreground">
|
|
84
|
+
{result ? JSON.stringify(result, null, 2) : "No result yet"}
|
|
85
|
+
</pre>
|
|
86
|
+
</PanelFrame>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ArrowRight } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/design-system/primitives/button";
|
|
5
|
+
import { appRoutes } from "@/lib/app-routes";
|
|
6
|
+
|
|
7
|
+
export function StaticPublicPage({
|
|
8
|
+
eyebrow,
|
|
9
|
+
title,
|
|
10
|
+
body,
|
|
11
|
+
}: {
|
|
12
|
+
eyebrow: string;
|
|
13
|
+
title: string;
|
|
14
|
+
body: string;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<main className="min-h-screen bg-background text-foreground">
|
|
18
|
+
<div className="mx-auto flex min-h-screen max-w-6xl flex-col px-6 py-6">
|
|
19
|
+
<nav className="flex items-center justify-between">
|
|
20
|
+
<Link className="text-sm font-semibold text-foreground" href={appRoutes.publicHome}>
|
|
21
|
+
Tenant App
|
|
22
|
+
</Link>
|
|
23
|
+
<div className="flex items-center gap-2">
|
|
24
|
+
<Button asChild variant="ghost">
|
|
25
|
+
<Link href={appRoutes.pricing}>Pricing</Link>
|
|
26
|
+
</Button>
|
|
27
|
+
<Button asChild variant="ghost">
|
|
28
|
+
<Link href={appRoutes.docsIndex}>Docs</Link>
|
|
29
|
+
</Button>
|
|
30
|
+
<Button asChild>
|
|
31
|
+
<Link href={appRoutes.login}>Log in</Link>
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<section className="grid flex-1 content-center gap-8 py-16">
|
|
37
|
+
<div className="max-w-3xl space-y-5">
|
|
38
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
39
|
+
{eyebrow}
|
|
40
|
+
</p>
|
|
41
|
+
<h1 className="text-5xl font-semibold tracking-tight text-balance sm:text-6xl">
|
|
42
|
+
{title}
|
|
43
|
+
</h1>
|
|
44
|
+
<p className="max-w-2xl text-lg leading-8 text-muted-foreground">
|
|
45
|
+
{body}
|
|
46
|
+
</p>
|
|
47
|
+
<Button asChild className="w-fit">
|
|
48
|
+
<Link href={appRoutes.login}>
|
|
49
|
+
Continue
|
|
50
|
+
<ArrowRight className="size-4" />
|
|
51
|
+
</Link>
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
</div>
|
|
56
|
+
</main>
|
|
57
|
+
);
|
|
58
|
+
}
|