@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,41 @@
1
+ import 'server-only'
2
+
3
+ import { logger } from '@meith/core'
4
+ import { currentRequestId } from '@meith/core/logger'
5
+ import { requireSlot, slotCopy } from '@meith/theme-kit'
6
+
7
+ import { getTranslator } from '@/server/i18n'
8
+ import { buildErrorNotice, CRASH_NOTICE, type ErrorNoticeCopy } from '@/view/error-notice'
9
+
10
+ import { filterView } from './plugin-view'
11
+ import { currentTheme } from './theme'
12
+
13
+ export async function renderErrorNotice(copy: ErrorNoticeCopy): Promise<React.ReactNode> {
14
+ const requestId = currentRequestId() ?? null
15
+ const ErrorNotice = requireSlot(await currentTheme(), 'ErrorNotice')
16
+ const translator = await getTranslator()
17
+
18
+ const model = await filterView(
19
+ 'view.error-notice',
20
+ buildErrorNotice(copy, requestId, translator),
21
+ {
22
+ userId: null,
23
+ isGuest: true,
24
+ requestId,
25
+ },
26
+ )
27
+
28
+ return <ErrorNotice {...model} copy={slotCopy(await currentTheme(), 'ErrorNotice', translator)} />
29
+ }
30
+
31
+ export async function crashNotice(): Promise<React.ReactNode> {
32
+ try {
33
+ return await renderErrorNotice(CRASH_NOTICE)
34
+ } catch (error) {
35
+ logger({ module: 'error-notice' }).warn(
36
+ { err: String(error) },
37
+ 'could not resolve the themed crash notice',
38
+ )
39
+ return null
40
+ }
41
+ }
@@ -0,0 +1,113 @@
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 { assertDemoAccountChangeable } from './demo'
13
+ import {
14
+ federationService,
15
+ memberManagedSignIns,
16
+ passkeyService,
17
+ passkeysEnabled,
18
+ signInProviders,
19
+ } from './federation'
20
+ import { formStateReporter } from './form-state-reporter'
21
+ import { text } from './form-values'
22
+ import { tr } from './i18n'
23
+
24
+ const toFormState = formStateReporter(
25
+ 'federation-actions',
26
+ 'unexpected error while changing a sign-in',
27
+ )
28
+
29
+ /**
30
+ * Only a credential the board would actually accept today counts as a way back
31
+ * in. A passkey on a board that has since switched passkeys off, or a link to a
32
+ * provider an operator has turned off, opens nothing — counting it would let a
33
+ * member unlink their way out of their own account in one click.
34
+ */
35
+ async function usableWaysIn(userId: number): Promise<{
36
+ readonly usablePasskeys: number
37
+ readonly usableProviders: readonly string[]
38
+ }> {
39
+ const providers = await signInProviders()
40
+ const passkeys = (await passkeysEnabled())
41
+ ? (await getContainer().accountStore.passkeys.listForUser(userId)).length
42
+ : 0
43
+
44
+ return {
45
+ usablePasskeys: passkeys,
46
+ usableProviders: providers.map((provider) => provider.id),
47
+ }
48
+ }
49
+
50
+ async function requireOwnAccount(): Promise<{
51
+ readonly userId: number
52
+ readonly hasPassword: boolean
53
+ }> {
54
+ const actor = await getActor()
55
+ if (actor.userId === null) throw new ForbiddenError(msg('error.app.must-logged'))
56
+
57
+ if (!(await memberManagedSignIns())) {
58
+ throw new ForbiddenError(msg('error.app.board-manages-sign-ins-for-its'))
59
+ }
60
+
61
+ await assertDemoAccountChangeable(actor.userId, 'password')
62
+
63
+ const account = await getContainer().accountStore.accounts.findById(actor.userId)
64
+ if (account === null) throw new ForbiddenError(msg('error.app.account-longer-exists'))
65
+
66
+ return { userId: actor.userId, hasPassword: account.passwordHash !== null }
67
+ }
68
+
69
+ export async function unlinkIdentityAction(_prev: FormState, form: FormData): Promise<FormState> {
70
+ try {
71
+ const { userId, hasPassword } = await requireOwnAccount()
72
+ const identityId = Number(text(form, 'identityId'))
73
+ if (!Number.isInteger(identityId)) {
74
+ return { error: await tr('notice.app.sign-in-could-found') }
75
+ }
76
+
77
+ await (await federationService()).unlink({
78
+ userId,
79
+ identityId,
80
+ hasPassword,
81
+ ...(await usableWaysIn(userId)),
82
+ })
83
+
84
+ await recordAuthEvent({ userId, kind: 'identity_unlinked' })
85
+ } catch (err) {
86
+ return toFormState(err)
87
+ }
88
+
89
+ redirect('/usercp/security?sso=unlinked')
90
+ }
91
+
92
+ export async function removePasskeyAction(_prev: FormState, form: FormData): Promise<FormState> {
93
+ try {
94
+ const { userId, hasPassword } = await requireOwnAccount()
95
+ const passkeyId = Number(text(form, 'passkeyId'))
96
+ if (!Number.isInteger(passkeyId)) {
97
+ return { error: await tr('notice.app.passkey-could-found') }
98
+ }
99
+
100
+ await (await passkeyService()).remove({
101
+ userId,
102
+ passkeyId,
103
+ hasPassword,
104
+ usableProviders: (await usableWaysIn(userId)).usableProviders,
105
+ })
106
+
107
+ await recordAuthEvent({ userId, kind: 'passkey_removed' })
108
+ } catch (err) {
109
+ return toFormState(err)
110
+ }
111
+
112
+ redirect('/usercp/security?passkey=removed')
113
+ }
@@ -0,0 +1,138 @@
1
+ import 'server-only'
2
+
3
+ import { headers } from 'next/headers'
4
+
5
+ import {
6
+ configuredProviders,
7
+ type FederationOptions,
8
+ FederationService,
9
+ type IdentityProvider,
10
+ isProviderKind,
11
+ PasskeyService,
12
+ type ProviderKind,
13
+ providerFor,
14
+ type RelyingParty,
15
+ } from '@meith/accounts'
16
+ import { env } from '@meith/core'
17
+ import type { SettingsSnapshot } from '@meith/settings'
18
+
19
+ import { boardUrl } from './board-url'
20
+ import { configuredIdentity, getContainer } from './container'
21
+ import { getSettings, getSettingsUncached } from './settings'
22
+
23
+ export const SSO_CALLBACK_PREFIX = '/auth/sso'
24
+
25
+ export interface ProviderButton {
26
+ readonly id: ProviderKind
27
+ readonly label: string
28
+ }
29
+
30
+ export function federationOptions(settings: SettingsSnapshot): FederationOptions {
31
+ return {
32
+ github: {
33
+ enabled: settings.get('federation.github_enabled'),
34
+ clientId: settings.get('federation.github_client_id'),
35
+ clientSecret: settings.get('federation.github_client_secret'),
36
+ },
37
+ google: {
38
+ enabled: settings.get('federation.google_enabled'),
39
+ clientId: settings.get('federation.google_client_id'),
40
+ clientSecret: settings.get('federation.google_client_secret'),
41
+ },
42
+ oidc: {
43
+ enabled: settings.get('federation.oidc_enabled'),
44
+ clientId: settings.get('federation.oidc_client_id'),
45
+ clientSecret: settings.get('federation.oidc_client_secret'),
46
+ issuer: settings.get('federation.oidc_issuer'),
47
+ label: settings.get('federation.oidc_label'),
48
+ scopes: settings.get('federation.oidc_scopes'),
49
+ },
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Every read here goes past the settings cache, because every one of them
55
+ * decides whether a sign-in may start rather than what a page displays. The
56
+ * cached snapshot is held per module instance, so a route handler can be a
57
+ * minute behind the render that invalidated it — long enough for a provider an
58
+ * operator has just switched on to answer that it is switched off.
59
+ */
60
+ export async function federationProvider(id: string): Promise<IdentityProvider | null> {
61
+ if (!isProviderKind(id)) return null
62
+ if (getContainer().dataSource !== 'postgres') return null
63
+
64
+ return providerFor(id, federationOptions(await getSettingsUncached()))
65
+ }
66
+
67
+ export async function signInProviders(): Promise<readonly ProviderButton[]> {
68
+ if (getContainer().dataSource !== 'postgres') return []
69
+
70
+ return configuredProviders(federationOptions(await getSettingsUncached())).map((provider) => ({
71
+ id: provider.id,
72
+ label: provider.label,
73
+ }))
74
+ }
75
+
76
+ export async function passkeysEnabled(): Promise<boolean> {
77
+ if (getContainer().dataSource !== 'postgres') return false
78
+ return (await getSettingsUncached()).get('federation.passkeys_enabled')
79
+ }
80
+
81
+ export async function memberManagedSignIns(): Promise<boolean> {
82
+ return (await getSettingsUncached()).get('federation.link_from_usercp')
83
+ }
84
+
85
+ export async function federationService(): Promise<FederationService> {
86
+ const { accountStore } = getContainer()
87
+
88
+ return new FederationService({
89
+ identity: await configuredIdentity(),
90
+ accounts: accountStore.accounts,
91
+ identities: accountStore.identities,
92
+ })
93
+ }
94
+
95
+ export async function passkeyService(): Promise<PasskeyService> {
96
+ const { accountStore } = getContainer()
97
+
98
+ return new PasskeyService({
99
+ identity: await configuredIdentity(),
100
+ accounts: accountStore.accounts,
101
+ passkeys: accountStore.passkeys,
102
+ identities: accountStore.identities,
103
+ })
104
+ }
105
+
106
+ export function callbackPath(provider: ProviderKind): string {
107
+ return `${SSO_CALLBACK_PREFIX}/${provider}/callback`
108
+ }
109
+
110
+ export async function callbackUrl(provider: ProviderKind): Promise<string> {
111
+ return new URL(callbackPath(provider), await requestOrigin()).toString()
112
+ }
113
+
114
+ export async function relyingParty(): Promise<RelyingParty> {
115
+ const origin = await requestOrigin()
116
+ return { id: new URL(origin).hostname, origin }
117
+ }
118
+
119
+ export async function passkeyRelyingPartyName(): Promise<string> {
120
+ const name = (await getSettings()).get('board.name')
121
+ return name.trim() === '' ? 'this board' : name
122
+ }
123
+
124
+ async function requestOrigin(): Promise<string> {
125
+ const configured = await boardUrl()
126
+ if (configured !== '') return configured
127
+
128
+ const incoming = await headers()
129
+ const host = incoming.get('x-forwarded-host') ?? incoming.get('host')
130
+ if (host === null || host.trim() === '') {
131
+ throw new Error('the board has no address to send an identity provider back to')
132
+ }
133
+
134
+ const scheme =
135
+ incoming.get('x-forwarded-proto') ?? (env.NODE_ENV === 'development' ? 'http' : 'https')
136
+
137
+ return `${scheme.split(',')[0]!.trim()}://${host.trim()}`
138
+ }
@@ -0,0 +1,72 @@
1
+ import 'server-only'
2
+
3
+ import type { FeedPost, FeedThread } from '@meith/db'
4
+ import type { CompiledWordFilter } from '@meith/markdown'
5
+
6
+ import { type FeedChannel, type FeedEntry, summarise } from '@/view/feed'
7
+ import { postLink } from '@/view/post-link'
8
+ import { filterWords } from '@/view/word-filter'
9
+
10
+ import { absoluteTo } from './syndication'
11
+
12
+ export function feedFor(
13
+ origin: string,
14
+ wordFilter: CompiledWordFilter | undefined,
15
+ ): {
16
+ threadEntry: (thread: FeedThread) => FeedEntry
17
+ postEntry: (post: FeedPost) => FeedEntry
18
+ channel: (input: ChannelInput) => FeedChannel
19
+ } {
20
+ const absolute = (path: string): string => absoluteTo(origin, path)
21
+
22
+ const tagId = (kind: 'thread' | 'post', id: number): string => {
23
+ const host = origin.replace(/^https?:\/\//, '')
24
+ return `tag:${host},2026:${kind}/${id}`
25
+ }
26
+
27
+ function threadEntry(thread: FeedThread): FeedEntry {
28
+ return {
29
+ id: tagId('thread', thread.threadId),
30
+ title: thread.title,
31
+ href: absolute(`/thread/${thread.threadId}-${thread.slug}`),
32
+ author: thread.authorUsername,
33
+ published: thread.createdAt,
34
+ updated: thread.lastPostAt,
35
+ summary: filterWords(summarise(thread.excerptSource), wordFilter),
36
+ }
37
+ }
38
+
39
+ function postEntry(post: FeedPost): FeedEntry {
40
+ return {
41
+ id: tagId('post', post.postId),
42
+ title: post.threadTitle,
43
+ href: absolute(postLink(`/thread/${post.threadId}-${post.threadSlug}`, post.postId)),
44
+ author: post.authorUsername,
45
+ published: post.createdAt,
46
+ updated: post.createdAt,
47
+ summary: filterWords(summarise(post.messageSource), wordFilter),
48
+ }
49
+ }
50
+
51
+ function channel(input: ChannelInput): FeedChannel {
52
+ return {
53
+ title: input.title,
54
+ description: input.description,
55
+ href: absolute(input.path),
56
+ selfHref: absolute(input.selfPath),
57
+ updated: input.entries[0]?.updated ?? input.now,
58
+ entries: input.entries,
59
+ }
60
+ }
61
+
62
+ return { threadEntry, postEntry, channel }
63
+ }
64
+
65
+ interface ChannelInput {
66
+ readonly title: string
67
+ readonly description: string
68
+ readonly path: string
69
+ readonly selfPath: string
70
+ readonly entries: readonly FeedEntry[]
71
+ readonly now: Date
72
+ }
@@ -0,0 +1,122 @@
1
+ import 'server-only'
2
+
3
+ import { type FeedChannel, type FeedEntry, renderAtom, renderRss } from '@/view/feed'
4
+
5
+ import { boardOffline } from './board-offline'
6
+ import { activeWordFilter } from './content-admin'
7
+ import { feedFor } from './feed-builder'
8
+ import { filterView } from './plugin-view'
9
+ import { getSettings } from './settings'
10
+ import { FEED_LIMIT, feedRepository, origin, publicScope } from './syndication'
11
+
12
+ const CACHE = 'public, max-age=300, stale-while-revalidate=3600'
13
+
14
+ export type FeedFormat = 'rss' | 'atom'
15
+
16
+ export type FeedScope = 'board' | 'forum' | 'thread'
17
+
18
+ /**
19
+ * A plugin may reorder, drop, or add. An entry it adds keeps the board's own
20
+ * fields where one matches by href, and is otherwise taken as given — a feed
21
+ * is rendered as a guest, so everything in it is already public.
22
+ */
23
+ async function filterEntries(
24
+ entries: readonly FeedEntry[],
25
+ feed: FeedScope,
26
+ ): Promise<readonly FeedEntry[]> {
27
+ const filtered = await filterView(
28
+ 'feed.items',
29
+ entries.map((entry) => ({
30
+ title: entry.title,
31
+ href: entry.href,
32
+ publishedAt: entry.published.toISOString(),
33
+ summary: entry.summary,
34
+ })),
35
+ { feed },
36
+ )
37
+
38
+ const byHref = new Map(entries.map((entry) => [entry.href, entry]))
39
+ return filtered.map((row) => {
40
+ const original = byHref.get(row.href)
41
+ const published = new Date(row.publishedAt)
42
+ return {
43
+ id: original?.id ?? row.href,
44
+ title: row.title,
45
+ href: row.href,
46
+ author: original?.author ?? '',
47
+ published: Number.isNaN(published.getTime())
48
+ ? (original?.published ?? new Date())
49
+ : published,
50
+ updated: original?.updated ?? published,
51
+ summary: row.summary,
52
+ }
53
+ })
54
+ }
55
+
56
+ export async function feedResponse(
57
+ channel: FeedChannel,
58
+ format: FeedFormat,
59
+ feed: FeedScope,
60
+ ): Promise<Response> {
61
+ const withEntries = { ...channel, entries: await filterEntries(channel.entries, feed) }
62
+ const body = format === 'atom' ? renderAtom(withEntries) : renderRss(withEntries)
63
+ return new Response(body, {
64
+ headers: {
65
+ 'content-type':
66
+ format === 'atom'
67
+ ? 'application/atom+xml; charset=utf-8'
68
+ : 'application/rss+xml; charset=utf-8',
69
+ 'cache-control': CACHE,
70
+ },
71
+ })
72
+ }
73
+
74
+ export function xmlResponse(body: string): Response {
75
+ return new Response(body, {
76
+ headers: { 'content-type': 'application/xml; charset=utf-8', 'cache-control': CACHE },
77
+ })
78
+ }
79
+
80
+ export async function offlineFeed(): Promise<Response | null> {
81
+ const offline = await boardOffline()
82
+ if (offline === null) return null
83
+
84
+ return new Response(offline.message, {
85
+ status: 503,
86
+ headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },
87
+ })
88
+ }
89
+
90
+ export async function boardFeed(format: FeedFormat, selfPath: string): Promise<Response> {
91
+ const offline = await offlineFeed()
92
+ if (offline !== null) return offline
93
+
94
+ const repo = feedRepository()
95
+ if (repo === null) return noFeed()
96
+
97
+ const scope = await publicScope()
98
+ const threads = await repo.recentThreads(FEED_LIMIT, scope)
99
+ const settings = await getSettings()
100
+
101
+ const feed = feedFor(await origin(), await activeWordFilter())
102
+
103
+ return feedResponse(
104
+ feed.channel({
105
+ title: settings.get('board.name'),
106
+ description: settings.get('board.description'),
107
+ path: '/',
108
+ selfPath,
109
+ entries: threads.map(feed.threadEntry),
110
+ now: new Date(),
111
+ }),
112
+ format,
113
+ 'board',
114
+ )
115
+ }
116
+
117
+ export function noFeed(): Response {
118
+ return new Response('Not found', {
119
+ status: 404,
120
+ headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' },
121
+ })
122
+ }
@@ -0,0 +1,62 @@
1
+ import 'server-only'
2
+
3
+ import type { AccountStore } from '@meith/accounts'
4
+ import {
5
+ type Actor,
6
+ type ActorSource,
7
+ type ActorState,
8
+ combinePermissionSets,
9
+ type GroupDefaults,
10
+ } from '@meith/authorization'
11
+ import { emptyPermissionSet } from '@meith/core'
12
+
13
+ import { SEED_BOARD, SEED_GROUP } from './seed-board'
14
+
15
+ function mapState(state: 'active' | 'awaiting_activation' | 'banned'): ActorState {
16
+ return state
17
+ }
18
+
19
+ export class FixtureActorSource implements ActorSource {
20
+ private readonly groupsById: Map<number, GroupDefaults>
21
+
22
+ constructor(private readonly store: AccountStore) {
23
+ this.groupsById = new Map(SEED_BOARD.groups.map((g) => [g.groupId, g]))
24
+ }
25
+
26
+ async buildGuest(): Promise<Actor> {
27
+ const groupIds = [SEED_GROUP.guest]
28
+ return {
29
+ userId: null,
30
+ groupIds,
31
+ primaryGroupId: SEED_GROUP.guest,
32
+ state: 'guest',
33
+ global: this.combine(groupIds),
34
+ permissionVersion: 1,
35
+ }
36
+ }
37
+
38
+ async buildForUser(userId: number): Promise<Actor | null> {
39
+ const account = await this.store.accounts.findById(userId)
40
+ if (!account) return null
41
+
42
+ const primaryGroupId = account.primaryGroupId ?? SEED_GROUP.registered
43
+ const groupIds = [primaryGroupId]
44
+
45
+ return {
46
+ userId: account.id,
47
+ groupIds,
48
+ primaryGroupId,
49
+ state: mapState(account.state),
50
+ global: this.combine(groupIds),
51
+ permissionVersion: 1,
52
+ }
53
+ }
54
+
55
+ private combine(groupIds: readonly number[]) {
56
+ const sets = groupIds
57
+ .map((id) => this.groupsById.get(id)?.permissions)
58
+ .filter((p): p is NonNullable<typeof p> => p !== undefined)
59
+ if (sets.length === 0) return emptyPermissionSet()
60
+ return combinePermissionSets(sets)
61
+ }
62
+ }
@@ -0,0 +1,59 @@
1
+ import 'server-only'
2
+
3
+ import { ConfigurationError } from '@meith/core'
4
+ import type {
5
+ ForumListingRow,
6
+ ForumRepository,
7
+ ForumRow,
8
+ MovePlan,
9
+ MoveTarget,
10
+ NewForum,
11
+ } from '@meith/forums'
12
+
13
+ import { SEED_FORUM_ROWS } from './seed-board'
14
+
15
+ function structural(row: ForumListingRow): ForumRow {
16
+ const { threadCount: _t, postCount: _p, lastPost: _l, ...rest } = row
17
+ return rest
18
+ }
19
+
20
+ function unsupported(operation: string): never {
21
+ throw new ConfigurationError(
22
+ `Cannot ${operation} in fixture mode: the board is in-memory sample data and ` +
23
+ 'any change is lost when the process exits. Set DATABASE_URL and ' +
24
+ 'DATA_SOURCE=postgres to manage a real board.',
25
+ )
26
+ }
27
+
28
+ export class FixtureForumRepository implements ForumRepository {
29
+ constructor(private readonly rows: readonly ForumListingRow[] = SEED_FORUM_ROWS) {}
30
+
31
+ async listAll(): Promise<ForumRow[]> {
32
+ return this.ordered().map(structural)
33
+ }
34
+
35
+ async listListing(): Promise<ForumListingRow[]> {
36
+ return this.ordered().map((row) => ({ ...row }))
37
+ }
38
+
39
+ async findById(id: number): Promise<ForumRow | null> {
40
+ const row = this.rows.find((r) => r.id === id)
41
+ return row ? structural(row) : null
42
+ }
43
+
44
+ private ordered(): ForumListingRow[] {
45
+ return [...this.rows].sort((a, b) => a.displayOrder - b.displayOrder || a.id - b.id)
46
+ }
47
+
48
+ async create(_input: NewForum): Promise<ForumRow> {
49
+ unsupported('create a forum')
50
+ }
51
+
52
+ async move(_forumId: number, _target: MoveTarget): Promise<void> {
53
+ unsupported('move a forum')
54
+ }
55
+
56
+ async applyMove(_plan: MovePlan): Promise<void> {
57
+ unsupported('move a forum')
58
+ }
59
+ }
@@ -0,0 +1,30 @@
1
+ import 'server-only'
2
+
3
+ import type {
4
+ MemberProfileRecord,
5
+ MemberProfileRepository,
6
+ MemberSuggestion,
7
+ } from '@meith/accounts'
8
+
9
+ import { SEED_MEMBER_PROFILES } from './seed-board'
10
+
11
+ export class FixtureMemberProfileRepository implements MemberProfileRepository {
12
+ constructor(private readonly rows: readonly MemberProfileRecord[] = SEED_MEMBER_PROFILES) {}
13
+
14
+ async findPublicById(id: number): Promise<MemberProfileRecord | null> {
15
+ return this.rows.find((row) => row.id === id) ?? null
16
+ }
17
+
18
+ async searchByUsernamePrefix(
19
+ prefix: string,
20
+ limit: number,
21
+ ): Promise<readonly MemberSuggestion[]> {
22
+ if (prefix === '') return []
23
+ const lower = prefix.toLowerCase()
24
+ return this.rows
25
+ .filter((row) => row.username.toLowerCase().startsWith(lower))
26
+ .sort((a, b) => a.username.localeCompare(b.username))
27
+ .slice(0, limit)
28
+ .map((row) => ({ id: row.id, username: row.username }))
29
+ }
30
+ }