@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.
Files changed (333) hide show
  1. package/package.json +90 -0
  2. package/src/audit/__tests__/audit.integration.ts +328 -0
  3. package/src/audit/constants.ts +7 -0
  4. package/src/audit/feature.ts +23 -0
  5. package/src/audit/handlers/list.query.ts +98 -0
  6. package/src/audit/index.ts +2 -0
  7. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
  8. package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
  9. package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
  10. package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
  11. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
  12. package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
  13. package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
  14. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
  15. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
  16. package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
  17. package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
  18. package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
  19. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
  20. package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
  21. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
  22. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
  23. package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
  24. package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
  25. package/src/auth-email-password/auth-user-row.ts +41 -0
  26. package/src/auth-email-password/constants.ts +101 -0
  27. package/src/auth-email-password/email-templates.ts +283 -0
  28. package/src/auth-email-password/feature.ts +140 -0
  29. package/src/auth-email-password/handlers/change-password.write.ts +58 -0
  30. package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
  31. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
  32. package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
  33. package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
  34. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
  35. package/src/auth-email-password/handlers/login.write.ts +208 -0
  36. package/src/auth-email-password/handlers/logout.write.ts +12 -0
  37. package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
  38. package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
  39. package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
  40. package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
  41. package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
  42. package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
  43. package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
  44. package/src/auth-email-password/i18n.ts +211 -0
  45. package/src/auth-email-password/identity-v3-hash.ts +97 -0
  46. package/src/auth-email-password/index.ts +35 -0
  47. package/src/auth-email-password/invite-token-store.ts +92 -0
  48. package/src/auth-email-password/lockout-store.ts +118 -0
  49. package/src/auth-email-password/password-hashing.ts +43 -0
  50. package/src/auth-email-password/reset-token.ts +28 -0
  51. package/src/auth-email-password/seeding.ts +183 -0
  52. package/src/auth-email-password/signed-token.ts +85 -0
  53. package/src/auth-email-password/signup-token-store.ts +104 -0
  54. package/src/auth-email-password/stream-tenant.ts +31 -0
  55. package/src/auth-email-password/testing.ts +5 -0
  56. package/src/auth-email-password/token-burn-store.ts +57 -0
  57. package/src/auth-email-password/verification-token.ts +27 -0
  58. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
  59. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
  60. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
  61. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
  62. package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
  63. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
  64. package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
  65. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
  66. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
  67. package/src/auth-email-password/web/auth-client.ts +350 -0
  68. package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
  69. package/src/auth-email-password/web/auth-gate.tsx +33 -0
  70. package/src/auth-email-password/web/client-plugin.ts +48 -0
  71. package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
  72. package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
  73. package/src/auth-email-password/web/index.ts +56 -0
  74. package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
  75. package/src/auth-email-password/web/login-screen.tsx +150 -0
  76. package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
  77. package/src/auth-email-password/web/session.tsx +171 -0
  78. package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
  79. package/src/auth-email-password/web/signup-screen.tsx +130 -0
  80. package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
  81. package/src/auth-email-password/web/use-shell-user.ts +34 -0
  82. package/src/auth-email-password/web/user-menu.tsx +89 -0
  83. package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
  84. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
  85. package/src/billing-foundation/__tests__/feature.test.ts +110 -0
  86. package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
  87. package/src/billing-foundation/aggregate-id.ts +21 -0
  88. package/src/billing-foundation/constants.ts +70 -0
  89. package/src/billing-foundation/entities.ts +50 -0
  90. package/src/billing-foundation/events.ts +71 -0
  91. package/src/billing-foundation/feature.ts +122 -0
  92. package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
  93. package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
  94. package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
  95. package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
  96. package/src/billing-foundation/handlers/process-event.write.ts +160 -0
  97. package/src/billing-foundation/index.ts +42 -0
  98. package/src/billing-foundation/projection.ts +135 -0
  99. package/src/billing-foundation/types.ts +157 -0
  100. package/src/billing-foundation/webhook-handler.ts +184 -0
  101. package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
  102. package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
  103. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
  104. package/src/cap-counter/aggregate-id.ts +61 -0
  105. package/src/cap-counter/constants.ts +32 -0
  106. package/src/cap-counter/enforce-cap.ts +404 -0
  107. package/src/cap-counter/entity.ts +48 -0
  108. package/src/cap-counter/feature.ts +90 -0
  109. package/src/cap-counter/handlers/get-counter.query.ts +43 -0
  110. package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
  111. package/src/cap-counter/handlers/increment.write.ts +92 -0
  112. package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
  113. package/src/cap-counter/index.ts +34 -0
  114. package/src/cap-counter/with-cap-enforcement.ts +179 -0
  115. package/src/channel-email/email-channel.ts +48 -0
  116. package/src/channel-email/feature.ts +15 -0
  117. package/src/channel-email/index.ts +4 -0
  118. package/src/channel-email/smtp-transport.ts +65 -0
  119. package/src/channel-email/types.ts +34 -0
  120. package/src/channel-in-app/constants.ts +11 -0
  121. package/src/channel-in-app/feature.ts +30 -0
  122. package/src/channel-in-app/handlers/inbox.query.ts +28 -0
  123. package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
  124. package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
  125. package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
  126. package/src/channel-in-app/in-app-channel.ts +44 -0
  127. package/src/channel-in-app/index.ts +4 -0
  128. package/src/channel-in-app/tables.ts +22 -0
  129. package/src/channel-push/feature.ts +15 -0
  130. package/src/channel-push/index.ts +3 -0
  131. package/src/channel-push/push-channel.ts +33 -0
  132. package/src/channel-push/types.ts +22 -0
  133. package/src/config/__tests__/app-overrides.test.ts +118 -0
  134. package/src/config/__tests__/config.integration.ts +1246 -0
  135. package/src/config/constants.ts +23 -0
  136. package/src/config/feature.ts +117 -0
  137. package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
  138. package/src/config/handlers/reset.write.ts +45 -0
  139. package/src/config/handlers/schema.query.ts +22 -0
  140. package/src/config/handlers/set.write.ts +93 -0
  141. package/src/config/handlers/values.query.ts +43 -0
  142. package/src/config/index.ts +15 -0
  143. package/src/config/resolver.ts +283 -0
  144. package/src/config/table.ts +35 -0
  145. package/src/config/write-helpers.ts +268 -0
  146. package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
  147. package/src/delivery/__tests__/delivery.integration.ts +1405 -0
  148. package/src/delivery/constants.ts +33 -0
  149. package/src/delivery/delivery-service.ts +489 -0
  150. package/src/delivery/events.ts +18 -0
  151. package/src/delivery/feature.ts +70 -0
  152. package/src/delivery/handlers/log.query.ts +21 -0
  153. package/src/delivery/handlers/preferences.query.ts +18 -0
  154. package/src/delivery/handlers/set-preference.write.ts +28 -0
  155. package/src/delivery/index.ts +35 -0
  156. package/src/delivery/tables.ts +74 -0
  157. package/src/delivery/testing.ts +47 -0
  158. package/src/delivery/types.ts +71 -0
  159. package/src/delivery/unsubscribe.ts +99 -0
  160. package/src/delivery/upsert-preference.ts +145 -0
  161. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
  162. package/src/feature-toggles/constants.ts +20 -0
  163. package/src/feature-toggles/events.ts +18 -0
  164. package/src/feature-toggles/feature.ts +98 -0
  165. package/src/feature-toggles/global-feature-state-table.ts +28 -0
  166. package/src/feature-toggles/handlers/list.query.ts +26 -0
  167. package/src/feature-toggles/handlers/registered.query.ts +56 -0
  168. package/src/feature-toggles/handlers/set.write.ts +158 -0
  169. package/src/feature-toggles/index.ts +9 -0
  170. package/src/feature-toggles/toggle-runtime.ts +73 -0
  171. package/src/file-foundation/__tests__/feature.test.ts +35 -0
  172. package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
  173. package/src/file-foundation/feature.ts +123 -0
  174. package/src/file-foundation/index.ts +7 -0
  175. package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
  176. package/src/file-provider-inmemory/feature.ts +73 -0
  177. package/src/file-provider-inmemory/index.ts +3 -0
  178. package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
  179. package/src/file-provider-s3/feature.ts +169 -0
  180. package/src/file-provider-s3/index.ts +3 -0
  181. package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
  182. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
  183. package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
  184. package/src/files-provider-s3/env-helper.ts +49 -0
  185. package/src/files-provider-s3/index.ts +3 -0
  186. package/src/files-provider-s3/s3-provider.ts +114 -0
  187. package/src/foundation-shared/config-helpers.ts +67 -0
  188. package/src/foundation-shared/index.ts +4 -0
  189. package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
  190. package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
  191. package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
  192. package/src/jobs/constants.ts +21 -0
  193. package/src/jobs/events.ts +39 -0
  194. package/src/jobs/feature.ts +150 -0
  195. package/src/jobs/handlers/detail.query.ts +30 -0
  196. package/src/jobs/handlers/list.query.ts +36 -0
  197. package/src/jobs/handlers/retry.write.ts +69 -0
  198. package/src/jobs/handlers/trigger.write.ts +39 -0
  199. package/src/jobs/index.ts +5 -0
  200. package/src/jobs/job-run-logger.ts +213 -0
  201. package/src/jobs/job-run-table.ts +55 -0
  202. package/src/legal-pages/README.md +195 -0
  203. package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
  204. package/src/legal-pages/constants.ts +36 -0
  205. package/src/legal-pages/feature.ts +187 -0
  206. package/src/legal-pages/index.ts +13 -0
  207. package/src/legal-pages/markdown.ts +69 -0
  208. package/src/mail-foundation/__tests__/feature.test.ts +46 -0
  209. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
  210. package/src/mail-foundation/feature.ts +160 -0
  211. package/src/mail-foundation/index.ts +14 -0
  212. package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
  213. package/src/mail-transport-inmemory/feature.ts +90 -0
  214. package/src/mail-transport-inmemory/index.ts +3 -0
  215. package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
  216. package/src/mail-transport-smtp/feature.ts +182 -0
  217. package/src/mail-transport-smtp/index.ts +3 -0
  218. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
  219. package/src/rate-limiting/constants.ts +9 -0
  220. package/src/rate-limiting/feature.ts +16 -0
  221. package/src/rate-limiting/handlers/status.query.ts +52 -0
  222. package/src/rate-limiting/index.ts +2 -0
  223. package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
  224. package/src/renderer-simple/feature.ts +12 -0
  225. package/src/renderer-simple/index.ts +2 -0
  226. package/src/renderer-simple/simple-renderer.ts +72 -0
  227. package/src/secrets/__tests__/rotate.integration.ts +176 -0
  228. package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
  229. package/src/secrets/__tests__/secrets.integration.ts +118 -0
  230. package/src/secrets/feature.ts +84 -0
  231. package/src/secrets/handlers/delete.write.ts +20 -0
  232. package/src/secrets/handlers/list.query.ts +38 -0
  233. package/src/secrets/handlers/rotate.job.ts +193 -0
  234. package/src/secrets/handlers/set.write.ts +50 -0
  235. package/src/secrets/index.ts +16 -0
  236. package/src/secrets/secrets-context.ts +296 -0
  237. package/src/secrets/table.ts +68 -0
  238. package/src/sessions/__tests__/cleanup.integration.ts +175 -0
  239. package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
  240. package/src/sessions/__tests__/sessions.integration.ts +472 -0
  241. package/src/sessions/__tests__/test-helpers.ts +66 -0
  242. package/src/sessions/constants.ts +43 -0
  243. package/src/sessions/feature.ts +84 -0
  244. package/src/sessions/handlers/cleanup.job.ts +109 -0
  245. package/src/sessions/handlers/list.query.ts +35 -0
  246. package/src/sessions/handlers/mine.query.ts +37 -0
  247. package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
  248. package/src/sessions/handlers/revoke.write.ts +76 -0
  249. package/src/sessions/index.ts +17 -0
  250. package/src/sessions/schema/index.ts +5 -0
  251. package/src/sessions/schema/user-session.ts +67 -0
  252. package/src/sessions/session-callbacks.ts +110 -0
  253. package/src/sessions/testing.ts +42 -0
  254. package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
  255. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
  256. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
  257. package/src/subscription-mollie/constants.ts +33 -0
  258. package/src/subscription-mollie/feature.ts +144 -0
  259. package/src/subscription-mollie/index.ts +13 -0
  260. package/src/subscription-mollie/plugin-methods.ts +79 -0
  261. package/src/subscription-mollie/verify-webhook.ts +244 -0
  262. package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
  263. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
  264. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
  265. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
  266. package/src/subscription-stripe/constants.ts +20 -0
  267. package/src/subscription-stripe/feature.ts +120 -0
  268. package/src/subscription-stripe/index.ts +14 -0
  269. package/src/subscription-stripe/plugin-methods.ts +91 -0
  270. package/src/subscription-stripe/verify-webhook.ts +235 -0
  271. package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
  272. package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
  273. package/src/tenant/__tests__/tenant.integration.ts +347 -0
  274. package/src/tenant/command-schemas.ts +37 -0
  275. package/src/tenant/constants.ts +37 -0
  276. package/src/tenant/feature.ts +109 -0
  277. package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
  278. package/src/tenant/handlers/add-member.write.ts +53 -0
  279. package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
  280. package/src/tenant/handlers/create.write.ts +21 -0
  281. package/src/tenant/handlers/disable.write.ts +18 -0
  282. package/src/tenant/handlers/invitations.query.ts +31 -0
  283. package/src/tenant/handlers/list.query.ts +17 -0
  284. package/src/tenant/handlers/me.query.ts +17 -0
  285. package/src/tenant/handlers/members.query.ts +22 -0
  286. package/src/tenant/handlers/memberships.query.ts +24 -0
  287. package/src/tenant/handlers/remove-member.write.ts +40 -0
  288. package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
  289. package/src/tenant/handlers/update-member-roles.write.ts +54 -0
  290. package/src/tenant/handlers/update.write.ts +20 -0
  291. package/src/tenant/index.ts +12 -0
  292. package/src/tenant/invitation-table.ts +93 -0
  293. package/src/tenant/membership-table.ts +35 -0
  294. package/src/tenant/schema/index.ts +5 -0
  295. package/src/tenant/schema/tenant.ts +27 -0
  296. package/src/tenant/seeding.ts +155 -0
  297. package/src/tenant/testing.ts +8 -0
  298. package/src/text-content/README.md +190 -0
  299. package/src/text-content/__tests__/text-content.integration.ts +415 -0
  300. package/src/text-content/api.ts +92 -0
  301. package/src/text-content/constants.ts +19 -0
  302. package/src/text-content/feature.ts +29 -0
  303. package/src/text-content/handlers/by-slug.query.ts +55 -0
  304. package/src/text-content/handlers/set.write.ts +118 -0
  305. package/src/text-content/index.ts +14 -0
  306. package/src/text-content/seeding.ts +91 -0
  307. package/src/text-content/table.ts +45 -0
  308. package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
  309. package/src/tier-engine/__tests__/drift.test.ts +42 -0
  310. package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
  311. package/src/tier-engine/aggregate-id.ts +27 -0
  312. package/src/tier-engine/compose-app.ts +150 -0
  313. package/src/tier-engine/constants.ts +15 -0
  314. package/src/tier-engine/entity.ts +30 -0
  315. package/src/tier-engine/feature.ts +72 -0
  316. package/src/tier-engine/handlers/active-tier.query.ts +23 -0
  317. package/src/tier-engine/index.ts +22 -0
  318. package/src/user/__tests__/seed-testing.integration.ts +127 -0
  319. package/src/user/__tests__/user.integration.ts +198 -0
  320. package/src/user/command-schemas.ts +15 -0
  321. package/src/user/constants.ts +23 -0
  322. package/src/user/feature.ts +32 -0
  323. package/src/user/handlers/create.write.ts +54 -0
  324. package/src/user/handlers/detail.query.ts +9 -0
  325. package/src/user/handlers/find-for-auth.query.ts +38 -0
  326. package/src/user/handlers/list.query.ts +8 -0
  327. package/src/user/handlers/me.query.ts +15 -0
  328. package/src/user/handlers/update.write.ts +54 -0
  329. package/src/user/index.ts +4 -0
  330. package/src/user/schema/index.ts +5 -0
  331. package/src/user/schema/user.ts +69 -0
  332. package/src/user/seeding.ts +93 -0
  333. 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
+ }