@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,492 @@
1
+ 'use server'
2
+
3
+ import { revalidatePath } from 'next/cache'
4
+
5
+ import { CacheTags, ValidationError } from '@meith/core'
6
+ import type {
7
+ AnnouncementInput,
8
+ PostgresAnnouncementRepository,
9
+ PostgresCaptchaQuestionRepository,
10
+ SmileyRow,
11
+ } from '@meith/db'
12
+ import { drivers } from '@meith/drivers'
13
+ import { msg } from '@meith/i18n'
14
+ import { compileSmilies, createDirectiveRegistry } from '@meith/markdown'
15
+
16
+ import { recordAdminAction, requireAdmin } from './admin'
17
+ import { announcementRepository } from './announcements'
18
+ import { captchaQuestionRepository } from './antispam'
19
+ import type { FormState } from './auth-form-state'
20
+ import { requireAttachmentAdmin, requireContentAdmin } from './content-admin'
21
+ import { getActor } from './context'
22
+ import { formStateReporter } from './form-state-reporter'
23
+ import { trimmedText } from './form-values'
24
+ import { emitEvent, viewerRef } from './plugin-view'
25
+
26
+ function id(form: FormData, name = 'id'): number {
27
+ const value = Number(trimmedText(form, name))
28
+ if (!Number.isSafeInteger(value) || value <= 0)
29
+ throw new ValidationError(msg('error.app.such-row'))
30
+ return value
31
+ }
32
+
33
+ const toFormState = formStateReporter('content-admin', 'content administration write failed')
34
+
35
+ function refreshPanel(path: string): void {
36
+ revalidatePath(path)
37
+ }
38
+
39
+ async function invalidateWordFilters(): Promise<void> {
40
+ await drivers().cache.invalidateTags([CacheTags.wordFilters()])
41
+ refreshPanel('/admin/content')
42
+ }
43
+
44
+ export async function createWordFilterAction(_prev: FormState, form: FormData): Promise<FormState> {
45
+ try {
46
+ await requireAdmin()
47
+
48
+ const filterId = await requireContentAdmin().createWordFilter({
49
+ pattern: trimmedText(form, 'pattern'),
50
+ replacement:
51
+ typeof form.get('replacement') === 'string' ? (form.get('replacement') as string) : '',
52
+ wholeWord: form.get('wholeWord') !== null,
53
+ })
54
+
55
+ await invalidateWordFilters()
56
+ await recordAdminAction({ action: 'content.word_filter_added', detail: { filterId } })
57
+
58
+ return { notice: 'saved' }
59
+ } catch (err) {
60
+ return toFormState(err)
61
+ }
62
+ }
63
+
64
+ export async function updateWordFilterAction(_prev: FormState, form: FormData): Promise<FormState> {
65
+ try {
66
+ await requireAdmin()
67
+ const filterId = id(form)
68
+
69
+ await requireContentAdmin().updateWordFilter(filterId, {
70
+ pattern: trimmedText(form, 'pattern'),
71
+ replacement:
72
+ typeof form.get('replacement') === 'string' ? (form.get('replacement') as string) : '',
73
+ wholeWord: form.get('wholeWord') !== null,
74
+ enabled: form.get('enabled') !== null,
75
+ })
76
+
77
+ await invalidateWordFilters()
78
+ await recordAdminAction({ action: 'content.word_filter_changed', detail: { filterId } })
79
+
80
+ return { notice: 'saved' }
81
+ } catch (err) {
82
+ return toFormState(err)
83
+ }
84
+ }
85
+
86
+ export async function deleteWordFilterAction(_prev: FormState, form: FormData): Promise<FormState> {
87
+ try {
88
+ await requireAdmin()
89
+ const filterId = id(form)
90
+
91
+ await requireContentAdmin().deleteWordFilter(filterId)
92
+
93
+ await invalidateWordFilters()
94
+ await recordAdminAction({ action: 'content.word_filter_removed', detail: { filterId } })
95
+
96
+ return { notice: 'removed' }
97
+ } catch (err) {
98
+ return toFormState(err)
99
+ }
100
+ }
101
+
102
+ export async function createPrefixAction(_prev: FormState, form: FormData): Promise<FormState> {
103
+ try {
104
+ await requireAdmin()
105
+
106
+ const order = Number(trimmedText(form, 'displayOrder') || '0')
107
+ if (!Number.isSafeInteger(order) || order < 0) {
108
+ throw new ValidationError(msg('error.app.display-order-must-whole-number'))
109
+ }
110
+
111
+ const prefixId = await requireContentAdmin().createPrefix({
112
+ label: trimmedText(form, 'label'),
113
+ token: trimmedText(form, 'token') === '' ? null : trimmedText(form, 'token'),
114
+ displayOrder: order,
115
+ forumPathPrefix:
116
+ trimmedText(form, 'forumPathPrefix') === '' ? null : trimmedText(form, 'forumPathPrefix'),
117
+ })
118
+
119
+ await drivers().cache.invalidateTags([CacheTags.prefixes()])
120
+ refreshPanel('/admin/content')
121
+ await recordAdminAction({ action: 'content.prefix_added', detail: { prefixId } })
122
+
123
+ return { notice: 'saved' }
124
+ } catch (err) {
125
+ return toFormState(err)
126
+ }
127
+ }
128
+
129
+ export async function deletePrefixAction(_prev: FormState, form: FormData): Promise<FormState> {
130
+ try {
131
+ await requireAdmin()
132
+ const prefixId = id(form)
133
+
134
+ await requireContentAdmin().deletePrefix(prefixId)
135
+
136
+ await drivers().cache.invalidateTags([CacheTags.prefixes()])
137
+ refreshPanel('/admin/content')
138
+ await recordAdminAction({ action: 'content.prefix_removed', detail: { prefixId } })
139
+
140
+ return { notice: 'removed' }
141
+ } catch (err) {
142
+ return toFormState(err)
143
+ }
144
+ }
145
+
146
+ async function invalidateVocabulary(): Promise<void> {
147
+ await drivers().cache.invalidateTags([CacheTags.markdownVocabulary()])
148
+ refreshPanel('/admin/content')
149
+ }
150
+
151
+ function assertSmileyCompiles(
152
+ existing: readonly SmileyRow[],
153
+ candidate: { code: string; src: string; alt: string | null },
154
+ replacingId: number | null,
155
+ ): void {
156
+ const others = existing
157
+ .filter((row) => row.id !== replacingId)
158
+ .map((row) => ({ code: row.code, src: row.src, ...(row.alt === null ? {} : { alt: row.alt }) }))
159
+
160
+ try {
161
+ compileSmilies([
162
+ ...others,
163
+ {
164
+ code: candidate.code,
165
+ src: candidate.src,
166
+ ...(candidate.alt === null ? {} : { alt: candidate.alt }),
167
+ },
168
+ ])
169
+ } catch (err) {
170
+ throw new ValidationError(err instanceof Error ? err.message : 'That smiley is not valid.')
171
+ }
172
+ }
173
+
174
+ export async function createSmileyAction(_prev: FormState, form: FormData): Promise<FormState> {
175
+ try {
176
+ await requireAdmin()
177
+
178
+ const repository = requireContentAdmin()
179
+ const candidate = {
180
+ code: trimmedText(form, 'code'),
181
+ src: trimmedText(form, 'src'),
182
+ alt: trimmedText(form, 'alt') === '' ? null : trimmedText(form, 'alt'),
183
+ }
184
+ assertSmileyCompiles(await repository.listSmilies(), candidate, null)
185
+
186
+ const smileyId = await repository.createSmiley(candidate)
187
+
188
+ await invalidateVocabulary()
189
+ await recordAdminAction({ action: 'content.smiley_added', detail: { smileyId } })
190
+
191
+ return { notice: 'saved' }
192
+ } catch (err) {
193
+ return toFormState(err)
194
+ }
195
+ }
196
+
197
+ export async function updateSmileyAction(_prev: FormState, form: FormData): Promise<FormState> {
198
+ try {
199
+ await requireAdmin()
200
+
201
+ const repository = requireContentAdmin()
202
+ const smileyId = id(form)
203
+ const candidate = {
204
+ code: trimmedText(form, 'code'),
205
+ src: trimmedText(form, 'src'),
206
+ alt: trimmedText(form, 'alt') === '' ? null : trimmedText(form, 'alt'),
207
+ }
208
+ assertSmileyCompiles(await repository.listSmilies(), candidate, smileyId)
209
+
210
+ await repository.updateSmiley(smileyId, { ...candidate, enabled: form.get('enabled') !== null })
211
+
212
+ await invalidateVocabulary()
213
+ await recordAdminAction({ action: 'content.smiley_changed', detail: { smileyId } })
214
+
215
+ return { notice: 'saved' }
216
+ } catch (err) {
217
+ return toFormState(err)
218
+ }
219
+ }
220
+
221
+ export async function deleteSmileyAction(_prev: FormState, form: FormData): Promise<FormState> {
222
+ try {
223
+ await requireAdmin()
224
+
225
+ const smileyId = id(form)
226
+ await requireContentAdmin().deleteSmiley(smileyId)
227
+
228
+ await invalidateVocabulary()
229
+ await recordAdminAction({ action: 'content.smiley_removed', detail: { smileyId } })
230
+
231
+ return { notice: 'deleted' }
232
+ } catch (err) {
233
+ return toFormState(err)
234
+ }
235
+ }
236
+
237
+ function assertDirectiveCompiles(name: string, block: boolean): void {
238
+ try {
239
+ createDirectiveRegistry([{ name, block }])
240
+ } catch (err) {
241
+ throw new ValidationError(
242
+ err instanceof Error ? err.message : 'That directive name is not valid.',
243
+ )
244
+ }
245
+ }
246
+
247
+ export async function createDirectiveAction(_prev: FormState, form: FormData): Promise<FormState> {
248
+ try {
249
+ await requireAdmin()
250
+
251
+ const name = trimmedText(form, 'name').toLowerCase()
252
+ const block = form.get('block') !== null
253
+ assertDirectiveCompiles(name, block)
254
+
255
+ const description = trimmedText(form, 'description')
256
+ const tagId = await requireContentAdmin().createDirective({
257
+ name,
258
+ block,
259
+ description: description === '' ? null : description,
260
+ })
261
+
262
+ await invalidateVocabulary()
263
+ await recordAdminAction({ action: 'content.directive_added', detail: { tagId, name } })
264
+
265
+ return { notice: 'saved' }
266
+ } catch (err) {
267
+ return toFormState(err)
268
+ }
269
+ }
270
+
271
+ export async function updateDirectiveAction(_prev: FormState, form: FormData): Promise<FormState> {
272
+ try {
273
+ await requireAdmin()
274
+
275
+ const tagId = id(form)
276
+ const name = trimmedText(form, 'name').toLowerCase()
277
+ const block = form.get('block') !== null
278
+ assertDirectiveCompiles(name, block)
279
+
280
+ const description = trimmedText(form, 'description')
281
+ await requireContentAdmin().updateDirective(tagId, {
282
+ name,
283
+ block,
284
+ description: description === '' ? null : description,
285
+ enabled: form.get('enabled') !== null,
286
+ })
287
+
288
+ await invalidateVocabulary()
289
+ await recordAdminAction({ action: 'content.directive_changed', detail: { tagId, name } })
290
+
291
+ return { notice: 'saved' }
292
+ } catch (err) {
293
+ return toFormState(err)
294
+ }
295
+ }
296
+
297
+ export async function deleteDirectiveAction(_prev: FormState, form: FormData): Promise<FormState> {
298
+ try {
299
+ await requireAdmin()
300
+
301
+ const tagId = id(form)
302
+ await requireContentAdmin().deleteDirective(tagId)
303
+
304
+ await invalidateVocabulary()
305
+ await recordAdminAction({ action: 'content.directive_removed', detail: { tagId } })
306
+
307
+ return { notice: 'deleted' }
308
+ } catch (err) {
309
+ return toFormState(err)
310
+ }
311
+ }
312
+
313
+ export async function deleteAttachmentAction(_prev: FormState, form: FormData): Promise<FormState> {
314
+ try {
315
+ await requireAdmin()
316
+
317
+ const attachmentId = id(form)
318
+ const removed = await requireAttachmentAdmin().delete(attachmentId)
319
+ if (!removed) return { notice: 'deleted' }
320
+
321
+ await emitEvent('attachment.deleted', { attachmentId }, viewerRef(await getActor()))
322
+ await recordAdminAction({ action: 'content.attachment_removed', detail: { attachmentId } })
323
+
324
+ return { notice: 'deleted' }
325
+ } catch (err) {
326
+ return toFormState(err)
327
+ }
328
+ }
329
+
330
+ function requireAnnouncements(): PostgresAnnouncementRepository {
331
+ const repository = announcementRepository()
332
+ if (repository === null) {
333
+ throw new ValidationError(msg('error.app.board-running-in-memory-sample-data-13'))
334
+ }
335
+ return repository
336
+ }
337
+
338
+ function moment(form: FormData, name: string): Date | null {
339
+ const value = trimmedText(form, name)
340
+ if (value === '') return null
341
+
342
+ const parsed = new Date(`${value}Z`)
343
+ if (Number.isNaN(parsed.getTime())) throw new ValidationError(msg('error.app.valid-date'))
344
+ return parsed
345
+ }
346
+
347
+ function announcementInput(form: FormData): AnnouncementInput {
348
+ const forumText = trimmedText(form, 'forumId')
349
+ return {
350
+ forumId: forumText === '' ? null : id(form, 'forumId'),
351
+ title: trimmedText(form, 'title'),
352
+ message: typeof form.get('message') === 'string' ? (form.get('message') as string) : '',
353
+ startsAt: moment(form, 'startsAt') ?? new Date(),
354
+ endsAt: moment(form, 'endsAt'),
355
+ enabled: form.get('enabled') !== null,
356
+ }
357
+ }
358
+
359
+ export async function createAnnouncementAction(
360
+ _prev: FormState,
361
+ form: FormData,
362
+ ): Promise<FormState> {
363
+ try {
364
+ const context = await requireAdmin()
365
+
366
+ const announcementId = await requireAnnouncements().create({
367
+ ...announcementInput(form),
368
+ authorUserId: context.userId,
369
+ })
370
+
371
+ await recordAdminAction({ action: 'content.announcement_added', detail: { announcementId } })
372
+ refreshPanel('/admin/content/announcements')
373
+
374
+ return { notice: 'saved' }
375
+ } catch (err) {
376
+ return toFormState(err)
377
+ }
378
+ }
379
+
380
+ export async function updateAnnouncementAction(
381
+ _prev: FormState,
382
+ form: FormData,
383
+ ): Promise<FormState> {
384
+ try {
385
+ await requireAdmin()
386
+
387
+ const announcementId = id(form)
388
+ await requireAnnouncements().update(announcementId, announcementInput(form))
389
+
390
+ await recordAdminAction({ action: 'content.announcement_changed', detail: { announcementId } })
391
+ refreshPanel('/admin/content/announcements')
392
+
393
+ return { notice: 'saved' }
394
+ } catch (err) {
395
+ return toFormState(err)
396
+ }
397
+ }
398
+
399
+ export async function deleteAnnouncementAction(
400
+ _prev: FormState,
401
+ form: FormData,
402
+ ): Promise<FormState> {
403
+ try {
404
+ await requireAdmin()
405
+
406
+ const announcementId = id(form)
407
+ await requireAnnouncements().delete(announcementId)
408
+
409
+ await recordAdminAction({ action: 'content.announcement_removed', detail: { announcementId } })
410
+ refreshPanel('/admin/content/announcements')
411
+
412
+ return { notice: 'deleted' }
413
+ } catch (err) {
414
+ return toFormState(err)
415
+ }
416
+ }
417
+
418
+ function requireCaptcha(): PostgresCaptchaQuestionRepository {
419
+ const repository = captchaQuestionRepository()
420
+ if (repository === null) {
421
+ throw new ValidationError(msg('error.app.board-running-in-memory-sample-data-14'))
422
+ }
423
+ return repository
424
+ }
425
+
426
+ function answersField(form: FormData): string {
427
+ const value = form.get('answers')
428
+ return typeof value === 'string' ? value : ''
429
+ }
430
+
431
+ export async function createCaptchaQuestionAction(
432
+ _prev: FormState,
433
+ form: FormData,
434
+ ): Promise<FormState> {
435
+ try {
436
+ await requireAdmin()
437
+
438
+ const questionId = await requireCaptcha().create({
439
+ question: trimmedText(form, 'question'),
440
+ answers: answersField(form),
441
+ })
442
+
443
+ await recordAdminAction({ action: 'content.captcha_added', detail: { questionId } })
444
+ refreshPanel('/admin/antispam')
445
+
446
+ return { notice: 'saved' }
447
+ } catch (err) {
448
+ return toFormState(err)
449
+ }
450
+ }
451
+
452
+ export async function updateCaptchaQuestionAction(
453
+ _prev: FormState,
454
+ form: FormData,
455
+ ): Promise<FormState> {
456
+ try {
457
+ await requireAdmin()
458
+
459
+ const questionId = id(form)
460
+ await requireCaptcha().update(questionId, {
461
+ question: trimmedText(form, 'question'),
462
+ answers: answersField(form),
463
+ enabled: form.get('enabled') !== null,
464
+ })
465
+
466
+ await recordAdminAction({ action: 'content.captcha_changed', detail: { questionId } })
467
+ refreshPanel('/admin/antispam')
468
+
469
+ return { notice: 'saved' }
470
+ } catch (err) {
471
+ return toFormState(err)
472
+ }
473
+ }
474
+
475
+ export async function deleteCaptchaQuestionAction(
476
+ _prev: FormState,
477
+ form: FormData,
478
+ ): Promise<FormState> {
479
+ try {
480
+ await requireAdmin()
481
+
482
+ const questionId = id(form)
483
+ await requireCaptcha().delete(questionId)
484
+
485
+ await recordAdminAction({ action: 'content.captcha_removed', detail: { questionId } })
486
+ refreshPanel('/admin/antispam')
487
+
488
+ return { notice: 'deleted' }
489
+ } catch (err) {
490
+ return toFormState(err)
491
+ }
492
+ }
@@ -0,0 +1,95 @@
1
+ import { msg } from '@meith/i18n'
2
+ import 'server-only'
3
+
4
+ import { CacheTags, cachedGlobal, ForbiddenError } from '@meith/core'
5
+ import {
6
+ getDb,
7
+ PostgresAttachmentAdminRepository,
8
+ PostgresContentAdminRepository,
9
+ readVocabularySource,
10
+ } from '@meith/db'
11
+ import { drivers } from '@meith/drivers'
12
+ import {
13
+ type BoardVocabulary,
14
+ type CompiledWordFilter,
15
+ compileVocabulary,
16
+ compileWordFilter,
17
+ NO_VOCABULARY_SOURCE,
18
+ } from '@meith/markdown'
19
+
20
+ import { getContainer } from './container'
21
+ import { boardRendering } from './markdown-pipeline'
22
+ import { filterView } from './plugin-view'
23
+
24
+ export function contentAdminRepository(): PostgresContentAdminRepository | null {
25
+ return getContainer().dataSource === 'postgres'
26
+ ? new PostgresContentAdminRepository(getDb())
27
+ : null
28
+ }
29
+
30
+ export function requireContentAdmin(): PostgresContentAdminRepository {
31
+ const repository = contentAdminRepository()
32
+ if (repository === null) {
33
+ throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-22'))
34
+ }
35
+ return repository
36
+ }
37
+
38
+ const TTL_SECONDS = 60
39
+
40
+ function loadFilters(): ReturnType<PostgresContentAdminRepository['activeWordFilters']> {
41
+ return cachedGlobal(
42
+ drivers().cache,
43
+ {
44
+ key: ['word-filters'],
45
+ tags: [CacheTags.wordFilters()],
46
+ revalidate: TTL_SECONDS,
47
+ },
48
+ () => new PostgresContentAdminRepository(getDb()).activeWordFilters(),
49
+ )
50
+ }
51
+
52
+ export async function activeWordFilter(): Promise<CompiledWordFilter | undefined> {
53
+ const stored = getContainer().dataSource === 'postgres' ? await loadFilters() : []
54
+ const rules = await filterView('word-filter.patterns', stored, {})
55
+
56
+ return rules.length === 0 ? undefined : compileWordFilter(rules)
57
+ }
58
+
59
+ function loadVocabularySource(): ReturnType<typeof readVocabularySource> {
60
+ return cachedGlobal(
61
+ drivers().cache,
62
+ {
63
+ key: ['markdown-vocabulary-rows'],
64
+ tags: [CacheTags.markdownVocabulary()],
65
+ revalidate: TTL_SECONDS,
66
+ },
67
+ () => readVocabularySource(getDb()),
68
+ )
69
+ }
70
+
71
+ export async function activeVocabulary(): Promise<BoardVocabulary | undefined> {
72
+ const stored =
73
+ getContainer().dataSource === 'postgres' ? await loadVocabularySource() : NO_VOCABULARY_SOURCE
74
+ const source = await boardRendering.vocabulary(stored ?? NO_VOCABULARY_SOURCE)
75
+
76
+ if (source.revision === 0 && source.smilies.length === 0 && source.directives.length === 0) {
77
+ return undefined
78
+ }
79
+
80
+ return compileVocabulary(source)
81
+ }
82
+
83
+ export function attachmentAdminRepository(): PostgresAttachmentAdminRepository | null {
84
+ return getContainer().dataSource === 'postgres'
85
+ ? new PostgresAttachmentAdminRepository(getDb())
86
+ : null
87
+ }
88
+
89
+ export function requireAttachmentAdmin(): PostgresAttachmentAdminRepository {
90
+ const repository = attachmentAdminRepository()
91
+ if (repository === null) {
92
+ throw new ForbiddenError(msg('error.app.board-running-in-memory-sample-data-23'))
93
+ }
94
+ return repository
95
+ }
@@ -0,0 +1,35 @@
1
+ export interface PolicyOptions {
2
+ readonly nonce: string
3
+ readonly development?: boolean
4
+ readonly remoteImages?: boolean
5
+ }
6
+
7
+ export function contentSecurityPolicy(options: PolicyOptions): string {
8
+ const development = options.development ?? false
9
+ const remoteImages = options.remoteImages ?? false
10
+
11
+ return [
12
+ "default-src 'self'",
13
+ `img-src 'self' data:${remoteImages ? ' https:' : ''}`,
14
+ "style-src 'self' 'unsafe-inline'",
15
+ `script-src 'self' 'nonce-${options.nonce}' 'strict-dynamic'${
16
+ development ? " 'unsafe-eval'" : ''
17
+ }`,
18
+ "connect-src 'self'",
19
+ "worker-src 'self'",
20
+ "manifest-src 'self'",
21
+ "frame-ancestors 'self'",
22
+ "object-src 'none'",
23
+ "base-uri 'self'",
24
+ "form-action 'self'",
25
+ ].join('; ')
26
+ }
27
+
28
+ export function newNonce(): string {
29
+ return btoa(crypto.randomUUID())
30
+ }
31
+
32
+ export function readsAsAsset(pathname: string): boolean {
33
+ const last = pathname.slice(pathname.lastIndexOf('/') + 1)
34
+ return last.includes('.')
35
+ }
@@ -0,0 +1,32 @@
1
+ import 'server-only'
2
+
3
+ import { cookies } from 'next/headers'
4
+ import { cache } from 'react'
5
+
6
+ import type { Actor } from '@meith/authorization'
7
+ import { env } from '@meith/core'
8
+
9
+ import { getContainer } from './container'
10
+ import { sessionCookieName } from './cookies'
11
+
12
+ export const getActor = cache(async (): Promise<Actor> => {
13
+ const { identity, actorSource } = getContainer()
14
+
15
+ const jar = await cookies()
16
+ const token = jar.get(sessionCookieName(env.NODE_ENV !== 'development'))?.value
17
+
18
+ if (token) {
19
+ const resolved = await identity.resolveSession(token)
20
+ if (resolved) {
21
+ const actor = await actorSource.buildForUser(resolved.userId)
22
+ if (actor) return actor
23
+ }
24
+ }
25
+
26
+ return actorSource.buildGuest()
27
+ })
28
+
29
+ export const getUserId = cache(async (): Promise<number | null> => {
30
+ const actor = await getActor()
31
+ return actor.userId
32
+ })