create-ailk 0.18.0 → 0.19.1
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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +19 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +218 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +135 -14
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /[locale]/account/general — User settings › General (#8339 D8; builds on
|
|
3
|
+
* #8232 D2).
|
|
4
|
+
*
|
|
5
|
+
* RSC, outside the account layout's SignedInShell (#8430), in the member's
|
|
6
|
+
* four-tab settings frame (D7) — standalone, the Admin's shape. Renders the shared
|
|
7
|
+
* `AccountSettings` composition (@working-theory/templates/account, spec
|
|
8
|
+
* 8339 D4) — the same General form the Admin renders — through the client
|
|
9
|
+
* adapter `GeneralSettings` (`profile-form.tsx`). Below the form:
|
|
10
|
+
*
|
|
11
|
+
* - the contact details (`AccountContactDetails`, #8435 D13) — only on the
|
|
12
|
+
* member path, seeded from `member.contact` and saved by `saveContact`;
|
|
13
|
+
* - Request account deletion (`DeletionRequest`, #8232 D5);
|
|
14
|
+
* - any section a module contributes (`accountSettingsSections`);
|
|
15
|
+
* - `BillingAnchor`, which forwards a `#billing` fragment to Billing.
|
|
16
|
+
*
|
|
17
|
+
* Sign-in moved to Security and cookie settings to Notifications (#8339
|
|
18
|
+
* C6) — General no longer renders either.
|
|
19
|
+
*
|
|
20
|
+
* Whether the profile and deletion go through the member API is
|
|
21
|
+
* `loadProfileSource` (D3), never a topology test here.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { member } from "@working-theory/api-client";
|
|
25
|
+
import {
|
|
26
|
+
AccountContactDetails,
|
|
27
|
+
AccountSettingsFrame,
|
|
28
|
+
AccountSettingsPane,
|
|
29
|
+
type AccountContact,
|
|
30
|
+
} from "@working-theory/templates/account";
|
|
31
|
+
import { localeHref } from "@working-theory/ui";
|
|
32
|
+
import { Separator, Text } from "@working-theory/ui/primitives";
|
|
33
|
+
import type { Metadata } from "next";
|
|
34
|
+
import { getTranslations } from "next-intl/server";
|
|
35
|
+
import type { ReactElement } from "react";
|
|
36
|
+
|
|
37
|
+
import { accountSettingsSections } from "../../../../../lib/account-config";
|
|
38
|
+
import { accountSession } from "../account-data";
|
|
39
|
+
import { siteThemeModeCount } from "../settings/theme-modes";
|
|
40
|
+
import { memberSettingsFrame, SettingsSection, type Translate } from "../settings-frame";
|
|
41
|
+
|
|
42
|
+
import { BillingAnchor } from "./billing-anchor";
|
|
43
|
+
import { saveContact } from "./contact-actions";
|
|
44
|
+
import { DeletionRequest } from "./deletion-request";
|
|
45
|
+
import { GeneralSettings } from "./profile-form";
|
|
46
|
+
import { loadProfileSource } from "./profile-source";
|
|
47
|
+
|
|
48
|
+
interface Props {
|
|
49
|
+
params: Promise<{ locale: string }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
53
|
+
const { locale } = await params;
|
|
54
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
55
|
+
return { title: t("settings.general.title"), description: t("settings.general.lead") };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default async function AccountGeneralPage({ params }: Props): Promise<ReactElement> {
|
|
59
|
+
const { locale } = await params;
|
|
60
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
61
|
+
const session = await accountSession(locale);
|
|
62
|
+
|
|
63
|
+
// Unreachable behind the (authed) gate; a session that expired between
|
|
64
|
+
// the layout and the page renders the pane's title alone.
|
|
65
|
+
if (!session) {
|
|
66
|
+
return (
|
|
67
|
+
<AccountSettingsFrame {...memberSettingsFrame(locale, t as unknown as Translate, "general")} name="">
|
|
68
|
+
<AccountSettingsPane
|
|
69
|
+
id="account-settings-general"
|
|
70
|
+
title={t("settings.general.title")}
|
|
71
|
+
lead={t("settings.general.lead")}
|
|
72
|
+
>
|
|
73
|
+
{null}
|
|
74
|
+
</AccountSettingsPane>
|
|
75
|
+
</AccountSettingsFrame>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { ctx } = session;
|
|
80
|
+
const source = await loadProfileSource(ctx);
|
|
81
|
+
|
|
82
|
+
// #8435 D13 — the person's confirmed contact on this site. A failed read
|
|
83
|
+
// renders the section's "unavailable" line, never the form: a form seeded
|
|
84
|
+
// from nothing would offer to replace details it never showed.
|
|
85
|
+
let contactRead: { ok: true; contact: AccountContact | null } | { ok: false } | null = null;
|
|
86
|
+
if (source.via === "member") {
|
|
87
|
+
try {
|
|
88
|
+
const res = await member.contact(ctx.init);
|
|
89
|
+
contactRead = res.ok ? { ok: true, contact: res.body.contact } : { ok: false };
|
|
90
|
+
} catch {
|
|
91
|
+
contactRead = { ok: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<GeneralSettings
|
|
97
|
+
frame={memberSettingsFrame(locale, t as unknown as Translate, "general")}
|
|
98
|
+
user={{ name: ctx.user.name ?? "", email: ctx.user.email, image: source.image }}
|
|
99
|
+
labels={{
|
|
100
|
+
title: t("settings.general.title"),
|
|
101
|
+
lead: t("settings.general.lead"),
|
|
102
|
+
colorMode: t("settings.general.colorMode"),
|
|
103
|
+
light: t("settings.general.light"),
|
|
104
|
+
auto: t("settings.general.auto"),
|
|
105
|
+
dark: t("settings.general.dark"),
|
|
106
|
+
emailLabel: t("settings.general.email"),
|
|
107
|
+
nameLabel: t("settings.general.name"),
|
|
108
|
+
photoLabel: t("settings.general.photo"),
|
|
109
|
+
photoHint: t("settings.general.photoHint"),
|
|
110
|
+
photoPlaceholder: t("settings.general.photoPlaceholder"),
|
|
111
|
+
saveButton: t("settings.general.save"),
|
|
112
|
+
saving: t("settings.general.saving"),
|
|
113
|
+
saved: t("settings.general.saved"),
|
|
114
|
+
saveError: t("settings.general.saveError"),
|
|
115
|
+
nameRequired: t("settings.general.nameRequired"),
|
|
116
|
+
photoInvalid: t("settings.general.photoInvalid"),
|
|
117
|
+
unverified: t("settings.general.unverified"),
|
|
118
|
+
unavailable: t("settings.general.unavailable"),
|
|
119
|
+
}}
|
|
120
|
+
colorMode={
|
|
121
|
+
siteThemeModeCount() > 1
|
|
122
|
+
? { cookieName: process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme" }
|
|
123
|
+
: null
|
|
124
|
+
}
|
|
125
|
+
via={source.via}
|
|
126
|
+
>
|
|
127
|
+
{contactRead ? (
|
|
128
|
+
<>
|
|
129
|
+
<Separator />
|
|
130
|
+
<div data-testid="account-contact-section">
|
|
131
|
+
{contactRead.ok ? (
|
|
132
|
+
<AccountContactDetails
|
|
133
|
+
contact={contactRead.contact}
|
|
134
|
+
onSave={saveContact}
|
|
135
|
+
labels={{
|
|
136
|
+
title: t("settings.general.contact.title"),
|
|
137
|
+
lead: t("settings.general.contact.lead"),
|
|
138
|
+
givenName: t("settings.general.contact.givenName"),
|
|
139
|
+
familyName: t("settings.general.contact.familyName"),
|
|
140
|
+
telephone: t("settings.general.contact.telephone"),
|
|
141
|
+
address: t("settings.general.contact.address"),
|
|
142
|
+
companyName: t("settings.general.contact.companyName"),
|
|
143
|
+
jobTitle: t("settings.general.contact.jobTitle"),
|
|
144
|
+
save: t("settings.general.contact.save"),
|
|
145
|
+
saving: t("settings.general.contact.saving"),
|
|
146
|
+
saved: t("settings.general.contact.saved"),
|
|
147
|
+
addressIncomplete: t("settings.general.contact.addressIncomplete"),
|
|
148
|
+
errors: {
|
|
149
|
+
telephone: t("settings.general.contact.errors.telephone"),
|
|
150
|
+
invalid: t("settings.general.contact.errors.invalid"),
|
|
151
|
+
unverified: t("settings.general.contact.errors.unverified"),
|
|
152
|
+
unavailable: t("settings.general.contact.errors.unavailable"),
|
|
153
|
+
failed: t("settings.general.contact.errors.failed"),
|
|
154
|
+
},
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
<span data-testid="account-contact-unavailable">
|
|
159
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
160
|
+
{t("settings.general.contact.unavailable")}
|
|
161
|
+
</Text>
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</>
|
|
166
|
+
) : null}
|
|
167
|
+
<Separator />
|
|
168
|
+
<div data-testid="account-deletion">
|
|
169
|
+
<DeletionRequest
|
|
170
|
+
via={source.via}
|
|
171
|
+
supportHref={localeHref(locale, "/account/support")}
|
|
172
|
+
labels={{
|
|
173
|
+
lead: t("settings.privacy.deletionLead"),
|
|
174
|
+
action: t("settings.privacy.deletion"),
|
|
175
|
+
sentSite: t("settings.privacy.deletionSentSite"),
|
|
176
|
+
sentHub: t("settings.privacy.deletionSentHub"),
|
|
177
|
+
unverified: t("settings.privacy.deletionUnverified"),
|
|
178
|
+
unavailable: t("settings.privacy.deletionUnavailable"),
|
|
179
|
+
error: t("settings.privacy.deletionError"),
|
|
180
|
+
viaSupport: t("settings.privacy.deletionViaSupport"),
|
|
181
|
+
supportLink: t("settings.privacy.deletionSupportLink"),
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
{accountSettingsSections.map((section) => (
|
|
186
|
+
<SettingsSection key={section.id} id={section.id} title={ctx.t(section.title)}>
|
|
187
|
+
{section.render(ctx)}
|
|
188
|
+
</SettingsSection>
|
|
189
|
+
))}
|
|
190
|
+
<BillingAnchor locale={locale} />
|
|
191
|
+
</GeneralSettings>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"; // owns the save call and the router.refresh() that follows it
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GeneralSettings — User settings › General's adapter over the shared
|
|
5
|
+
* `AccountSettings` composition (#8339 D8; builds on #8232 D3, D4).
|
|
6
|
+
*
|
|
7
|
+
* Rewritten in place from `ProfileForm` (#7430 → #8232), which rendered its
|
|
8
|
+
* own form. The form, the color logic and the markup now live in
|
|
9
|
+
* `@working-theory/templates/account`'s `AccountSettings` (spec 8339 D4);
|
|
10
|
+
* this file supplies `profile` and `onSave` from `via` (General's
|
|
11
|
+
* `loadProfileSource`, D3):
|
|
12
|
+
*
|
|
13
|
+
* - `member` — `saveProfile` (`PATCH /v1/member/profile`, D6). `image` is
|
|
14
|
+
* sent only when the photo field changed (an untouched field never
|
|
15
|
+
* clears a photo, 8232 D4/D6); a refusal keeps both fields as typed.
|
|
16
|
+
* - `session` — a build with no member API: the name saves through
|
|
17
|
+
* Better Auth's own `updateUser`, and no photo field shows
|
|
18
|
+
* (`profile="name-only"`).
|
|
19
|
+
* - `none` — a hub-mode spoke whose hub answers no member route: no form,
|
|
20
|
+
* the name as read-only text (`profile="read-only"`).
|
|
21
|
+
*
|
|
22
|
+
* On success, both paths call `router.refresh()` so the rail's name follows
|
|
23
|
+
* the save.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
AccountSettings,
|
|
28
|
+
type AccountGeneralLabels,
|
|
29
|
+
type AccountSaveResult,
|
|
30
|
+
type AccountSettingsFrameProps,
|
|
31
|
+
} from "@working-theory/templates/account";
|
|
32
|
+
import { createAuthClient } from "better-auth/react";
|
|
33
|
+
import { useRouter } from "next/navigation";
|
|
34
|
+
import type { ReactElement, ReactNode } from "react";
|
|
35
|
+
|
|
36
|
+
import { saveProfile } from "../profile-actions";
|
|
37
|
+
|
|
38
|
+
const authClient = createAuthClient();
|
|
39
|
+
|
|
40
|
+
export interface GeneralSettingsLabels extends AccountGeneralLabels {
|
|
41
|
+
/** The `saveProfile` refusal lines not already in `AccountGeneralLabels`. */
|
|
42
|
+
unverified: string;
|
|
43
|
+
unavailable: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function GeneralSettings({
|
|
47
|
+
frame,
|
|
48
|
+
user,
|
|
49
|
+
labels,
|
|
50
|
+
colorMode,
|
|
51
|
+
via,
|
|
52
|
+
children,
|
|
53
|
+
}: {
|
|
54
|
+
frame: Omit<AccountSettingsFrameProps, "name" | "children">;
|
|
55
|
+
user: { name: string; email: string; image: string | null };
|
|
56
|
+
labels: GeneralSettingsLabels;
|
|
57
|
+
colorMode: { cookieName: string } | null;
|
|
58
|
+
via: "member" | "session" | "none";
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
}): ReactElement {
|
|
61
|
+
const router = useRouter();
|
|
62
|
+
|
|
63
|
+
async function onSave({
|
|
64
|
+
name,
|
|
65
|
+
image,
|
|
66
|
+
imageChanged,
|
|
67
|
+
}: {
|
|
68
|
+
name: string;
|
|
69
|
+
image: string | null;
|
|
70
|
+
imageChanged: boolean;
|
|
71
|
+
}): Promise<AccountSaveResult> {
|
|
72
|
+
if (via === "session") {
|
|
73
|
+
const { error } = await authClient.updateUser({ name });
|
|
74
|
+
if (error) return { ok: false, message: labels.saveError };
|
|
75
|
+
router.refresh();
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
const res = await saveProfile(imageChanged ? { name, image } : { name });
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
if (res.reason === "photo") return { ok: false, field: "photo", message: labels.photoInvalid };
|
|
81
|
+
if (res.reason === "required") return { ok: false, field: "name", message: labels.nameRequired };
|
|
82
|
+
if (res.reason === "unverified") return { ok: false, message: labels.unverified };
|
|
83
|
+
if (res.reason === "unavailable") return { ok: false, message: labels.unavailable };
|
|
84
|
+
return { ok: false, message: labels.saveError };
|
|
85
|
+
}
|
|
86
|
+
router.refresh();
|
|
87
|
+
// exactOptionalPropertyTypes: omit `name` entirely rather than set it to
|
|
88
|
+
// `undefined` — AccountSaveResult's `name?: string` has no `undefined` value.
|
|
89
|
+
return { ok: true, image: res.image, ...(res.name !== null ? { name: res.name } : {}) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<AccountSettings
|
|
94
|
+
frame={frame}
|
|
95
|
+
user={user}
|
|
96
|
+
labels={labels}
|
|
97
|
+
colorMode={colorMode}
|
|
98
|
+
profile={via === "member" ? "editable" : via === "session" ? "name-only" : "read-only"}
|
|
99
|
+
{...(via === "none" ? {} : { onSave })}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</AccountSettings>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* general/profile-source.ts (server code) — who saves User settings ›
|
|
3
|
+
* General's profile and takes its deletion request, and the photo the
|
|
4
|
+
* profile form starts with (#8232, spec
|
|
5
|
+
* docs/specs/8232-member-settings-account-general-and-spec.md D3, C2, C5).
|
|
6
|
+
*
|
|
7
|
+
* `via` — whether the member API answers, read once per request from
|
|
8
|
+
* `member.pieces`, the cheapest member read:
|
|
9
|
+
*
|
|
10
|
+
* - `member` — any answer but a 404: a 2xx, any other refusal, a preflight
|
|
11
|
+
* refusal (status 0) or a throw. A refusal of another kind belongs to the
|
|
12
|
+
* write, which reports it honestly. A `503 tenant_unresolved` is
|
|
13
|
+
* `member` too: the writes answer the same 503, shown as unavailable.
|
|
14
|
+
* - on a 404, the sign-in seam (`signInMethod()`, D7) says who holds the
|
|
15
|
+
* account. A `site` sign-in gives `session`: this site's own sign-in
|
|
16
|
+
* holds it, and the 404 is a build with no tenancy module — every member
|
|
17
|
+
* read and write registers in one module block — so the name saves
|
|
18
|
+
* through Better Auth as before and deletion points at Support. A `hub`
|
|
19
|
+
* sign-in gives `none`: the hub holds the account and answers no member
|
|
20
|
+
* route for this spoke, and the spoke has no `/api/auth` of its own, so
|
|
21
|
+
* no write can reach the account from here.
|
|
22
|
+
*
|
|
23
|
+
* A 404 is a module-presence test, as `read()` in `lib/account-config.ts`
|
|
24
|
+
* already makes; this file names no topology constant itself.
|
|
25
|
+
*
|
|
26
|
+
* `image` — the session's photo, which Better Auth's session carries at run
|
|
27
|
+
* time though the generated `SiteSession` type does not declare it. A hub
|
|
28
|
+
* session carries none (C5), so on a spoke the form starts empty.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { member } from "@working-theory/api-client";
|
|
32
|
+
import type { AccountContext } from "@working-theory/templates/account";
|
|
33
|
+
import { headers } from "next/headers";
|
|
34
|
+
import { cache } from "react";
|
|
35
|
+
|
|
36
|
+
import { readSession } from "../../../../../lib/topology.generated";
|
|
37
|
+
import { signInMethod } from "../sign-in-method";
|
|
38
|
+
|
|
39
|
+
export interface ProfileSource {
|
|
40
|
+
via: "member" | "session" | "none";
|
|
41
|
+
image: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `user.image` as a trimmed non-blank string, else `null`. */
|
|
45
|
+
function sessionImage(session: unknown): string | null {
|
|
46
|
+
if (typeof session !== "object" || session === null) return null;
|
|
47
|
+
const user = (session as { user?: unknown }).user;
|
|
48
|
+
if (typeof user !== "object" || user === null) return null;
|
|
49
|
+
const image = (user as { image?: unknown }).image;
|
|
50
|
+
return typeof image === "string" && image.trim() !== "" ? image.trim() : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function memberApiAnswers(ctx: AccountContext): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
const res = await member.pieces(ctx.init);
|
|
56
|
+
return res.ok || res.status !== 404;
|
|
57
|
+
} catch {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const loadProfileSource = cache(async (ctx: AccountContext): Promise<ProfileSource> => {
|
|
63
|
+
// The photo is optional: a failed session read starts the field empty
|
|
64
|
+
// rather than failing the page.
|
|
65
|
+
const [answers, session] = await Promise.all([
|
|
66
|
+
memberApiAnswers(ctx),
|
|
67
|
+
headers()
|
|
68
|
+
.then((h) => readSession(h))
|
|
69
|
+
.catch(() => null),
|
|
70
|
+
]);
|
|
71
|
+
const via = answers ? "member" : signInMethod().kind === "site" ? "session" : "none";
|
|
72
|
+
return { via, image: sessionImage(session) };
|
|
73
|
+
});
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* `forwardedCookieInit()` and streams the bytes back. The api-client's
|
|
13
13
|
* `member.messageFile` is the one external boundary mocked; the seal, the
|
|
14
14
|
* cookie read and the route's decisions are real. Covers:
|
|
15
|
-
* - a
|
|
15
|
+
* - on a single site (`SIGN_IN_PROVIDER` `"site"`) the read carries the
|
|
16
|
+
* caller's session cookie to the site's own API, and serves the same
|
|
17
|
+
* way (#8231 D9) — the route itself tests no topology;
|
|
16
18
|
* - the read carries the signed-in caller's own credential, and the bytes
|
|
17
19
|
* come back with all five hub headers unchanged;
|
|
18
20
|
* - a hub answer missing any of the five is refused 502 — never bytes on
|
|
@@ -36,10 +38,11 @@ jest.mock("../../../../../../../../../lib/topology.generated", () => ({
|
|
|
36
38
|
}));
|
|
37
39
|
|
|
38
40
|
const mockHeaders = jest.fn();
|
|
41
|
+
const mockCookies = jest.fn();
|
|
39
42
|
jest.mock("next/headers", () => ({
|
|
40
43
|
__esModule: true,
|
|
41
44
|
headers: (...args: unknown[]) => mockHeaders(...args),
|
|
42
|
-
cookies:
|
|
45
|
+
cookies: (...args: unknown[]) => mockCookies(...args),
|
|
43
46
|
}));
|
|
44
47
|
|
|
45
48
|
import { seal } from "../../../../../../../../../lib/hub-session/seal";
|
|
@@ -102,6 +105,7 @@ beforeEach(() => {
|
|
|
102
105
|
};
|
|
103
106
|
mockProvider.value = "hub";
|
|
104
107
|
mockHeaders.mockReset().mockResolvedValue(new Headers());
|
|
108
|
+
mockCookies.mockReset().mockResolvedValue({ toString: () => "" });
|
|
105
109
|
mockMessageFile.mockReset().mockResolvedValue(fileResult());
|
|
106
110
|
});
|
|
107
111
|
|
|
@@ -194,12 +198,18 @@ describe("the member file read on a spoke that signs in through its hub", () =>
|
|
|
194
198
|
});
|
|
195
199
|
});
|
|
196
200
|
|
|
197
|
-
describe("on a site
|
|
198
|
-
it
|
|
199
|
-
mockProvider.value =
|
|
200
|
-
|
|
201
|
+
describe("on a single site — its own sign-in (#8231 D9)", () => {
|
|
202
|
+
it("reads the file with the caller's session cookie and serves it with all five headers unchanged", async () => {
|
|
203
|
+
mockProvider.value = "site";
|
|
204
|
+
mockCookies.mockResolvedValue({ toString: () => "session=abc" });
|
|
201
205
|
const res = await get();
|
|
202
|
-
expect(
|
|
203
|
-
expect(
|
|
206
|
+
expect(mockMessageFile).toHaveBeenCalledWith(ID, { headers: { cookie: "session=abc" } });
|
|
207
|
+
expect(res.status).toBe(200);
|
|
208
|
+
expect(res.headers.get("content-type")).toBe(FILE_HEADERS.contentType);
|
|
209
|
+
expect(res.headers.get("content-disposition")).toBe(FILE_HEADERS.contentDisposition);
|
|
210
|
+
expect(res.headers.get("x-content-type-options")).toBe(FILE_HEADERS.contentTypeOptions);
|
|
211
|
+
expect(res.headers.get("content-security-policy")).toBe(FILE_HEADERS.contentSecurityPolicy);
|
|
212
|
+
expect(res.headers.get("cache-control")).toBe(FILE_HEADERS.cacheControl);
|
|
213
|
+
await expect(res.text()).resolves.toBe("%PDF-1.7 bytes");
|
|
204
214
|
});
|
|
205
215
|
});
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A message's file action (the `message` Inbox kind, `lib/account-config.ts`)
|
|
5
5
|
* links here. The browser holds neither the client's hub credential (sealed in
|
|
6
|
-
* the `hub-session` cookie) nor this site's spoke key, so this route reads
|
|
7
|
-
*
|
|
6
|
+
* the `hub-session` cookie) nor this site's spoke key, so this route reads
|
|
7
|
+
* `GET /v1/member/messages/:id/file` with `forwardedCookieInit()` — the
|
|
8
8
|
* caller's own credential, so a client only ever gets their own file — and
|
|
9
9
|
* streams the bytes back unread.
|
|
10
10
|
*
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* A hub answer missing any of the five is refused 502 rather than served
|
|
17
17
|
* without it.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* On a single site the read carries the caller's session cookie; on a spoke
|
|
20
|
+
* that signs in through its hub, the client's hub credential (#8231 D9).
|
|
21
21
|
* A refusal has no body: no credential (signed out) is a 401, a hub 4xx keeps
|
|
22
22
|
* its status, and a hub 5xx, a transport the api-client refuses or a hub call
|
|
23
23
|
* that throws is a 502.
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
import { member, type MemberFileHeaders } from "@working-theory/api-client";
|
|
26
26
|
|
|
27
27
|
import { forwardedCookieInit } from "../../../../../../../../lib/server-api";
|
|
28
|
-
import { SIGN_IN_PROVIDER } from "../../../../../../../../lib/topology.generated";
|
|
29
28
|
|
|
30
29
|
interface Context {
|
|
31
30
|
params: Promise<{ locale: string; id: string }>;
|
|
@@ -55,7 +54,6 @@ function refusalStatus(status: number, body: unknown): number {
|
|
|
55
54
|
|
|
56
55
|
// platform-decisions-waiver: api-versioning — a page-scoped download link on the site's own account area, not an API contract; it re-serves the versioned hub route /v1/member/messages/:id/file
|
|
57
56
|
export async function GET(_request: Request, { params }: Context): Promise<Response> {
|
|
58
|
-
if (SIGN_IN_PROVIDER !== "hub") return refusal(404);
|
|
59
57
|
const { id } = await params;
|
|
60
58
|
|
|
61
59
|
// The api-client throws, rather than answering `{ ok: false }`, on a
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* The account layout — the base account area's frame (#7430, spec 7423 U4 ·
|
|
3
3
|
* D4, D13; DESIGN.md §2b).
|
|
4
4
|
*
|
|
5
|
-
* RSC. Every `/account` route
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* RSC. Every `/account` route but the four Settings tabs renders inside
|
|
6
|
+
* `SignedInShell` (through the client `AccountFrame`; the tabs draw their own
|
|
7
|
+
* standalone settings frame, #8430): the site's logo as the rail's top tile, linking
|
|
8
|
+
* home; the rail's rows — Inbox, a Library group holding Artifacts, then
|
|
9
|
+
* Settings (#8231 D1); a footer of the avatar and ? alone;
|
|
8
10
|
* the card avatar menu (name, then Settings and Log out); the ? help menu
|
|
9
11
|
* (Contact support, and Help center when `AILK_HELP_CENTER_URL` is set);
|
|
10
12
|
* and below `md` a tab bar reading avatar · Inbox · For you · Help (#7469).
|
|
@@ -15,6 +17,10 @@
|
|
|
15
17
|
* area's own "Add your name" item (`loadForYou`, deduped per request with
|
|
16
18
|
* the Inbox's phone For you tab); with no items there is no aside.
|
|
17
19
|
*
|
|
20
|
+
* #8465 — `SIGN_OUT_ENDPOINT` (set only on a spoke that signs in through its
|
|
21
|
+
* hub) and `SIGN_IN_PATH` ride down as props; the client frame cannot import
|
|
22
|
+
* the generated seam, and the signed-in Nav reads it the same way.
|
|
23
|
+
*
|
|
18
24
|
* The `(authed)` layout above owns the signed-out gate, so a missing session
|
|
19
25
|
* here is a race (it expired between the two layouts): the page renders
|
|
20
26
|
* unframed and degrades on its own.
|
|
@@ -25,11 +31,22 @@ import { getTranslations } from "next-intl/server";
|
|
|
25
31
|
import type { ReactElement, ReactNode } from "react";
|
|
26
32
|
|
|
27
33
|
import { siteLogo } from "../../../../lib/site-logo";
|
|
34
|
+
import { SIGN_IN_PATH, SIGN_OUT_ENDPOINT } from "../../../../lib/topology.generated";
|
|
28
35
|
|
|
29
36
|
import { accountSession, loadForYou } from "./account-data";
|
|
30
37
|
import { AccountFrame } from "./account-frame";
|
|
31
38
|
import { forYouLabels } from "./for-you-labels";
|
|
32
39
|
|
|
40
|
+
/** The value when it parses as an https URL, else undefined: operator config is rendered as an href. */
|
|
41
|
+
function httpsUrl(value: string | undefined): string | undefined {
|
|
42
|
+
if (!value) return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return new URL(value).protocol === "https:" ? value : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
interface Props {
|
|
34
51
|
children: ReactNode;
|
|
35
52
|
params: Promise<{ locale: string }>;
|
|
@@ -46,7 +63,7 @@ export default async function AccountLayout({
|
|
|
46
63
|
const t = await getTranslations({ locale, namespace: "account" });
|
|
47
64
|
const forYou = await loadForYou(locale);
|
|
48
65
|
const { name, email } = session.ctx.user;
|
|
49
|
-
const helpCenterUrl = process.env.AILK_HELP_CENTER_URL;
|
|
66
|
+
const helpCenterUrl = httpsUrl(process.env.AILK_HELP_CENTER_URL);
|
|
50
67
|
|
|
51
68
|
return (
|
|
52
69
|
<AccountFrame
|
|
@@ -64,11 +81,20 @@ export default async function AccountLayout({
|
|
|
64
81
|
: []),
|
|
65
82
|
]}
|
|
66
83
|
forYou={{ items: forYou.items, now: forYou.now, labels: forYouLabels(session.ctx.t) }}
|
|
84
|
+
{...(SIGN_OUT_ENDPOINT
|
|
85
|
+
? {
|
|
86
|
+
signOutEndpoint: SIGN_OUT_ENDPOINT,
|
|
87
|
+
signInHref: localeHref(locale, SIGN_IN_PATH ?? "/"),
|
|
88
|
+
}
|
|
89
|
+
: {})}
|
|
67
90
|
labels={{
|
|
68
91
|
home: t("frame.home"),
|
|
69
92
|
inbox: t("frame.inbox"),
|
|
70
93
|
forYou: t("frame.forYou"),
|
|
71
94
|
settings: t("frame.settings"),
|
|
95
|
+
library: t("frame.library"),
|
|
96
|
+
artifacts: t("frame.artifacts"),
|
|
97
|
+
signOutFailed: t("frame.signOutFailed"),
|
|
72
98
|
signOut: t("frame.signOut"),
|
|
73
99
|
help: t("frame.help"),
|
|
74
100
|
accountMenu: t("frame.accountMenu"),
|