@cosmicdrift/kumiko-bundled-features 0.1.0
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 +90 -0
- package/src/audit/__tests__/audit.integration.ts +328 -0
- package/src/audit/constants.ts +7 -0
- package/src/audit/feature.ts +23 -0
- package/src/audit/handlers/list.query.ts +98 -0
- package/src/audit/index.ts +2 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
- package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
- package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
- package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
- package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
- package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
- package/src/auth-email-password/auth-user-row.ts +41 -0
- package/src/auth-email-password/constants.ts +101 -0
- package/src/auth-email-password/email-templates.ts +283 -0
- package/src/auth-email-password/feature.ts +140 -0
- package/src/auth-email-password/handlers/change-password.write.ts +58 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
- package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
- package/src/auth-email-password/handlers/login.write.ts +208 -0
- package/src/auth-email-password/handlers/logout.write.ts +12 -0
- package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
- package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
- package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
- package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
- package/src/auth-email-password/i18n.ts +211 -0
- package/src/auth-email-password/identity-v3-hash.ts +97 -0
- package/src/auth-email-password/index.ts +35 -0
- package/src/auth-email-password/invite-token-store.ts +92 -0
- package/src/auth-email-password/lockout-store.ts +118 -0
- package/src/auth-email-password/password-hashing.ts +43 -0
- package/src/auth-email-password/reset-token.ts +28 -0
- package/src/auth-email-password/seeding.ts +183 -0
- package/src/auth-email-password/signed-token.ts +85 -0
- package/src/auth-email-password/signup-token-store.ts +104 -0
- package/src/auth-email-password/stream-tenant.ts +31 -0
- package/src/auth-email-password/testing.ts +5 -0
- package/src/auth-email-password/token-burn-store.ts +57 -0
- package/src/auth-email-password/verification-token.ts +27 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
- package/src/auth-email-password/web/auth-client.ts +350 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
- package/src/auth-email-password/web/auth-gate.tsx +33 -0
- package/src/auth-email-password/web/client-plugin.ts +48 -0
- package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
- package/src/auth-email-password/web/index.ts +56 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
- package/src/auth-email-password/web/login-screen.tsx +150 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
- package/src/auth-email-password/web/session.tsx +171 -0
- package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
- package/src/auth-email-password/web/signup-screen.tsx +130 -0
- package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
- package/src/auth-email-password/web/use-shell-user.ts +34 -0
- package/src/auth-email-password/web/user-menu.tsx +89 -0
- package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
- package/src/billing-foundation/__tests__/feature.test.ts +110 -0
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
- package/src/billing-foundation/aggregate-id.ts +21 -0
- package/src/billing-foundation/constants.ts +70 -0
- package/src/billing-foundation/entities.ts +50 -0
- package/src/billing-foundation/events.ts +71 -0
- package/src/billing-foundation/feature.ts +122 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
- package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
- package/src/billing-foundation/handlers/process-event.write.ts +160 -0
- package/src/billing-foundation/index.ts +42 -0
- package/src/billing-foundation/projection.ts +135 -0
- package/src/billing-foundation/types.ts +157 -0
- package/src/billing-foundation/webhook-handler.ts +184 -0
- package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
- package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
- package/src/cap-counter/aggregate-id.ts +61 -0
- package/src/cap-counter/constants.ts +32 -0
- package/src/cap-counter/enforce-cap.ts +404 -0
- package/src/cap-counter/entity.ts +48 -0
- package/src/cap-counter/feature.ts +90 -0
- package/src/cap-counter/handlers/get-counter.query.ts +43 -0
- package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
- package/src/cap-counter/handlers/increment.write.ts +92 -0
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
- package/src/cap-counter/index.ts +34 -0
- package/src/cap-counter/with-cap-enforcement.ts +179 -0
- package/src/channel-email/email-channel.ts +48 -0
- package/src/channel-email/feature.ts +15 -0
- package/src/channel-email/index.ts +4 -0
- package/src/channel-email/smtp-transport.ts +65 -0
- package/src/channel-email/types.ts +34 -0
- package/src/channel-in-app/constants.ts +11 -0
- package/src/channel-in-app/feature.ts +30 -0
- package/src/channel-in-app/handlers/inbox.query.ts +28 -0
- package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
- package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
- package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
- package/src/channel-in-app/in-app-channel.ts +44 -0
- package/src/channel-in-app/index.ts +4 -0
- package/src/channel-in-app/tables.ts +22 -0
- package/src/channel-push/feature.ts +15 -0
- package/src/channel-push/index.ts +3 -0
- package/src/channel-push/push-channel.ts +33 -0
- package/src/channel-push/types.ts +22 -0
- package/src/config/__tests__/app-overrides.test.ts +118 -0
- package/src/config/__tests__/config.integration.ts +1246 -0
- package/src/config/constants.ts +23 -0
- package/src/config/feature.ts +117 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
- package/src/config/handlers/reset.write.ts +45 -0
- package/src/config/handlers/schema.query.ts +22 -0
- package/src/config/handlers/set.write.ts +93 -0
- package/src/config/handlers/values.query.ts +43 -0
- package/src/config/index.ts +15 -0
- package/src/config/resolver.ts +283 -0
- package/src/config/table.ts +35 -0
- package/src/config/write-helpers.ts +268 -0
- package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
- package/src/delivery/__tests__/delivery.integration.ts +1405 -0
- package/src/delivery/constants.ts +33 -0
- package/src/delivery/delivery-service.ts +489 -0
- package/src/delivery/events.ts +18 -0
- package/src/delivery/feature.ts +70 -0
- package/src/delivery/handlers/log.query.ts +21 -0
- package/src/delivery/handlers/preferences.query.ts +18 -0
- package/src/delivery/handlers/set-preference.write.ts +28 -0
- package/src/delivery/index.ts +35 -0
- package/src/delivery/tables.ts +74 -0
- package/src/delivery/testing.ts +47 -0
- package/src/delivery/types.ts +71 -0
- package/src/delivery/unsubscribe.ts +99 -0
- package/src/delivery/upsert-preference.ts +145 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
- package/src/feature-toggles/constants.ts +20 -0
- package/src/feature-toggles/events.ts +18 -0
- package/src/feature-toggles/feature.ts +98 -0
- package/src/feature-toggles/global-feature-state-table.ts +28 -0
- package/src/feature-toggles/handlers/list.query.ts +26 -0
- package/src/feature-toggles/handlers/registered.query.ts +56 -0
- package/src/feature-toggles/handlers/set.write.ts +158 -0
- package/src/feature-toggles/index.ts +9 -0
- package/src/feature-toggles/toggle-runtime.ts +73 -0
- package/src/file-foundation/__tests__/feature.test.ts +35 -0
- package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
- package/src/file-foundation/feature.ts +123 -0
- package/src/file-foundation/index.ts +7 -0
- package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
- package/src/file-provider-inmemory/feature.ts +73 -0
- package/src/file-provider-inmemory/index.ts +3 -0
- package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
- package/src/file-provider-s3/feature.ts +169 -0
- package/src/file-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
- package/src/files-provider-s3/env-helper.ts +49 -0
- package/src/files-provider-s3/index.ts +3 -0
- package/src/files-provider-s3/s3-provider.ts +114 -0
- package/src/foundation-shared/config-helpers.ts +67 -0
- package/src/foundation-shared/index.ts +4 -0
- package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
- package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
- package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
- package/src/jobs/constants.ts +21 -0
- package/src/jobs/events.ts +39 -0
- package/src/jobs/feature.ts +150 -0
- package/src/jobs/handlers/detail.query.ts +30 -0
- package/src/jobs/handlers/list.query.ts +36 -0
- package/src/jobs/handlers/retry.write.ts +69 -0
- package/src/jobs/handlers/trigger.write.ts +39 -0
- package/src/jobs/index.ts +5 -0
- package/src/jobs/job-run-logger.ts +213 -0
- package/src/jobs/job-run-table.ts +55 -0
- package/src/legal-pages/README.md +195 -0
- package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
- package/src/legal-pages/constants.ts +36 -0
- package/src/legal-pages/feature.ts +187 -0
- package/src/legal-pages/index.ts +13 -0
- package/src/legal-pages/markdown.ts +69 -0
- package/src/mail-foundation/__tests__/feature.test.ts +46 -0
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
- package/src/mail-foundation/feature.ts +160 -0
- package/src/mail-foundation/index.ts +14 -0
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
- package/src/mail-transport-inmemory/feature.ts +90 -0
- package/src/mail-transport-inmemory/index.ts +3 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
- package/src/mail-transport-smtp/feature.ts +182 -0
- package/src/mail-transport-smtp/index.ts +3 -0
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
- package/src/rate-limiting/constants.ts +9 -0
- package/src/rate-limiting/feature.ts +16 -0
- package/src/rate-limiting/handlers/status.query.ts +52 -0
- package/src/rate-limiting/index.ts +2 -0
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
- package/src/renderer-simple/feature.ts +12 -0
- package/src/renderer-simple/index.ts +2 -0
- package/src/renderer-simple/simple-renderer.ts +72 -0
- package/src/secrets/__tests__/rotate.integration.ts +176 -0
- package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
- package/src/secrets/__tests__/secrets.integration.ts +118 -0
- package/src/secrets/feature.ts +84 -0
- package/src/secrets/handlers/delete.write.ts +20 -0
- package/src/secrets/handlers/list.query.ts +38 -0
- package/src/secrets/handlers/rotate.job.ts +193 -0
- package/src/secrets/handlers/set.write.ts +50 -0
- package/src/secrets/index.ts +16 -0
- package/src/secrets/secrets-context.ts +296 -0
- package/src/secrets/table.ts +68 -0
- package/src/sessions/__tests__/cleanup.integration.ts +175 -0
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
- package/src/sessions/__tests__/sessions.integration.ts +472 -0
- package/src/sessions/__tests__/test-helpers.ts +66 -0
- package/src/sessions/constants.ts +43 -0
- package/src/sessions/feature.ts +84 -0
- package/src/sessions/handlers/cleanup.job.ts +109 -0
- package/src/sessions/handlers/list.query.ts +35 -0
- package/src/sessions/handlers/mine.query.ts +37 -0
- package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
- package/src/sessions/handlers/revoke.write.ts +76 -0
- package/src/sessions/index.ts +17 -0
- package/src/sessions/schema/index.ts +5 -0
- package/src/sessions/schema/user-session.ts +67 -0
- package/src/sessions/session-callbacks.ts +110 -0
- package/src/sessions/testing.ts +42 -0
- package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
- package/src/subscription-mollie/constants.ts +33 -0
- package/src/subscription-mollie/feature.ts +144 -0
- package/src/subscription-mollie/index.ts +13 -0
- package/src/subscription-mollie/plugin-methods.ts +79 -0
- package/src/subscription-mollie/verify-webhook.ts +244 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
- package/src/subscription-stripe/constants.ts +20 -0
- package/src/subscription-stripe/feature.ts +120 -0
- package/src/subscription-stripe/index.ts +14 -0
- package/src/subscription-stripe/plugin-methods.ts +91 -0
- package/src/subscription-stripe/verify-webhook.ts +235 -0
- package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
- package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
- package/src/tenant/__tests__/tenant.integration.ts +347 -0
- package/src/tenant/command-schemas.ts +37 -0
- package/src/tenant/constants.ts +37 -0
- package/src/tenant/feature.ts +109 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
- package/src/tenant/handlers/add-member.write.ts +53 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
- package/src/tenant/handlers/create.write.ts +21 -0
- package/src/tenant/handlers/disable.write.ts +18 -0
- package/src/tenant/handlers/invitations.query.ts +31 -0
- package/src/tenant/handlers/list.query.ts +17 -0
- package/src/tenant/handlers/me.query.ts +17 -0
- package/src/tenant/handlers/members.query.ts +22 -0
- package/src/tenant/handlers/memberships.query.ts +24 -0
- package/src/tenant/handlers/remove-member.write.ts +40 -0
- package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
- package/src/tenant/handlers/update-member-roles.write.ts +54 -0
- package/src/tenant/handlers/update.write.ts +20 -0
- package/src/tenant/index.ts +12 -0
- package/src/tenant/invitation-table.ts +93 -0
- package/src/tenant/membership-table.ts +35 -0
- package/src/tenant/schema/index.ts +5 -0
- package/src/tenant/schema/tenant.ts +27 -0
- package/src/tenant/seeding.ts +155 -0
- package/src/tenant/testing.ts +8 -0
- package/src/text-content/README.md +190 -0
- package/src/text-content/__tests__/text-content.integration.ts +415 -0
- package/src/text-content/api.ts +92 -0
- package/src/text-content/constants.ts +19 -0
- package/src/text-content/feature.ts +29 -0
- package/src/text-content/handlers/by-slug.query.ts +55 -0
- package/src/text-content/handlers/set.write.ts +118 -0
- package/src/text-content/index.ts +14 -0
- package/src/text-content/seeding.ts +91 -0
- package/src/text-content/table.ts +45 -0
- package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
- package/src/tier-engine/__tests__/drift.test.ts +42 -0
- package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
- package/src/tier-engine/aggregate-id.ts +27 -0
- package/src/tier-engine/compose-app.ts +150 -0
- package/src/tier-engine/constants.ts +15 -0
- package/src/tier-engine/entity.ts +30 -0
- package/src/tier-engine/feature.ts +72 -0
- package/src/tier-engine/handlers/active-tier.query.ts +23 -0
- package/src/tier-engine/index.ts +22 -0
- package/src/user/__tests__/seed-testing.integration.ts +127 -0
- package/src/user/__tests__/user.integration.ts +198 -0
- package/src/user/command-schemas.ts +15 -0
- package/src/user/constants.ts +23 -0
- package/src/user/feature.ts +32 -0
- package/src/user/handlers/create.write.ts +54 -0
- package/src/user/handlers/detail.query.ts +9 -0
- package/src/user/handlers/find-for-auth.query.ts +38 -0
- package/src/user/handlers/list.query.ts +8 -0
- package/src/user/handlers/me.query.ts +15 -0
- package/src/user/handlers/update.write.ts +54 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/index.ts +5 -0
- package/src/user/schema/user.ts +69 -0
- package/src/user/seeding.ts +93 -0
- package/src/user/testing.ts +5 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Browser-Seite der Auth-Routes. Dünne fetch-Wrapper um /api/auth/*
|
|
3
|
+
// mit Cookie-Transport: JWT lebt im HttpOnly kumiko_auth-Cookie,
|
|
4
|
+
// Double-Submit-CSRF-Token im JS-lesbaren kumiko_csrf-Cookie. Alle
|
|
5
|
+
// state-changing Requests echo'n den CSRF-Token via X-CSRF-Token —
|
|
6
|
+
// der Server rejected sonst mit csrf_token_missing.
|
|
7
|
+
//
|
|
8
|
+
// Die dispatcher-live nutzt denselben readCsrfToken-Helper; wir
|
|
9
|
+
// reuse'n ihn hier, damit die Konstanten (Cookie-Name, Header-Name)
|
|
10
|
+
// nicht divergieren.
|
|
11
|
+
|
|
12
|
+
import { CSRF_HEADER_NAME, readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
13
|
+
|
|
14
|
+
export type TenantSummary = {
|
|
15
|
+
readonly tenantId: string;
|
|
16
|
+
readonly roles: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type LoginRequest = {
|
|
20
|
+
readonly email: string;
|
|
21
|
+
readonly password: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LoginResponse = {
|
|
25
|
+
readonly token: string;
|
|
26
|
+
readonly user: {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly tenantId: string;
|
|
29
|
+
readonly roles: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LoginFailure = {
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly message?: string;
|
|
36
|
+
readonly retryAfterSeconds?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function csrfHeader(): Record<string, string> {
|
|
40
|
+
const token = readCsrfToken();
|
|
41
|
+
return token !== undefined ? { [CSRF_HEADER_NAME]: token } : {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// POST /api/auth/login. Erfolg → token + user; Fehler → strukturiertes
|
|
45
|
+
// failure-objekt mit reason (invalid_credentials, account_locked,
|
|
46
|
+
// no_membership, rate_limited). Das UI rendert darüber eine passende
|
|
47
|
+
// Fehler-Meldung; der Server setzt Cookies bei 200 automatisch.
|
|
48
|
+
export async function login(
|
|
49
|
+
req: LoginRequest,
|
|
50
|
+
): Promise<{ ok: true; data: LoginResponse } | { ok: false; error: LoginFailure }> {
|
|
51
|
+
const res = await fetch("/api/auth/login", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
credentials: "same-origin",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify(req),
|
|
56
|
+
});
|
|
57
|
+
if (res.status === 429) {
|
|
58
|
+
return { ok: false, error: { reason: "rate_limited" } };
|
|
59
|
+
}
|
|
60
|
+
// @cast-boundary engine-payload — HTTP-API contract, server-side schema-validated
|
|
61
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
62
|
+
isSuccess?: boolean;
|
|
63
|
+
token?: string;
|
|
64
|
+
user?: LoginResponse["user"];
|
|
65
|
+
error?:
|
|
66
|
+
| {
|
|
67
|
+
code?: string;
|
|
68
|
+
message?: string;
|
|
69
|
+
details?: { reason?: string; retryAfterSeconds?: number };
|
|
70
|
+
}
|
|
71
|
+
| string;
|
|
72
|
+
};
|
|
73
|
+
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
74
|
+
return { ok: true, data: { token: body.token, user: body.user } };
|
|
75
|
+
}
|
|
76
|
+
// Der Server schickt error entweder als string ("invalid_body") oder als
|
|
77
|
+
// strukturiertes Objekt. Wir ziehen uns den sprechendsten Reason raus.
|
|
78
|
+
const err = body.error;
|
|
79
|
+
if (typeof err === "string") {
|
|
80
|
+
return { ok: false, error: { reason: err } };
|
|
81
|
+
}
|
|
82
|
+
const reason = err?.details?.reason ?? err?.code ?? "login_failed";
|
|
83
|
+
const retry = err?.details?.retryAfterSeconds;
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: {
|
|
87
|
+
reason,
|
|
88
|
+
...(err?.message !== undefined && { message: err.message }),
|
|
89
|
+
...(retry !== undefined && { retryAfterSeconds: retry }),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// POST /api/auth/logout. Server revoked die Session (wenn sessionRevoker
|
|
95
|
+
// gewired ist) und clear't die Cookies. Wir triggern hinterher einen
|
|
96
|
+
// Navigation-Refresh, damit alle caches (React-State, query-cache) auf
|
|
97
|
+
// Null gehen — billigster Weg zu sauberer Ausgangslage.
|
|
98
|
+
export async function logout(): Promise<void> {
|
|
99
|
+
await fetch("/api/auth/logout", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
credentials: "same-origin",
|
|
102
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Gemeinsamer Failure-Type für die vier Token-Flow-Endpoints (request-
|
|
107
|
+
// password-reset, reset-password, request-email-verification, verify-
|
|
108
|
+
// email). Server collapses alle Token-Verify-Fehler (malformed / bad-
|
|
109
|
+
// signature / expired) auf einen einzigen Code pro Flow (anti-
|
|
110
|
+
// enumeration); UI mappt reason → i18n-Key. Plus rate-limit (429) wird
|
|
111
|
+
// als reason "rate_limited" + retryAfterSeconds durchgereicht — gleiche
|
|
112
|
+
// Shape wie LoginFailure damit Apps die Errors uniform mappen können.
|
|
113
|
+
export type AuthTokenFailure = {
|
|
114
|
+
readonly reason: string;
|
|
115
|
+
readonly retryAfterSeconds?: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Backward-compat-Aliase für die alten Type-Namen — damit Code, der
|
|
119
|
+
// `ResetPasswordFailure` / `VerifyEmailFailure` importiert hat, ohne
|
|
120
|
+
// Änderung weiterläuft. Für neuen Code direkt `AuthTokenFailure` nutzen.
|
|
121
|
+
export type ResetPasswordFailure = AuthTokenFailure;
|
|
122
|
+
export type VerifyEmailFailure = AuthTokenFailure;
|
|
123
|
+
|
|
124
|
+
// 4xx/5xx → typed AuthTokenFailure parsen. 429 (Rate-Limit) hat einen
|
|
125
|
+
// dedizierten reason damit das UI einen Retry-Hinweis zeigen kann.
|
|
126
|
+
async function parseTokenFailure(res: Response): Promise<AuthTokenFailure> {
|
|
127
|
+
if (res.status === 429) {
|
|
128
|
+
// @cast-boundary engine-payload — server schickt details.retryAfterSeconds bei 429
|
|
129
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
130
|
+
error?: { details?: { retryAfterSeconds?: number } };
|
|
131
|
+
};
|
|
132
|
+
const retry = body.error?.details?.retryAfterSeconds;
|
|
133
|
+
return { reason: "rate_limited", ...(retry !== undefined && { retryAfterSeconds: retry }) };
|
|
134
|
+
}
|
|
135
|
+
// @cast-boundary engine-payload — server-side schema-validated body
|
|
136
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
137
|
+
error?: { code?: string; details?: { reason?: string } };
|
|
138
|
+
};
|
|
139
|
+
const reason = body.error?.details?.reason ?? body.error?.code ?? "unknown";
|
|
140
|
+
return { reason };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// POST /api/auth/request-password-reset. 200 silent-success: auch wenn
|
|
144
|
+
// die Email nicht existiert, sieht der caller `{ ok: true }` — kein
|
|
145
|
+
// account-enumeration. Server triggert Mail nur intern wenn user
|
|
146
|
+
// gefunden. 429 → typed rate-limit-Failure. 5xx → unknown-error.
|
|
147
|
+
export async function requestPasswordReset(
|
|
148
|
+
email: string,
|
|
149
|
+
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
150
|
+
const res = await fetch("/api/auth/request-password-reset", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
credentials: "same-origin",
|
|
153
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
154
|
+
body: JSON.stringify({ email }),
|
|
155
|
+
});
|
|
156
|
+
if (res.ok) return { ok: true };
|
|
157
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// POST /api/auth/reset-password. Token aus URL + neues Passwort. Auf
|
|
161
|
+
// 422 collapses der Server alle Token-Verify-Fehler (malformed / bad-
|
|
162
|
+
// signature / expired) auf den einzigen Code `invalid_reset_token` —
|
|
163
|
+
// anti-enumeration. Plus zod-validation-failures (newPassword < 8) als
|
|
164
|
+
// eigene 4xx mit code "validation_failed". UI mappt reason → i18n-Key.
|
|
165
|
+
export async function resetPassword(
|
|
166
|
+
token: string,
|
|
167
|
+
newPassword: string,
|
|
168
|
+
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
169
|
+
const res = await fetch("/api/auth/reset-password", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
credentials: "same-origin",
|
|
172
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
173
|
+
body: JSON.stringify({ token, newPassword }),
|
|
174
|
+
});
|
|
175
|
+
if (res.ok) return { ok: true };
|
|
176
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// POST /api/auth/request-email-verification. Same silent-success
|
|
180
|
+
// semantik wie request-password-reset. 429 → rate-limit-Failure.
|
|
181
|
+
export async function requestEmailVerification(
|
|
182
|
+
email: string,
|
|
183
|
+
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
184
|
+
const res = await fetch("/api/auth/request-email-verification", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
credentials: "same-origin",
|
|
187
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
188
|
+
body: JSON.stringify({ email }),
|
|
189
|
+
});
|
|
190
|
+
if (res.ok) return { ok: true };
|
|
191
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// POST /api/auth/verify-email. Auto-submitted vom VerifyEmailScreen
|
|
195
|
+
// nach `?token=...`-parse. Server collapses alle Verify-Failures auf
|
|
196
|
+
// `invalid_verification_token` (anti-enumeration, parallel zu reset).
|
|
197
|
+
export async function verifyEmail(
|
|
198
|
+
token: string,
|
|
199
|
+
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
200
|
+
const res = await fetch("/api/auth/verify-email", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
credentials: "same-origin",
|
|
203
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
204
|
+
body: JSON.stringify({ token }),
|
|
205
|
+
});
|
|
206
|
+
if (res.ok) return { ok: true };
|
|
207
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// POST /api/auth/signup-request. Always-200 (anti-enumeration; wir
|
|
211
|
+
// sagen nicht ob die Email schon registriert ist). Server schickt
|
|
212
|
+
// Activation-Mail an die Adresse — beim Klick auf den Link landet der
|
|
213
|
+
// User auf /signup/complete?token=… wo er sein Password setzt.
|
|
214
|
+
export async function requestSignup(
|
|
215
|
+
email: string,
|
|
216
|
+
): Promise<{ ok: true } | { ok: false; error: AuthTokenFailure }> {
|
|
217
|
+
const res = await fetch("/api/auth/signup-request", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
credentials: "same-origin",
|
|
220
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
221
|
+
body: JSON.stringify({ email }),
|
|
222
|
+
});
|
|
223
|
+
if (res.ok) return { ok: true };
|
|
224
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// POST /api/auth/signup-confirm. Token aus URL + Password. Erfolgreich:
|
|
228
|
+
// Cookies (kumiko_auth + kumiko_csrf) werden gesetzt — User ist sofort
|
|
229
|
+
// eingeloggt. Response liefert tenantKey für den Post-Signup-Redirect.
|
|
230
|
+
// 422 invalid_signup_token bei abgelaufenem/unbekanntem Token.
|
|
231
|
+
export type SignupConfirmSuccess = {
|
|
232
|
+
readonly user: { readonly id: string; readonly tenantId: string; readonly roles: string[] };
|
|
233
|
+
readonly tenantKey: string;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export async function confirmSignup(
|
|
237
|
+
token: string,
|
|
238
|
+
password: string,
|
|
239
|
+
): Promise<{ ok: true; data: SignupConfirmSuccess } | { ok: false; error: AuthTokenFailure }> {
|
|
240
|
+
const res = await fetch("/api/auth/signup-confirm", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
credentials: "same-origin",
|
|
243
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
244
|
+
body: JSON.stringify({ token, password }),
|
|
245
|
+
});
|
|
246
|
+
if (res.ok) {
|
|
247
|
+
const body = (await res.json()) as SignupConfirmSuccess;
|
|
248
|
+
return { ok: true, data: body };
|
|
249
|
+
}
|
|
250
|
+
return { ok: false, error: await parseTokenFailure(res) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// GET /api/auth/tenants. Liefert die Memberships des aktuellen Users;
|
|
254
|
+
// der Server liefert 401 wenn das Cookie fehlt oder abgelaufen ist.
|
|
255
|
+
export async function fetchTenants(): Promise<{
|
|
256
|
+
readonly tenants: readonly TenantSummary[];
|
|
257
|
+
readonly activeTenantId: string;
|
|
258
|
+
} | null> {
|
|
259
|
+
const res = await fetch("/api/auth/tenants", {
|
|
260
|
+
method: "GET",
|
|
261
|
+
credentials: "same-origin",
|
|
262
|
+
});
|
|
263
|
+
if (res.status === 401) return null;
|
|
264
|
+
if (!res.ok) throw new Error(`auth/tenants failed: ${res.status}`);
|
|
265
|
+
// @cast-boundary engine-payload — HTTP-API contract, server-side schema-validated
|
|
266
|
+
return (await res.json()) as {
|
|
267
|
+
tenants: readonly TenantSummary[];
|
|
268
|
+
activeTenantId: string;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// POST /api/auth/switch-tenant. Mintet ein neues JWT für den Ziel-Tenant
|
|
273
|
+
// und rotated beide Cookies. 400 wenn already_in_tenant oder tenant_
|
|
274
|
+
// switch_not_available, 403 wenn not_a_member.
|
|
275
|
+
export async function switchTenant(tenantId: string): Promise<void> {
|
|
276
|
+
const res = await fetch("/api/auth/switch-tenant", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
credentials: "same-origin",
|
|
279
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
280
|
+
body: JSON.stringify({ tenantId }),
|
|
281
|
+
});
|
|
282
|
+
if (!res.ok) {
|
|
283
|
+
const body = await res.json().catch(() => ({}));
|
|
284
|
+
throw new Error(`switch-tenant failed: ${res.status} ${JSON.stringify(body)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// POST /api/query → user:query:user:me. Profil-Daten (email, displayName)
|
|
289
|
+
// für das UserMenu im Topbar. 401 → kein Cookie / abgelaufen, wird
|
|
290
|
+
// vom SessionProvider als "ausgeloggt" interpretiert.
|
|
291
|
+
//
|
|
292
|
+
// globalRoles: tenant-unabhängige user-rollen (z.B. SystemAdmin) aus
|
|
293
|
+
// users.roles. Im JWT schon mit tenant-membership-roles gemerged, aber
|
|
294
|
+
// das JWT ist HttpOnly + nicht JS-lesbar — der Client muss die globalen
|
|
295
|
+
// Rollen separat aus dem user-row holen damit nav-filtering greift.
|
|
296
|
+
export type CurrentUserProfile = {
|
|
297
|
+
readonly id: string;
|
|
298
|
+
readonly email: string;
|
|
299
|
+
readonly displayName: string;
|
|
300
|
+
readonly locale?: string;
|
|
301
|
+
readonly globalRoles: readonly string[];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export async function fetchCurrentUser(): Promise<CurrentUserProfile | null> {
|
|
305
|
+
const res = await fetch("/api/query", {
|
|
306
|
+
method: "POST",
|
|
307
|
+
credentials: "same-origin",
|
|
308
|
+
headers: { "Content-Type": "application/json", ...csrfHeader() },
|
|
309
|
+
body: JSON.stringify({ type: "user:query:user:me", payload: {} }),
|
|
310
|
+
});
|
|
311
|
+
if (res.status === 401) return null;
|
|
312
|
+
if (!res.ok) throw new Error(`user:me failed: ${res.status}`);
|
|
313
|
+
// @cast-boundary engine-payload — HTTP-API contract, server-side schema-validated
|
|
314
|
+
const body = (await res.json()) as {
|
|
315
|
+
data?: {
|
|
316
|
+
id: string;
|
|
317
|
+
email: string;
|
|
318
|
+
displayName: string;
|
|
319
|
+
locale?: string;
|
|
320
|
+
// JSON-encoded string[] — siehe userEntity.roles. Default "[]" wenn
|
|
321
|
+
// keine globalen Rollen.
|
|
322
|
+
roles?: string;
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
if (!body.data) return null;
|
|
326
|
+
return {
|
|
327
|
+
id: body.data.id,
|
|
328
|
+
email: body.data.email,
|
|
329
|
+
displayName: body.data.displayName,
|
|
330
|
+
...(body.data.locale !== undefined && { locale: body.data.locale }),
|
|
331
|
+
globalRoles: parseGlobalRoles(body.data.roles),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Defensive parse — server-side ist die Spalte JSON-encoded string[],
|
|
336
|
+
// aber bei migration-drift oder corrupted-row liefern wir [] statt einen
|
|
337
|
+
// runtime-throw der die ganze SessionProvider-mount blockt.
|
|
338
|
+
function parseGlobalRoles(raw: string | undefined): readonly string[] {
|
|
339
|
+
if (typeof raw !== "string" || raw.length === 0) return [];
|
|
340
|
+
try {
|
|
341
|
+
// @cast-boundary user-row.roles is JSON-encoded string[] per server contract
|
|
342
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
343
|
+
if (Array.isArray(parsed) && parsed.every((r) => typeof r === "string")) {
|
|
344
|
+
return parsed;
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
// malformed JSON → behave as empty
|
|
348
|
+
}
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Shared Web-Primitives für die Auth-Screens. Nur noch Layout/Style-
|
|
3
|
+
// Helpers — Form/Field/Input/Button/Banner kommen jetzt über
|
|
4
|
+
// usePrimitives() aus dem Framework-Vertrag, damit Native dieselben
|
|
5
|
+
// Auth-Screens rendern kann (renderer-native registriert eigene
|
|
6
|
+
// Implementations).
|
|
7
|
+
//
|
|
8
|
+
// <AuthCard> — Card-Wrapper für die Auth-Screen-Layouts
|
|
9
|
+
// (full-screen, zentriert, max-w-sm). Web-only;
|
|
10
|
+
// eine Native-Variante landet bei Bedarf
|
|
11
|
+
// daneben (z.B. SafeArea + ScrollView).
|
|
12
|
+
// authButtonClass — Tailwind-Class für anchor-styled-as-button
|
|
13
|
+
// (z.B. "Zum Login"-Link nach Reset-Success).
|
|
14
|
+
// Nur dort, wo ein <a>-Tag rendert.
|
|
15
|
+
// authMutedLinkClass — Subtle-Link-Style.
|
|
16
|
+
// parseUrlToken — URL-Param-Helper (window.location.search).
|
|
17
|
+
|
|
18
|
+
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
19
|
+
import type { ReactNode } from "react";
|
|
20
|
+
|
|
21
|
+
export type AuthCardProps = {
|
|
22
|
+
readonly title?: string;
|
|
23
|
+
readonly subtitle?: ReactNode;
|
|
24
|
+
readonly children: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function AuthCard({ title, subtitle, children }: AuthCardProps): ReactNode {
|
|
28
|
+
return (
|
|
29
|
+
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
|
30
|
+
<div className="w-full max-w-sm rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
31
|
+
{(title !== undefined || subtitle !== undefined) && (
|
|
32
|
+
<div className="flex flex-col space-y-1.5 p-6 pb-4">
|
|
33
|
+
{title !== undefined && (
|
|
34
|
+
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
35
|
+
)}
|
|
36
|
+
{subtitle !== undefined && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Primary-button-Style für anchor-Tags die wie ein Button aussehen
|
|
46
|
+
// (z.B. "Zum Login"-Link nach Reset-Success — kein <Button> weil <a>).
|
|
47
|
+
export const authButtonClass = cn(
|
|
48
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors",
|
|
49
|
+
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
50
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
51
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Subtle-Link-Style (für "Zurück zum Login"-Anchors). Fixed margin/
|
|
55
|
+
// alignment-classes lassen wir den Caller setzen — nur Farbe + hover.
|
|
56
|
+
export const authMutedLinkClass =
|
|
57
|
+
"text-sm text-muted-foreground hover:text-foreground underline-offset-4 hover:underline";
|
|
58
|
+
|
|
59
|
+
// Liest `?<paramName>=<value>` aus der aktuellen URL — typisches
|
|
60
|
+
// Pattern für Token-bearing Pages (reset, verify). Returnt "" wenn der
|
|
61
|
+
// Browser nicht da ist (SSR-safety) oder der Parameter fehlt.
|
|
62
|
+
//
|
|
63
|
+
// Nicht über useState/useEffect - das wäre ein read-once-on-mount
|
|
64
|
+
// pattern aber URL-changes sind hier irrelevant (Token-Pages re-loaden
|
|
65
|
+
// für neue Tokens). Caller setzt useState(() => parseUrlToken(...)) wenn
|
|
66
|
+
// gewünscht.
|
|
67
|
+
export function parseUrlToken(paramName = "token"): string {
|
|
68
|
+
if (typeof window === "undefined") return "";
|
|
69
|
+
return new URLSearchParams(window.location.search).get(paramName) ?? "";
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Auth-Gate: rendert den LoginScreen solange der Session-Status
|
|
3
|
+
// "unauthenticated" ist, sonst die Kinder. "loading" zeigt einen
|
|
4
|
+
// minimalen Placeholder — die initiale Refresh-Runde liefert in der
|
|
5
|
+
// Regel in <100ms, also kein Spinner-Overkill.
|
|
6
|
+
//
|
|
7
|
+
// Die Factory makeAuthGate schließt die LoginScreen-Komponente in,
|
|
8
|
+
// damit das Gate der ClientFeatureDefinition-Signatur entspricht
|
|
9
|
+
// (nur `{ children }`-Prop). Der Sample kann so einen eigenen Login-
|
|
10
|
+
// Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
|
|
11
|
+
|
|
12
|
+
import type { ComponentType, ReactNode } from "react";
|
|
13
|
+
import { LoginScreen, type LoginScreenProps } from "./login-screen";
|
|
14
|
+
import { useSession } from "./session";
|
|
15
|
+
|
|
16
|
+
export function makeAuthGate(
|
|
17
|
+
LoginComponent: ComponentType<LoginScreenProps> = LoginScreen,
|
|
18
|
+
loginProps?: LoginScreenProps,
|
|
19
|
+
): ComponentType<{ children: ReactNode }> {
|
|
20
|
+
function AuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
21
|
+
const { status } = useSession();
|
|
22
|
+
if (status === "loading") {
|
|
23
|
+
return (
|
|
24
|
+
<div className="min-h-screen flex items-center justify-center text-muted-foreground text-sm" />
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (status === "unauthenticated") {
|
|
28
|
+
return <LoginComponent {...loginProps} />;
|
|
29
|
+
}
|
|
30
|
+
return <>{children}</>;
|
|
31
|
+
}
|
|
32
|
+
return AuthGate;
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Client-Feature-Factory für auth-email-password. Wird vom App-Code
|
|
3
|
+
// in createKumikoApp({ clientFeatures: [emailPasswordClient()] })
|
|
4
|
+
// eingehängt und bringt Session-Context + AuthGate + Default-UI-
|
|
5
|
+
// Translations (de/en) mit. Alles ist overridbar — Login-Screen,
|
|
6
|
+
// Strings pro Locale, pro Key.
|
|
7
|
+
|
|
8
|
+
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
|
+
import type { ComponentType, ReactNode } from "react";
|
|
10
|
+
import { defaultTranslations, mergeTranslations } from "../i18n";
|
|
11
|
+
import { makeAuthGate } from "./auth-gate";
|
|
12
|
+
import type { LoginScreenProps } from "./login-screen";
|
|
13
|
+
import { SessionProvider } from "./session";
|
|
14
|
+
|
|
15
|
+
export type EmailPasswordClientOptions = {
|
|
16
|
+
/** Eigener Login-Screen. Default: der shadcn-stylte LoginScreen
|
|
17
|
+
* aus diesem Modul. Für Branding- oder Layout-Overrides einfach
|
|
18
|
+
* eine eigene Komponente mit derselben Signatur reichen. */
|
|
19
|
+
readonly loginScreen?: ComponentType<LoginScreenProps>;
|
|
20
|
+
readonly loginScreenProps?: LoginScreenProps;
|
|
21
|
+
/** Key-Overrides pro Locale. Wird mit den Default-Bundles (de/en)
|
|
22
|
+
* aus `translations.ts` gemerged — jeder hier gesetzte Key gewinnt.
|
|
23
|
+
* Für Branding ("Sign in" → "Login to Acme") oder weitere Sprachen
|
|
24
|
+
* (`fr`, `es`, …) zusätzlich. */
|
|
25
|
+
readonly translations?: TranslationsByLocale;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Struktural identisch zur renderer-web ClientFeatureDefinition, aber
|
|
29
|
+
// ohne harte Dep auf @cosmicdrift/kumiko-renderer-web — so bleibt das Feature auch
|
|
30
|
+
// für React-Native-Renderer (wenn sie kommen) nutzbar.
|
|
31
|
+
export type EmailPasswordClientFeature = {
|
|
32
|
+
readonly name: "auth-email-password";
|
|
33
|
+
readonly providers: readonly ComponentType<{ children: ReactNode }>[];
|
|
34
|
+
readonly gates: readonly ComponentType<{ children: ReactNode }>[];
|
|
35
|
+
readonly translations: TranslationsByLocale;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function emailPasswordClient(
|
|
39
|
+
options: EmailPasswordClientOptions = {},
|
|
40
|
+
): EmailPasswordClientFeature {
|
|
41
|
+
const translations = mergeTranslations(defaultTranslations, options.translations ?? {});
|
|
42
|
+
return {
|
|
43
|
+
name: "auth-email-password",
|
|
44
|
+
providers: [SessionProvider],
|
|
45
|
+
gates: [makeAuthGate(options.loginScreen, options.loginScreenProps)],
|
|
46
|
+
translations,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Standard-Topbar-Actions Komposition für Apps mit Auth + Workspaces.
|
|
3
|
+
// Bündelt das Pattern das jeder App-Sample sonst hand-ausschreibt:
|
|
4
|
+
// TenantSwitcher (links) → optional Extras (z.B. LanguageSwitcher) →
|
|
5
|
+
// ThemeToggle → UserMenu (rechts). Apps mit eigener Anordnung
|
|
6
|
+
// importieren weiter die Einzelkomponenten direkt — DefaultTopbarActions
|
|
7
|
+
// ist Convenience, kein Muss.
|
|
8
|
+
|
|
9
|
+
import { ThemeToggle } from "@cosmicdrift/kumiko-renderer-web";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import { TenantSwitcher } from "./tenant-switcher";
|
|
12
|
+
import { UserMenu } from "./user-menu";
|
|
13
|
+
|
|
14
|
+
export type DefaultTopbarActionsProps = {
|
|
15
|
+
/** Mapped Tenant-ID auf einen sprechenden Namen (z.B. branded label
|
|
16
|
+
* pro Tenant). TenantSwitcher's Default zeigt sonst die ersten 8
|
|
17
|
+
* Zeichen der UUID. */
|
|
18
|
+
readonly tenantName?: (tenantId: string) => string;
|
|
19
|
+
/** Slot zwischen TenantSwitcher und ThemeToggle. Typischer Use-Case:
|
|
20
|
+
* LanguageSwitcher pro App. ReactNode (nicht Array) damit die App
|
|
21
|
+
* selbst Reihenfolge + Spacing bestimmt. */
|
|
22
|
+
readonly extras?: ReactNode;
|
|
23
|
+
/** Light-Mode-Icon im ThemeToggle. Default: Unicode ☀. Apps die
|
|
24
|
+
* Lucide-Icons o.ä. wollen, übergeben ein eigenes Icon-Element. */
|
|
25
|
+
readonly lightIcon?: ReactNode;
|
|
26
|
+
/** Dark-Mode-Icon. Default: Unicode ☾. */
|
|
27
|
+
readonly darkIcon?: ReactNode;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function DefaultTopbarActions({
|
|
31
|
+
tenantName,
|
|
32
|
+
extras,
|
|
33
|
+
lightIcon,
|
|
34
|
+
darkIcon,
|
|
35
|
+
}: DefaultTopbarActionsProps = {}): ReactNode {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center gap-2">
|
|
38
|
+
<TenantSwitcher {...(tenantName !== undefined && { tenantName })} />
|
|
39
|
+
{extras}
|
|
40
|
+
<ThemeToggle
|
|
41
|
+
{...(lightIcon !== undefined && { lightIcon })}
|
|
42
|
+
{...(darkIcon !== undefined && { darkIcon })}
|
|
43
|
+
/>
|
|
44
|
+
<UserMenu />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// ForgotPasswordScreen — Form mit email-input. Submit triggert
|
|
3
|
+
// /api/auth/request-password-reset (silent-success, kein account-
|
|
4
|
+
// enumeration). UI zeigt unconditional ein "Wenn Account existiert,
|
|
5
|
+
// Mail unterwegs"-Confirm — auch wenn der Server intern erkannt hat
|
|
6
|
+
// dass die Email nicht existiert.
|
|
7
|
+
//
|
|
8
|
+
// App ist verantwortlich, den Screen unter einer URL zu mounten (z.B.
|
|
9
|
+
// /forgot-password) und ihn zu erreichen — der LoginScreen kann einen
|
|
10
|
+
// "Passwort vergessen?"-Link auf die App-Route setzen.
|
|
11
|
+
|
|
12
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
|
+
import { type FormEvent, type ReactNode, useState } from "react";
|
|
14
|
+
import { requestPasswordReset } from "./auth-client";
|
|
15
|
+
import { AuthCard, authMutedLinkClass } from "./auth-form-primitives";
|
|
16
|
+
|
|
17
|
+
export type ForgotPasswordScreenProps = {
|
|
18
|
+
readonly title?: string;
|
|
19
|
+
readonly subtitle?: ReactNode;
|
|
20
|
+
/** href für den "Zurück zum Login"-Link in success + form. App-
|
|
21
|
+
* spezifisch — Default "/login". */
|
|
22
|
+
readonly loginHref?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function ForgotPasswordScreen({
|
|
26
|
+
title,
|
|
27
|
+
subtitle,
|
|
28
|
+
loginHref = "/login",
|
|
29
|
+
}: ForgotPasswordScreenProps): ReactNode {
|
|
30
|
+
const t = useTranslation();
|
|
31
|
+
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
32
|
+
const [email, setEmail] = useState("");
|
|
33
|
+
const [submitting, setSubmitting] = useState(false);
|
|
34
|
+
const [done, setDone] = useState(false);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const doSubmit = async (): Promise<void> => {
|
|
38
|
+
setSubmitting(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
const res = await requestPasswordReset(email);
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
setDone(true);
|
|
44
|
+
} else if (res.error.reason === "rate_limited") {
|
|
45
|
+
const minutes =
|
|
46
|
+
res.error.retryAfterSeconds !== undefined
|
|
47
|
+
? Math.ceil(res.error.retryAfterSeconds / 60)
|
|
48
|
+
: undefined;
|
|
49
|
+
setError(
|
|
50
|
+
minutes !== undefined
|
|
51
|
+
? t("auth.errors.accountLockedRetry", { minutes })
|
|
52
|
+
: t("auth.errors.rateLimited"),
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
setError(t("auth.errors.unknownError"));
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
setError(t("auth.errors.unknownError"));
|
|
59
|
+
} finally {
|
|
60
|
+
setSubmitting(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onSubmit = (e?: FormEvent): void => {
|
|
65
|
+
e?.preventDefault();
|
|
66
|
+
void doSubmit();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const effectiveTitle = title ?? t("auth.forgotPassword.title");
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<AuthCard title={effectiveTitle} subtitle={subtitle}>
|
|
73
|
+
{done ? (
|
|
74
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
75
|
+
<Banner variant="info">
|
|
76
|
+
<p className="font-medium text-foreground">{t("auth.forgotPassword.successTitle")}</p>
|
|
77
|
+
<p className="mt-1">{t("auth.forgotPassword.successBody")}</p>
|
|
78
|
+
</Banner>
|
|
79
|
+
<a href={loginHref} className={authMutedLinkClass}>
|
|
80
|
+
{t("auth.forgotPassword.backToLogin")}
|
|
81
|
+
</a>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="p-6 pt-0 flex flex-col gap-4">
|
|
85
|
+
<p className="text-sm text-muted-foreground">{t("auth.forgotPassword.intro")}</p>
|
|
86
|
+
<Form onSubmit={onSubmit}>
|
|
87
|
+
<Field id="forgot-email" label={t("auth.forgotPassword.email")} required>
|
|
88
|
+
<Input
|
|
89
|
+
kind="text"
|
|
90
|
+
id="forgot-email"
|
|
91
|
+
name="forgot-email"
|
|
92
|
+
value={email}
|
|
93
|
+
onChange={setEmail}
|
|
94
|
+
disabled={submitting}
|
|
95
|
+
required
|
|
96
|
+
/>
|
|
97
|
+
</Field>
|
|
98
|
+
{error !== null && <Banner variant="error">{error}</Banner>}
|
|
99
|
+
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
100
|
+
{submitting ? t("auth.forgotPassword.submitting") : t("auth.forgotPassword.submit")}
|
|
101
|
+
</Button>
|
|
102
|
+
</Form>
|
|
103
|
+
<a href={loginHref} className={`${authMutedLinkClass} self-center`}>
|
|
104
|
+
{t("auth.forgotPassword.backToLogin")}
|
|
105
|
+
</a>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</AuthCard>
|
|
109
|
+
);
|
|
110
|
+
}
|