minutework 0.1.31 → 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/EXTERNAL_ALPHA.md +33 -33
- package/README.md +34 -34
- package/assets/claude-local/CLAUDE.md.template +12 -12
- package/assets/claude-local/skills/README.md +3 -1
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +17 -4
- package/assets/claude-local/skills/capability-gap-reporting/SKILL.md +3 -3
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +12 -5
- package/assets/claude-local/skills/layering-and-import-modes/SKILL.md +2 -2
- package/assets/claude-local/skills/openclaw-skill-importer/SKILL.md +2 -2
- package/assets/claude-local/skills/project-overview-and-strategy/SKILL.md +8 -8
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +11 -8
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +6 -5
- package/assets/claude-local/skills/vuilder-discovery-output-contract/SKILL.md +6 -6
- package/assets/claude-local/skills/workspace-guidance-refresh/SKILL.md +4 -4
- package/assets/templates/fastapi-sidecar/pyproject.toml +1 -1
- package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +1 -1
- package/assets/templates/mobile-app/.env.example +4 -4
- package/assets/templates/mobile-app/AGENTS.md +3 -3
- package/assets/templates/mobile-app/README.md +10 -10
- package/assets/templates/mobile-app/app/(app)/_layout.tsx +2 -2
- package/assets/templates/mobile-app/app/(app)/index.tsx +2 -2
- package/assets/templates/mobile-app/app/(auth)/login.tsx +3 -3
- package/assets/templates/mobile-app/app/_layout.tsx +1 -1
- package/assets/templates/mobile-app/babel.config.js +1 -1
- package/assets/templates/mobile-app/eas.json +1 -1
- package/assets/templates/mobile-app/expo-env.d.ts +1 -1
- package/assets/templates/mobile-app/metro.config.js +2 -2
- package/assets/templates/mobile-app/package.json +1 -1
- package/assets/templates/mobile-app/src/mw/client.ts +3 -3
- package/assets/templates/mobile-app/src/mw/contracts.ts +2 -2
- package/assets/templates/mobile-app/src/mw/endpoints.ts +2 -2
- package/assets/templates/mobile-app/src/mw/env.ts +4 -4
- package/assets/templates/mobile-app/src/mw/session.ts +1 -1
- package/assets/templates/mobile-app/template.json +1 -1
- package/assets/templates/mobile-app/tools/template/validate-template.mjs +2 -2
- package/assets/templates/mobile-app/tsconfig.json +1 -1
- package/assets/templates/next-tenant-app/.env.example +1 -1
- 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.test.ts +1 -1
- 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/assets/templates/next-tenant-app/tools/template/with-public-site-fixture.mjs +2 -2
- package/bin/minutework.js +1 -1
- package/dist/agent.js +7 -7
- package/dist/agent.js.map +1 -1
- package/dist/auth.js +7 -7
- package/dist/auth.js.map +1 -1
- package/dist/compile.js +5 -5
- package/dist/config.js +6 -6
- package/dist/config.js.map +1 -1
- package/dist/deploy.js +7 -7
- package/dist/deploy.js.map +1 -1
- package/dist/developer-client.js +2 -2
- package/dist/developer-client.js.map +1 -1
- package/dist/index.js +30 -30
- package/dist/index.js.map +1 -1
- package/dist/init.js +10 -10
- package/dist/init.js.map +1 -1
- package/dist/launcher.js +1 -1
- package/dist/launcher.js.map +1 -1
- package/dist/managed-engine.js +6 -6
- package/dist/managed-engine.js.map +1 -1
- package/dist/orchestrator-context.js +1 -1
- package/dist/orchestrator-context.js.map +1 -1
- package/dist/orchestrator.js +15 -15
- package/dist/orchestrator.js.map +1 -1
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/publish.js +3 -3
- package/dist/publish.js.map +1 -1
- package/dist/reporting.js +8 -8
- package/dist/reporting.js.map +1 -1
- package/dist/sandbox.js +5 -5
- package/dist/sandbox.js.map +1 -1
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/tokens.js +9 -9
- package/dist/tokens.js.map +1 -1
- package/dist/workspace-assets.js +6 -6
- package/dist/workspace-assets.js.map +1 -1
- package/dist/workspace.js +3 -3
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -3
- package/vendor/workspace-mcp/context.d.ts +6 -6
- package/vendor/workspace-mcp/context.js +56 -56
- package/vendor/workspace-mcp/context.js.map +1 -1
- 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
package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx
CHANGED
|
@@ -1,115 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { startTransition, useEffect, useEffectEvent, useState } from "react";
|
|
5
|
-
import dynamic from "next/dynamic";
|
|
3
|
+
import { startTransition } from "react";
|
|
6
4
|
import Link from "next/link";
|
|
7
5
|
import { useRouter } from "next/navigation";
|
|
8
|
-
import {
|
|
9
|
-
Building2,
|
|
10
|
-
KeyRound,
|
|
11
|
-
LayoutDashboard,
|
|
12
|
-
Loader2,
|
|
13
|
-
LogOut,
|
|
14
|
-
RefreshCcw,
|
|
15
|
-
ShieldCheck,
|
|
16
|
-
Workflow,
|
|
17
|
-
} from "lucide-react";
|
|
6
|
+
import { LayoutDashboard, LogOut, PlaySquare } from "lucide-react";
|
|
18
7
|
|
|
19
|
-
import { TenantDashboard } from "@/features/dashboard/components/tenant-dashboard";
|
|
20
8
|
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
21
|
-
import { StatusBadge } from "@/design-system/patterns/status-badge";
|
|
22
9
|
import { ThemeModeToggle } from "@/design-system/patterns/theme-mode-toggle";
|
|
23
10
|
import { Button } from "@/design-system/primitives/button";
|
|
24
|
-
import {
|
|
11
|
+
import { TenantDashboard } from "@/features/dashboard/components/tenant-dashboard";
|
|
25
12
|
import { appRoutes } from "@/lib/app-routes";
|
|
26
|
-
import
|
|
27
|
-
AuthenticatedPlatformSession,
|
|
28
|
-
PasswordStatus,
|
|
29
|
-
SessionMembership,
|
|
30
|
-
} from "@/lib/platform/contracts";
|
|
31
|
-
|
|
32
|
-
type ShellView = "dashboard" | "runtime-command-demo";
|
|
33
|
-
|
|
34
|
-
function RuntimeCommandDemoFallback() {
|
|
35
|
-
return (
|
|
36
|
-
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-3">
|
|
37
|
-
<div className="flex items-center gap-2">
|
|
38
|
-
<Loader2 className="size-4 animate-spin text-primary" />
|
|
39
|
-
<p className="text-sm font-semibold text-foreground">
|
|
40
|
-
Loading runtime example
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
44
|
-
The optional example module is loaded only when the runtime example route
|
|
45
|
-
is active.
|
|
46
|
-
</p>
|
|
47
|
-
</PanelFrame>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const RuntimeCommandDemo = dynamic(
|
|
52
|
-
() =>
|
|
53
|
-
import(
|
|
54
|
-
"@/features/examples/runtime-command-demo/components/runtime-command-demo"
|
|
55
|
-
).then((module) => module.RuntimeCommandDemo),
|
|
56
|
-
{
|
|
57
|
-
loading: () => <RuntimeCommandDemoFallback />,
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
function membershipById(
|
|
62
|
-
memberships: SessionMembership[],
|
|
63
|
-
tenantId: string,
|
|
64
|
-
) {
|
|
65
|
-
return memberships.find((membership) => membership.tenant_id === tenantId) ?? null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function readErrorDetail(response: Response, fallback: string) {
|
|
69
|
-
const payload = (await response.json().catch(() => null)) as
|
|
70
|
-
| { detail?: string }
|
|
71
|
-
| null;
|
|
13
|
+
import { useMinuteWorkAuth, useMinuteWorkSession } from "@minutework/web-auth/react";
|
|
72
14
|
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function PrivateAppShell({
|
|
77
|
-
appName,
|
|
78
|
-
initialSession,
|
|
79
|
-
operatorConsoleHref,
|
|
80
|
-
runtimeCommandExampleEnabled,
|
|
81
|
-
view,
|
|
82
|
-
}: {
|
|
83
|
-
appName: string;
|
|
84
|
-
initialSession: AuthenticatedPlatformSession;
|
|
85
|
-
operatorConsoleHref?: string;
|
|
86
|
-
runtimeCommandExampleEnabled: boolean;
|
|
87
|
-
view: ShellView;
|
|
88
|
-
}) {
|
|
15
|
+
export function PrivateAppShell({ appName }: { appName: string }) {
|
|
89
16
|
const router = useRouter();
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
const [signingOut, setSigningOut] = useState(false);
|
|
95
|
-
const [tenantPending, setTenantPending] = useState(false);
|
|
96
|
-
const [tenantError, setTenantError] = useState("");
|
|
97
|
-
const [tenantNotice, setTenantNotice] = useState("");
|
|
98
|
-
const [passwordStatus, setPasswordStatus] = useState<PasswordStatus | null>(null);
|
|
99
|
-
const [passwordLoading, setPasswordLoading] = useState(true);
|
|
100
|
-
const [passwordPending, setPasswordPending] = useState(false);
|
|
101
|
-
const [passwordError, setPasswordError] = useState("");
|
|
102
|
-
const [passwordNotice, setPasswordNotice] = useState("");
|
|
103
|
-
const [currentPassword, setCurrentPassword] = useState("");
|
|
104
|
-
const [newPassword, setNewPassword] = useState("");
|
|
105
|
-
const [confirmPassword, setConfirmPassword] = useState("");
|
|
106
|
-
|
|
107
|
-
const activeTenant = session.active_tenant;
|
|
108
|
-
const activeMembership = membershipById(
|
|
109
|
-
session.memberships,
|
|
110
|
-
session.active_tenant_id,
|
|
111
|
-
);
|
|
112
|
-
const pendingMembership = membershipById(session.memberships, selectedTenantId);
|
|
17
|
+
const { logout } = useMinuteWorkAuth();
|
|
18
|
+
const { session, loading, authenticated, emailVerificationRequired } =
|
|
19
|
+
useMinuteWorkSession();
|
|
113
20
|
|
|
114
21
|
function redirectToLogin() {
|
|
115
22
|
startTransition(() => {
|
|
@@ -118,188 +25,42 @@ export function PrivateAppShell({
|
|
|
118
25
|
});
|
|
119
26
|
}
|
|
120
27
|
|
|
121
|
-
const loadPasswordStatus = useEffectEvent(async () => {
|
|
122
|
-
setPasswordLoading(true);
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const response = await fetch("/api/auth/password-status", {
|
|
126
|
-
method: "GET",
|
|
127
|
-
cache: "no-store",
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (response.status === 401) {
|
|
131
|
-
redirectToLogin();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!response.ok) {
|
|
136
|
-
setPasswordError(
|
|
137
|
-
await readErrorDetail(response, "Unable to load password settings."),
|
|
138
|
-
);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const payload = (await response.json()) as PasswordStatus;
|
|
143
|
-
setPasswordStatus(payload);
|
|
144
|
-
setPasswordError("");
|
|
145
|
-
} catch {
|
|
146
|
-
setPasswordError("Unable to load password settings.");
|
|
147
|
-
} finally {
|
|
148
|
-
setPasswordLoading(false);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
setSelectedTenantId(session.active_tenant_id);
|
|
154
|
-
}, [session.active_tenant_id]);
|
|
155
|
-
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
void loadPasswordStatus();
|
|
158
|
-
}, []);
|
|
159
|
-
|
|
160
28
|
async function handleLogout() {
|
|
161
|
-
|
|
162
|
-
await fetch("/api/auth/logout", { method: "POST" }).catch(() => null);
|
|
29
|
+
await logout().catch(() => undefined);
|
|
163
30
|
redirectToLogin();
|
|
164
31
|
}
|
|
165
32
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const response = await fetch("/api/auth/context", {
|
|
177
|
-
method: "PUT",
|
|
178
|
-
headers: { "Content-Type": "application/json" },
|
|
179
|
-
body: JSON.stringify({ tenant_id: selectedTenantId }),
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (response.status === 401) {
|
|
183
|
-
redirectToLogin();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!response.ok) {
|
|
188
|
-
setTenantError(
|
|
189
|
-
await readErrorDetail(response, "Unable to update tenant context."),
|
|
190
|
-
);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const payload = (await response.json()) as AuthenticatedPlatformSession;
|
|
195
|
-
setSession(payload);
|
|
196
|
-
setTenantNotice("Tenant context updated.");
|
|
197
|
-
} catch {
|
|
198
|
-
setTenantError("Unable to update tenant context.");
|
|
199
|
-
} finally {
|
|
200
|
-
setTenantPending(false);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function handleTenantReset() {
|
|
205
|
-
setTenantPending(true);
|
|
206
|
-
setTenantError("");
|
|
207
|
-
setTenantNotice("");
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const response = await fetch("/api/auth/context", {
|
|
211
|
-
method: "DELETE",
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (response.status === 401) {
|
|
215
|
-
redirectToLogin();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!response.ok) {
|
|
220
|
-
setTenantError(
|
|
221
|
-
await readErrorDetail(response, "Unable to reset tenant context."),
|
|
222
|
-
);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const payload = (await response.json()) as AuthenticatedPlatformSession;
|
|
227
|
-
setSession(payload);
|
|
228
|
-
setTenantNotice("Tenant context reset to the platform default.");
|
|
229
|
-
} catch {
|
|
230
|
-
setTenantError("Unable to reset tenant context.");
|
|
231
|
-
} finally {
|
|
232
|
-
setTenantPending(false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async function handlePasswordSubmit(event: FormEvent<HTMLFormElement>) {
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
|
|
239
|
-
if (newPassword !== confirmPassword) {
|
|
240
|
-
setPasswordError("Passwords must match.");
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
setPasswordPending(true);
|
|
245
|
-
setPasswordError("");
|
|
246
|
-
setPasswordNotice("");
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const response = await fetch("/api/auth/password-change", {
|
|
250
|
-
method: "POST",
|
|
251
|
-
headers: { "Content-Type": "application/json" },
|
|
252
|
-
body: JSON.stringify({
|
|
253
|
-
current_password: currentPassword,
|
|
254
|
-
new_password: newPassword,
|
|
255
|
-
confirm_password: confirmPassword,
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
if (response.status === 401) {
|
|
260
|
-
redirectToLogin();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (!response.ok) {
|
|
265
|
-
setPasswordError(
|
|
266
|
-
await readErrorDetail(response, "Unable to change password."),
|
|
267
|
-
);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const payload = (await response.json()) as PasswordStatus;
|
|
272
|
-
setPasswordStatus(payload);
|
|
273
|
-
setCurrentPassword("");
|
|
274
|
-
setNewPassword("");
|
|
275
|
-
setConfirmPassword("");
|
|
276
|
-
setPasswordNotice("Password saved.");
|
|
277
|
-
} catch {
|
|
278
|
-
setPasswordError("Unable to change password.");
|
|
279
|
-
} finally {
|
|
280
|
-
setPasswordPending(false);
|
|
281
|
-
}
|
|
33
|
+
if (loading) {
|
|
34
|
+
return (
|
|
35
|
+
<main className="min-h-screen bg-background text-foreground">
|
|
36
|
+
<div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
|
|
37
|
+
<PanelFrame tone="floating" radius="xl" padding="lg" className="w-full">
|
|
38
|
+
<p className="text-sm text-muted-foreground">Loading session</p>
|
|
39
|
+
</PanelFrame>
|
|
40
|
+
</div>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
282
43
|
}
|
|
283
44
|
|
|
284
|
-
if (!
|
|
45
|
+
if (!authenticated || !session?.customer_membership) {
|
|
285
46
|
return (
|
|
286
47
|
<main className="min-h-screen bg-background text-foreground">
|
|
287
48
|
<div className="mx-auto flex min-h-screen max-w-5xl items-center px-6 py-10">
|
|
288
49
|
<PanelFrame tone="floating" radius="xl" padding="lg" className="w-full space-y-4">
|
|
289
50
|
<div className="space-y-2">
|
|
290
|
-
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
291
|
-
Missing tenant context
|
|
292
|
-
</p>
|
|
293
51
|
<h1 className="text-3xl font-semibold tracking-tight">
|
|
294
|
-
|
|
52
|
+
{emailVerificationRequired
|
|
53
|
+
? "Email verification required"
|
|
54
|
+
: "Log in to continue"}
|
|
295
55
|
</h1>
|
|
296
56
|
<p className="text-sm leading-7 text-muted-foreground">
|
|
297
|
-
|
|
298
|
-
|
|
57
|
+
{emailVerificationRequired
|
|
58
|
+
? "Finish verification from your email before opening the workspace."
|
|
59
|
+
: "This area is available to verified customers."}
|
|
299
60
|
</p>
|
|
300
61
|
</div>
|
|
301
|
-
<Button onClick={
|
|
302
|
-
|
|
62
|
+
<Button onClick={redirectToLogin} className="w-fit">
|
|
63
|
+
Open login
|
|
303
64
|
</Button>
|
|
304
65
|
</PanelFrame>
|
|
305
66
|
</div>
|
|
@@ -307,317 +68,52 @@ export function PrivateAppShell({
|
|
|
307
68
|
);
|
|
308
69
|
}
|
|
309
70
|
|
|
310
|
-
const content =
|
|
311
|
-
view === "runtime-command-demo" ? (
|
|
312
|
-
<RuntimeCommandDemo
|
|
313
|
-
activeTenant={activeTenant}
|
|
314
|
-
onUnauthorized={redirectToLogin}
|
|
315
|
-
viewerName={session.user.username}
|
|
316
|
-
/>
|
|
317
|
-
) : (
|
|
318
|
-
<TenantDashboard
|
|
319
|
-
appName={appName}
|
|
320
|
-
runtimeCommandExampleEnabled={runtimeCommandExampleEnabled}
|
|
321
|
-
session={session}
|
|
322
|
-
/>
|
|
323
|
-
);
|
|
324
|
-
|
|
325
71
|
return (
|
|
326
72
|
<main className="min-h-screen bg-background text-foreground">
|
|
327
73
|
<div className="mx-auto flex min-h-screen max-w-7xl flex-col gap-6 px-6 py-8">
|
|
328
74
|
<header className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
|
|
329
|
-
<div className="space-y-
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Builder apps. The browser talks only to this Next.js app, and the
|
|
340
|
-
app keeps platform auth and CSRF credentials server-owned.
|
|
341
|
-
</p>
|
|
342
|
-
</div>
|
|
343
|
-
|
|
344
|
-
<div className="flex flex-wrap gap-3">
|
|
345
|
-
<StatusBadge tone="primary">platform_session_bff</StatusBadge>
|
|
346
|
-
<StatusBadge tone="info">Active tenant aware</StatusBadge>
|
|
347
|
-
<StatusBadge tone="success">Builder-ready scaffold</StatusBadge>
|
|
348
|
-
</div>
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
77
|
+
{session.tenant.name}
|
|
78
|
+
</p>
|
|
79
|
+
<h1 className="max-w-3xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
|
|
80
|
+
{appName}
|
|
81
|
+
</h1>
|
|
82
|
+
<p className="max-w-3xl text-base leading-7 text-muted-foreground sm:text-lg">
|
|
83
|
+
Signed in as {session.user?.email || session.user?.username}.
|
|
84
|
+
</p>
|
|
349
85
|
</div>
|
|
350
86
|
|
|
351
87
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start">
|
|
352
88
|
<ThemeModeToggle className="w-full sm:w-72" />
|
|
353
|
-
{operatorConsoleHref ? (
|
|
354
|
-
<Button asChild type="button" variant="outline">
|
|
355
|
-
<a href={operatorConsoleHref} target="_blank" rel="noreferrer">
|
|
356
|
-
Open /ops
|
|
357
|
-
</a>
|
|
358
|
-
</Button>
|
|
359
|
-
) : null}
|
|
360
89
|
<Button
|
|
361
90
|
type="button"
|
|
362
91
|
variant="outline"
|
|
363
92
|
className="gap-2"
|
|
364
93
|
onClick={handleLogout}
|
|
365
|
-
disabled={signingOut}
|
|
366
94
|
>
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
) : (
|
|
370
|
-
<LogOut className="size-4" />
|
|
371
|
-
)}
|
|
372
|
-
{signingOut ? "Signing out" : "Log out"}
|
|
95
|
+
<LogOut className="size-4" />
|
|
96
|
+
Log out
|
|
373
97
|
</Button>
|
|
374
98
|
</div>
|
|
375
99
|
</header>
|
|
376
100
|
|
|
377
101
|
<nav className="flex flex-wrap gap-2">
|
|
378
|
-
<Button asChild variant=
|
|
102
|
+
<Button asChild variant="default">
|
|
379
103
|
<Link href={appRoutes.appHome}>
|
|
380
104
|
<LayoutDashboard className="size-4" />
|
|
381
105
|
Dashboard
|
|
382
106
|
</Link>
|
|
383
107
|
</Button>
|
|
384
|
-
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
>
|
|
389
|
-
|
|
390
|
-
<Workflow className="size-4" />
|
|
391
|
-
Runtime example
|
|
392
|
-
</Link>
|
|
393
|
-
</Button>
|
|
394
|
-
) : null}
|
|
108
|
+
<Button asChild variant="outline">
|
|
109
|
+
<Link href={appRoutes.demo}>
|
|
110
|
+
<PlaySquare className="size-4" />
|
|
111
|
+
Demo
|
|
112
|
+
</Link>
|
|
113
|
+
</Button>
|
|
395
114
|
</nav>
|
|
396
115
|
|
|
397
|
-
<
|
|
398
|
-
<div>{content}</div>
|
|
399
|
-
|
|
400
|
-
<div className="grid gap-6">
|
|
401
|
-
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
|
|
402
|
-
<div className="space-y-2">
|
|
403
|
-
<div className="flex items-center gap-2">
|
|
404
|
-
<Building2 className="size-4 text-primary" />
|
|
405
|
-
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
406
|
-
Tenant context
|
|
407
|
-
</p>
|
|
408
|
-
</div>
|
|
409
|
-
<h2 className="text-2xl font-semibold tracking-tight">
|
|
410
|
-
{activeTenant.tenant_name}
|
|
411
|
-
</h2>
|
|
412
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
413
|
-
Signed in as{" "}
|
|
414
|
-
<span className="font-medium text-foreground">
|
|
415
|
-
{session.user.username}
|
|
416
|
-
</span>{" "}
|
|
417
|
-
with role{" "}
|
|
418
|
-
<span className="font-medium text-foreground">
|
|
419
|
-
{activeMembership?.role ?? activeTenant.role}
|
|
420
|
-
</span>
|
|
421
|
-
.
|
|
422
|
-
</p>
|
|
423
|
-
</div>
|
|
424
|
-
|
|
425
|
-
<div className="space-y-2">
|
|
426
|
-
<label
|
|
427
|
-
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
428
|
-
htmlFor="tenant-context"
|
|
429
|
-
>
|
|
430
|
-
Active tenant
|
|
431
|
-
</label>
|
|
432
|
-
<select
|
|
433
|
-
id="tenant-context"
|
|
434
|
-
className="flex h-12 w-full rounded-xl border border-border bg-background px-4 text-sm text-foreground outline-none transition-colors focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
435
|
-
value={selectedTenantId}
|
|
436
|
-
onChange={(event) => {
|
|
437
|
-
setSelectedTenantId(event.target.value);
|
|
438
|
-
setTenantError("");
|
|
439
|
-
setTenantNotice("");
|
|
440
|
-
}}
|
|
441
|
-
disabled={tenantPending}
|
|
442
|
-
>
|
|
443
|
-
{session.memberships.map((membership) => (
|
|
444
|
-
<option key={membership.tenant_id} value={membership.tenant_id}>
|
|
445
|
-
{membership.tenant_name} ({membership.tenant_slug})
|
|
446
|
-
</option>
|
|
447
|
-
))}
|
|
448
|
-
</select>
|
|
449
|
-
</div>
|
|
450
|
-
|
|
451
|
-
{pendingMembership ? (
|
|
452
|
-
<PanelFrame tone="raised" radius="xl" padding="md" className="space-y-2">
|
|
453
|
-
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
454
|
-
Selected membership
|
|
455
|
-
</p>
|
|
456
|
-
<p className="text-sm font-semibold text-foreground">
|
|
457
|
-
{pendingMembership.tenant_name}
|
|
458
|
-
</p>
|
|
459
|
-
<p className="text-sm text-muted-foreground">
|
|
460
|
-
{pendingMembership.tenant_slug} · {pendingMembership.role}
|
|
461
|
-
</p>
|
|
462
|
-
</PanelFrame>
|
|
463
|
-
) : null}
|
|
464
|
-
|
|
465
|
-
<div className="flex flex-wrap gap-3">
|
|
466
|
-
<Button
|
|
467
|
-
type="button"
|
|
468
|
-
className="gap-2"
|
|
469
|
-
onClick={handleTenantSwitch}
|
|
470
|
-
disabled={tenantPending || selectedTenantId === session.active_tenant_id}
|
|
471
|
-
>
|
|
472
|
-
{tenantPending ? (
|
|
473
|
-
<Loader2 className="size-4 animate-spin" />
|
|
474
|
-
) : (
|
|
475
|
-
<Building2 className="size-4" />
|
|
476
|
-
)}
|
|
477
|
-
Apply tenant
|
|
478
|
-
</Button>
|
|
479
|
-
<Button
|
|
480
|
-
type="button"
|
|
481
|
-
variant="outline"
|
|
482
|
-
className="gap-2"
|
|
483
|
-
onClick={handleTenantReset}
|
|
484
|
-
disabled={tenantPending}
|
|
485
|
-
>
|
|
486
|
-
<RefreshCcw className="size-4" />
|
|
487
|
-
Reset context
|
|
488
|
-
</Button>
|
|
489
|
-
</div>
|
|
490
|
-
|
|
491
|
-
{tenantError ? <p className="text-sm text-destructive">{tenantError}</p> : null}
|
|
492
|
-
{tenantNotice ? <p className="text-sm text-primary">{tenantNotice}</p> : null}
|
|
493
|
-
</PanelFrame>
|
|
494
|
-
|
|
495
|
-
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-5">
|
|
496
|
-
<div className="space-y-2">
|
|
497
|
-
<div className="flex items-center gap-2">
|
|
498
|
-
<ShieldCheck className="size-4 text-primary" />
|
|
499
|
-
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
500
|
-
Password settings
|
|
501
|
-
</p>
|
|
502
|
-
</div>
|
|
503
|
-
<h2 className="text-2xl font-semibold tracking-tight">
|
|
504
|
-
Account security
|
|
505
|
-
</h2>
|
|
506
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
507
|
-
Password status and updates are proxied through the same
|
|
508
|
-
server-owned BFF as login, logout, and session restore.
|
|
509
|
-
</p>
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
{passwordLoading ? (
|
|
513
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
|
|
514
|
-
<div className="flex items-center gap-2">
|
|
515
|
-
<Loader2 className="size-4 animate-spin text-primary" />
|
|
516
|
-
<p className="text-sm font-semibold text-foreground">
|
|
517
|
-
Loading password status
|
|
518
|
-
</p>
|
|
519
|
-
</div>
|
|
520
|
-
</PanelFrame>
|
|
521
|
-
) : (
|
|
522
|
-
<form className="space-y-4" onSubmit={handlePasswordSubmit}>
|
|
523
|
-
<div className="flex flex-wrap gap-3">
|
|
524
|
-
<StatusBadge tone={passwordStatus?.has_password ? "success" : "info"}>
|
|
525
|
-
{passwordStatus?.has_password
|
|
526
|
-
? "Password enabled"
|
|
527
|
-
: "Password missing"}
|
|
528
|
-
</StatusBadge>
|
|
529
|
-
<StatusBadge tone="default">Platform session</StatusBadge>
|
|
530
|
-
</div>
|
|
531
|
-
|
|
532
|
-
{passwordStatus?.has_password ? (
|
|
533
|
-
<div className="space-y-2">
|
|
534
|
-
<label
|
|
535
|
-
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
536
|
-
htmlFor="current-password"
|
|
537
|
-
>
|
|
538
|
-
Current password
|
|
539
|
-
</label>
|
|
540
|
-
<Input
|
|
541
|
-
id="current-password"
|
|
542
|
-
type="password"
|
|
543
|
-
autoComplete="current-password"
|
|
544
|
-
className="h-12"
|
|
545
|
-
value={currentPassword}
|
|
546
|
-
onChange={(event) => {
|
|
547
|
-
setCurrentPassword(event.target.value);
|
|
548
|
-
setPasswordError("");
|
|
549
|
-
setPasswordNotice("");
|
|
550
|
-
}}
|
|
551
|
-
/>
|
|
552
|
-
</div>
|
|
553
|
-
) : null}
|
|
554
|
-
|
|
555
|
-
<div className="space-y-2">
|
|
556
|
-
<label
|
|
557
|
-
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
558
|
-
htmlFor="new-password"
|
|
559
|
-
>
|
|
560
|
-
New password
|
|
561
|
-
</label>
|
|
562
|
-
<Input
|
|
563
|
-
id="new-password"
|
|
564
|
-
type="password"
|
|
565
|
-
autoComplete="new-password"
|
|
566
|
-
className="h-12"
|
|
567
|
-
value={newPassword}
|
|
568
|
-
onChange={(event) => {
|
|
569
|
-
setNewPassword(event.target.value);
|
|
570
|
-
setPasswordError("");
|
|
571
|
-
setPasswordNotice("");
|
|
572
|
-
}}
|
|
573
|
-
/>
|
|
574
|
-
</div>
|
|
575
|
-
|
|
576
|
-
<div className="space-y-2">
|
|
577
|
-
<label
|
|
578
|
-
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
579
|
-
htmlFor="confirm-password"
|
|
580
|
-
>
|
|
581
|
-
Confirm password
|
|
582
|
-
</label>
|
|
583
|
-
<Input
|
|
584
|
-
id="confirm-password"
|
|
585
|
-
type="password"
|
|
586
|
-
autoComplete="new-password"
|
|
587
|
-
className="h-12"
|
|
588
|
-
value={confirmPassword}
|
|
589
|
-
onChange={(event) => {
|
|
590
|
-
setConfirmPassword(event.target.value);
|
|
591
|
-
setPasswordError("");
|
|
592
|
-
setPasswordNotice("");
|
|
593
|
-
}}
|
|
594
|
-
/>
|
|
595
|
-
</div>
|
|
596
|
-
|
|
597
|
-
{passwordError ? (
|
|
598
|
-
<p className="text-sm text-destructive">{passwordError}</p>
|
|
599
|
-
) : null}
|
|
600
|
-
{passwordNotice ? (
|
|
601
|
-
<p className="text-sm text-primary">{passwordNotice}</p>
|
|
602
|
-
) : null}
|
|
603
|
-
|
|
604
|
-
<Button
|
|
605
|
-
type="submit"
|
|
606
|
-
className="gap-2"
|
|
607
|
-
disabled={passwordPending || passwordLoading}
|
|
608
|
-
>
|
|
609
|
-
{passwordPending ? (
|
|
610
|
-
<Loader2 className="size-4 animate-spin" />
|
|
611
|
-
) : (
|
|
612
|
-
<KeyRound className="size-4" />
|
|
613
|
-
)}
|
|
614
|
-
{passwordStatus?.has_password ? "Update password" : "Create password"}
|
|
615
|
-
</Button>
|
|
616
|
-
</form>
|
|
617
|
-
)}
|
|
618
|
-
</PanelFrame>
|
|
619
|
-
</div>
|
|
620
|
-
</section>
|
|
116
|
+
<TenantDashboard appName={appName} session={session} />
|
|
621
117
|
</div>
|
|
622
118
|
</main>
|
|
623
119
|
);
|
|
@@ -38,7 +38,7 @@ const docsIndexRoute: Route = "/docs";
|
|
|
38
38
|
const blogIndexRoute: Route = "/blog";
|
|
39
39
|
const loginRoute: Route = "/login";
|
|
40
40
|
const appHomeRoute: Route = "/app";
|
|
41
|
-
const
|
|
41
|
+
const demoRoute = "/app/demo" as Route;
|
|
42
42
|
|
|
43
43
|
export const appRoutes = {
|
|
44
44
|
publicHome: publicHomeRoute,
|
|
@@ -55,5 +55,5 @@ export const appRoutes = {
|
|
|
55
55
|
},
|
|
56
56
|
login: loginRoute,
|
|
57
57
|
appHome: appHomeRoute,
|
|
58
|
-
|
|
58
|
+
demo: demoRoute,
|
|
59
59
|
};
|