@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.
- package/LICENSE.md +165 -0
- package/app/(auth)/layout.tsx +6 -0
- package/app/(auth)/login/page.tsx +106 -0
- package/app/(auth)/login/verify/page.tsx +55 -0
- package/app/(auth)/register/page.tsx +83 -0
- package/app/(auth)/reset/confirm/page.tsx +45 -0
- package/app/(auth)/reset/page.tsx +22 -0
- package/app/(auth)/verify/page.tsx +44 -0
- package/app/(auth)/verify/resend/page.tsx +51 -0
- package/app/(board)/[slug]/feed.xml/route.ts +48 -0
- package/app/(board)/[slug]/new/page.tsx +112 -0
- package/app/(board)/[slug]/page.tsx +403 -0
- package/app/(board)/discover/[view]/page.tsx +115 -0
- package/app/(board)/discover/page.tsx +5 -0
- package/app/(board)/layout.tsx +22 -0
- package/app/(board)/member/[id]/page.tsx +187 -0
- package/app/(board)/member/[id]/reputation/page.tsx +195 -0
- package/app/(board)/member/by-name/[name]/page.tsx +32 -0
- package/app/(board)/messages/[id]/page.tsx +103 -0
- package/app/(board)/messages/compose/page.tsx +60 -0
- package/app/(board)/messages/layout.tsx +5 -0
- package/app/(board)/messages/page.tsx +179 -0
- package/app/(board)/modcp/forums/page.tsx +59 -0
- package/app/(board)/modcp/ip/page.tsx +158 -0
- package/app/(board)/modcp/layout.tsx +5 -0
- package/app/(board)/modcp/log/page.tsx +98 -0
- package/app/(board)/modcp/page.tsx +112 -0
- package/app/(board)/moderation/layout.tsx +5 -0
- package/app/(board)/moderation/page.tsx +111 -0
- package/app/(board)/moderation/reports/page.tsx +193 -0
- package/app/(board)/moderation/warn/page.tsx +158 -0
- package/app/(board)/notifications/layout.tsx +5 -0
- package/app/(board)/notifications/page.tsx +144 -0
- package/app/(board)/notifications/preferences/page.tsx +65 -0
- package/app/(board)/online/page.tsx +119 -0
- package/app/(board)/page.tsx +195 -0
- package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
- package/app/(board)/privacy/page.tsx +12 -0
- package/app/(board)/report/page.tsx +71 -0
- package/app/(board)/rules/page.tsx +12 -0
- package/app/(board)/search/[token]/page.tsx +130 -0
- package/app/(board)/search/page.tsx +289 -0
- package/app/(board)/stats/page.tsx +202 -0
- package/app/(board)/subscriptions/layout.tsx +5 -0
- package/app/(board)/subscriptions/page.tsx +142 -0
- package/app/(board)/terms/page.tsx +12 -0
- package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
- package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
- package/app/(board)/thread/[slug]/page.tsx +707 -0
- package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
- package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
- package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
- package/app/(board)/unsubscribe/page.tsx +73 -0
- package/app/(board)/usercp/avatar/page.tsx +56 -0
- package/app/(board)/usercp/contacts/page.tsx +141 -0
- package/app/(board)/usercp/email/confirm/page.tsx +52 -0
- package/app/(board)/usercp/layout.tsx +23 -0
- package/app/(board)/usercp/options/page.tsx +56 -0
- package/app/(board)/usercp/page.tsx +71 -0
- package/app/(board)/usercp/profile/page.tsx +81 -0
- package/app/(board)/usercp/security/page.tsx +238 -0
- package/app/(board)/usercp/security/verify/page.tsx +92 -0
- package/app/(board)/usercp/signature/page.tsx +67 -0
- package/app/admin/antispam/page.tsx +155 -0
- package/app/admin/api/marketplace/screenshot/route.ts +68 -0
- package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/admin/api-tokens/page.tsx +129 -0
- package/app/admin/content/announcements/page.tsx +110 -0
- package/app/admin/content/attachments/page.tsx +199 -0
- package/app/admin/content/navigation/page.tsx +123 -0
- package/app/admin/content/page.tsx +170 -0
- package/app/admin/forums/[id]/page.tsx +96 -0
- package/app/admin/forums/[id]/permissions/page.tsx +108 -0
- package/app/admin/forums/page.tsx +45 -0
- package/app/admin/groups/[id]/page.tsx +133 -0
- package/app/admin/groups/memberships/page.tsx +56 -0
- package/app/admin/groups/page.tsx +104 -0
- package/app/admin/groups/promotions/page.tsx +143 -0
- package/app/admin/layout.tsx +86 -0
- package/app/admin/log/page.tsx +116 -0
- package/app/admin/page.tsx +206 -0
- package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
- package/app/admin/plugins/browse/page.tsx +70 -0
- package/app/admin/plugins/page.tsx +165 -0
- package/app/admin/security/page.tsx +124 -0
- package/app/admin/settings/page.tsx +190 -0
- package/app/admin/system/page.tsx +303 -0
- package/app/admin/themes/[key]/page.tsx +110 -0
- package/app/admin/themes/browse/page.tsx +70 -0
- package/app/admin/themes/page.tsx +163 -0
- package/app/admin/users/[id]/merge/page.tsx +136 -0
- package/app/admin/users/[id]/page.tsx +208 -0
- package/app/admin/users/mail/page.tsx +68 -0
- package/app/admin/users/page.tsx +260 -0
- package/app/admin/users/prune/page.tsx +186 -0
- package/app/api/health/route.ts +11 -0
- package/app/api/metrics/route.ts +66 -0
- package/app/api/plugins/[key]/[...path]/route.ts +27 -0
- package/app/api/read/all/route.ts +15 -0
- package/app/api/read/forum/[id]/route.ts +29 -0
- package/app/api/read/thread/[id]/route.ts +40 -0
- package/app/api/ready/route.ts +63 -0
- package/app/api/system/tick/route.ts +77 -0
- package/app/api/v1/[...path]/route.ts +208 -0
- package/app/api/v1/openapi.json/route.ts +12 -0
- package/app/apple-icon.ts +11 -0
- package/app/atom.xml/route.ts +7 -0
- package/app/attachment/[id]/route.ts +10 -0
- package/app/attachment/[id]/thumb/route.ts +10 -0
- package/app/auth/passkey/options/route.ts +145 -0
- package/app/auth/passkey/verify/route.ts +241 -0
- package/app/auth/resume/route.ts +55 -0
- package/app/auth/sso/[provider]/callback/route.ts +173 -0
- package/app/auth/sso/[provider]/go/route.ts +57 -0
- package/app/auth/sso/[provider]/route.ts +105 -0
- package/app/avatar/[id]/route.ts +31 -0
- package/app/brand/icon-192.png/route.ts +7 -0
- package/app/brand/icon-512.png/route.ts +7 -0
- package/app/brand/icon-maskable.png/route.ts +7 -0
- package/app/demo/stripe/[[...path]]/route.ts +27 -0
- package/app/error.tsx +15 -0
- package/app/feed.xml/route.ts +7 -0
- package/app/forumdisplay.php/page.tsx +11 -0
- package/app/group/[id]/badge/[scheme]/route.ts +26 -0
- package/app/icon.ts +13 -0
- package/app/install/page.tsx +221 -0
- package/app/jump/page.tsx +28 -0
- package/app/layout.tsx +124 -0
- package/app/logo/[scheme]/route.ts +24 -0
- package/app/manifest.ts +43 -0
- package/app/member.php/page.tsx +11 -0
- package/app/memberlist.php/page.tsx +11 -0
- package/app/not-found.tsx +14 -0
- package/app/opengraph-image.ts +11 -0
- package/app/redirect/page.tsx +44 -0
- package/app/robots.txt/route.ts +35 -0
- package/app/showthread.php/page.tsx +11 -0
- package/app/sitemap/[page]/route.ts +64 -0
- package/app/sitemap.xml/route.ts +31 -0
- package/app/twitter-image.ts +11 -0
- package/app/viewforum.php/page.tsx +11 -0
- package/app/viewtopic.php/page.tsx +11 -0
- package/bin/forum-web.mjs +241 -0
- package/components.json +21 -0
- package/instrumentation.ts +18 -0
- package/next.config.mjs +182 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/proxy.ts +160 -0
- package/src/components/account/active-sessions.tsx +87 -0
- package/src/components/account/avatar-form.tsx +119 -0
- package/src/components/account/credential-proof-form.tsx +76 -0
- package/src/components/account/logout-form.tsx +14 -0
- package/src/components/account/notification-forms.tsx +157 -0
- package/src/components/account/passkey-enrol.tsx +81 -0
- package/src/components/account/passkey-proof-button.tsx +47 -0
- package/src/components/account/push-device-form.tsx +193 -0
- package/src/components/account/relation-forms.tsx +69 -0
- package/src/components/account/reputation-forms.tsx +124 -0
- package/src/components/account/security-activity.tsx +46 -0
- package/src/components/account/sign-in-methods.tsx +166 -0
- package/src/components/account/subscription-forms.tsx +151 -0
- package/src/components/account/two-factor-forms.tsx +226 -0
- package/src/components/account/usercp-forms.tsx +387 -0
- package/src/components/account/usercp-nav.tsx +14 -0
- package/src/components/account/usercp-shell.tsx +34 -0
- package/src/components/admin/admin-forms.tsx +92 -0
- package/src/components/admin/admin-nav.tsx +52 -0
- package/src/components/admin/admin-undo.tsx +32 -0
- package/src/components/admin/api-token-forms.tsx +89 -0
- package/src/components/admin/badge-forms.tsx +97 -0
- package/src/components/admin/branding-forms.tsx +102 -0
- package/src/components/admin/content-forms.tsx +712 -0
- package/src/components/admin/form-bits.tsx +11 -0
- package/src/components/admin/forum-forms.tsx +524 -0
- package/src/components/admin/forum-tree.tsx +454 -0
- package/src/components/admin/group-forms.tsx +646 -0
- package/src/components/admin/mail-test-card.tsx +90 -0
- package/src/components/admin/marketplace-forms.tsx +30 -0
- package/src/components/admin/marketplace-listing.tsx +123 -0
- package/src/components/admin/navigation-forms.tsx +229 -0
- package/src/components/admin/navigation-tree.tsx +442 -0
- package/src/components/admin/oklch-picker.tsx +180 -0
- package/src/components/admin/plugin-forms.tsx +225 -0
- package/src/components/admin/settings-form.tsx +148 -0
- package/src/components/admin/system-forms.tsx +169 -0
- package/src/components/admin/theme-changes.tsx +354 -0
- package/src/components/admin/theme-forms.tsx +636 -0
- package/src/components/admin/theme-palette.tsx +119 -0
- package/src/components/admin/theme-preview.tsx +461 -0
- package/src/components/admin/user-bulk-toolbar.tsx +99 -0
- package/src/components/admin/user-forms.tsx +488 -0
- package/src/components/auth/auth-page.tsx +45 -0
- package/src/components/auth/form-controls.tsx +117 -0
- package/src/components/auth/login-form.tsx +47 -0
- package/src/components/auth/passkey-client.ts +171 -0
- package/src/components/auth/passkey-second-factor.tsx +69 -0
- package/src/components/auth/passkey-sign-in.tsx +61 -0
- package/src/components/auth/register-form.tsx +136 -0
- package/src/components/auth/resend-verification-form.tsx +34 -0
- package/src/components/auth/reset-confirm-form.tsx +43 -0
- package/src/components/auth/reset-request-form.tsx +35 -0
- package/src/components/auth/second-factor-form.tsx +51 -0
- package/src/components/auth/sso-buttons.tsx +47 -0
- package/src/components/board/live-region.tsx +74 -0
- package/src/components/board/search-off-notice.tsx +18 -0
- package/src/components/board/section-page.tsx +114 -0
- package/src/components/content/attachment-field.tsx +51 -0
- package/src/components/content/composer-field.ts +31 -0
- package/src/components/content/composer-ids.ts +6 -0
- package/src/components/content/composer-intents.tsx +27 -0
- package/src/components/content/composer-recovery.tsx +215 -0
- package/src/components/content/edit-post-form.tsx +121 -0
- package/src/components/content/markdown-editor.tsx +590 -0
- package/src/components/content/markdown-syntax.ts +53 -0
- package/src/components/content/mention-token.ts +23 -0
- package/src/components/content/multiquote-button.tsx +32 -0
- package/src/components/content/multiquote-selection.tsx +105 -0
- package/src/components/content/multiquote.ts +125 -0
- package/src/components/content/new-thread-form.tsx +169 -0
- package/src/components/content/poll-edit-form.tsx +96 -0
- package/src/components/content/poll.tsx +89 -0
- package/src/components/content/quote-in-place.tsx +51 -0
- package/src/components/content/reply-form.tsx +113 -0
- package/src/components/content/thanks-button.tsx +65 -0
- package/src/components/content/thread-rating.tsx +143 -0
- package/src/components/install/install-form.tsx +497 -0
- package/src/components/legal/legal-document.tsx +22 -0
- package/src/components/messages/message-forms.tsx +152 -0
- package/src/components/moderation/inline-moderation-form.tsx +158 -0
- package/src/components/moderation/modcp-nav.tsx +15 -0
- package/src/components/moderation/modcp-shell.tsx +41 -0
- package/src/components/moderation/queue-form.tsx +96 -0
- package/src/components/moderation/report-forms.tsx +120 -0
- package/src/components/moderation/signature-lock-form.tsx +83 -0
- package/src/components/moderation/thread-surgery-form.tsx +96 -0
- package/src/components/moderation/thread-tools-form.tsx +137 -0
- package/src/components/moderation/warning-forms.tsx +125 -0
- package/src/components/profile/custom-field.tsx +79 -0
- package/src/components/shell/appearance-icons.tsx +35 -0
- package/src/components/shell/board-notice.tsx +21 -0
- package/src/components/shell/copy-record.ts +20 -0
- package/src/components/shell/copy.tsx +17 -0
- package/src/components/shell/crash-notice.tsx +86 -0
- package/src/components/shell/demo-banner.tsx +49 -0
- package/src/components/shell/group-name-style.tsx +6 -0
- package/src/components/shell/measure.ts +1 -0
- package/src/components/shell/notification-menu.tsx +361 -0
- package/src/components/shell/offline-notice.tsx +29 -0
- package/src/components/shell/onboarding-banner.tsx +39 -0
- package/src/components/shell/page-shell.tsx +167 -0
- package/src/components/shell/panel-list.tsx +95 -0
- package/src/components/shell/panel-nav.tsx +40 -0
- package/src/components/shell/panel-overview.tsx +81 -0
- package/src/components/shell/panel-page.tsx +96 -0
- package/src/components/shell/panel-pagination.tsx +28 -0
- package/src/components/shell/panel-shell.tsx +31 -0
- package/src/components/shell/theme-runtime-style.tsx +6 -0
- package/src/components/shell/theme-switcher.tsx +97 -0
- package/src/components/shell/timezone-probe.tsx +45 -0
- package/src/components/shell/view-tabs.tsx +60 -0
- package/src/config.ts +13 -0
- package/src/server/absent-services.ts +29 -0
- package/src/server/admin-actions.ts +171 -0
- package/src/server/admin-settings-actions.ts +83 -0
- package/src/server/admin-undo-actions.ts +70 -0
- package/src/server/admin-undo.ts +75 -0
- package/src/server/admin.ts +112 -0
- package/src/server/announcements.ts +82 -0
- package/src/server/antispam.ts +310 -0
- package/src/server/api/content.ts +486 -0
- package/src/server/api/http.ts +119 -0
- package/src/server/api/members.ts +109 -0
- package/src/server/api/messages.ts +113 -0
- package/src/server/api/registry.ts +30 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/subscriptions.ts +106 -0
- package/src/server/api-auth.ts +66 -0
- package/src/server/api-token-actions.ts +85 -0
- package/src/server/api-tokens-admin.ts +95 -0
- package/src/server/appearance-actions.ts +39 -0
- package/src/server/attachment-download.ts +43 -0
- package/src/server/attachment-embed.ts +63 -0
- package/src/server/attachment-upload-actions.ts +92 -0
- package/src/server/attachments.ts +257 -0
- package/src/server/auth-actions.ts +479 -0
- package/src/server/auth-config.ts +61 -0
- package/src/server/auth-events.ts +92 -0
- package/src/server/auth-form-state.ts +14 -0
- package/src/server/auth-mail.ts +104 -0
- package/src/server/avatars.ts +83 -0
- package/src/server/board-latest-actions.ts +7 -0
- package/src/server/board-latest.tsx +103 -0
- package/src/server/board-offline.ts +23 -0
- package/src/server/board-url.ts +14 -0
- package/src/server/brand-assets.ts +151 -0
- package/src/server/brand-mark.tsx +110 -0
- package/src/server/branding-actions.ts +53 -0
- package/src/server/branding.ts +101 -0
- package/src/server/cache-targets.ts +13 -0
- package/src/server/code-highlighting.ts +33 -0
- package/src/server/container.ts +402 -0
- package/src/server/content-actions.ts +459 -0
- package/src/server/content-admin-actions.ts +492 -0
- package/src/server/content-admin.ts +95 -0
- package/src/server/content-security-policy.ts +35 -0
- package/src/server/context.ts +32 -0
- package/src/server/cookies.ts +139 -0
- package/src/server/credential-proof-actions.ts +42 -0
- package/src/server/credential-proof.ts +35 -0
- package/src/server/current-location.ts +19 -0
- package/src/server/demo-stripe.ts +104 -0
- package/src/server/demo-uploads.ts +28 -0
- package/src/server/demo.ts +80 -0
- package/src/server/discovery.ts +104 -0
- package/src/server/embeds.ts +113 -0
- package/src/server/error-notice.tsx +41 -0
- package/src/server/federation-actions.ts +113 -0
- package/src/server/federation.ts +138 -0
- package/src/server/feed-builder.ts +72 -0
- package/src/server/feed-routes.ts +122 -0
- package/src/server/fixture-actor-source.ts +62 -0
- package/src/server/fixture-forum-repo.ts +59 -0
- package/src/server/fixture-member-profile-repo.ts +30 -0
- package/src/server/fixture-post-repo.ts +108 -0
- package/src/server/fixture-thread-repo.ts +98 -0
- package/src/server/form-state-reporter.ts +34 -0
- package/src/server/form-values.ts +23 -0
- package/src/server/forum-admin-actions.ts +353 -0
- package/src/server/forum-admin.ts +98 -0
- package/src/server/group-admin-actions.ts +341 -0
- package/src/server/group-admin.ts +98 -0
- package/src/server/group-badge-actions.ts +61 -0
- package/src/server/group-badge.ts +62 -0
- package/src/server/group-identity.ts +104 -0
- package/src/server/i18n-catalogs.ts +42 -0
- package/src/server/i18n.ts +69 -0
- package/src/server/image-upload.ts +109 -0
- package/src/server/inline-moderation-actions.ts +129 -0
- package/src/server/install-actions.ts +124 -0
- package/src/server/install.ts +281 -0
- package/src/server/legacy-redirect.ts +83 -0
- package/src/server/legal.ts +43 -0
- package/src/server/location-header.ts +37 -0
- package/src/server/mail-brand.ts +43 -0
- package/src/server/mail-health.ts +41 -0
- package/src/server/mail-send.ts +44 -0
- package/src/server/mail-test-actions.ts +67 -0
- package/src/server/mail-test.ts +56 -0
- package/src/server/markdown-pipeline.ts +31 -0
- package/src/server/marketplace-actions.ts +47 -0
- package/src/server/marketplace-admin.ts +260 -0
- package/src/server/mention-actions.ts +21 -0
- package/src/server/mention-search.ts +35 -0
- package/src/server/message-actions.ts +110 -0
- package/src/server/messages.ts +141 -0
- package/src/server/modcp.ts +125 -0
- package/src/server/moderation-actions.ts +165 -0
- package/src/server/navigation-admin-actions.ts +217 -0
- package/src/server/navigation.ts +112 -0
- package/src/server/nonce.ts +13 -0
- package/src/server/notification-actions.ts +84 -0
- package/src/server/notification-audience.ts +10 -0
- package/src/server/notification-menu-actions.ts +82 -0
- package/src/server/notification-menu.ts +96 -0
- package/src/server/notifications.ts +93 -0
- package/src/server/onboarding-actions.ts +27 -0
- package/src/server/onboarding.ts +89 -0
- package/src/server/page-metadata.ts +14 -0
- package/src/server/passkey-challenge.ts +42 -0
- package/src/server/plugin-admin-actions.ts +232 -0
- package/src/server/plugin-admin.ts +276 -0
- package/src/server/plugin-host.ts +143 -0
- package/src/server/plugin-pages.ts +192 -0
- package/src/server/plugin-panel.ts +49 -0
- package/src/server/plugin-routes.ts +298 -0
- package/src/server/plugin-view.tsx +58 -0
- package/src/server/poll-actions.ts +104 -0
- package/src/server/poll-scope.ts +52 -0
- package/src/server/post-notifications.ts +71 -0
- package/src/server/post-scope.ts +86 -0
- package/src/server/presence.ts +177 -0
- package/src/server/profile-fields.ts +112 -0
- package/src/server/push-actions.ts +113 -0
- package/src/server/push.ts +40 -0
- package/src/server/redirect-back.ts +15 -0
- package/src/server/registration.ts +7 -0
- package/src/server/relation-actions.ts +89 -0
- package/src/server/relations.ts +45 -0
- package/src/server/reply-core.ts +173 -0
- package/src/server/report-actions.ts +165 -0
- package/src/server/report-scope.ts +21 -0
- package/src/server/reputation-actions.ts +146 -0
- package/src/server/reputation-target.ts +49 -0
- package/src/server/reputation.ts +70 -0
- package/src/server/request-fingerprint.ts +65 -0
- package/src/server/safe-path.ts +16 -0
- package/src/server/same-origin.ts +45 -0
- package/src/server/search-page.ts +313 -0
- package/src/server/search.ts +45 -0
- package/src/server/secret-auth.ts +20 -0
- package/src/server/see-other.ts +11 -0
- package/src/server/seed-board.ts +417 -0
- package/src/server/session-actions.ts +78 -0
- package/src/server/session-cookies.ts +124 -0
- package/src/server/session-key.ts +17 -0
- package/src/server/settings.ts +47 -0
- package/src/server/signatures.ts +62 -0
- package/src/server/sso-handshake.ts +79 -0
- package/src/server/stats.ts +63 -0
- package/src/server/subscription-actions.ts +161 -0
- package/src/server/surgery-actions.ts +180 -0
- package/src/server/syndication.ts +77 -0
- package/src/server/system-admin-actions.ts +134 -0
- package/src/server/system-admin.ts +90 -0
- package/src/server/test-container.ts +114 -0
- package/src/server/theme-admin-actions.ts +242 -0
- package/src/server/theme-admin.ts +203 -0
- package/src/server/theme-runtime.ts +89 -0
- package/src/server/theme-style.ts +279 -0
- package/src/server/theme.ts +52 -0
- package/src/server/thread-core.ts +207 -0
- package/src/server/thread-rating-actions.ts +55 -0
- package/src/server/thread-tool-actions.ts +154 -0
- package/src/server/two-factor-actions.ts +178 -0
- package/src/server/two-factor.ts +86 -0
- package/src/server/upgrade-notice.ts +41 -0
- package/src/server/user-admin-actions.ts +572 -0
- package/src/server/user-admin.ts +165 -0
- package/src/server/usercp-actions.ts +263 -0
- package/src/server/usercp-mail.ts +120 -0
- package/src/server/viewer-preferences.ts +64 -0
- package/src/server/warning-actions.ts +122 -0
- package/src/styles/globals.css +969 -0
- package/src/theme/contract.fixture.ts +793 -0
- package/src/view/account-copy.ts +351 -0
- package/src/view/admin-content-copy.ts +66 -0
- package/src/view/admin-copy.ts +106 -0
- package/src/view/admin-forum-tree-copy.ts +40 -0
- package/src/view/admin-group-copy.ts +80 -0
- package/src/view/admin-log.ts +58 -0
- package/src/view/admin-nav.ts +164 -0
- package/src/view/admin-navigation-copy.ts +58 -0
- package/src/view/admin-panel-copy.ts +194 -0
- package/src/view/admin-settings.ts +173 -0
- package/src/view/admin-theme-copy.ts +229 -0
- package/src/view/admin-user-copy.ts +96 -0
- package/src/view/attachments.ts +51 -0
- package/src/view/auth-copy.ts +147 -0
- package/src/view/board-index.ts +167 -0
- package/src/view/board-latest.ts +101 -0
- package/src/view/breadcrumb.ts +50 -0
- package/src/view/contacts.ts +85 -0
- package/src/view/content-copy.ts +118 -0
- package/src/view/contrast.ts +286 -0
- package/src/view/copy.ts +101 -0
- package/src/view/count.ts +8 -0
- package/src/view/discovery-view.ts +74 -0
- package/src/view/editor-toolbar.ts +84 -0
- package/src/view/error-notice.ts +52 -0
- package/src/view/feed.ts +130 -0
- package/src/view/forum-cursor.ts +54 -0
- package/src/view/forum-display.ts +152 -0
- package/src/view/forum-jump.ts +60 -0
- package/src/view/forum-notice.ts +24 -0
- package/src/view/inline-moderation.ts +87 -0
- package/src/view/install-copy.ts +95 -0
- package/src/view/legal.ts +58 -0
- package/src/view/locale.ts +40 -0
- package/src/view/marketplace-panel.ts +51 -0
- package/src/view/member-identity.ts +23 -0
- package/src/view/member-profile.ts +65 -0
- package/src/view/messages.ts +222 -0
- package/src/view/metadata.ts +75 -0
- package/src/view/modcp-nav.ts +76 -0
- package/src/view/moderation-copy.ts +84 -0
- package/src/view/moderation-queue.ts +67 -0
- package/src/view/navigation-arrange.ts +188 -0
- package/src/view/navigation.ts +173 -0
- package/src/view/notification-menu.ts +202 -0
- package/src/view/notifications.ts +84 -0
- package/src/view/onboarding.ts +26 -0
- package/src/view/pager.ts +191 -0
- package/src/view/panel-nav.ts +203 -0
- package/src/view/plugin-panel.ts +42 -0
- package/src/view/post-form.ts +61 -0
- package/src/view/post-link.ts +29 -0
- package/src/view/presence.ts +99 -0
- package/src/view/promotion-rules.ts +52 -0
- package/src/view/redirect-notice.ts +21 -0
- package/src/view/reputation.ts +107 -0
- package/src/view/search-controls.ts +103 -0
- package/src/view/search-results.ts +291 -0
- package/src/view/security-activity.ts +73 -0
- package/src/view/setting-groups.ts +43 -0
- package/src/view/shell.ts +164 -0
- package/src/view/slug-id.ts +12 -0
- package/src/view/sso-hand-off.ts +44 -0
- package/src/view/sso-notices.ts +48 -0
- package/src/view/subscriptions.ts +76 -0
- package/src/view/theme-draft.ts +167 -0
- package/src/view/theme-preference.ts +30 -0
- package/src/view/theme-tokens.ts +333 -0
- package/src/view/thread-view.ts +280 -0
- package/src/view/time.ts +31 -0
- package/src/view/timezone.ts +35 -0
- package/src/view/two-factor.ts +6 -0
- package/src/view/usercp-nav.ts +103 -0
- package/src/view/usercp.ts +196 -0
- package/src/view/warnings.ts +128 -0
- package/src/view/word-filter.ts +5 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AuthConfig,
|
|
5
|
+
DEFAULT_AUTH_POLICY,
|
|
6
|
+
IdentityService,
|
|
7
|
+
rejectedField,
|
|
8
|
+
resolveAuthPolicy,
|
|
9
|
+
} from '@meith/accounts'
|
|
10
|
+
import { env, GLOBAL_TAGS, logger } from '@meith/core'
|
|
11
|
+
import {
|
|
12
|
+
canConnect as canConnectTo,
|
|
13
|
+
countUsers,
|
|
14
|
+
createPostgresAccountStore,
|
|
15
|
+
getDb,
|
|
16
|
+
isInstalled,
|
|
17
|
+
markInstalled,
|
|
18
|
+
PostgresAdminRepository,
|
|
19
|
+
PostgresForumRepository,
|
|
20
|
+
PostgresSettingsRepository,
|
|
21
|
+
runMigrations,
|
|
22
|
+
SEED_GROUP_KEY,
|
|
23
|
+
withInstallLock,
|
|
24
|
+
} from '@meith/db'
|
|
25
|
+
import { currentMailConfig, drivers } from '@meith/drivers'
|
|
26
|
+
import {
|
|
27
|
+
type Check,
|
|
28
|
+
defaultForumSlug,
|
|
29
|
+
INSTALL_STEPS,
|
|
30
|
+
type InstallInput,
|
|
31
|
+
type MailProbe,
|
|
32
|
+
mailConfigFromInstallInput,
|
|
33
|
+
preflight,
|
|
34
|
+
type StepOutcome,
|
|
35
|
+
} from '@meith/install'
|
|
36
|
+
import {
|
|
37
|
+
canSendMail,
|
|
38
|
+
describeMailConfig,
|
|
39
|
+
mailConfigFromEnvironment,
|
|
40
|
+
SettingsSnapshot,
|
|
41
|
+
saveSettings,
|
|
42
|
+
} from '@meith/settings'
|
|
43
|
+
|
|
44
|
+
const INSTALLED_VERSION = '0.1.0'
|
|
45
|
+
|
|
46
|
+
const INSTALL_IN_FLIGHT = 'install.raced.inFlight'
|
|
47
|
+
|
|
48
|
+
const INSTALL_RACED = 'install.raced.sealed'
|
|
49
|
+
|
|
50
|
+
export async function gatherPreflight(): Promise<readonly Check[]> {
|
|
51
|
+
const dataSource = env.DATA_SOURCE === 'postgres' ? 'postgres' : 'fixture'
|
|
52
|
+
const databaseUrl = env.DATABASE_URL ?? null
|
|
53
|
+
|
|
54
|
+
let connected: boolean | null = null
|
|
55
|
+
let users: number | null = null
|
|
56
|
+
let installed = false
|
|
57
|
+
|
|
58
|
+
if (dataSource === 'postgres' && databaseUrl !== null && databaseUrl !== '') {
|
|
59
|
+
try {
|
|
60
|
+
const db = getDb()
|
|
61
|
+
connected = await canConnectTo(db)
|
|
62
|
+
if (connected) {
|
|
63
|
+
users = await countUsers(db)
|
|
64
|
+
installed = await isInstalled(db)
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger().warn({ err: String(error) }, 'install preflight could not reach the database')
|
|
68
|
+
connected = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return preflight({
|
|
73
|
+
dataSource,
|
|
74
|
+
databaseUrl,
|
|
75
|
+
hasAuthSecret: (env.AUTH_SECRET ?? '') !== '',
|
|
76
|
+
hasTickSecret: (env.TICK_SECRET ?? '') !== '',
|
|
77
|
+
publicUrl: env.APP_URL ?? null,
|
|
78
|
+
mail: await probeMail(),
|
|
79
|
+
canConnect: connected,
|
|
80
|
+
pendingMigrations: null,
|
|
81
|
+
userCount: users,
|
|
82
|
+
alreadyInstalled: installed,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function probeMail(): Promise<MailProbe> {
|
|
87
|
+
const fromEnvironment = mailConfigFromEnvironment(env)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const config = fromEnvironment ?? (await currentMailConfig())
|
|
91
|
+
return {
|
|
92
|
+
configured: canSendMail(config),
|
|
93
|
+
source: fromEnvironment === null ? 'board' : 'environment',
|
|
94
|
+
summary: describeMailConfig(config),
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger().warn({ err: String(error) }, 'install preflight could not read mail settings')
|
|
98
|
+
return {
|
|
99
|
+
configured: false,
|
|
100
|
+
source: fromEnvironment === null ? 'board' : 'environment',
|
|
101
|
+
summary: 'Not sending — messages are written to the server log',
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function installerIsSealed(): Promise<boolean> {
|
|
107
|
+
if (env.DATA_SOURCE !== 'postgres') return false
|
|
108
|
+
try {
|
|
109
|
+
return await isInstalled(getDb())
|
|
110
|
+
} catch {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function forgetCachedBoard(): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
await drivers().cache.invalidateTags(GLOBAL_TAGS)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger().warn(
|
|
120
|
+
{ err: String(error) },
|
|
121
|
+
'install could not clear the cache; restart the server if the board looks empty',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type InstallRun = { readonly sealed: true } | { readonly report: readonly StepOutcome[] }
|
|
127
|
+
|
|
128
|
+
export async function runInstall(input: InstallInput): Promise<InstallRun> {
|
|
129
|
+
const url = env.DATABASE_URL ?? ''
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const run = await withInstallLock(url, async (): Promise<InstallRun> => {
|
|
133
|
+
if (await isInstalled(getDb())) return { sealed: true }
|
|
134
|
+
return { report: await performInstall(input) }
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return run ?? { report: [{ id: 'migrate', status: 'failed', error: INSTALL_IN_FLIGHT }] }
|
|
138
|
+
} finally {
|
|
139
|
+
await forgetCachedBoard()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function performInstall(input: InstallInput): Promise<readonly StepOutcome[]> {
|
|
144
|
+
const report: StepOutcome[] = []
|
|
145
|
+
const db = getDb()
|
|
146
|
+
|
|
147
|
+
const step = async (id: string, body: () => Promise<void>): Promise<boolean> => {
|
|
148
|
+
try {
|
|
149
|
+
await body()
|
|
150
|
+
report.push({ id, status: 'done' })
|
|
151
|
+
return true
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
154
|
+
logger().error({ step: id, err: message }, 'install step failed')
|
|
155
|
+
const field = rejectedField(error)
|
|
156
|
+
report.push({
|
|
157
|
+
id,
|
|
158
|
+
status: 'failed',
|
|
159
|
+
error: message.slice(0, 300),
|
|
160
|
+
...(field === null ? {} : { field }),
|
|
161
|
+
})
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!(await step('migrate', async () => void (await runMigrations())))) return report
|
|
167
|
+
|
|
168
|
+
const settings = new PostgresSettingsRepository(db)
|
|
169
|
+
if (
|
|
170
|
+
!(await step('settings', async () => {
|
|
171
|
+
await settings.save(
|
|
172
|
+
new Map([
|
|
173
|
+
['board.name', input.boardName],
|
|
174
|
+
['board.url', input.boardUrl],
|
|
175
|
+
]),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
const mail = mailConfigFromInstallInput(input)
|
|
179
|
+
if (mail.transport === 'log') return
|
|
180
|
+
|
|
181
|
+
await saveSettings(
|
|
182
|
+
settings,
|
|
183
|
+
mail.transport === 'http'
|
|
184
|
+
? {
|
|
185
|
+
'mail.transport': 'http',
|
|
186
|
+
'mail.from': mail.from,
|
|
187
|
+
'mail.http_endpoint': mail.endpoint,
|
|
188
|
+
'mail.http_token': mail.token,
|
|
189
|
+
}
|
|
190
|
+
: {
|
|
191
|
+
'mail.transport': 'smtp',
|
|
192
|
+
'mail.from': mail.from,
|
|
193
|
+
'mail.smtp_host': mail.host,
|
|
194
|
+
'mail.smtp_port': mail.port,
|
|
195
|
+
'mail.smtp_security': mail.security,
|
|
196
|
+
'mail.smtp_username': mail.username,
|
|
197
|
+
'mail.smtp_password': mail.password,
|
|
198
|
+
},
|
|
199
|
+
SettingsSnapshot.fromOverrides(await settings.loadAll()),
|
|
200
|
+
)
|
|
201
|
+
}))
|
|
202
|
+
) {
|
|
203
|
+
return report
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const admin = new PostgresAdminRepository(db)
|
|
207
|
+
let adminUserId: number | null = null
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
!(await step('admin', async () => {
|
|
211
|
+
const registered = await admin.findGroup(SEED_GROUP_KEY.registered)
|
|
212
|
+
const administrator = await admin.findGroup(SEED_GROUP_KEY.administrators)
|
|
213
|
+
if (registered === null || administrator === null) {
|
|
214
|
+
const missing =
|
|
215
|
+
registered === null ? SEED_GROUP_KEY.registered : SEED_GROUP_KEY.administrators
|
|
216
|
+
const present = await admin.listGroupKeys()
|
|
217
|
+
throw new Error(
|
|
218
|
+
`No usergroup has the key "${missing}". ` +
|
|
219
|
+
(present.length === 0
|
|
220
|
+
? 'The usergroups table is empty, so the seed migration did not run.'
|
|
221
|
+
: `The board has: ${present.join(', ')}.`),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const stored = SettingsSnapshot.fromOverrides(await settings.loadAll())
|
|
226
|
+
|
|
227
|
+
const config: AuthConfig = {
|
|
228
|
+
...DEFAULT_AUTH_POLICY,
|
|
229
|
+
...resolveAuthPolicy((key) => stored.get(key as never), {
|
|
230
|
+
...DEFAULT_AUTH_POLICY,
|
|
231
|
+
activationMethod: 'none',
|
|
232
|
+
}),
|
|
233
|
+
registrationEnabled: true,
|
|
234
|
+
activationMethod: 'none',
|
|
235
|
+
defaultMemberGroupId: registered.id,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const identity = new IdentityService({ store: createPostgresAccountStore(db), config })
|
|
239
|
+
const result = await identity.register({
|
|
240
|
+
username: input.username,
|
|
241
|
+
email: input.email,
|
|
242
|
+
password: input.password,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
await admin.setPrimaryGroup(result.account.id, administrator.id)
|
|
246
|
+
adminUserId = result.account.id
|
|
247
|
+
}))
|
|
248
|
+
) {
|
|
249
|
+
return report
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const forums = new PostgresForumRepository(db)
|
|
253
|
+
if (
|
|
254
|
+
!(await step('forum', async () => {
|
|
255
|
+
const category = await forums.create({
|
|
256
|
+
type: 'category',
|
|
257
|
+
title: input.boardName,
|
|
258
|
+
slug: defaultForumSlug(input.boardName),
|
|
259
|
+
parentId: null,
|
|
260
|
+
})
|
|
261
|
+
await forums.create({
|
|
262
|
+
type: 'forum',
|
|
263
|
+
title: 'General discussion',
|
|
264
|
+
slug: 'general-discussion',
|
|
265
|
+
description: 'Anything that does not fit elsewhere.',
|
|
266
|
+
parentId: category.id,
|
|
267
|
+
})
|
|
268
|
+
}))
|
|
269
|
+
) {
|
|
270
|
+
return report
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await step('seal', async () => {
|
|
274
|
+
if (!(await markInstalled(db, INSTALLED_VERSION))) throw new Error(INSTALL_RACED)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
logger().info({ adminUserId, steps: report.length }, 'board installed')
|
|
278
|
+
return report
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { INSTALL_STEPS }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { notFound, permanentRedirect } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
import { env } from '@meith/core'
|
|
6
|
+
import { getDb, resolveLegacyId } from '@meith/db'
|
|
7
|
+
import { type LegacyTarget, legacyRedirectPath, resolveLegacyUrl } from '@meith/import'
|
|
8
|
+
|
|
9
|
+
import { getContainer } from './container'
|
|
10
|
+
import { getSettings } from './settings'
|
|
11
|
+
|
|
12
|
+
type Kind = 'thread' | 'forum' | 'post' | 'user'
|
|
13
|
+
|
|
14
|
+
const LEGACY_KIND: Readonly<Record<Kind, 'thread' | 'forum' | 'post' | 'user'>> = {
|
|
15
|
+
thread: 'thread',
|
|
16
|
+
forum: 'forum',
|
|
17
|
+
post: 'post',
|
|
18
|
+
user: 'user',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LegacyScript =
|
|
22
|
+
| 'showthread.php'
|
|
23
|
+
| 'forumdisplay.php'
|
|
24
|
+
| 'member.php'
|
|
25
|
+
| 'viewtopic.php'
|
|
26
|
+
| 'viewforum.php'
|
|
27
|
+
| 'memberlist.php'
|
|
28
|
+
|
|
29
|
+
function searchStringOf(params: Record<string, string | string[] | undefined>): string {
|
|
30
|
+
const search = new URLSearchParams()
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(params)) {
|
|
33
|
+
if (value === undefined) continue
|
|
34
|
+
if (Array.isArray(value)) for (const item of value) search.append(key, item)
|
|
35
|
+
else search.append(key, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rendered = search.toString()
|
|
39
|
+
return rendered === '' ? '' : `?${rendered}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function legacyDestination(
|
|
43
|
+
pathname: string,
|
|
44
|
+
searchParams: Record<string, string | string[] | undefined>,
|
|
45
|
+
): Promise<string | null> {
|
|
46
|
+
if (env.DATA_SOURCE !== 'postgres') return null
|
|
47
|
+
|
|
48
|
+
const settings = await getSettings()
|
|
49
|
+
if (settings.get('board.legacy_redirects') !== true) return null
|
|
50
|
+
|
|
51
|
+
const target = resolveLegacyUrl(pathname, searchStringOf(searchParams))
|
|
52
|
+
if (target === null) return null
|
|
53
|
+
|
|
54
|
+
if (target.kind === 'home') return '/'
|
|
55
|
+
|
|
56
|
+
const newId = await resolveLegacyId(getDb(), LEGACY_KIND[target.kind], target.legacyId)
|
|
57
|
+
if (newId === null) return null
|
|
58
|
+
|
|
59
|
+
return legacyRedirectPath(target, newId, await slugFor(target, newId))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function serveLegacyUrl(
|
|
63
|
+
script: LegacyScript,
|
|
64
|
+
searchParams: Record<string, string | string[] | undefined>,
|
|
65
|
+
): Promise<never> {
|
|
66
|
+
const path = await legacyDestination(`/${script}`, searchParams)
|
|
67
|
+
if (path === null) notFound()
|
|
68
|
+
|
|
69
|
+
permanentRedirect(path)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function slugFor(target: LegacyTarget, newId: number): Promise<string | null> {
|
|
73
|
+
try {
|
|
74
|
+
const { forums } = getContainer()
|
|
75
|
+
if (target.kind === 'forum') {
|
|
76
|
+
const all = await forums.listAll()
|
|
77
|
+
return all.find((forum) => forum.id === newId)?.slug ?? null
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { renderMarkdown } from '@meith/markdown'
|
|
4
|
+
import type { LinkModel } from '@meith/theme-kit'
|
|
5
|
+
|
|
6
|
+
import { buildLegalLinks, isPublished, type LegalPage, legalPage, TERMS_PAGE } from '../view/legal'
|
|
7
|
+
import { getTranslator } from './i18n'
|
|
8
|
+
import { getSettings } from './settings'
|
|
9
|
+
|
|
10
|
+
export interface LegalDocument {
|
|
11
|
+
readonly title: string
|
|
12
|
+
readonly href: string
|
|
13
|
+
readonly bodyHtml: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function bodyOf(page: LegalPage): Promise<string> {
|
|
17
|
+
return (await getSettings()).get(page.settingKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function legalDocument(slug: string): Promise<LegalDocument | null> {
|
|
21
|
+
const page = legalPage(slug)
|
|
22
|
+
if (page === null) return null
|
|
23
|
+
|
|
24
|
+
const body = await bodyOf(page)
|
|
25
|
+
if (!isPublished(body)) return null
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
title: (await getTranslator()).t(page.titleKey),
|
|
29
|
+
href: page.href,
|
|
30
|
+
bodyHtml: renderMarkdown(body, { headingOffset: 0 }).html,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function legalFooterLinks(): Promise<readonly LinkModel[]> {
|
|
35
|
+
const settings = await getSettings()
|
|
36
|
+
return buildLegalLinks((page) => settings.get(page.settingKey), await getTranslator())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function termsAcceptance(): Promise<LinkModel | null> {
|
|
40
|
+
return isPublished(await bodyOf(TERMS_PAGE))
|
|
41
|
+
? { label: (await getTranslator()).t(TERMS_PAGE.titleKey), href: TERMS_PAGE.href }
|
|
42
|
+
: null
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const PATH_HEADER = 'x-forum-path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The query string that came with `PATH_HEADER`, leading `?` included, or
|
|
5
|
+
* absent when there was none.
|
|
6
|
+
*
|
|
7
|
+
* Separate from the path because the path has readers that must not see a
|
|
8
|
+
* query: a presence row records where somebody is, and `/admin/settings` is one
|
|
9
|
+
* place however many groups it can be filtered by. The panel rail is the reader
|
|
10
|
+
* that needs both — its settings sections differ only by `?group=`, so a rail
|
|
11
|
+
* given the path alone cannot say which one is open.
|
|
12
|
+
*/
|
|
13
|
+
export const QUERY_HEADER = 'x-forum-query'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set by the middleware when it has just minted the guest cookie, meaning the
|
|
17
|
+
* client has not sent one back yet.
|
|
18
|
+
*
|
|
19
|
+
* It exists because the obvious test does not work: a cookie set on a
|
|
20
|
+
* `NextResponse.next()` response is reflected into the request the render sees,
|
|
21
|
+
* so neither `cookies()` nor the raw `Cookie` header can tell "the client kept
|
|
22
|
+
* this" from "we made it up a millisecond ago". Without the distinction every
|
|
23
|
+
* request from something that discards cookies wrote a presence row, which
|
|
24
|
+
* measured as twelve rows from twelve crawler requests.
|
|
25
|
+
*
|
|
26
|
+
* The middleware sets it or deletes it on every request, so a client cannot
|
|
27
|
+
* supply its own.
|
|
28
|
+
*/
|
|
29
|
+
export const FRESH_GUEST_HEADER = 'x-forum-fresh-guest'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The per-request Content-Security-Policy nonce minted by `proxy.ts`, for the
|
|
33
|
+
* handful of inline `<script>` tags the app emits itself. Next reads the nonce
|
|
34
|
+
* out of the policy header on its own; this one is for our tags. See
|
|
35
|
+
* `docs/operating.md`.
|
|
36
|
+
*/
|
|
37
|
+
export const NONCE_HEADER = 'x-nonce'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { cache } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { Translator } from '@meith/i18n'
|
|
6
|
+
import { buildMailBrand, type MailBrand } from '@meith/mail'
|
|
7
|
+
|
|
8
|
+
import { boardUrl } from './board-url'
|
|
9
|
+
import { getSettings } from './settings'
|
|
10
|
+
import { getBoardThemeStyle } from './theme-runtime'
|
|
11
|
+
|
|
12
|
+
export const mailBrand = cache(async (): Promise<MailBrand> => {
|
|
13
|
+
const [settings, origin, theme] = await Promise.all([
|
|
14
|
+
getSettings(),
|
|
15
|
+
boardUrl(),
|
|
16
|
+
getBoardThemeStyle(),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const boardName = settings.get('board.name')
|
|
20
|
+
const alt = settings.get('board.logo_alt').trim()
|
|
21
|
+
|
|
22
|
+
return buildMailBrand({
|
|
23
|
+
boardName,
|
|
24
|
+
fromName: settings.get('mail.from_name'),
|
|
25
|
+
boardUrl: origin,
|
|
26
|
+
tokens: theme.defaultTokens,
|
|
27
|
+
logo: {
|
|
28
|
+
lightKey: settings.get('board.logo_light'),
|
|
29
|
+
darkKey: settings.get('board.logo_dark'),
|
|
30
|
+
alt: alt === '' ? boardName : alt,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export async function brandedFor(
|
|
36
|
+
t: Translator,
|
|
37
|
+
fallbackKey: string,
|
|
38
|
+
): Promise<{ readonly brand: MailBrand; readonly boardName: string }> {
|
|
39
|
+
const brand = await mailBrand()
|
|
40
|
+
const boardName = brand.boardName === '' ? t.t(fallbackKey) : brand.boardName
|
|
41
|
+
|
|
42
|
+
return { brand: { ...brand, boardName }, boardName }
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { env } from '@meith/core'
|
|
4
|
+
import { currentMailConfig } from '@meith/drivers'
|
|
5
|
+
import { canSendMail, describeMailConfig, type MailConfig, type MailSource } from '@meith/settings'
|
|
6
|
+
|
|
7
|
+
import { boardAuthConfig } from './auth-config'
|
|
8
|
+
|
|
9
|
+
export interface MailReadiness {
|
|
10
|
+
readonly config: MailConfig
|
|
11
|
+
readonly source: MailSource
|
|
12
|
+
readonly summary: string
|
|
13
|
+
readonly sends: boolean
|
|
14
|
+
readonly activationMethod: 'none' | 'email' | 'admin' | 'both'
|
|
15
|
+
readonly unactivatable: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function assessMailReadiness(): Promise<MailReadiness> {
|
|
19
|
+
const { activationMethod } = await boardAuthConfig()
|
|
20
|
+
|
|
21
|
+
let config: MailConfig
|
|
22
|
+
try {
|
|
23
|
+
config = await currentMailConfig()
|
|
24
|
+
} catch {
|
|
25
|
+
config = { transport: 'log' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sends = canSendMail(config)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
config,
|
|
32
|
+
source: env.MAIL_DRIVER === 'log' ? 'board' : 'environment',
|
|
33
|
+
summary: describeMailConfig(config),
|
|
34
|
+
sends,
|
|
35
|
+
activationMethod,
|
|
36
|
+
unactivatable:
|
|
37
|
+
!sends &&
|
|
38
|
+
env.NODE_ENV !== 'development' &&
|
|
39
|
+
(activationMethod === 'email' || activationMethod === 'both'),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import type { OutgoingMail } from '@meith/core'
|
|
4
|
+
import { currentRequestId } from '@meith/core/logger'
|
|
5
|
+
import { drivers } from '@meith/drivers'
|
|
6
|
+
|
|
7
|
+
import { emitEvent, filterView } from './plugin-view'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every message the board sends from a request, past the plugins first. A
|
|
11
|
+
* plugin may rewrite the recipient, the subject or either body, or return null
|
|
12
|
+
* to drop the message — which is silent, because the member asked the board to
|
|
13
|
+
* do something and the board's answer is the page, not the mail.
|
|
14
|
+
*/
|
|
15
|
+
export async function sendBoardMail(template: string, mail: OutgoingMail): Promise<boolean> {
|
|
16
|
+
const proposed = await filterView(
|
|
17
|
+
'mail.send.before',
|
|
18
|
+
{
|
|
19
|
+
to: mail.to,
|
|
20
|
+
subject: mail.subject,
|
|
21
|
+
textBody: mail.text,
|
|
22
|
+
htmlBody: mail.html ?? null,
|
|
23
|
+
},
|
|
24
|
+
{ template },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (proposed === null) return false
|
|
28
|
+
|
|
29
|
+
await drivers().mail.send({
|
|
30
|
+
...mail,
|
|
31
|
+
to: proposed.to,
|
|
32
|
+
subject: proposed.subject,
|
|
33
|
+
text: proposed.textBody,
|
|
34
|
+
...(proposed.htmlBody === null ? {} : { html: proposed.htmlBody }),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await emitEvent(
|
|
38
|
+
'mail.sent',
|
|
39
|
+
{ to: proposed.to, template },
|
|
40
|
+
{ requestId: currentRequestId() ?? null },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { isAppError, logger } from '@meith/core'
|
|
4
|
+
import { currentMailConfig } from '@meith/drivers'
|
|
5
|
+
import { canSendMail, describeMailConfig } from '@meith/settings'
|
|
6
|
+
|
|
7
|
+
import { recordAdminAction, requireAdmin } from './admin'
|
|
8
|
+
import type { FormState } from './auth-form-state'
|
|
9
|
+
import { getContainer } from './container'
|
|
10
|
+
import { getTranslator, tr } from './i18n'
|
|
11
|
+
import { sendTestMail } from './mail-test'
|
|
12
|
+
import { getSettings } from './settings'
|
|
13
|
+
|
|
14
|
+
export async function sendTestMailAction(
|
|
15
|
+
_previous: FormState,
|
|
16
|
+
_form: FormData,
|
|
17
|
+
): Promise<FormState> {
|
|
18
|
+
try {
|
|
19
|
+
const admin = await requireAdmin()
|
|
20
|
+
const t = await getTranslator()
|
|
21
|
+
|
|
22
|
+
const config = await currentMailConfig()
|
|
23
|
+
if (!canSendMail(config)) {
|
|
24
|
+
return {
|
|
25
|
+
error:
|
|
26
|
+
(await tr('notice.app.board-working-mail-configuration-there')) +
|
|
27
|
+
t.t('mailTest.currentConfiguration', { configuration: describeMailConfig(config) }),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const account = await getContainer().accountStore.accounts.findById(admin.userId)
|
|
32
|
+
if (account === null || account.email === '') {
|
|
33
|
+
return { error: await tr('notice.app.account-e-mail-address-send-test') }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const settings = await getSettings()
|
|
37
|
+
const result = await sendTestMail({
|
|
38
|
+
config,
|
|
39
|
+
to: account.email,
|
|
40
|
+
boardName: settings.get('board.name'),
|
|
41
|
+
t,
|
|
42
|
+
fromName: settings.get('mail.from_name'),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
await recordAdminAction({
|
|
46
|
+
action: 'mail.tested',
|
|
47
|
+
detail: { ok: result.ok },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
return {
|
|
52
|
+
error: t.t('mailTest.action.failed', {
|
|
53
|
+
error: result.error ?? '',
|
|
54
|
+
configuration: describeMailConfig(config),
|
|
55
|
+
}),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
notice: t.t('mailTest.action.sent', { email: account.email }),
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isAppError(error)) return { error: error.message }
|
|
64
|
+
logger({ module: 'mail-test' }).error({ err: String(error) }, 'test send failed')
|
|
65
|
+
return { error: await tr('notice.app.test-could-run') }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
3
|
+
import { isAppError, logger } from '@meith/core'
|
|
4
|
+
import { createMailDriver } from '@meith/drivers'
|
|
5
|
+
import type { Translator } from '@meith/i18n'
|
|
6
|
+
import { describeMailConfig, type MailConfig } from '@meith/settings'
|
|
7
|
+
|
|
8
|
+
export interface MailTestResult {
|
|
9
|
+
readonly ok: boolean
|
|
10
|
+
readonly error?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function sendTestMail(input: {
|
|
14
|
+
readonly config: MailConfig
|
|
15
|
+
readonly to: string
|
|
16
|
+
readonly boardName: string
|
|
17
|
+
readonly t: Translator
|
|
18
|
+
readonly fromName?: string
|
|
19
|
+
}): Promise<MailTestResult> {
|
|
20
|
+
const board =
|
|
21
|
+
input.boardName.trim() === '' ? input.t.t('mailTest.boardFallback') : input.boardName.trim()
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const driver = createMailDriver(input.config)
|
|
25
|
+
|
|
26
|
+
await driver.send({
|
|
27
|
+
to: input.to,
|
|
28
|
+
subject: `[${board}] ${input.t.t('mailTest.subject')}`,
|
|
29
|
+
text: [
|
|
30
|
+
input.t.t('mailTest.intro', { board }),
|
|
31
|
+
'',
|
|
32
|
+
input.t.t('mailTest.body'),
|
|
33
|
+
'',
|
|
34
|
+
input.t.t('mailTest.sentThrough', { configuration: describeMailConfig(input.config) }),
|
|
35
|
+
].join('\n'),
|
|
36
|
+
...(input.fromName === undefined || input.fromName.trim() === ''
|
|
37
|
+
? {}
|
|
38
|
+
: { fromName: input.fromName }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return { ok: true }
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = isAppError(error)
|
|
44
|
+
? error.message
|
|
45
|
+
: error instanceof Error
|
|
46
|
+
? error.message
|
|
47
|
+
: String(error)
|
|
48
|
+
|
|
49
|
+
logger({ module: 'mail-test' }).warn(
|
|
50
|
+
{ err: message, config: describeMailConfig(input.config) },
|
|
51
|
+
'test message could not be sent',
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return { ok: false, error: message.slice(0, 400) }
|
|
55
|
+
}
|
|
56
|
+
}
|