@meith/web 0.16.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 (511) hide show
  1. package/LICENSE.md +165 -0
  2. package/app/(auth)/layout.tsx +6 -0
  3. package/app/(auth)/login/page.tsx +106 -0
  4. package/app/(auth)/login/verify/page.tsx +55 -0
  5. package/app/(auth)/register/page.tsx +83 -0
  6. package/app/(auth)/reset/confirm/page.tsx +45 -0
  7. package/app/(auth)/reset/page.tsx +22 -0
  8. package/app/(auth)/verify/page.tsx +44 -0
  9. package/app/(auth)/verify/resend/page.tsx +51 -0
  10. package/app/(board)/[slug]/feed.xml/route.ts +48 -0
  11. package/app/(board)/[slug]/new/page.tsx +112 -0
  12. package/app/(board)/[slug]/page.tsx +403 -0
  13. package/app/(board)/discover/[view]/page.tsx +115 -0
  14. package/app/(board)/discover/page.tsx +5 -0
  15. package/app/(board)/layout.tsx +22 -0
  16. package/app/(board)/member/[id]/page.tsx +187 -0
  17. package/app/(board)/member/[id]/reputation/page.tsx +195 -0
  18. package/app/(board)/member/by-name/[name]/page.tsx +32 -0
  19. package/app/(board)/messages/[id]/page.tsx +103 -0
  20. package/app/(board)/messages/compose/page.tsx +60 -0
  21. package/app/(board)/messages/layout.tsx +5 -0
  22. package/app/(board)/messages/page.tsx +179 -0
  23. package/app/(board)/modcp/forums/page.tsx +59 -0
  24. package/app/(board)/modcp/ip/page.tsx +158 -0
  25. package/app/(board)/modcp/layout.tsx +5 -0
  26. package/app/(board)/modcp/log/page.tsx +98 -0
  27. package/app/(board)/modcp/page.tsx +112 -0
  28. package/app/(board)/moderation/layout.tsx +5 -0
  29. package/app/(board)/moderation/page.tsx +111 -0
  30. package/app/(board)/moderation/reports/page.tsx +193 -0
  31. package/app/(board)/moderation/warn/page.tsx +158 -0
  32. package/app/(board)/notifications/layout.tsx +5 -0
  33. package/app/(board)/notifications/page.tsx +144 -0
  34. package/app/(board)/notifications/preferences/page.tsx +65 -0
  35. package/app/(board)/online/page.tsx +119 -0
  36. package/app/(board)/page.tsx +195 -0
  37. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
  38. package/app/(board)/privacy/page.tsx +12 -0
  39. package/app/(board)/report/page.tsx +71 -0
  40. package/app/(board)/rules/page.tsx +12 -0
  41. package/app/(board)/search/[token]/page.tsx +130 -0
  42. package/app/(board)/search/page.tsx +289 -0
  43. package/app/(board)/stats/page.tsx +202 -0
  44. package/app/(board)/subscriptions/layout.tsx +5 -0
  45. package/app/(board)/subscriptions/page.tsx +142 -0
  46. package/app/(board)/terms/page.tsx +12 -0
  47. package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
  48. package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
  49. package/app/(board)/thread/[slug]/page.tsx +707 -0
  50. package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
  51. package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
  52. package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
  53. package/app/(board)/unsubscribe/page.tsx +73 -0
  54. package/app/(board)/usercp/avatar/page.tsx +56 -0
  55. package/app/(board)/usercp/contacts/page.tsx +141 -0
  56. package/app/(board)/usercp/email/confirm/page.tsx +52 -0
  57. package/app/(board)/usercp/layout.tsx +23 -0
  58. package/app/(board)/usercp/options/page.tsx +56 -0
  59. package/app/(board)/usercp/page.tsx +71 -0
  60. package/app/(board)/usercp/profile/page.tsx +81 -0
  61. package/app/(board)/usercp/security/page.tsx +238 -0
  62. package/app/(board)/usercp/security/verify/page.tsx +92 -0
  63. package/app/(board)/usercp/signature/page.tsx +67 -0
  64. package/app/admin/antispam/page.tsx +155 -0
  65. package/app/admin/api/marketplace/screenshot/route.ts +68 -0
  66. package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
  67. package/app/admin/api-tokens/page.tsx +129 -0
  68. package/app/admin/content/announcements/page.tsx +110 -0
  69. package/app/admin/content/attachments/page.tsx +199 -0
  70. package/app/admin/content/navigation/page.tsx +123 -0
  71. package/app/admin/content/page.tsx +170 -0
  72. package/app/admin/forums/[id]/page.tsx +96 -0
  73. package/app/admin/forums/[id]/permissions/page.tsx +108 -0
  74. package/app/admin/forums/page.tsx +45 -0
  75. package/app/admin/groups/[id]/page.tsx +133 -0
  76. package/app/admin/groups/memberships/page.tsx +56 -0
  77. package/app/admin/groups/page.tsx +104 -0
  78. package/app/admin/groups/promotions/page.tsx +143 -0
  79. package/app/admin/layout.tsx +86 -0
  80. package/app/admin/log/page.tsx +116 -0
  81. package/app/admin/page.tsx +206 -0
  82. package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
  83. package/app/admin/plugins/browse/page.tsx +70 -0
  84. package/app/admin/plugins/page.tsx +165 -0
  85. package/app/admin/security/page.tsx +124 -0
  86. package/app/admin/settings/page.tsx +190 -0
  87. package/app/admin/system/page.tsx +303 -0
  88. package/app/admin/themes/[key]/page.tsx +110 -0
  89. package/app/admin/themes/browse/page.tsx +70 -0
  90. package/app/admin/themes/page.tsx +163 -0
  91. package/app/admin/users/[id]/merge/page.tsx +136 -0
  92. package/app/admin/users/[id]/page.tsx +208 -0
  93. package/app/admin/users/mail/page.tsx +68 -0
  94. package/app/admin/users/page.tsx +260 -0
  95. package/app/admin/users/prune/page.tsx +186 -0
  96. package/app/api/health/route.ts +11 -0
  97. package/app/api/metrics/route.ts +66 -0
  98. package/app/api/plugins/[key]/[...path]/route.ts +27 -0
  99. package/app/api/read/all/route.ts +15 -0
  100. package/app/api/read/forum/[id]/route.ts +29 -0
  101. package/app/api/read/thread/[id]/route.ts +40 -0
  102. package/app/api/ready/route.ts +63 -0
  103. package/app/api/system/tick/route.ts +77 -0
  104. package/app/api/v1/[...path]/route.ts +208 -0
  105. package/app/api/v1/openapi.json/route.ts +12 -0
  106. package/app/apple-icon.ts +11 -0
  107. package/app/atom.xml/route.ts +7 -0
  108. package/app/attachment/[id]/route.ts +10 -0
  109. package/app/attachment/[id]/thumb/route.ts +10 -0
  110. package/app/auth/passkey/options/route.ts +145 -0
  111. package/app/auth/passkey/verify/route.ts +241 -0
  112. package/app/auth/resume/route.ts +55 -0
  113. package/app/auth/sso/[provider]/callback/route.ts +173 -0
  114. package/app/auth/sso/[provider]/go/route.ts +57 -0
  115. package/app/auth/sso/[provider]/route.ts +105 -0
  116. package/app/avatar/[id]/route.ts +31 -0
  117. package/app/brand/icon-192.png/route.ts +7 -0
  118. package/app/brand/icon-512.png/route.ts +7 -0
  119. package/app/brand/icon-maskable.png/route.ts +7 -0
  120. package/app/demo/stripe/[[...path]]/route.ts +27 -0
  121. package/app/error.tsx +15 -0
  122. package/app/feed.xml/route.ts +7 -0
  123. package/app/forumdisplay.php/page.tsx +11 -0
  124. package/app/group/[id]/badge/[scheme]/route.ts +26 -0
  125. package/app/icon.ts +13 -0
  126. package/app/install/page.tsx +221 -0
  127. package/app/jump/page.tsx +28 -0
  128. package/app/layout.tsx +124 -0
  129. package/app/logo/[scheme]/route.ts +24 -0
  130. package/app/manifest.ts +43 -0
  131. package/app/member.php/page.tsx +11 -0
  132. package/app/memberlist.php/page.tsx +11 -0
  133. package/app/not-found.tsx +14 -0
  134. package/app/opengraph-image.ts +11 -0
  135. package/app/redirect/page.tsx +44 -0
  136. package/app/robots.txt/route.ts +35 -0
  137. package/app/showthread.php/page.tsx +11 -0
  138. package/app/sitemap/[page]/route.ts +64 -0
  139. package/app/sitemap.xml/route.ts +31 -0
  140. package/app/twitter-image.ts +11 -0
  141. package/app/viewforum.php/page.tsx +11 -0
  142. package/app/viewtopic.php/page.tsx +11 -0
  143. package/bin/forum-web.mjs +241 -0
  144. package/components.json +21 -0
  145. package/instrumentation.ts +18 -0
  146. package/next.config.mjs +182 -0
  147. package/package.json +99 -0
  148. package/postcss.config.mjs +7 -0
  149. package/proxy.ts +160 -0
  150. package/src/components/account/active-sessions.tsx +87 -0
  151. package/src/components/account/avatar-form.tsx +119 -0
  152. package/src/components/account/credential-proof-form.tsx +76 -0
  153. package/src/components/account/logout-form.tsx +14 -0
  154. package/src/components/account/notification-forms.tsx +157 -0
  155. package/src/components/account/passkey-enrol.tsx +81 -0
  156. package/src/components/account/passkey-proof-button.tsx +47 -0
  157. package/src/components/account/push-device-form.tsx +193 -0
  158. package/src/components/account/relation-forms.tsx +69 -0
  159. package/src/components/account/reputation-forms.tsx +124 -0
  160. package/src/components/account/security-activity.tsx +46 -0
  161. package/src/components/account/sign-in-methods.tsx +166 -0
  162. package/src/components/account/subscription-forms.tsx +151 -0
  163. package/src/components/account/two-factor-forms.tsx +226 -0
  164. package/src/components/account/usercp-forms.tsx +387 -0
  165. package/src/components/account/usercp-nav.tsx +14 -0
  166. package/src/components/account/usercp-shell.tsx +34 -0
  167. package/src/components/admin/admin-forms.tsx +92 -0
  168. package/src/components/admin/admin-nav.tsx +52 -0
  169. package/src/components/admin/admin-undo.tsx +32 -0
  170. package/src/components/admin/api-token-forms.tsx +89 -0
  171. package/src/components/admin/badge-forms.tsx +97 -0
  172. package/src/components/admin/branding-forms.tsx +102 -0
  173. package/src/components/admin/content-forms.tsx +712 -0
  174. package/src/components/admin/form-bits.tsx +11 -0
  175. package/src/components/admin/forum-forms.tsx +524 -0
  176. package/src/components/admin/forum-tree.tsx +454 -0
  177. package/src/components/admin/group-forms.tsx +646 -0
  178. package/src/components/admin/mail-test-card.tsx +90 -0
  179. package/src/components/admin/marketplace-forms.tsx +30 -0
  180. package/src/components/admin/marketplace-listing.tsx +123 -0
  181. package/src/components/admin/navigation-forms.tsx +229 -0
  182. package/src/components/admin/navigation-tree.tsx +442 -0
  183. package/src/components/admin/oklch-picker.tsx +180 -0
  184. package/src/components/admin/plugin-forms.tsx +225 -0
  185. package/src/components/admin/settings-form.tsx +148 -0
  186. package/src/components/admin/system-forms.tsx +169 -0
  187. package/src/components/admin/theme-changes.tsx +354 -0
  188. package/src/components/admin/theme-forms.tsx +636 -0
  189. package/src/components/admin/theme-palette.tsx +119 -0
  190. package/src/components/admin/theme-preview.tsx +461 -0
  191. package/src/components/admin/user-bulk-toolbar.tsx +99 -0
  192. package/src/components/admin/user-forms.tsx +488 -0
  193. package/src/components/auth/auth-page.tsx +45 -0
  194. package/src/components/auth/form-controls.tsx +117 -0
  195. package/src/components/auth/login-form.tsx +47 -0
  196. package/src/components/auth/passkey-client.ts +171 -0
  197. package/src/components/auth/passkey-second-factor.tsx +69 -0
  198. package/src/components/auth/passkey-sign-in.tsx +61 -0
  199. package/src/components/auth/register-form.tsx +136 -0
  200. package/src/components/auth/resend-verification-form.tsx +34 -0
  201. package/src/components/auth/reset-confirm-form.tsx +43 -0
  202. package/src/components/auth/reset-request-form.tsx +35 -0
  203. package/src/components/auth/second-factor-form.tsx +51 -0
  204. package/src/components/auth/sso-buttons.tsx +47 -0
  205. package/src/components/board/live-region.tsx +74 -0
  206. package/src/components/board/search-off-notice.tsx +18 -0
  207. package/src/components/board/section-page.tsx +114 -0
  208. package/src/components/content/attachment-field.tsx +51 -0
  209. package/src/components/content/composer-field.ts +31 -0
  210. package/src/components/content/composer-ids.ts +6 -0
  211. package/src/components/content/composer-intents.tsx +27 -0
  212. package/src/components/content/composer-recovery.tsx +215 -0
  213. package/src/components/content/edit-post-form.tsx +121 -0
  214. package/src/components/content/markdown-editor.tsx +590 -0
  215. package/src/components/content/markdown-syntax.ts +53 -0
  216. package/src/components/content/mention-token.ts +23 -0
  217. package/src/components/content/multiquote-button.tsx +32 -0
  218. package/src/components/content/multiquote-selection.tsx +105 -0
  219. package/src/components/content/multiquote.ts +125 -0
  220. package/src/components/content/new-thread-form.tsx +169 -0
  221. package/src/components/content/poll-edit-form.tsx +96 -0
  222. package/src/components/content/poll.tsx +89 -0
  223. package/src/components/content/quote-in-place.tsx +51 -0
  224. package/src/components/content/reply-form.tsx +113 -0
  225. package/src/components/content/thanks-button.tsx +65 -0
  226. package/src/components/content/thread-rating.tsx +143 -0
  227. package/src/components/install/install-form.tsx +497 -0
  228. package/src/components/legal/legal-document.tsx +22 -0
  229. package/src/components/messages/message-forms.tsx +152 -0
  230. package/src/components/moderation/inline-moderation-form.tsx +158 -0
  231. package/src/components/moderation/modcp-nav.tsx +15 -0
  232. package/src/components/moderation/modcp-shell.tsx +41 -0
  233. package/src/components/moderation/queue-form.tsx +96 -0
  234. package/src/components/moderation/report-forms.tsx +120 -0
  235. package/src/components/moderation/signature-lock-form.tsx +83 -0
  236. package/src/components/moderation/thread-surgery-form.tsx +96 -0
  237. package/src/components/moderation/thread-tools-form.tsx +137 -0
  238. package/src/components/moderation/warning-forms.tsx +125 -0
  239. package/src/components/profile/custom-field.tsx +79 -0
  240. package/src/components/shell/appearance-icons.tsx +35 -0
  241. package/src/components/shell/board-notice.tsx +21 -0
  242. package/src/components/shell/copy-record.ts +20 -0
  243. package/src/components/shell/copy.tsx +17 -0
  244. package/src/components/shell/crash-notice.tsx +86 -0
  245. package/src/components/shell/demo-banner.tsx +49 -0
  246. package/src/components/shell/group-name-style.tsx +6 -0
  247. package/src/components/shell/measure.ts +1 -0
  248. package/src/components/shell/notification-menu.tsx +361 -0
  249. package/src/components/shell/offline-notice.tsx +29 -0
  250. package/src/components/shell/onboarding-banner.tsx +39 -0
  251. package/src/components/shell/page-shell.tsx +167 -0
  252. package/src/components/shell/panel-list.tsx +95 -0
  253. package/src/components/shell/panel-nav.tsx +40 -0
  254. package/src/components/shell/panel-overview.tsx +81 -0
  255. package/src/components/shell/panel-page.tsx +96 -0
  256. package/src/components/shell/panel-pagination.tsx +28 -0
  257. package/src/components/shell/panel-shell.tsx +31 -0
  258. package/src/components/shell/theme-runtime-style.tsx +6 -0
  259. package/src/components/shell/theme-switcher.tsx +97 -0
  260. package/src/components/shell/timezone-probe.tsx +45 -0
  261. package/src/components/shell/view-tabs.tsx +60 -0
  262. package/src/config.ts +13 -0
  263. package/src/server/absent-services.ts +29 -0
  264. package/src/server/admin-actions.ts +171 -0
  265. package/src/server/admin-settings-actions.ts +83 -0
  266. package/src/server/admin-undo-actions.ts +70 -0
  267. package/src/server/admin-undo.ts +75 -0
  268. package/src/server/admin.ts +112 -0
  269. package/src/server/announcements.ts +82 -0
  270. package/src/server/antispam.ts +310 -0
  271. package/src/server/api/content.ts +486 -0
  272. package/src/server/api/http.ts +119 -0
  273. package/src/server/api/members.ts +109 -0
  274. package/src/server/api/messages.ts +113 -0
  275. package/src/server/api/registry.ts +30 -0
  276. package/src/server/api/search.ts +98 -0
  277. package/src/server/api/subscriptions.ts +106 -0
  278. package/src/server/api-auth.ts +66 -0
  279. package/src/server/api-token-actions.ts +85 -0
  280. package/src/server/api-tokens-admin.ts +95 -0
  281. package/src/server/appearance-actions.ts +39 -0
  282. package/src/server/attachment-download.ts +43 -0
  283. package/src/server/attachment-embed.ts +63 -0
  284. package/src/server/attachment-upload-actions.ts +92 -0
  285. package/src/server/attachments.ts +257 -0
  286. package/src/server/auth-actions.ts +479 -0
  287. package/src/server/auth-config.ts +61 -0
  288. package/src/server/auth-events.ts +92 -0
  289. package/src/server/auth-form-state.ts +14 -0
  290. package/src/server/auth-mail.ts +104 -0
  291. package/src/server/avatars.ts +83 -0
  292. package/src/server/board-latest-actions.ts +7 -0
  293. package/src/server/board-latest.tsx +103 -0
  294. package/src/server/board-offline.ts +23 -0
  295. package/src/server/board-url.ts +14 -0
  296. package/src/server/brand-assets.ts +151 -0
  297. package/src/server/brand-mark.tsx +110 -0
  298. package/src/server/branding-actions.ts +53 -0
  299. package/src/server/branding.ts +101 -0
  300. package/src/server/cache-targets.ts +13 -0
  301. package/src/server/code-highlighting.ts +33 -0
  302. package/src/server/container.ts +402 -0
  303. package/src/server/content-actions.ts +459 -0
  304. package/src/server/content-admin-actions.ts +492 -0
  305. package/src/server/content-admin.ts +95 -0
  306. package/src/server/content-security-policy.ts +35 -0
  307. package/src/server/context.ts +32 -0
  308. package/src/server/cookies.ts +139 -0
  309. package/src/server/credential-proof-actions.ts +42 -0
  310. package/src/server/credential-proof.ts +35 -0
  311. package/src/server/current-location.ts +19 -0
  312. package/src/server/demo-stripe.ts +104 -0
  313. package/src/server/demo-uploads.ts +28 -0
  314. package/src/server/demo.ts +80 -0
  315. package/src/server/discovery.ts +104 -0
  316. package/src/server/embeds.ts +113 -0
  317. package/src/server/error-notice.tsx +41 -0
  318. package/src/server/federation-actions.ts +113 -0
  319. package/src/server/federation.ts +138 -0
  320. package/src/server/feed-builder.ts +72 -0
  321. package/src/server/feed-routes.ts +122 -0
  322. package/src/server/fixture-actor-source.ts +62 -0
  323. package/src/server/fixture-forum-repo.ts +59 -0
  324. package/src/server/fixture-member-profile-repo.ts +30 -0
  325. package/src/server/fixture-post-repo.ts +108 -0
  326. package/src/server/fixture-thread-repo.ts +98 -0
  327. package/src/server/form-state-reporter.ts +34 -0
  328. package/src/server/form-values.ts +23 -0
  329. package/src/server/forum-admin-actions.ts +353 -0
  330. package/src/server/forum-admin.ts +98 -0
  331. package/src/server/group-admin-actions.ts +341 -0
  332. package/src/server/group-admin.ts +98 -0
  333. package/src/server/group-badge-actions.ts +61 -0
  334. package/src/server/group-badge.ts +62 -0
  335. package/src/server/group-identity.ts +104 -0
  336. package/src/server/i18n-catalogs.ts +42 -0
  337. package/src/server/i18n.ts +69 -0
  338. package/src/server/image-upload.ts +109 -0
  339. package/src/server/inline-moderation-actions.ts +129 -0
  340. package/src/server/install-actions.ts +124 -0
  341. package/src/server/install.ts +281 -0
  342. package/src/server/legacy-redirect.ts +83 -0
  343. package/src/server/legal.ts +43 -0
  344. package/src/server/location-header.ts +37 -0
  345. package/src/server/mail-brand.ts +43 -0
  346. package/src/server/mail-health.ts +41 -0
  347. package/src/server/mail-send.ts +44 -0
  348. package/src/server/mail-test-actions.ts +67 -0
  349. package/src/server/mail-test.ts +56 -0
  350. package/src/server/markdown-pipeline.ts +31 -0
  351. package/src/server/marketplace-actions.ts +47 -0
  352. package/src/server/marketplace-admin.ts +260 -0
  353. package/src/server/mention-actions.ts +21 -0
  354. package/src/server/mention-search.ts +35 -0
  355. package/src/server/message-actions.ts +110 -0
  356. package/src/server/messages.ts +141 -0
  357. package/src/server/modcp.ts +125 -0
  358. package/src/server/moderation-actions.ts +165 -0
  359. package/src/server/navigation-admin-actions.ts +217 -0
  360. package/src/server/navigation.ts +112 -0
  361. package/src/server/nonce.ts +13 -0
  362. package/src/server/notification-actions.ts +84 -0
  363. package/src/server/notification-audience.ts +10 -0
  364. package/src/server/notification-menu-actions.ts +82 -0
  365. package/src/server/notification-menu.ts +96 -0
  366. package/src/server/notifications.ts +93 -0
  367. package/src/server/onboarding-actions.ts +27 -0
  368. package/src/server/onboarding.ts +89 -0
  369. package/src/server/page-metadata.ts +14 -0
  370. package/src/server/passkey-challenge.ts +42 -0
  371. package/src/server/plugin-admin-actions.ts +232 -0
  372. package/src/server/plugin-admin.ts +276 -0
  373. package/src/server/plugin-host.ts +143 -0
  374. package/src/server/plugin-pages.ts +192 -0
  375. package/src/server/plugin-panel.ts +49 -0
  376. package/src/server/plugin-routes.ts +298 -0
  377. package/src/server/plugin-view.tsx +58 -0
  378. package/src/server/poll-actions.ts +104 -0
  379. package/src/server/poll-scope.ts +52 -0
  380. package/src/server/post-notifications.ts +71 -0
  381. package/src/server/post-scope.ts +86 -0
  382. package/src/server/presence.ts +177 -0
  383. package/src/server/profile-fields.ts +112 -0
  384. package/src/server/push-actions.ts +113 -0
  385. package/src/server/push.ts +40 -0
  386. package/src/server/redirect-back.ts +15 -0
  387. package/src/server/registration.ts +7 -0
  388. package/src/server/relation-actions.ts +89 -0
  389. package/src/server/relations.ts +45 -0
  390. package/src/server/reply-core.ts +173 -0
  391. package/src/server/report-actions.ts +165 -0
  392. package/src/server/report-scope.ts +21 -0
  393. package/src/server/reputation-actions.ts +146 -0
  394. package/src/server/reputation-target.ts +49 -0
  395. package/src/server/reputation.ts +70 -0
  396. package/src/server/request-fingerprint.ts +65 -0
  397. package/src/server/safe-path.ts +16 -0
  398. package/src/server/same-origin.ts +45 -0
  399. package/src/server/search-page.ts +313 -0
  400. package/src/server/search.ts +45 -0
  401. package/src/server/secret-auth.ts +20 -0
  402. package/src/server/see-other.ts +11 -0
  403. package/src/server/seed-board.ts +417 -0
  404. package/src/server/session-actions.ts +78 -0
  405. package/src/server/session-cookies.ts +124 -0
  406. package/src/server/session-key.ts +17 -0
  407. package/src/server/settings.ts +47 -0
  408. package/src/server/signatures.ts +62 -0
  409. package/src/server/sso-handshake.ts +79 -0
  410. package/src/server/stats.ts +63 -0
  411. package/src/server/subscription-actions.ts +161 -0
  412. package/src/server/surgery-actions.ts +180 -0
  413. package/src/server/syndication.ts +77 -0
  414. package/src/server/system-admin-actions.ts +134 -0
  415. package/src/server/system-admin.ts +90 -0
  416. package/src/server/test-container.ts +114 -0
  417. package/src/server/theme-admin-actions.ts +242 -0
  418. package/src/server/theme-admin.ts +203 -0
  419. package/src/server/theme-runtime.ts +89 -0
  420. package/src/server/theme-style.ts +279 -0
  421. package/src/server/theme.ts +52 -0
  422. package/src/server/thread-core.ts +207 -0
  423. package/src/server/thread-rating-actions.ts +55 -0
  424. package/src/server/thread-tool-actions.ts +154 -0
  425. package/src/server/two-factor-actions.ts +178 -0
  426. package/src/server/two-factor.ts +86 -0
  427. package/src/server/upgrade-notice.ts +41 -0
  428. package/src/server/user-admin-actions.ts +572 -0
  429. package/src/server/user-admin.ts +165 -0
  430. package/src/server/usercp-actions.ts +263 -0
  431. package/src/server/usercp-mail.ts +120 -0
  432. package/src/server/viewer-preferences.ts +64 -0
  433. package/src/server/warning-actions.ts +122 -0
  434. package/src/styles/globals.css +969 -0
  435. package/src/theme/contract.fixture.ts +793 -0
  436. package/src/view/account-copy.ts +351 -0
  437. package/src/view/admin-content-copy.ts +66 -0
  438. package/src/view/admin-copy.ts +106 -0
  439. package/src/view/admin-forum-tree-copy.ts +40 -0
  440. package/src/view/admin-group-copy.ts +80 -0
  441. package/src/view/admin-log.ts +58 -0
  442. package/src/view/admin-nav.ts +164 -0
  443. package/src/view/admin-navigation-copy.ts +58 -0
  444. package/src/view/admin-panel-copy.ts +194 -0
  445. package/src/view/admin-settings.ts +173 -0
  446. package/src/view/admin-theme-copy.ts +229 -0
  447. package/src/view/admin-user-copy.ts +96 -0
  448. package/src/view/attachments.ts +51 -0
  449. package/src/view/auth-copy.ts +147 -0
  450. package/src/view/board-index.ts +167 -0
  451. package/src/view/board-latest.ts +101 -0
  452. package/src/view/breadcrumb.ts +50 -0
  453. package/src/view/contacts.ts +85 -0
  454. package/src/view/content-copy.ts +118 -0
  455. package/src/view/contrast.ts +286 -0
  456. package/src/view/copy.ts +101 -0
  457. package/src/view/count.ts +8 -0
  458. package/src/view/discovery-view.ts +74 -0
  459. package/src/view/editor-toolbar.ts +84 -0
  460. package/src/view/error-notice.ts +52 -0
  461. package/src/view/feed.ts +130 -0
  462. package/src/view/forum-cursor.ts +54 -0
  463. package/src/view/forum-display.ts +152 -0
  464. package/src/view/forum-jump.ts +60 -0
  465. package/src/view/forum-notice.ts +24 -0
  466. package/src/view/inline-moderation.ts +87 -0
  467. package/src/view/install-copy.ts +95 -0
  468. package/src/view/legal.ts +58 -0
  469. package/src/view/locale.ts +40 -0
  470. package/src/view/marketplace-panel.ts +51 -0
  471. package/src/view/member-identity.ts +23 -0
  472. package/src/view/member-profile.ts +65 -0
  473. package/src/view/messages.ts +222 -0
  474. package/src/view/metadata.ts +75 -0
  475. package/src/view/modcp-nav.ts +76 -0
  476. package/src/view/moderation-copy.ts +84 -0
  477. package/src/view/moderation-queue.ts +67 -0
  478. package/src/view/navigation-arrange.ts +188 -0
  479. package/src/view/navigation.ts +173 -0
  480. package/src/view/notification-menu.ts +202 -0
  481. package/src/view/notifications.ts +84 -0
  482. package/src/view/onboarding.ts +26 -0
  483. package/src/view/pager.ts +191 -0
  484. package/src/view/panel-nav.ts +203 -0
  485. package/src/view/plugin-panel.ts +42 -0
  486. package/src/view/post-form.ts +61 -0
  487. package/src/view/post-link.ts +29 -0
  488. package/src/view/presence.ts +99 -0
  489. package/src/view/promotion-rules.ts +52 -0
  490. package/src/view/redirect-notice.ts +21 -0
  491. package/src/view/reputation.ts +107 -0
  492. package/src/view/search-controls.ts +103 -0
  493. package/src/view/search-results.ts +291 -0
  494. package/src/view/security-activity.ts +73 -0
  495. package/src/view/setting-groups.ts +43 -0
  496. package/src/view/shell.ts +164 -0
  497. package/src/view/slug-id.ts +12 -0
  498. package/src/view/sso-hand-off.ts +44 -0
  499. package/src/view/sso-notices.ts +48 -0
  500. package/src/view/subscriptions.ts +76 -0
  501. package/src/view/theme-draft.ts +167 -0
  502. package/src/view/theme-preference.ts +30 -0
  503. package/src/view/theme-tokens.ts +333 -0
  504. package/src/view/thread-view.ts +280 -0
  505. package/src/view/time.ts +31 -0
  506. package/src/view/timezone.ts +35 -0
  507. package/src/view/two-factor.ts +6 -0
  508. package/src/view/usercp-nav.ts +103 -0
  509. package/src/view/usercp.ts +196 -0
  510. package/src/view/warnings.ts +128 -0
  511. package/src/view/word-filter.ts +5 -0
@@ -0,0 +1,171 @@
1
+ 'use client'
2
+
3
+ import { type Copy, fromCopy } from '../shell/copy-record'
4
+
5
+ export interface PasskeyProblem {
6
+ readonly message: string
7
+ }
8
+
9
+ function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {
10
+ const padded = value.replace(/-/g, '+').replace(/_/g, '/')
11
+ const binary = atob(padded.padEnd(Math.ceil(padded.length / 4) * 4, '='))
12
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0))
13
+ }
14
+
15
+ function bytesToBase64Url(buffer: ArrayBuffer): string {
16
+ let binary = ''
17
+ for (const byte of new Uint8Array(buffer)) binary += String.fromCharCode(byte)
18
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
19
+ }
20
+
21
+ export function passkeysAvailable(): boolean {
22
+ return (
23
+ typeof window !== 'undefined' &&
24
+ typeof window.PublicKeyCredential === 'function' &&
25
+ typeof navigator.credentials?.create === 'function'
26
+ )
27
+ }
28
+
29
+ async function readProblem(response: Response, copy: Copy): Promise<string> {
30
+ try {
31
+ const body = (await response.json()) as { error?: { message?: string } }
32
+ return body.error?.message ?? fromCopy(copy, 'authForm.passkey.failed')
33
+ } catch {
34
+ return fromCopy(copy, 'authForm.passkey.failed')
35
+ }
36
+ }
37
+
38
+ type Purpose = 'register' | 'authenticate' | 'second-factor' | 'credential-proof'
39
+
40
+ async function options(purpose: Purpose, copy: Copy): Promise<Record<string, unknown>> {
41
+ const response = await fetch(`/auth/passkey/options?for=${purpose}`, {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json' },
44
+ body: '{}',
45
+ })
46
+
47
+ if (!response.ok) throw new Error(await readProblem(response, copy))
48
+ return (await response.json()) as Record<string, unknown>
49
+ }
50
+
51
+ async function submit(
52
+ purpose: Purpose,
53
+ body: Record<string, unknown>,
54
+ copy: Copy,
55
+ ): Promise<Record<string, unknown>> {
56
+ const response = await fetch(`/auth/passkey/verify?for=${purpose}`, {
57
+ method: 'POST',
58
+ headers: { 'content-type': 'application/json' },
59
+ body: JSON.stringify(body),
60
+ })
61
+
62
+ if (!response.ok) throw new Error(await readProblem(response, copy))
63
+ return (await response.json()) as Record<string, unknown>
64
+ }
65
+
66
+ export async function enrolPasskey(label: string, copy: Copy): Promise<void> {
67
+ const issued = await options('register', copy)
68
+
69
+ const credential = (await navigator.credentials.create({
70
+ publicKey: {
71
+ challenge: base64UrlToBytes(issued.challenge as string),
72
+ rp: issued.rp as PublicKeyCredentialRpEntity,
73
+ user: {
74
+ ...(issued.user as { name: string; displayName: string }),
75
+ id: base64UrlToBytes((issued.user as { id: string }).id),
76
+ },
77
+ pubKeyCredParams: issued.pubKeyCredParams as PublicKeyCredentialParameters[],
78
+ excludeCredentials: (issued.excludeCredentials as { id: string }[]).map((entry) => ({
79
+ type: 'public-key' as const,
80
+ id: base64UrlToBytes(entry.id),
81
+ })),
82
+ authenticatorSelection: issued.authenticatorSelection as AuthenticatorSelectionCriteria,
83
+ timeout: issued.timeout as number,
84
+ attestation: 'none',
85
+ },
86
+ })) as PublicKeyCredential | null
87
+
88
+ if (credential === null) throw new Error(fromCopy(copy, 'authForm.passkey.noneCreated'))
89
+
90
+ const response = credential.response as AuthenticatorAttestationResponse
91
+
92
+ await submit(
93
+ 'register',
94
+ {
95
+ label,
96
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
97
+ attestationObject: bytesToBase64Url(response.attestationObject),
98
+ transports: typeof response.getTransports === 'function' ? response.getTransports() : [],
99
+ },
100
+ copy,
101
+ )
102
+ }
103
+
104
+ export async function signInWithPasskey(next: string | undefined, copy: Copy): Promise<string> {
105
+ return assertPasskey('authenticate', next, copy)
106
+ }
107
+
108
+ /**
109
+ * The same exchange, against a sign-in that has already given its password.
110
+ * The board scopes the challenge to that member's own credentials, so the
111
+ * browser offers only the keys that could finish this particular sign-in.
112
+ */
113
+ export async function confirmWithPasskey(next: string | undefined, copy: Copy): Promise<string> {
114
+ return assertPasskey('second-factor', next, copy)
115
+ }
116
+
117
+ export async function proveWithPasskey(next: string | undefined, copy: Copy): Promise<string> {
118
+ return assertPasskey('credential-proof', next, copy)
119
+ }
120
+
121
+ async function assertPasskey(
122
+ purpose: Exclude<Purpose, 'register'>,
123
+ next: string | undefined,
124
+ copy: Copy,
125
+ ): Promise<string> {
126
+ const issued = await options(purpose, copy)
127
+
128
+ const allow = issued.allowCredentials as { id: string }[] | undefined
129
+
130
+ const credential = (await navigator.credentials.get({
131
+ publicKey: {
132
+ challenge: base64UrlToBytes(issued.challenge as string),
133
+ rpId: issued.rpId as string,
134
+ userVerification: issued.userVerification as UserVerificationRequirement,
135
+ timeout: issued.timeout as number,
136
+ ...(allow === undefined
137
+ ? {}
138
+ : {
139
+ allowCredentials: allow.map((entry) => ({
140
+ type: 'public-key' as const,
141
+ id: base64UrlToBytes(entry.id),
142
+ })),
143
+ }),
144
+ },
145
+ })) as PublicKeyCredential | null
146
+
147
+ if (credential === null) throw new Error(fromCopy(copy, 'authForm.passkey.noneOffered'))
148
+
149
+ const response = credential.response as AuthenticatorAssertionResponse
150
+
151
+ const outcome = await submit(
152
+ purpose,
153
+ {
154
+ id: credential.id,
155
+ clientDataJSON: bytesToBase64Url(response.clientDataJSON),
156
+ authenticatorData: bytesToBase64Url(response.authenticatorData),
157
+ signature: bytesToBase64Url(response.signature),
158
+ ...(next === undefined ? {} : { next }),
159
+ },
160
+ copy,
161
+ )
162
+
163
+ return typeof outcome.next === 'string' ? outcome.next : '/'
164
+ }
165
+
166
+ export function passkeyMessage(error: unknown, copy: Copy): string {
167
+ if (error instanceof DOMException && error.name === 'NotAllowedError') {
168
+ return fromCopy(copy, 'authForm.passkey.cancelled')
169
+ }
170
+ return error instanceof Error ? error.message : fromCopy(copy, 'authForm.passkey.failed')
171
+ }
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { Alert, AlertDescription, Separator } from '@meith/ui'
6
+ import { Button } from '@meith/ui/button'
7
+
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+ import { confirmWithPasskey, passkeyMessage, passkeysAvailable } from './passkey-client'
10
+
11
+ export function PasskeySecondFactor({
12
+ next,
13
+ copy,
14
+ }: {
15
+ readonly next?: string | undefined
16
+ readonly copy: Copy
17
+ }) {
18
+ const [available, setAvailable] = useState(false)
19
+ const [busy, setBusy] = useState(false)
20
+ const [error, setError] = useState<string | null>(null)
21
+
22
+ useEffect(() => {
23
+ setAvailable(passkeysAvailable())
24
+ }, [])
25
+
26
+ if (!available) return null
27
+
28
+ return (
29
+ <div className="flex flex-col gap-3">
30
+ <div className="flex items-center gap-3">
31
+ <Separator className="flex-1" />
32
+ <span className="text-xs uppercase tracking-wide text-muted-foreground">
33
+ {fromCopy(copy, 'authForm.or')}
34
+ </span>
35
+ <Separator className="flex-1" />
36
+ </div>
37
+
38
+ {error === null ? null : (
39
+ <Alert tone="error">
40
+ <AlertDescription>{error}</AlertDescription>
41
+ </Alert>
42
+ )}
43
+
44
+ <Button
45
+ type="button"
46
+ variant="outline"
47
+ size="lg"
48
+ className="w-full"
49
+ disabled={busy}
50
+ onClick={() => {
51
+ setError(null)
52
+ setBusy(true)
53
+ confirmWithPasskey(next, copy)
54
+ .then((destination) => {
55
+ window.location.assign(destination)
56
+ })
57
+ .catch((problem: unknown) => {
58
+ setError(passkeyMessage(problem, copy))
59
+ setBusy(false)
60
+ })
61
+ }}
62
+ >
63
+ {busy
64
+ ? fromCopy(copy, 'authForm.passkey.waiting')
65
+ : fromCopy(copy, 'authForm.passkey.confirm')}
66
+ </Button>
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { Alert, AlertDescription } from '@meith/ui'
6
+ import { Button } from '@meith/ui/button'
7
+
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+ import { passkeyMessage, passkeysAvailable, signInWithPasskey } from './passkey-client'
10
+
11
+ export function PasskeySignIn({
12
+ next,
13
+ copy,
14
+ }: {
15
+ readonly next?: string | undefined
16
+ readonly copy: Copy
17
+ }) {
18
+ const [available, setAvailable] = useState(false)
19
+ const [busy, setBusy] = useState(false)
20
+ const [error, setError] = useState<string | null>(null)
21
+
22
+ useEffect(() => {
23
+ setAvailable(passkeysAvailable())
24
+ }, [])
25
+
26
+ if (!available) return null
27
+
28
+ return (
29
+ <div className="flex flex-col gap-2">
30
+ {error === null ? null : (
31
+ <Alert tone="error">
32
+ <AlertDescription>{error}</AlertDescription>
33
+ </Alert>
34
+ )}
35
+
36
+ <Button
37
+ type="button"
38
+ variant="outline"
39
+ size="lg"
40
+ className="w-full"
41
+ disabled={busy}
42
+ onClick={() => {
43
+ setError(null)
44
+ setBusy(true)
45
+ signInWithPasskey(next, copy)
46
+ .then((destination) => {
47
+ window.location.assign(destination)
48
+ })
49
+ .catch((problem: unknown) => {
50
+ setError(passkeyMessage(problem, copy))
51
+ setBusy(false)
52
+ })
53
+ }}
54
+ >
55
+ {busy
56
+ ? fromCopy(copy, 'authForm.passkey.waiting')
57
+ : fromCopy(copy, 'authForm.passkey.signIn')}
58
+ </Button>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,136 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { registerAction } from '@/server/auth-actions'
6
+ import { EMPTY_STATE } from '@/server/auth-form-state'
7
+
8
+ import { CustomField, type CustomFieldInput } from '../profile/custom-field'
9
+ import { type Copy, fromCopy } from '../shell/copy'
10
+ import { Field, FormError, SubmitButton } from './form-controls'
11
+
12
+ const CONTROL =
13
+ 'h-10 rounded-md border border-input bg-background px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40'
14
+
15
+ export interface ChallengeInput {
16
+ readonly prompt: string | null
17
+ readonly token: string
18
+ readonly honeypot: boolean
19
+ readonly issuedAt: number
20
+ }
21
+
22
+ export interface RegistrationLimits {
23
+ readonly minPasswordLength: number
24
+ readonly usernameMin: number
25
+ readonly usernameMax: number
26
+ }
27
+
28
+ export interface TermsInput {
29
+ readonly label: string
30
+ readonly href: string
31
+ }
32
+
33
+ export function RegisterForm({
34
+ customFields = [],
35
+ challenge,
36
+ limits,
37
+ terms = null,
38
+ copy,
39
+ }: {
40
+ customFields?: readonly CustomFieldInput[]
41
+ challenge?: ChallengeInput
42
+ limits: RegistrationLimits
43
+ terms?: TermsInput | null | undefined
44
+ copy: Copy
45
+ }) {
46
+ const [state, action] = useActionState(registerAction, EMPTY_STATE)
47
+ return (
48
+ <form action={action} className="flex flex-col gap-4" noValidate>
49
+ <FormError message={state.error} />
50
+ <Field
51
+ label={fromCopy(copy, 'authForm.register.username')}
52
+ name="username"
53
+ autoComplete="username"
54
+ minLength={limits.usernameMin}
55
+ maxLength={limits.usernameMax}
56
+ defaultValue={state.values?.username}
57
+ hint={fromCopy(copy, 'authForm.register.usernameHint')}
58
+ />
59
+ <Field
60
+ label={fromCopy(copy, 'authForm.email')}
61
+ name="email"
62
+ type="email"
63
+ autoComplete="email"
64
+ defaultValue={state.values?.email}
65
+ />
66
+ <Field
67
+ label={fromCopy(copy, 'authForm.register.password')}
68
+ name="password"
69
+ type="password"
70
+ autoComplete="new-password"
71
+ minLength={limits.minPasswordLength}
72
+ hint={fromCopy(copy, 'authForm.passwordHint')}
73
+ />
74
+ {customFields.map((field) => (
75
+ <CustomField key={field.key} field={field} className={CONTROL} />
76
+ ))}
77
+
78
+ {challenge !== undefined && (
79
+ <>
80
+ <input type="hidden" name="_issued" value={challenge.issuedAt} />
81
+ <input type="hidden" name="_challenge" value={challenge.token} />
82
+
83
+ {challenge.honeypot && (
84
+ <div aria-hidden="true" className="hidden">
85
+ <label htmlFor="contact_url">{fromCopy(copy, 'authForm.register.honeypot')}</label>
86
+ <input
87
+ id="contact_url"
88
+ name="contact_url"
89
+ type="text"
90
+ tabIndex={-1}
91
+ autoComplete="off"
92
+ defaultValue=""
93
+ />
94
+ </div>
95
+ )}
96
+
97
+ {challenge.prompt !== null && (
98
+ <Field
99
+ label={challenge.prompt}
100
+ name="challenge_answer"
101
+ autoComplete="off"
102
+ hint={fromCopy(copy, 'authForm.register.challengeHint')}
103
+ />
104
+ )}
105
+ </>
106
+ )}
107
+
108
+ {terms !== null && (
109
+ <label className="flex items-start gap-2 text-sm text-muted-foreground">
110
+ <input
111
+ type="checkbox"
112
+ name="terms"
113
+ value="1"
114
+ required
115
+ defaultChecked={state.values?.terms === '1'}
116
+ className="mt-0.5 size-4 rounded border-input accent-primary"
117
+ />
118
+ <span>
119
+ {copy['authForm.register.termsLead']}
120
+ <a
121
+ href={terms.href}
122
+ target="_blank"
123
+ rel="noreferrer"
124
+ className="font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
125
+ >
126
+ {terms.label}
127
+ </a>
128
+ {copy['authForm.register.termsTail']}
129
+ </span>
130
+ </label>
131
+ )}
132
+
133
+ <SubmitButton>{fromCopy(copy, 'authForm.register.submit')}</SubmitButton>
134
+ </form>
135
+ )
136
+ }
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { resendVerificationAction } from '@/server/auth-actions'
6
+ import { EMPTY_STATE } from '@/server/auth-form-state'
7
+
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+ import { Field, FormError, FormNotice, SubmitButton } from './form-controls'
10
+
11
+ export function ResendVerificationForm({
12
+ email,
13
+ copy,
14
+ }: {
15
+ email?: string | undefined
16
+ copy: Copy
17
+ }) {
18
+ const [state, action] = useActionState(resendVerificationAction, EMPTY_STATE)
19
+
20
+ return (
21
+ <form action={action} className="flex flex-col gap-4" noValidate>
22
+ <FormNotice message={state.notice} />
23
+ <FormError message={state.error} />
24
+ <Field
25
+ label={fromCopy(copy, 'authForm.email')}
26
+ name="email"
27
+ type="email"
28
+ autoComplete="email"
29
+ defaultValue={state.values?.email ?? email}
30
+ />
31
+ <SubmitButton>{fromCopy(copy, 'authForm.resend.submit')}</SubmitButton>
32
+ </form>
33
+ )
34
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { confirmResetAction } from '@/server/auth-actions'
6
+ import { EMPTY_STATE } from '@/server/auth-form-state'
7
+
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+ import { Field, FormError, SubmitButton } from './form-controls'
10
+
11
+ export function ResetConfirmForm({
12
+ token,
13
+ minLength,
14
+ copy,
15
+ }: {
16
+ token: string
17
+ minLength: number
18
+ copy: Copy
19
+ }) {
20
+ const [state, action] = useActionState(confirmResetAction, EMPTY_STATE)
21
+ return (
22
+ <form action={action} className="flex flex-col gap-4" noValidate>
23
+ <FormError message={state.error} />
24
+ <input type="hidden" name="token" value={token} />
25
+ <Field
26
+ label={fromCopy(copy, 'authForm.resetConfirm.password')}
27
+ name="password"
28
+ type="password"
29
+ autoComplete="new-password"
30
+ minLength={minLength}
31
+ hint={fromCopy(copy, 'authForm.passwordHint')}
32
+ />
33
+ <Field
34
+ label={fromCopy(copy, 'authForm.resetConfirm.confirm')}
35
+ name="confirm"
36
+ type="password"
37
+ autoComplete="new-password"
38
+ minLength={minLength}
39
+ />
40
+ <SubmitButton>{fromCopy(copy, 'authForm.resetConfirm.submit')}</SubmitButton>
41
+ </form>
42
+ )
43
+ }
@@ -0,0 +1,35 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { requestResetAction } from '@/server/auth-actions'
6
+ import { EMPTY_STATE } from '@/server/auth-form-state'
7
+
8
+ import { type Copy, fromCopy } from '../shell/copy'
9
+ import { Field, FormError, FormNotice, SubmitButton } from './form-controls'
10
+
11
+ export function ResetRequestForm({ copy }: { copy: Copy }) {
12
+ const [state, action] = useActionState(requestResetAction, EMPTY_STATE)
13
+ const devToken = state.values?.devToken
14
+ return (
15
+ <form action={action} className="flex flex-col gap-4" noValidate>
16
+ <FormNotice message={state.notice} />
17
+ <FormError message={state.error} />
18
+ {devToken ? (
19
+ <a
20
+ href={`/reset/confirm?token=${devToken}`}
21
+ className="rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground underline underline-offset-2"
22
+ >
23
+ {fromCopy(copy, 'authForm.reset.continue')}
24
+ </a>
25
+ ) : null}
26
+ <Field
27
+ label={fromCopy(copy, 'authForm.email')}
28
+ name="email"
29
+ type="email"
30
+ autoComplete="email"
31
+ />
32
+ <SubmitButton>{fromCopy(copy, 'authForm.reset.submit')}</SubmitButton>
33
+ </form>
34
+ )
35
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { useActionState } from 'react'
4
+
5
+ import { Button } from '@meith/ui/button'
6
+
7
+ import { abandonSecondFactorAction, verifySecondFactorAction } from '@/server/auth-actions'
8
+ import { EMPTY_STATE } from '@/server/auth-form-state'
9
+
10
+ import { type Copy, fromCopy } from '../shell/copy'
11
+ import { Field, FormError, SubmitButton } from './form-controls'
12
+
13
+ export function SecondFactorForm({
14
+ next,
15
+ recoveryCodesLeft,
16
+ copy,
17
+ }: {
18
+ readonly next?: string | undefined
19
+ readonly recoveryCodesLeft: number
20
+ readonly copy: Copy
21
+ }) {
22
+ const [state, action] = useActionState(verifySecondFactorAction, EMPTY_STATE)
23
+
24
+ return (
25
+ <div className="flex flex-col gap-4">
26
+ <form action={action} className="flex flex-col gap-4" noValidate>
27
+ <FormError message={state.error} />
28
+
29
+ <Field
30
+ label={fromCopy(copy, 'authForm.secondFactor.code')}
31
+ name="code"
32
+ autoComplete="one-time-code"
33
+ hint={
34
+ recoveryCodesLeft > 0
35
+ ? fromCopy(copy, 'authForm.secondFactor.recoveryHint')
36
+ : fromCopy(copy, 'authForm.secondFactor.recoveryHintExhausted')
37
+ }
38
+ />
39
+
40
+ {next ? <input type="hidden" name="next" value={next} /> : null}
41
+ <SubmitButton>{fromCopy(copy, 'authForm.secondFactor.submit')}</SubmitButton>
42
+ </form>
43
+
44
+ <form action={abandonSecondFactorAction}>
45
+ <Button type="submit" variant="ghost" size="sm">
46
+ {fromCopy(copy, 'authForm.secondFactor.cancel')}
47
+ </Button>
48
+ </form>
49
+ </div>
50
+ )
51
+ }
@@ -0,0 +1,47 @@
1
+ import { buttonVariants, Separator } from '@meith/ui'
2
+
3
+ import type { SsoButtonModel } from '@/view/auth-copy'
4
+
5
+ import { type Copy, fromCopy } from '../shell/copy-record'
6
+
7
+ export function SsoButtons({
8
+ providers,
9
+ next,
10
+ lede,
11
+ copy,
12
+ }: {
13
+ readonly providers: readonly SsoButtonModel[]
14
+ readonly next?: string | undefined
15
+ readonly lede?: string | undefined
16
+ readonly copy: Copy
17
+ }) {
18
+ if (providers.length === 0) return null
19
+
20
+ return (
21
+ <div className="flex flex-col gap-3">
22
+ <div className="flex flex-col gap-2">
23
+ {providers.map((provider) => (
24
+ <form key={provider.id} method="post" action={`/auth/sso/${provider.id}`}>
25
+ {next ? <input type="hidden" name="next" value={next} /> : null}
26
+ <button
27
+ type="submit"
28
+ className={buttonVariants({ variant: 'outline', size: 'lg', className: 'w-full' })}
29
+ >
30
+ {provider.cta}
31
+ </button>
32
+ </form>
33
+ ))}
34
+ </div>
35
+
36
+ <p className="text-xs text-muted-foreground">{lede ?? fromCopy(copy, 'authForm.sso.lede')}</p>
37
+
38
+ <div className="flex items-center gap-3">
39
+ <Separator className="flex-1" />
40
+ <span className="text-xs uppercase tracking-wide text-muted-foreground">
41
+ {fromCopy(copy, 'authForm.or')}
42
+ </span>
43
+ <Separator className="flex-1" />
44
+ </div>
45
+ </div>
46
+ )
47
+ }