@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,417 @@
1
+ import type { MemberProfileRecord } from '@meith/accounts'
2
+ import type { GroupDefaults, MemoryBoard } from '@meith/authorization'
3
+ import { emptyPermissionSet, type ForumPermissions, type PermissionSet } from '@meith/core'
4
+ import type { ForumListingRow } from '@meith/forums'
5
+ import { BodyFormat } from '@meith/markdown'
6
+ import type { PostListingRow } from '@meith/posts'
7
+ import type { ThreadListingRow } from '@meith/threads'
8
+
9
+ export const SEED_GROUP = {
10
+ guest: 1,
11
+ registered: 2,
12
+ administrators: 3,
13
+ superModerators: 4,
14
+ moderators: 5,
15
+ awaitingActivation: 6,
16
+ banned: 7,
17
+ } as const
18
+
19
+ export const FIXTURE_DATA_VERSION = 4
20
+
21
+ export const SEED_FORUM = {
22
+ main: 10,
23
+ announcements: 100,
24
+ general: 200,
25
+ generalOffTopic: 201,
26
+ } as const
27
+
28
+ function group(over: Partial<PermissionSet>): PermissionSet {
29
+ return { ...emptyPermissionSet(), ...over }
30
+ }
31
+
32
+ const READ = {
33
+ canView: true,
34
+ canViewThreads: true,
35
+ canViewOthersThreads: true,
36
+ canSearch: true,
37
+ canViewProfiles: true,
38
+ canDownloadAttachments: true,
39
+ } as const
40
+
41
+ const POST = {
42
+ ...READ,
43
+ canPostThreads: true,
44
+ canPostReplies: true,
45
+ canSubscribe: true,
46
+ canEditOwnPosts: true,
47
+ canDeleteOwnPosts: true,
48
+ canReportContent: true,
49
+ canUploadAvatar: true,
50
+ canUsePrivateMessages: true,
51
+ privateMessageQuota: 100,
52
+ canGiveReputation: true,
53
+ maxReputationPerDay: 10,
54
+ requiresThreadApproval: false,
55
+ requiresPostApproval: false,
56
+ requiresApprovalOnEdit: false,
57
+ } as const
58
+
59
+ const GROUPS: GroupDefaults[] = [
60
+ { groupId: SEED_GROUP.guest, permissions: group(READ) },
61
+ {
62
+ groupId: SEED_GROUP.registered,
63
+ permissions: group({ ...POST, canUploadAttachments: true }),
64
+ },
65
+ {
66
+ groupId: SEED_GROUP.administrators,
67
+ permissions: group({
68
+ ...POST,
69
+ canUploadAttachments: true,
70
+ isAdministrator: true,
71
+ canAccessAdminCp: true,
72
+ canAccessModCp: true,
73
+ }),
74
+ },
75
+ {
76
+ groupId: SEED_GROUP.superModerators,
77
+ permissions: group({
78
+ ...POST,
79
+ canUploadAttachments: true,
80
+ isSuperModerator: true,
81
+ canAccessModCp: true,
82
+ }),
83
+ },
84
+ ]
85
+
86
+ const ANNOUNCEMENT_READONLY: Partial<ForumPermissions> = {
87
+ canPostThreads: false,
88
+ canPostReplies: false,
89
+ }
90
+
91
+ export const SEED_BOARD: MemoryBoard = {
92
+ groups: GROUPS,
93
+ chains: {
94
+ [SEED_FORUM.main]: [SEED_FORUM.main],
95
+ [SEED_FORUM.announcements]: [SEED_FORUM.announcements, SEED_FORUM.main],
96
+ [SEED_FORUM.general]: [SEED_FORUM.general, SEED_FORUM.main],
97
+ [SEED_FORUM.generalOffTopic]: [SEED_FORUM.generalOffTopic, SEED_FORUM.general, SEED_FORUM.main],
98
+ },
99
+ overrides: [
100
+ {
101
+ forumId: SEED_FORUM.announcements,
102
+ groupId: SEED_GROUP.guest,
103
+ overrides: ANNOUNCEMENT_READONLY,
104
+ },
105
+ {
106
+ forumId: SEED_FORUM.announcements,
107
+ groupId: SEED_GROUP.registered,
108
+ overrides: ANNOUNCEMENT_READONLY,
109
+ },
110
+ ],
111
+ }
112
+
113
+ export const SEED_FORUM_ROWS: readonly ForumListingRow[] = [
114
+ {
115
+ id: SEED_FORUM.main,
116
+ type: 'category',
117
+ allowThreads: false,
118
+ title: 'Main',
119
+ slug: 'main',
120
+ description: null,
121
+ parentId: null,
122
+ path: '10',
123
+ depth: 0,
124
+ displayOrder: 1,
125
+ linkUrl: null,
126
+ threadCount: 3,
127
+ postCount: 6,
128
+ lastPost: {
129
+ postId: 143,
130
+ threadId: 22,
131
+ threadTitle: 'What are you reading this week?',
132
+ userId: null,
133
+ username: 'departed',
134
+ at: new Date('2026-07-30T08:41:00Z'),
135
+ },
136
+ },
137
+ {
138
+ id: SEED_FORUM.announcements,
139
+ type: 'forum',
140
+ allowThreads: true,
141
+ title: 'Announcements',
142
+ slug: 'announcements',
143
+ description: 'Board news and release notes. Staff post, everyone reads.',
144
+ parentId: SEED_FORUM.main,
145
+ path: '10.100',
146
+ depth: 1,
147
+ displayOrder: 1,
148
+ linkUrl: null,
149
+ threadCount: 1,
150
+ postCount: 2,
151
+ lastPost: {
152
+ postId: 11,
153
+ threadId: 4,
154
+ threadTitle: 'Version 0.1 is live',
155
+ userId: 1,
156
+ username: 'admin',
157
+ at: new Date('2026-07-29T14:05:00Z'),
158
+ },
159
+ },
160
+ {
161
+ id: SEED_FORUM.general,
162
+ type: 'forum',
163
+ allowThreads: true,
164
+ title: 'General Discussion',
165
+ slug: 'general',
166
+ description: 'Anything and everything.',
167
+ parentId: SEED_FORUM.main,
168
+ path: '10.200',
169
+ depth: 1,
170
+ displayOrder: 2,
171
+ linkUrl: null,
172
+ threadCount: 2,
173
+ postCount: 4,
174
+ lastPost: {
175
+ postId: 143,
176
+ threadId: 22,
177
+ threadTitle: 'What are you reading this week?',
178
+ userId: null,
179
+ username: 'departed',
180
+ at: new Date('2026-07-30T08:41:00Z'),
181
+ },
182
+ },
183
+ {
184
+ id: SEED_FORUM.generalOffTopic,
185
+ type: 'forum',
186
+ allowThreads: true,
187
+ title: 'Off Topic',
188
+ slug: 'off-topic',
189
+ description: 'Everything else.',
190
+ parentId: SEED_FORUM.general,
191
+ path: '10.200.201',
192
+ depth: 2,
193
+ displayOrder: 1,
194
+ linkUrl: null,
195
+ threadCount: 0,
196
+ postCount: 0,
197
+ lastPost: null,
198
+ },
199
+ ]
200
+
201
+ export const SEED_THREAD_ROWS: readonly ThreadListingRow[] = [
202
+ {
203
+ id: 4,
204
+ forumId: SEED_FORUM.announcements,
205
+ title: 'Version 0.1 is live',
206
+ slug: 'version-0-1-is-live',
207
+ prefix: null,
208
+ authorUserId: 1,
209
+ authorUsername: 'admin',
210
+ replyCount: 1,
211
+ viewCount: 64,
212
+ ratingTotal: 0,
213
+ ratingCount: 0,
214
+ visibility: 'visible',
215
+ isSticky: false,
216
+ isLocked: false,
217
+ isMoved: false,
218
+ lastPost: {
219
+ postId: 11,
220
+ userId: 1,
221
+ username: 'admin',
222
+ at: new Date('2026-07-29T14:05:00Z'),
223
+ },
224
+ lastPostAt: new Date('2026-07-29T14:05:00Z'),
225
+ },
226
+ {
227
+ id: 22,
228
+ forumId: SEED_FORUM.general,
229
+ title: 'What are you reading this week?',
230
+ slug: 'what-are-you-reading-this-week',
231
+ prefix: { label: 'Weekly', token: null },
232
+ authorUserId: null,
233
+ authorUsername: 'departed',
234
+ replyCount: 1,
235
+ viewCount: 241,
236
+ ratingTotal: 17,
237
+ ratingCount: 4,
238
+ visibility: 'visible',
239
+ isSticky: true,
240
+ isLocked: false,
241
+ isMoved: false,
242
+ lastPost: {
243
+ postId: 143,
244
+ userId: null,
245
+ username: 'departed',
246
+ at: new Date('2026-07-30T08:41:00Z'),
247
+ },
248
+ lastPostAt: new Date('2026-07-30T08:41:00Z'),
249
+ },
250
+ {
251
+ id: 21,
252
+ forumId: SEED_FORUM.general,
253
+ title: 'Show us your desk setup',
254
+ slug: 'show-us-your-desk-setup',
255
+ prefix: null,
256
+ authorUserId: 1,
257
+ authorUsername: 'admin',
258
+ replyCount: 1,
259
+ viewCount: 116,
260
+ ratingTotal: 0,
261
+ ratingCount: 0,
262
+ visibility: 'visible',
263
+ isSticky: false,
264
+ isLocked: false,
265
+ isMoved: false,
266
+ lastPost: {
267
+ postId: 132,
268
+ userId: 1,
269
+ username: 'admin',
270
+ at: new Date('2026-07-29T17:18:00Z'),
271
+ },
272
+ lastPostAt: new Date('2026-07-29T17:18:00Z'),
273
+ },
274
+ ]
275
+
276
+ export const SEED_POST_ROWS: readonly PostListingRow[] = [
277
+ {
278
+ id: 10,
279
+ threadId: 4,
280
+ forumId: SEED_FORUM.announcements,
281
+ number: 1,
282
+ authorUserId: 1,
283
+ authorUsername: 'admin',
284
+ authorPostCount: 5,
285
+ authorJoinedAt: new Date('2026-01-01T00:00:00Z'),
286
+ message:
287
+ 'Welcome to the **new forum**. We are glad you are here.\n\n' +
288
+ 'The rules live in [Announcements](/100-announcements).',
289
+ messageHtml: null,
290
+ renderVersion: 0,
291
+ bodyFormat: BodyFormat.Markdown,
292
+ editedAt: null,
293
+ editedByUsername: null,
294
+ editReason: null,
295
+ isFirstPost: true,
296
+ visibility: 'visible',
297
+ createdAt: new Date('2026-07-29T09:00:00Z'),
298
+ },
299
+ {
300
+ id: 11,
301
+ threadId: 4,
302
+ forumId: SEED_FORUM.announcements,
303
+ number: 2,
304
+ authorUserId: 1,
305
+ authorUsername: 'admin',
306
+ authorPostCount: 5,
307
+ authorJoinedAt: new Date('2026-01-01T00:00:00Z'),
308
+ message: 'Thanks for joining us for the first release.',
309
+ messageHtml: null,
310
+ renderVersion: 0,
311
+ bodyFormat: BodyFormat.Markdown,
312
+ editedAt: null,
313
+ editedByUsername: null,
314
+ editReason: null,
315
+ isFirstPost: false,
316
+ visibility: 'visible',
317
+ createdAt: new Date('2026-07-29T14:05:00Z'),
318
+ },
319
+ {
320
+ id: 121,
321
+ threadId: 21,
322
+ forumId: SEED_FORUM.general,
323
+ number: 1,
324
+ authorUserId: 1,
325
+ authorUsername: 'admin',
326
+ authorPostCount: 5,
327
+ authorJoinedAt: new Date('2026-01-01T00:00:00Z'),
328
+ message: 'Show us the place where you make things.',
329
+ messageHtml: null,
330
+ renderVersion: 0,
331
+ bodyFormat: BodyFormat.Markdown,
332
+ editedAt: null,
333
+ editedByUsername: null,
334
+ editReason: null,
335
+ isFirstPost: true,
336
+ visibility: 'visible',
337
+ createdAt: new Date('2026-07-29T12:00:00Z'),
338
+ },
339
+ {
340
+ id: 132,
341
+ threadId: 21,
342
+ forumId: SEED_FORUM.general,
343
+ number: 2,
344
+ authorUserId: 1,
345
+ authorUsername: 'admin',
346
+ authorPostCount: 5,
347
+ authorJoinedAt: new Date('2026-01-01T00:00:00Z'),
348
+ message:
349
+ '> **[admin](/member/by-name/admin) wrote:** ' +
350
+ '[View post](/thread/21-show-us-your-desk-setup?post=121)\n>\n' +
351
+ '> Show us the place where you make things.\n\n' +
352
+ 'A standing desk and a notebook are all I need.',
353
+ messageHtml: null,
354
+ renderVersion: 0,
355
+ bodyFormat: BodyFormat.Markdown,
356
+ editedAt: null,
357
+ editedByUsername: null,
358
+ editReason: null,
359
+ isFirstPost: false,
360
+ visibility: 'visible',
361
+ createdAt: new Date('2026-07-29T17:18:00Z'),
362
+ },
363
+ {
364
+ id: 133,
365
+ threadId: 22,
366
+ forumId: SEED_FORUM.general,
367
+ number: 1,
368
+ authorUserId: 1,
369
+ authorUsername: 'admin',
370
+ authorPostCount: 5,
371
+ authorJoinedAt: new Date('2026-01-01T00:00:00Z'),
372
+ message: 'Tell us what you are reading this week.',
373
+ messageHtml: null,
374
+ renderVersion: 0,
375
+ bodyFormat: BodyFormat.Markdown,
376
+ editedAt: null,
377
+ editedByUsername: null,
378
+ editReason: null,
379
+ isFirstPost: true,
380
+ visibility: 'visible',
381
+ createdAt: new Date('2026-07-28T09:00:00Z'),
382
+ },
383
+ {
384
+ id: 143,
385
+ threadId: 22,
386
+ forumId: SEED_FORUM.general,
387
+ number: 2,
388
+ authorUserId: null,
389
+ authorUsername: 'departed',
390
+ authorPostCount: 0,
391
+ authorJoinedAt: null,
392
+ message: 'I just started a mystery novel.',
393
+ messageHtml: null,
394
+ renderVersion: 0,
395
+ bodyFormat: BodyFormat.Markdown,
396
+ editedAt: null,
397
+ editedByUsername: null,
398
+ editReason: null,
399
+ isFirstPost: false,
400
+ visibility: 'visible',
401
+ createdAt: new Date('2026-07-30T08:41:00Z'),
402
+ },
403
+ ]
404
+
405
+ export const SEED_MEMBER_PROFILES: readonly MemberProfileRecord[] = [
406
+ {
407
+ id: 1,
408
+ username: 'admin',
409
+ title: 'Administrators',
410
+ postCount: 5,
411
+ createdAt: new Date('2026-01-01T00:00:00Z'),
412
+ lastActiveAt: new Date('2026-07-30T08:41:00Z'),
413
+ location: 'The server room',
414
+ website: 'https://example.test/',
415
+ bio: 'Runs this board. Fixture data — nothing here is durable.',
416
+ },
417
+ ]
@@ -0,0 +1,78 @@
1
+ 'use server'
2
+
3
+ import { redirect } from 'next/navigation'
4
+
5
+ import { ForbiddenError } from '@meith/core'
6
+ import { msg } from '@meith/i18n'
7
+
8
+ import { recordAuthEvent } from './auth-events'
9
+ import type { FormState } from './auth-form-state'
10
+ import { getContainer } from './container'
11
+ import { getActor } from './context'
12
+ import { formStateReporter } from './form-state-reporter'
13
+ import { positiveInt } from './form-values'
14
+ import { tr } from './i18n'
15
+ import { readSessionToken } from './session-cookies'
16
+
17
+ const toFormState = formStateReporter(
18
+ 'session-actions',
19
+ 'unexpected error while signing a session out',
20
+ )
21
+
22
+ async function requireViewer(): Promise<number> {
23
+ const actor = await getActor()
24
+ if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
25
+ return actor.userId
26
+ }
27
+
28
+ /** The row the member is reading the page on, so it can be marked and kept. */
29
+ export async function currentSessionId(): Promise<number | null> {
30
+ const token = await readSessionToken()
31
+ if (token === undefined || token === '') return null
32
+
33
+ const located = await getContainer().identity.locateSession(token)
34
+ return located?.sessionId ?? null
35
+ }
36
+
37
+ export async function revokeSessionAction(_prev: FormState, form: FormData): Promise<FormState> {
38
+ try {
39
+ const userId = await requireViewer()
40
+ const sessionId = positiveInt(form, 'sessionId')
41
+ if (sessionId === null) return { error: await tr('notice.app.session-could-found') }
42
+
43
+ const { accountStore } = getContainer()
44
+ const revoked = await accountStore.sessions.revokeOwned(userId, sessionId, new Date())
45
+
46
+ if (!revoked) return { error: await tr('notice.app.session-had-already-ended') }
47
+
48
+ await recordAuthEvent({ userId, kind: 'session_revoked' })
49
+ } catch (err) {
50
+ return toFormState(err)
51
+ }
52
+
53
+ redirect('/usercp/security?sessions=revoked')
54
+ }
55
+
56
+ export async function revokeOtherSessionsAction(
57
+ _prev: FormState,
58
+ _form: FormData,
59
+ ): Promise<FormState> {
60
+ try {
61
+ const userId = await requireViewer()
62
+ const { accountStore } = getContainer()
63
+
64
+ const keep = await currentSessionId()
65
+ if (keep === null) {
66
+ return { error: await tr('notice.app.session-could-identified-nothing-signed') }
67
+ }
68
+
69
+ const revoked = await accountStore.sessions.revokeAllForUserExcept(userId, keep)
70
+ await accountStore.remember.revokeAllForUser(userId, 'sign_out_everywhere', new Date())
71
+
72
+ if (revoked > 0) await recordAuthEvent({ userId, kind: 'sessions_revoked' })
73
+ } catch (err) {
74
+ return toFormState(err)
75
+ }
76
+
77
+ redirect('/usercp/security?sessions=elsewhere')
78
+ }
@@ -0,0 +1,124 @@
1
+ import 'server-only'
2
+
3
+ import { cookies } from 'next/headers'
4
+
5
+ import { env } from '@meith/core'
6
+
7
+ import {
8
+ ADMIN_COOKIE,
9
+ adminCookie,
10
+ clearedAdminCookie,
11
+ clearedCookie,
12
+ passkeyCookie,
13
+ passkeyCookieName,
14
+ rememberCookie,
15
+ rememberCookieName,
16
+ secondFactorCookie,
17
+ secondFactorCookieName,
18
+ sessionCookie,
19
+ sessionCookieName,
20
+ ssoCookie,
21
+ ssoCookieName,
22
+ } from './cookies'
23
+ import { retireGuestPresence } from './presence'
24
+
25
+ function secure(): boolean {
26
+ return env.NODE_ENV !== 'development'
27
+ }
28
+
29
+ export async function setSessionCookie(token: string, expiresAt: Date): Promise<void> {
30
+ const jar = await cookies()
31
+ const isSecure = secure()
32
+ jar.set(sessionCookieName(isSecure), token, sessionCookie(expiresAt, isSecure))
33
+
34
+ await retireGuestPresence()
35
+ }
36
+
37
+ export async function setRememberCookie(token: string, expiresAt: Date): Promise<void> {
38
+ const jar = await cookies()
39
+ const isSecure = secure()
40
+ jar.set(rememberCookieName(isSecure), token, rememberCookie(expiresAt, isSecure))
41
+ }
42
+
43
+ export async function setAdminCookie(token: string, expiresAt: Date): Promise<void> {
44
+ const jar = await cookies()
45
+ jar.set(ADMIN_COOKIE, token, adminCookie(expiresAt, secure()))
46
+ }
47
+
48
+ export async function clearAdminCookie(): Promise<void> {
49
+ const jar = await cookies()
50
+ jar.set(ADMIN_COOKIE, '', clearedAdminCookie(secure()))
51
+ }
52
+
53
+ export async function readAdminToken(): Promise<string | null> {
54
+ const jar = await cookies()
55
+ return jar.get(ADMIN_COOKIE)?.value ?? null
56
+ }
57
+
58
+ export async function clearSessionCookies(): Promise<void> {
59
+ const jar = await cookies()
60
+ const isSecure = secure()
61
+ jar.set(sessionCookieName(isSecure), '', clearedCookie(isSecure))
62
+ jar.set(rememberCookieName(isSecure), '', clearedCookie(isSecure))
63
+ }
64
+
65
+ export async function readRememberToken(): Promise<string | undefined> {
66
+ const jar = await cookies()
67
+ return jar.get(rememberCookieName(secure()))?.value
68
+ }
69
+
70
+ export async function readSessionToken(): Promise<string | undefined> {
71
+ const jar = await cookies()
72
+ return jar.get(sessionCookieName(secure()))?.value
73
+ }
74
+
75
+ export async function setHandshakeCookie(value: string): Promise<void> {
76
+ const jar = await cookies()
77
+ const isSecure = secure()
78
+ jar.set(ssoCookieName(isSecure), value, ssoCookie(isSecure))
79
+ }
80
+
81
+ export async function readHandshakeCookie(): Promise<string | undefined> {
82
+ const jar = await cookies()
83
+ return jar.get(ssoCookieName(secure()))?.value
84
+ }
85
+
86
+ export async function clearHandshakeCookie(): Promise<void> {
87
+ const jar = await cookies()
88
+ const isSecure = secure()
89
+ jar.set(ssoCookieName(isSecure), '', clearedCookie(isSecure))
90
+ }
91
+
92
+ export async function setSecondFactorCookie(token: string, expiresAt: Date): Promise<void> {
93
+ const jar = await cookies()
94
+ const isSecure = secure()
95
+ jar.set(secondFactorCookieName(isSecure), token, secondFactorCookie(expiresAt, isSecure))
96
+ }
97
+
98
+ export async function readSecondFactorToken(): Promise<string | undefined> {
99
+ const jar = await cookies()
100
+ return jar.get(secondFactorCookieName(secure()))?.value
101
+ }
102
+
103
+ export async function clearSecondFactorCookie(): Promise<void> {
104
+ const jar = await cookies()
105
+ const isSecure = secure()
106
+ jar.set(secondFactorCookieName(isSecure), '', clearedCookie(isSecure))
107
+ }
108
+
109
+ export async function setPasskeyChallengeCookie(value: string): Promise<void> {
110
+ const jar = await cookies()
111
+ const isSecure = secure()
112
+ jar.set(passkeyCookieName(isSecure), value, passkeyCookie(isSecure))
113
+ }
114
+
115
+ export async function readPasskeyChallengeCookie(): Promise<string | undefined> {
116
+ const jar = await cookies()
117
+ return jar.get(passkeyCookieName(secure()))?.value
118
+ }
119
+
120
+ export async function clearPasskeyChallengeCookie(): Promise<void> {
121
+ const jar = await cookies()
122
+ const isSecure = secure()
123
+ jar.set(passkeyCookieName(isSecure), '', clearedCookie(isSecure))
124
+ }
@@ -0,0 +1,17 @@
1
+ import 'server-only'
2
+
3
+ import { createHash } from 'node:crypto'
4
+
5
+ import { cookies } from 'next/headers'
6
+
7
+ import { env } from '@meith/core'
8
+
9
+ import { sessionCookieName } from './cookies'
10
+
11
+ export async function currentSessionKey(): Promise<string | null> {
12
+ const jar = await cookies()
13
+ const token = jar.get(sessionCookieName(env.NODE_ENV !== 'development'))?.value
14
+ if (token === undefined || token === '') return null
15
+
16
+ return createHash('sha256').update(token).digest('base64url').slice(0, 32)
17
+ }
@@ -0,0 +1,47 @@
1
+ import 'server-only'
2
+
3
+ import { CacheTags, cachedGlobal, env } from '@meith/core'
4
+ import { getDb, PostgresSettingsRepository } from '@meith/db'
5
+ import { drivers } from '@meith/drivers'
6
+ import { SettingsSnapshot } from '@meith/settings'
7
+
8
+ const TTL_SECONDS = 60
9
+
10
+ export async function getSettingOverrides(): Promise<ReadonlyMap<string, string>> {
11
+ if (env.DATA_SOURCE !== 'postgres') {
12
+ return new Map()
13
+ }
14
+
15
+ const overrides = await cachedGlobal<Array<[string, string]>>(
16
+ drivers().cache,
17
+ { key: ['settings', 'overrides'], tags: [CacheTags.settings()], revalidate: TTL_SECONDS },
18
+ async () => {
19
+ const stored = await new PostgresSettingsRepository(getDb()).loadAll()
20
+ return [...stored] as Array<[string, string]>
21
+ },
22
+ )
23
+
24
+ return new Map(overrides)
25
+ }
26
+
27
+ export async function getSettings(): Promise<SettingsSnapshot> {
28
+ return SettingsSnapshot.fromOverrides(new Map(await getSettingOverrides()))
29
+ }
30
+
31
+ /**
32
+ * The same settings, read past the cache.
33
+ *
34
+ * The cached snapshot is held per module instance, and a route handler is not
35
+ * always the same instance as the render that invalidated it — so a route can
36
+ * act on a value up to a minute out of date. That is harmless for anything the
37
+ * page merely displays, and wrong for a decision an operator has just changed:
38
+ * turning a sign-in provider on and having the button answer "that is not
39
+ * switched on" reads as a broken board, not as a stale cache. Sign-in happens
40
+ * once per member per session, so the query is cheaper than the confusion.
41
+ */
42
+ export async function getSettingsUncached(): Promise<SettingsSnapshot> {
43
+ if (env.DATA_SOURCE !== 'postgres') return SettingsSnapshot.fromOverrides(new Map())
44
+
45
+ const stored = await new PostgresSettingsRepository(getDb()).loadAll()
46
+ return SettingsSnapshot.fromOverrides(new Map(stored))
47
+ }