@tokenoftrust/storefront-runner 2.0.2 → 2.0.3
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/package.json +1 -1
- package/scripts/lib/tenant-dirs.mjs +80 -0
- package/scripts/tenant/lint-embeds.mjs +180 -0
- package/scripts/tenant/materialize-tenant-content.mjs +533 -0
- package/scripts/tenant/validate.mjs +837 -0
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
- package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
- package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
- package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
- package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
- package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
- package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
- package/apps/storefront/src/lib/publish/domainState.ts +0 -360
- package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
- package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
- package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
- package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
- package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
- package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
- package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
- package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
- package/apps/storefront/src/pages/admin/review-release.astro +0 -265
- package/apps/storefront/src/pages/admin/select.astro +0 -96
- package/apps/storefront/src/pages/admin.astro +0 -570
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
- package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
- package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
- package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
- package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
- package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
- package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
- package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
- package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
- package/apps/storefront/src/pages/cockpit.astro +0 -1548
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
- package/apps/storefront/src/pages/dashboard/index.astro +0 -160
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /dashboard/<appDomain>/apps — the console's app-health list (PrivateApps
|
|
4
|
-
* epic, D8 Chunk C). Guarded the same way as `../team.astro`: signed-in
|
|
5
|
-
* session + a membership for this appDomain, else bounce.
|
|
6
|
-
*
|
|
7
|
-
* Thin shell over one session-authed endpoint:
|
|
8
|
-
* GET /api/apps/admin/health?tenant=<appDomain> → AppInstallHealthView[]
|
|
9
|
-
* `canManage` is resolved server-side from the SAME `MANAGE_ROLES` set the
|
|
10
|
-
* mutating admin routes gate on, and passed to the client via `data-can-
|
|
11
|
-
* manage` so a `developer` viewer never even sees the suspend/uninstall/
|
|
12
|
-
* rotate buttons rendered (the routes still enforce it independently —
|
|
13
|
-
* this is UX, not the security boundary).
|
|
14
|
-
*/
|
|
15
|
-
export const prerender = false;
|
|
16
|
-
|
|
17
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
18
|
-
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
19
|
-
import { MANAGE_ROLES } from "@/lib/apps/adminSession.js";
|
|
20
|
-
|
|
21
|
-
const cspNonce = Astro.locals.cspNonce;
|
|
22
|
-
const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
|
|
23
|
-
|
|
24
|
-
const session = await readViewerSession(Astro);
|
|
25
|
-
if (!session) {
|
|
26
|
-
return Astro.redirect(
|
|
27
|
-
`/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}/apps`)}`,
|
|
28
|
-
302,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
const membership = membershipFor(session.tenants, appDomain, {
|
|
32
|
-
selection: session.staffSelection,
|
|
33
|
-
nowSeconds: Math.floor(Date.now() / 1000),
|
|
34
|
-
});
|
|
35
|
-
if (!membership) {
|
|
36
|
-
return Astro.redirect("/dashboard", 302);
|
|
37
|
-
}
|
|
38
|
-
const canManage = MANAGE_ROLES.has(membership.role);
|
|
39
|
-
const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
<html lang="en">
|
|
43
|
-
<head>
|
|
44
|
-
<meta charset="utf-8" />
|
|
45
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
47
|
-
<title>Apps — {appDomain}</title>
|
|
48
|
-
<style nonce={cspNonce}>
|
|
49
|
-
:root { color-scheme: light dark; }
|
|
50
|
-
* { box-sizing: border-box; }
|
|
51
|
-
body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
52
|
-
margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
|
|
53
|
-
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
54
|
-
main { max-width: 48rem; margin-inline: auto; }
|
|
55
|
-
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
|
|
56
|
-
h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
|
|
57
|
-
.sub { color: #6a6a6a; margin: 0 0 1.5rem; }
|
|
58
|
-
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
59
|
-
#apps { display: flex; flex-direction: column; gap: 1rem; }
|
|
60
|
-
.card { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.15rem 1.25rem; }
|
|
61
|
-
@media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; } }
|
|
62
|
-
.card .top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
63
|
-
.card .name { font-weight: 600; font-size: 1.02rem; }
|
|
64
|
-
.badge { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
|
|
65
|
-
border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
|
|
66
|
-
.badge.status-active, .badge.health-healthy { color: #2e8b45; }
|
|
67
|
-
.badge.status-suspended, .badge.health-degraded { color: #b5860b; }
|
|
68
|
-
.badge.status-uninstalled, .badge.health-unhealthy, .badge.egress-blocked { color: #b13a3a; }
|
|
69
|
-
.badge.egress-ok { color: #2e8b45; }
|
|
70
|
-
.badge.egress-no_endpoint { color: #8a8a8a; }
|
|
71
|
-
dl.meta { display: grid; grid-template-columns: max-content 1fr; gap: .25rem 1rem; margin: .8rem 0 0; font-size: .85rem; }
|
|
72
|
-
dl.meta dt { color: #8a8a8a; }
|
|
73
|
-
dl.meta dd { margin: 0; }
|
|
74
|
-
.traces { font: .78rem/1.4 ui-monospace, monospace; color: #8a8a8a; word-break: break-all; }
|
|
75
|
-
.actions { margin-top: 1rem; display: flex; gap: .5rem; flex-wrap: wrap; }
|
|
76
|
-
.rowbtn { padding: .4rem .8rem; font-size: .82rem; background: transparent; color: inherit;
|
|
77
|
-
border: 1px solid #cfccc4; border-radius: 7px; font-weight: 600; cursor: pointer; }
|
|
78
|
-
@media (prefers-color-scheme: dark) { .rowbtn { border-color: #40404d; } }
|
|
79
|
-
.rowbtn:hover:not(:disabled) { border-color: #b7d0ff; }
|
|
80
|
-
.rowbtn:disabled { opacity: .5; cursor: default; }
|
|
81
|
-
.rowbtn.danger { color: #b13a3a; border-color: #e3b3b3; }
|
|
82
|
-
.msg { margin-top: .8rem; font-size: .85rem; border-radius: 9px; padding: .5rem .7rem; display: none; }
|
|
83
|
-
.msg.ok { display: block; background: #eaf7ee; border: 1px solid #bfe4c9; color: #1b6b34; }
|
|
84
|
-
.msg.err { display: block; background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c; }
|
|
85
|
-
@media (prefers-color-scheme: dark) {
|
|
86
|
-
.msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
|
|
87
|
-
.msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
|
|
88
|
-
}
|
|
89
|
-
.muted { color: #9a9a9a; font-size: .9rem; }
|
|
90
|
-
.who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
|
|
91
|
-
.who a, a.back { color: inherit; }
|
|
92
|
-
.linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; margin: 0; }
|
|
93
|
-
.logout { display: inline; }
|
|
94
|
-
</style>
|
|
95
|
-
</head>
|
|
96
|
-
<body>
|
|
97
|
-
<main>
|
|
98
|
-
<p class="eyebrow"><a class="back" href={backHref}>← {appDomain}</a></p>
|
|
99
|
-
<h1>Apps</h1>
|
|
100
|
-
<p class="sub">Installed apps and their webhook health for <strong>{appDomain}</strong>.{!canManage &&
|
|
101
|
-
" Viewing read-only — ask an admin or owner to suspend, uninstall, or rotate credentials."}</p>
|
|
102
|
-
|
|
103
|
-
<div id="apps" data-tenant={appDomain} data-can-manage={String(canManage)}>
|
|
104
|
-
<p class="muted">Loading…</p>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<p class="who">Signed in as {session.email} ·
|
|
108
|
-
<a href="/dashboard">Switch vendor</a> ·
|
|
109
|
-
<form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
|
|
110
|
-
</main>
|
|
111
|
-
|
|
112
|
-
<!--
|
|
113
|
-
Client behavior is a STATIC same-origin file (public/js/dashboard-apps.js),
|
|
114
|
-
referenced via src so the strict CSP allows it through `script-src 'self'` — no
|
|
115
|
-
nonce, no per-build content-hash. Mirrors team.astro's script tag exactly.
|
|
116
|
-
-->
|
|
117
|
-
<script src="/js/dashboard-apps.js" is:inline defer></script>
|
|
118
|
-
</body>
|
|
119
|
-
</html>
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /dashboard/<appDomain> — a single vendor's console (#8425 Stage 0).
|
|
4
|
-
*
|
|
5
|
-
* Guarded by the signed-in session: the viewer must hold a membership for this
|
|
6
|
-
* appDomain (near-side authZ, deny-by-default), else bounce to the picker. The
|
|
7
|
-
* page is a thin hub — today it links to the Team page (invite developers) and
|
|
8
|
-
* the existing preview/grants surfaces for this tenant.
|
|
9
|
-
*/
|
|
10
|
-
export const prerender = false;
|
|
11
|
-
|
|
12
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
13
|
-
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
14
|
-
|
|
15
|
-
const cspNonce = Astro.locals.cspNonce;
|
|
16
|
-
const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
|
|
17
|
-
|
|
18
|
-
const session = await readViewerSession(Astro);
|
|
19
|
-
if (!session) {
|
|
20
|
-
return Astro.redirect(
|
|
21
|
-
`/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}`)}`,
|
|
22
|
-
302,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
// Member OR ToT-staff admitted via an explicit vendor selection (distinct path —
|
|
26
|
-
// the staff marker is not a tenants[] membership). membershipFor returns a
|
|
27
|
-
// staff-derived stand-in (role = the staffRoles capability) when the selection is live.
|
|
28
|
-
const membership = membershipFor(session.tenants, appDomain, {
|
|
29
|
-
selection: session.staffSelection,
|
|
30
|
-
nowSeconds: Math.floor(Date.now() / 1000),
|
|
31
|
-
});
|
|
32
|
-
if (!membership) {
|
|
33
|
-
// Signed in but no access to THIS vendor — back to the picker/search.
|
|
34
|
-
return Astro.redirect("/dashboard", 302);
|
|
35
|
-
}
|
|
36
|
-
const teamHref = `/dashboard/${encodeURIComponent(appDomain)}/team`;
|
|
37
|
-
const appsHref = `/dashboard/${encodeURIComponent(appDomain)}/apps`;
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
<html lang="en">
|
|
41
|
-
<head>
|
|
42
|
-
<meta charset="utf-8" />
|
|
43
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
44
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
45
|
-
<title>{appDomain} — vendor dashboard</title>
|
|
46
|
-
<style nonce={cspNonce}>
|
|
47
|
-
:root { color-scheme: light dark; }
|
|
48
|
-
* { box-sizing: border-box; }
|
|
49
|
-
body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
50
|
-
margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
|
|
51
|
-
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
52
|
-
main { max-width: 44rem; margin-inline: auto; }
|
|
53
|
-
.top { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
54
|
-
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
|
|
55
|
-
h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
|
|
56
|
-
.role { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
|
|
57
|
-
color: #4a6; border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
|
|
58
|
-
.sub { color: #6a6a6a; margin: .2rem 0 1.75rem; }
|
|
59
|
-
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
60
|
-
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
61
|
-
@media (max-width: 32rem) { .grid { grid-template-columns: 1fr; } }
|
|
62
|
-
a.card { display: block; text-decoration: none; color: inherit; background: #fff;
|
|
63
|
-
border: 1px solid #e6e4de; border-radius: 12px; padding: 1.15rem 1.25rem; }
|
|
64
|
-
@media (prefers-color-scheme: dark) { a.card { background: #1e1e28; border-color: #33333f; } }
|
|
65
|
-
a.card:hover { border-color: #b7d0ff; }
|
|
66
|
-
.card .h { font-weight: 600; margin: 0 0 .25rem; }
|
|
67
|
-
.card .d { color: #8a8a8a; font-size: .88rem; margin: 0; }
|
|
68
|
-
.who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
|
|
69
|
-
.who a { color: inherit; }
|
|
70
|
-
.linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; }
|
|
71
|
-
.logout { display: inline; }
|
|
72
|
-
</style>
|
|
73
|
-
</head>
|
|
74
|
-
<body>
|
|
75
|
-
<main>
|
|
76
|
-
<div class="top">
|
|
77
|
-
<div>
|
|
78
|
-
<p class="eyebrow">Vendor dashboard</p>
|
|
79
|
-
<h1>{appDomain}</h1>
|
|
80
|
-
</div>
|
|
81
|
-
<span class="role">{membership.role}</span>
|
|
82
|
-
</div>
|
|
83
|
-
<p class="sub">Manage this vendor's developer team and preview.</p>
|
|
84
|
-
|
|
85
|
-
<div class="grid">
|
|
86
|
-
<a class="card" href={teamHref}>
|
|
87
|
-
<p class="h">Team & invites →</p>
|
|
88
|
-
<p class="d">See who has developer access and invite new developers by email.</p>
|
|
89
|
-
</a>
|
|
90
|
-
<a class="card" href={`/preview/dashboard?tenant=${encodeURIComponent(appDomain)}`}>
|
|
91
|
-
<p class="h">Preview & deploy →</p>
|
|
92
|
-
<p class="d">The tenant's shared preview: target vs actual, reconcile, preview URL.</p>
|
|
93
|
-
</a>
|
|
94
|
-
<a class="card" href={appsHref}>
|
|
95
|
-
<p class="h">Apps →</p>
|
|
96
|
-
<p class="d">Installed apps, webhook health, and manage suspend/uninstall/rotate.</p>
|
|
97
|
-
</a>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<p class="who">Signed in as {session.email} ·
|
|
101
|
-
<a href="/dashboard">Switch vendor</a> ·
|
|
102
|
-
<form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
|
|
103
|
-
</main>
|
|
104
|
-
</body>
|
|
105
|
-
</html>
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /dashboard/<appDomain>/team — a vendor's developer team + invite-by-email
|
|
4
|
-
* (#8425 Stage 0).
|
|
5
|
-
*
|
|
6
|
-
* Guarded by the signed-in session (membership for this appDomain). The page is a
|
|
7
|
-
* thin client over two session-authed endpoints:
|
|
8
|
-
* GET /api/grants/team-members?tenant=<appDomain> → current developer invites
|
|
9
|
-
* POST /api/grants/team-invite {tenant, email} → invite a developer
|
|
10
|
-
* Both re-check membership server-side; the appDomain here is carried from the
|
|
11
|
-
* guarded route, and every call is authorized by the `tot_session` cookie.
|
|
12
|
-
*/
|
|
13
|
-
export const prerender = false;
|
|
14
|
-
|
|
15
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
16
|
-
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
17
|
-
import { readEnv } from "@/lib/env";
|
|
18
|
-
|
|
19
|
-
const cspNonce = Astro.locals.cspNonce;
|
|
20
|
-
const appDomain = decodeURIComponent(Astro.params.appDomain ?? "");
|
|
21
|
-
|
|
22
|
-
// qa-tier warn: derived from the SAME MCP origin the invite preflight uses
|
|
23
|
-
// (MCP_APP_BINDING_URL). We don't want people onboarding developers via qa now that
|
|
24
|
-
// prod+qa share data — so warn (non-blocking; qa is legitimate for testing).
|
|
25
|
-
let isQaTier = false;
|
|
26
|
-
try {
|
|
27
|
-
const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
|
|
28
|
-
isQaTier = !!bindingUrl && new URL(bindingUrl).hostname.startsWith("mcp.qa.");
|
|
29
|
-
} catch {
|
|
30
|
-
isQaTier = false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const session = await readViewerSession(Astro);
|
|
34
|
-
if (!session) {
|
|
35
|
-
return Astro.redirect(
|
|
36
|
-
`/auth/login?next=${encodeURIComponent(`/dashboard/${appDomain}/team`)}`,
|
|
37
|
-
302,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
// Member OR ToT-staff admitted via an explicit vendor selection (distinct path).
|
|
41
|
-
const membership = membershipFor(session.tenants, appDomain, {
|
|
42
|
-
selection: session.staffSelection,
|
|
43
|
-
nowSeconds: Math.floor(Date.now() / 1000),
|
|
44
|
-
});
|
|
45
|
-
if (!membership) {
|
|
46
|
-
return Astro.redirect("/dashboard", 302);
|
|
47
|
-
}
|
|
48
|
-
// #8453 (`staff-can-invite-governed`, REVERSING the earlier members-only block):
|
|
49
|
-
// a ToT-staff viewer admitted via an explicit vendor selection may now invite too —
|
|
50
|
-
// the write endpoint mints a signed broker assertion from their session email and
|
|
51
|
-
// tot20 verifies it to permit the invite. So the form shows for member OR staff
|
|
52
|
-
// (everyone admitted to this page). `isRealMember` is kept only to tailor the copy.
|
|
53
|
-
const isRealMember = !!membershipFor(session.tenants, appDomain);
|
|
54
|
-
const backHref = `/dashboard/${encodeURIComponent(appDomain)}`;
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
<html lang="en">
|
|
58
|
-
<head>
|
|
59
|
-
<meta charset="utf-8" />
|
|
60
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
61
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
62
|
-
<title>Team — {appDomain}</title>
|
|
63
|
-
<style nonce={cspNonce}>
|
|
64
|
-
:root { color-scheme: light dark; }
|
|
65
|
-
* { box-sizing: border-box; }
|
|
66
|
-
body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
67
|
-
margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
|
|
68
|
-
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
69
|
-
main { max-width: 44rem; margin-inline: auto; }
|
|
70
|
-
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
|
|
71
|
-
h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
|
|
72
|
-
h2 { font-size: .95rem; text-transform: uppercase; letter-spacing: .05em; color: #777; margin: 2rem 0 .6rem; }
|
|
73
|
-
.sub { color: #6a6a6a; margin: 0 0 1rem; }
|
|
74
|
-
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
75
|
-
.card { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.25rem; }
|
|
76
|
-
@media (prefers-color-scheme: dark) { .card { background: #1e1e28; border-color: #33333f; } }
|
|
77
|
-
label { display: block; font-weight: 600; font-size: .82rem; margin: 0 0 .4rem; }
|
|
78
|
-
label[for=role], label[for=note] { margin-top: .9rem; }
|
|
79
|
-
input[type=email], select, textarea { width: 100%; padding: .7rem .8rem; font-size: 1rem; border-radius: 9px;
|
|
80
|
-
border: 1px solid #cfccc4; background: #fff; color: inherit; font-family: inherit; }
|
|
81
|
-
select { margin-bottom: .2rem; }
|
|
82
|
-
textarea { min-height: 4.5rem; resize: vertical; line-height: 1.4; }
|
|
83
|
-
@media (prefers-color-scheme: dark) { input[type=email], select, textarea { background: #14141a; border-color: #40404d; } }
|
|
84
|
-
button { margin-top: 1rem; padding: .7rem 1.1rem; border: 0; border-radius: 9px; background: #1a1a1a;
|
|
85
|
-
color: #fff; font-weight: 600; font-size: .95rem; cursor: pointer; }
|
|
86
|
-
@media (prefers-color-scheme: dark) { button { background: #ececf1; color: #14141a; } }
|
|
87
|
-
button:disabled { opacity: .5; cursor: default; }
|
|
88
|
-
.rowbtn { margin: 0; padding: .3rem .6rem; font-size: .78rem; background: transparent; color: inherit;
|
|
89
|
-
border: 1px solid #cfccc4; border-radius: 7px; font-weight: 600; }
|
|
90
|
-
@media (prefers-color-scheme: dark) { .rowbtn { border-color: #40404d; } }
|
|
91
|
-
.rowbtn:hover:not(:disabled) { border-color: #b7d0ff; }
|
|
92
|
-
td.act { text-align: right; white-space: nowrap; }
|
|
93
|
-
table { width: 100%; border-collapse: collapse; }
|
|
94
|
-
th, td { text-align: left; padding: .5rem .4rem; border-bottom: 1px solid #e6e4de; font-size: .9rem; }
|
|
95
|
-
@media (prefers-color-scheme: dark) { th, td { border-color: #33333f; } }
|
|
96
|
-
th { font: 600 .7rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .05em; color: #999; }
|
|
97
|
-
.msg { margin-top: 1rem; font-size: .9rem; border-radius: 9px; padding: .6rem .8rem; display: none; }
|
|
98
|
-
.msg.ok { display: block; background: #eaf7ee; border: 1px solid #bfe4c9; color: #1b6b34; }
|
|
99
|
-
.msg.err { display: block; background: #fdecec; border: 1px solid #f5c2c2; color: #9a1c1c; }
|
|
100
|
-
@media (prefers-color-scheme: dark) {
|
|
101
|
-
.msg.ok { background: #16311f; border-color: #2f6b41; color: #a7e0ba; }
|
|
102
|
-
.msg.err { background: #3a1f22; border-color: #6b2f34; color: #f6b9b9; }
|
|
103
|
-
}
|
|
104
|
-
.warn { margin: 0 0 1rem; font-size: .88rem; border-radius: 9px; padding: .6rem .8rem;
|
|
105
|
-
background: #fff7e6; border: 1px solid #f0d999; color: #7a5b12; }
|
|
106
|
-
@media (prefers-color-scheme: dark) { .warn { background: #332a12; border-color: #6b5a2f; color: #e8d39a; } }
|
|
107
|
-
.muted { color: #9a9a9a; font-size: .85rem; }
|
|
108
|
-
.who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
|
|
109
|
-
.who a, a.back { color: inherit; }
|
|
110
|
-
.linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; margin: 0; }
|
|
111
|
-
.logout { display: inline; }
|
|
112
|
-
</style>
|
|
113
|
-
</head>
|
|
114
|
-
<body>
|
|
115
|
-
<main>
|
|
116
|
-
<p class="eyebrow"><a class="back" href={backHref}>← {appDomain}</a></p>
|
|
117
|
-
<h1>Team</h1>
|
|
118
|
-
<p class="sub">Developers with access to <strong>{appDomain}</strong>.{isRealMember
|
|
119
|
-
? " Invite by email — they'll get a branded sign-in link and become eligible on this vendor."
|
|
120
|
-
: " Viewing as Token of Trust staff — invite by email to grant a developer access."}</p>
|
|
121
|
-
|
|
122
|
-
<h2>Invite a developer</h2>
|
|
123
|
-
{isQaTier && (
|
|
124
|
-
<p class="warn">⚠ This invite connects the developer to the <strong>QA</strong> environment
|
|
125
|
-
— likely not what you want.</p>
|
|
126
|
-
)}
|
|
127
|
-
<div class="card">
|
|
128
|
-
<label for="email">Developer email</label>
|
|
129
|
-
<input id="email" type="email" placeholder="dev@agency.com" autocomplete="off" />
|
|
130
|
-
{/* tot20's role vocabulary: developer | appAdmin | appOwner | contributor.
|
|
131
|
-
`contributor` is a strictly-weaker self-downgrade a plain member may
|
|
132
|
-
request directly (tot20 honors it for a non-staff operator — handoff
|
|
133
|
-
2026-08-22-tot20-contributor-role); appAdmin/appOwner still require ToT
|
|
134
|
-
staff (#8453) and tot20 ignores them from a plain member (pins to
|
|
135
|
-
developer), so only staff see those two options. */}
|
|
136
|
-
<label for="role">Role</label>
|
|
137
|
-
<select id="role">
|
|
138
|
-
<option value="developer" selected>Developer</option>
|
|
139
|
-
<option value="contributor">Contributor (blog & content only)</option>
|
|
140
|
-
{!isRealMember && (
|
|
141
|
-
<>
|
|
142
|
-
<option value="appAdmin">Admin</option>
|
|
143
|
-
<option value="appOwner">Owner</option>
|
|
144
|
-
</>
|
|
145
|
-
)}
|
|
146
|
-
</select>
|
|
147
|
-
<label for="note">Note to the developer (optional)</label>
|
|
148
|
-
<textarea id="note" rows="3" maxlength="1000"
|
|
149
|
-
placeholder="Add a personal message — they'll see it in the invite email."></textarea>
|
|
150
|
-
<button id="inviteBtn" type="button">Send invite</button>
|
|
151
|
-
<div id="msg" class="msg"></div>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<h2>Current developers</h2>
|
|
155
|
-
<div class="card">
|
|
156
|
-
<table>
|
|
157
|
-
<thead><tr><th>Developer</th><th>Roles</th><th></th></tr></thead>
|
|
158
|
-
<tbody id="members" data-tenant={appDomain}><tr><td colspan="3" class="muted">Loading…</td></tr></tbody>
|
|
159
|
-
</table>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<p class="who">Signed in as {session.email} ·
|
|
163
|
-
<a href="/dashboard">Switch vendor</a> ·
|
|
164
|
-
<form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
|
|
165
|
-
</main>
|
|
166
|
-
|
|
167
|
-
<!--
|
|
168
|
-
Client behavior is a STATIC same-origin file (public/js/dashboard-team.js),
|
|
169
|
-
referenced via src so the strict CSP allows it through `script-src 'self'` — no
|
|
170
|
-
nonce, no per-build content-hash. An inline <script> here is blocked: Astro's
|
|
171
|
-
`is:inline` drops the templated nonce, and a processed inline script would need
|
|
172
|
-
its hash pinned in cspStaticHashes on every edit. `is:inline` keeps Astro from
|
|
173
|
-
trying to bundle the external-src tag; `defer` runs it after the DOM parses.
|
|
174
|
-
-->
|
|
175
|
-
<script src="/js/dashboard-team.js" is:inline defer></script>
|
|
176
|
-
</body>
|
|
177
|
-
</html>
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /dashboard — the vendor console entry (#8425 Stage 0).
|
|
4
|
-
*
|
|
5
|
-
* Federated: the viewer signed in via ToT (email OTP → offline-verified id_token →
|
|
6
|
-
* `tot_session`). This page holds NO identity of its own — it reads the session's
|
|
7
|
-
* tenant memberships and routes per the L3 contract
|
|
8
|
-
* (docs/architecture/identity-and-authorization.md):
|
|
9
|
-
* 0 memberships → "nothing to manage"; 1 → straight into that vendor; ≥2 → picker.
|
|
10
|
-
*
|
|
11
|
-
* STAGE-0: tot20's id_token currently carries only the one resource signed against,
|
|
12
|
-
* so this resolves to a single vendor in practice; the picker activates unchanged
|
|
13
|
-
* once tot20 populates all memberships (flagged to the workstream lead).
|
|
14
|
-
*/
|
|
15
|
-
export const prerender = false;
|
|
16
|
-
|
|
17
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
18
|
-
import { selectTenant } from "@/lib/dashboard/tenantSelection";
|
|
19
|
-
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
20
|
-
|
|
21
|
-
const cspNonce = Astro.locals.cspNonce;
|
|
22
|
-
const session = await readViewerSession(Astro);
|
|
23
|
-
if (!session) {
|
|
24
|
-
return Astro.redirect("/auth/login?next=%2Fdashboard", 302);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ToT-STAFF viewers reach ANY vendor by explicit SEARCH-and-enter (a distinct
|
|
28
|
-
// path from tenants[] membership). When staff, we always render the hub (their
|
|
29
|
-
// memberships, if any, PLUS a vendor search) and skip the single-membership
|
|
30
|
-
// auto-redirect so they can still search for a vendor they don't belong to.
|
|
31
|
-
const isStaff = isStaffSession(session);
|
|
32
|
-
const selection = selectTenant(session.tenants);
|
|
33
|
-
if (!isStaff && selection.mode === "single") {
|
|
34
|
-
return Astro.redirect(`/dashboard/${encodeURIComponent(selection.tenant.resource)}`, 302);
|
|
35
|
-
}
|
|
36
|
-
const vendors =
|
|
37
|
-
selection.mode === "picker"
|
|
38
|
-
? selection.tenants
|
|
39
|
-
: selection.mode === "single"
|
|
40
|
-
? [selection.tenant]
|
|
41
|
-
: [];
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
<html lang="en">
|
|
45
|
-
<head>
|
|
46
|
-
<meta charset="utf-8" />
|
|
47
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
48
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
49
|
-
<title>Your stores — Token of Trust</title>
|
|
50
|
-
<style nonce={cspNonce}>
|
|
51
|
-
:root { color-scheme: light dark; }
|
|
52
|
-
* { box-sizing: border-box; }
|
|
53
|
-
body { font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
54
|
-
margin: 0; min-height: 100vh; padding: 2.5rem 1.25rem; background: #f5f4f0; color: #1c1c1c; }
|
|
55
|
-
@media (prefers-color-scheme: dark) { body { background: #14141a; color: #ececf1; } }
|
|
56
|
-
main { max-width: 40rem; margin-inline: auto; }
|
|
57
|
-
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: #8a7f66; margin: 0 0 .5rem; }
|
|
58
|
-
h1 { font-size: 1.5rem; letter-spacing: -.01em; margin: 0 0 .3rem; }
|
|
59
|
-
.sub { color: #6a6a6a; margin: 0 0 1.75rem; }
|
|
60
|
-
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
61
|
-
ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
|
|
62
|
-
a.vendor { display: flex; align-items: center; justify-content: space-between; text-decoration: none;
|
|
63
|
-
background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1rem 1.25rem; color: inherit; }
|
|
64
|
-
@media (prefers-color-scheme: dark) { a.vendor { background: #1e1e28; border-color: #33333f; } }
|
|
65
|
-
a.vendor:hover { border-color: #b7d0ff; }
|
|
66
|
-
.name { font-weight: 600; }
|
|
67
|
-
.role { font: 600 .68rem/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em;
|
|
68
|
-
color: #4a6; border: 1px solid currentColor; border-radius: 999px; padding: .2rem .55rem; }
|
|
69
|
-
.who { margin-top: 2rem; font-size: .82rem; color: #8a8a8a; }
|
|
70
|
-
.who a { color: inherit; }
|
|
71
|
-
.linkbtn { background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; padding: 0; }
|
|
72
|
-
.logout { display: inline; }
|
|
73
|
-
.empty { background: #fff; border: 1px solid #e6e4de; border-radius: 12px; padding: 1.5rem; }
|
|
74
|
-
@media (prefers-color-scheme: dark) { .empty { background: #1e1e28; border-color: #33333f; } }
|
|
75
|
-
/* Staff vendor search */
|
|
76
|
-
.staff { margin-top: 2.25rem; }
|
|
77
|
-
h2 { font-size: .95rem; text-transform: uppercase; letter-spacing: .05em; color: #777; margin: 0 0 .3rem; }
|
|
78
|
-
.staff .hint { color: #8a8a8a; font-size: .85rem; margin: 0 0 .7rem; }
|
|
79
|
-
.staff .hint code { font: .8em ui-monospace, monospace; background: #ece9e1; color: #4a4a4a;
|
|
80
|
-
padding: .1em .35em; border-radius: 5px; }
|
|
81
|
-
@media (prefers-color-scheme: dark) { .staff .hint code { background: #2a2a33; color: #cfcfda; } }
|
|
82
|
-
.search { position: relative; }
|
|
83
|
-
#vendorSearch { width: 100%; padding: .75rem .85rem; font-size: 1rem; border-radius: 10px;
|
|
84
|
-
border: 1px solid #cfccc4; background: #fff; color: inherit; }
|
|
85
|
-
@media (prefers-color-scheme: dark) { #vendorSearch { background: #14141a; border-color: #40404d; } }
|
|
86
|
-
#vendorResults { list-style: none; margin: .6rem 0 0; padding: 0; display: grid; gap: .5rem; }
|
|
87
|
-
#vendorResults button { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 1rem;
|
|
88
|
-
text-align: left; background: #fff; border: 1px solid #e6e4de; border-radius: 10px; padding: .8rem 1rem;
|
|
89
|
-
color: inherit; font: inherit; cursor: pointer; }
|
|
90
|
-
@media (prefers-color-scheme: dark) { #vendorResults button { background: #1e1e28; border-color: #33333f; } }
|
|
91
|
-
#vendorResults button:hover, #vendorResults button:focus-visible { border-color: #b7d0ff; }
|
|
92
|
-
#vendorResults button:disabled { opacity: .5; cursor: default; }
|
|
93
|
-
#vendorResults .dn { font-weight: 600; }
|
|
94
|
-
#vendorResults .ad { font: 600 .68rem/1 ui-monospace, monospace; color: #8a8a8a; }
|
|
95
|
-
#vendorSearchMsg { margin-top: .6rem; font-size: .88rem; color: #8a8a8a; min-height: 1.1em; }
|
|
96
|
-
#vendorSearchMsg.err { color: #9a1c1c; }
|
|
97
|
-
@media (prefers-color-scheme: dark) { #vendorSearchMsg.err { color: #f6b9b9; } }
|
|
98
|
-
.badge { display: inline-block; font: 600 .6rem/1 ui-monospace, monospace; letter-spacing: .08em;
|
|
99
|
-
text-transform: uppercase; color: #8a7f66; border: 1px solid currentColor; border-radius: 999px;
|
|
100
|
-
padding: .18rem .5rem; margin-left: .5rem; vertical-align: middle; }
|
|
101
|
-
</style>
|
|
102
|
-
</head>
|
|
103
|
-
<body>
|
|
104
|
-
<main>
|
|
105
|
-
<p class="eyebrow">Token of Trust</p>
|
|
106
|
-
<h1>Your stores{isStaff && <span class="badge">ToT staff</span>}</h1>
|
|
107
|
-
{vendors.length ? (
|
|
108
|
-
<>
|
|
109
|
-
<p class="sub">Choose a store to manage its team and preview.</p>
|
|
110
|
-
<ul>
|
|
111
|
-
{vendors.map((v) => (
|
|
112
|
-
<li>
|
|
113
|
-
<a class="vendor" href={`/dashboard/${encodeURIComponent(v.resource)}`}>
|
|
114
|
-
<span class="name">{v.resource}</span>
|
|
115
|
-
<span class="role">{v.role}</span>
|
|
116
|
-
</a>
|
|
117
|
-
</li>
|
|
118
|
-
))}
|
|
119
|
-
</ul>
|
|
120
|
-
</>
|
|
121
|
-
) : (
|
|
122
|
-
!isStaff && (
|
|
123
|
-
<div class="empty">
|
|
124
|
-
<p style="margin:0">You're signed in as <strong>{session.email}</strong>, but
|
|
125
|
-
you don't have access to any store yet. Ask a store's merchant to invite you.</p>
|
|
126
|
-
</div>
|
|
127
|
-
)
|
|
128
|
-
)}
|
|
129
|
-
|
|
130
|
-
{isStaff && (
|
|
131
|
-
<section class="staff">
|
|
132
|
-
<h2>Find a store</h2>
|
|
133
|
-
<p class="hint">As Token of Trust staff you can enter any store's console. Search by
|
|
134
|
-
domain, then select one to enter — every admission is logged.</p>
|
|
135
|
-
<p class="hint">Tip: <code>^</code> matches the start, <code>$</code> the end — e.g.
|
|
136
|
-
<code>^acme.com$</code> for an exact match, <code>acme$</code> to end with “acme”.</p>
|
|
137
|
-
<div class="search">
|
|
138
|
-
<input id="vendorSearch" type="text" placeholder="acme.com"
|
|
139
|
-
autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false"
|
|
140
|
-
aria-label="Search stores by domain" />
|
|
141
|
-
<ul id="vendorResults"></ul>
|
|
142
|
-
<p id="vendorSearchMsg" aria-live="polite"></p>
|
|
143
|
-
</div>
|
|
144
|
-
</section>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
<p class="who">Signed in as {session.email} ·
|
|
148
|
-
<form class="logout" method="post" action="/api/auth/logout"><button type="submit" class="linkbtn">Sign out</button></form></p>
|
|
149
|
-
</main>
|
|
150
|
-
|
|
151
|
-
<!--
|
|
152
|
-
Staff vendor-search behavior is a STATIC same-origin file
|
|
153
|
-
(public/js/dashboard-vendor-search.js), referenced via src so the strict CSP
|
|
154
|
-
allows it through `script-src 'self'` — no nonce, no per-build content-hash
|
|
155
|
-
(see public/js/dashboard-team.js for the proven pattern). The file no-ops when
|
|
156
|
-
#vendorSearch is absent (non-staff), so it's safe to always include.
|
|
157
|
-
-->
|
|
158
|
-
<script src="/js/dashboard-vendor-search.js" is:inline defer></script>
|
|
159
|
-
</body>
|
|
160
|
-
</html>
|