create-nextblock 0.16.1 → 0.16.2
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/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -221
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -3092
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -123
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
'use server';
|
|
2
|
-
|
|
3
|
-
import { headers } from 'next/headers';
|
|
4
|
-
import { after } from 'next/server';
|
|
5
|
-
import { revalidatePath } from 'next/cache';
|
|
6
|
-
import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
7
|
-
|
|
8
|
-
import { getCookieValue } from '../../lib/auth/cookies';
|
|
9
|
-
import { THREAD_COOKIE, verifyThreadToken } from '../../lib/messages/thread-token';
|
|
10
|
-
import {
|
|
11
|
-
getFormEndpoint,
|
|
12
|
-
notifyAdminOfMessage,
|
|
13
|
-
resolveFormRecipient,
|
|
14
|
-
} from '../../lib/messages/threads';
|
|
15
|
-
import { resolveSellerContactEmail } from '../../lib/commerce/seller-contact';
|
|
16
|
-
import { verifyBotProtection } from '../../lib/botProtection/verify';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A visitor replying on their own thread page.
|
|
20
|
-
*
|
|
21
|
-
* The token is read from the HttpOnly COOKIE, never from a form field — so it is never
|
|
22
|
-
* in a POST body, never in a client-side variable, and cannot be swapped by editing the
|
|
23
|
-
* request. Everything else here mirrors the enquiry action: bot protection first, a
|
|
24
|
-
* fail-closed per-IP throttle, then a write.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const MAX_BODY_LENGTH = 5000;
|
|
28
|
-
const MAX_USER_AGENT_LENGTH = 500;
|
|
29
|
-
const THROTTLE_WINDOW_MINUTES = 10;
|
|
30
|
-
const THROTTLE_MAX_REPLIES = 10;
|
|
31
|
-
const UNKNOWN_IP_BUCKET = 'unknown';
|
|
32
|
-
|
|
33
|
-
export interface ThreadReplyState {
|
|
34
|
-
success: boolean;
|
|
35
|
-
messageKey: 'thread.sent' | 'thread.error' | 'thread.throttled' | 'thread.closed' | '';
|
|
36
|
-
message?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function firstHop(headerValue: string | null): string | null {
|
|
40
|
-
const [first] = (headerValue ?? '').split(',');
|
|
41
|
-
const trimmed = first?.trim() ?? '';
|
|
42
|
-
return trimmed || null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function maskIp(ip: string | null): string {
|
|
46
|
-
if (!ip) return UNKNOWN_IP_BUCKET;
|
|
47
|
-
if (ip.includes(':')) {
|
|
48
|
-
const hextets = ip.split(':').filter(Boolean);
|
|
49
|
-
return `${hextets.slice(0, 3).join(':')}::x`;
|
|
50
|
-
}
|
|
51
|
-
const octets = ip.split('.');
|
|
52
|
-
if (octets.length === 4) {
|
|
53
|
-
octets[3] = 'x';
|
|
54
|
-
return octets.join('.');
|
|
55
|
-
}
|
|
56
|
-
return UNKNOWN_IP_BUCKET;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function submitThreadReply(
|
|
60
|
-
_prevState: unknown,
|
|
61
|
-
formData: FormData
|
|
62
|
-
): Promise<ThreadReplyState> {
|
|
63
|
-
const verification = await verifyBotProtection(formData);
|
|
64
|
-
if (!verification.ok) {
|
|
65
|
-
if (verification.reason === 'honeypot') {
|
|
66
|
-
return { success: true, messageKey: 'thread.sent' };
|
|
67
|
-
}
|
|
68
|
-
return { success: false, messageKey: '', message: verification.message };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const rawBody = formData.get('body');
|
|
72
|
-
const body = typeof rawBody === 'string' ? rawBody.trim().slice(0, MAX_BODY_LENGTH) : '';
|
|
73
|
-
if (!body) return { success: false, messageKey: 'thread.error' };
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const supabase = getServiceRoleSupabaseClient();
|
|
77
|
-
|
|
78
|
-
const token = await getCookieValue(THREAD_COOKIE);
|
|
79
|
-
const check = await verifyThreadToken(supabase, token);
|
|
80
|
-
if (!check.valid) return { success: false, messageKey: 'thread.error' };
|
|
81
|
-
|
|
82
|
-
const thread = check.thread;
|
|
83
|
-
if (thread.status === 'closed') return { success: false, messageKey: 'thread.closed' };
|
|
84
|
-
|
|
85
|
-
const requestHeaders = await headers();
|
|
86
|
-
const ipMasked = maskIp(
|
|
87
|
-
firstHop(requestHeaders.get('x-vercel-forwarded-for')) ??
|
|
88
|
-
firstHop(requestHeaders.get('x-real-ip')) ??
|
|
89
|
-
firstHop(requestHeaders.get('x-forwarded-for'))
|
|
90
|
-
);
|
|
91
|
-
const userAgent = requestHeaders.get('user-agent');
|
|
92
|
-
|
|
93
|
-
const since = new Date(Date.now() - THROTTLE_WINDOW_MINUTES * 60_000).toISOString();
|
|
94
|
-
const { count } = await supabase
|
|
95
|
-
.from('thread_messages')
|
|
96
|
-
.select('id', { count: 'exact', head: true })
|
|
97
|
-
.eq('ip_masked', ipMasked)
|
|
98
|
-
.gte('created_at', since);
|
|
99
|
-
if ((count ?? 0) >= THROTTLE_MAX_REPLIES) {
|
|
100
|
-
return { success: false, messageKey: 'thread.throttled' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { data: message, error } = await supabase
|
|
104
|
-
.from('thread_messages')
|
|
105
|
-
.insert({
|
|
106
|
-
thread_id: thread.id,
|
|
107
|
-
direction: 'inbound',
|
|
108
|
-
body,
|
|
109
|
-
author_name: thread.sender_name,
|
|
110
|
-
ip_masked: ipMasked,
|
|
111
|
-
})
|
|
112
|
-
.select('id')
|
|
113
|
-
.single();
|
|
114
|
-
|
|
115
|
-
if (error || !message) {
|
|
116
|
-
console.error('[thread] Could not store a visitor reply:', error?.message);
|
|
117
|
-
return { success: false, messageKey: 'thread.error' };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
await supabase
|
|
121
|
-
.from('message_threads')
|
|
122
|
-
.update({
|
|
123
|
-
last_message_at: new Date().toISOString(),
|
|
124
|
-
unread_for_admin: true,
|
|
125
|
-
unread_for_visitor: false,
|
|
126
|
-
// A visitor writing back REOPENS the conversation. Marking it handled was a
|
|
127
|
-
// statement about the last exchange, not a permanent verdict — leaving it closed
|
|
128
|
-
// would drop their reply out of the inbox's default view, which is precisely
|
|
129
|
-
// where someone waiting on an answer must not go.
|
|
130
|
-
status: 'open',
|
|
131
|
-
user_agent: userAgent ? userAgent.slice(0, MAX_USER_AGENT_LENGTH) : null,
|
|
132
|
-
})
|
|
133
|
-
.eq('id', thread.id);
|
|
134
|
-
|
|
135
|
-
after(async () => {
|
|
136
|
-
// Route the notice the same way the original submission was routed, so a
|
|
137
|
-
// contact-form conversation keeps reaching that form's address.
|
|
138
|
-
const recipient =
|
|
139
|
-
thread.source === 'contact_form'
|
|
140
|
-
? await resolveFormRecipient(
|
|
141
|
-
// Load the ORIGINATING form's endpoint. Passing null here would skip the
|
|
142
|
-
// per-form address entirely and quietly deliver a careers-form follow-up
|
|
143
|
-
// to the general mailbox.
|
|
144
|
-
thread.form_key ? await getFormEndpoint(thread.form_key) : null
|
|
145
|
-
)
|
|
146
|
-
: (await resolveSellerContactEmail()).email;
|
|
147
|
-
|
|
148
|
-
await notifyAdminOfMessage({
|
|
149
|
-
threadId: thread.id,
|
|
150
|
-
source: thread.source as 'product_inquiry' | 'contact_form',
|
|
151
|
-
messageId: message.id,
|
|
152
|
-
subjectLabel: thread.subject_label,
|
|
153
|
-
senderName: thread.sender_name,
|
|
154
|
-
senderEmail: thread.sender_email,
|
|
155
|
-
message: body,
|
|
156
|
-
recipient,
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
revalidatePath('/thread');
|
|
161
|
-
return { success: true, messageKey: 'thread.sent' };
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error('[thread] Visitor reply failed:', error);
|
|
164
|
-
return { success: false, messageKey: 'thread.error' };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import { after } from 'next/server';
|
|
5
|
+
import { revalidatePath } from 'next/cache';
|
|
6
|
+
import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
7
|
+
|
|
8
|
+
import { getCookieValue } from '../../lib/auth/cookies';
|
|
9
|
+
import { THREAD_COOKIE, verifyThreadToken } from '../../lib/messages/thread-token';
|
|
10
|
+
import {
|
|
11
|
+
getFormEndpoint,
|
|
12
|
+
notifyAdminOfMessage,
|
|
13
|
+
resolveFormRecipient,
|
|
14
|
+
} from '../../lib/messages/threads';
|
|
15
|
+
import { resolveSellerContactEmail } from '../../lib/commerce/seller-contact';
|
|
16
|
+
import { verifyBotProtection } from '../../lib/botProtection/verify';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A visitor replying on their own thread page.
|
|
20
|
+
*
|
|
21
|
+
* The token is read from the HttpOnly COOKIE, never from a form field — so it is never
|
|
22
|
+
* in a POST body, never in a client-side variable, and cannot be swapped by editing the
|
|
23
|
+
* request. Everything else here mirrors the enquiry action: bot protection first, a
|
|
24
|
+
* fail-closed per-IP throttle, then a write.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const MAX_BODY_LENGTH = 5000;
|
|
28
|
+
const MAX_USER_AGENT_LENGTH = 500;
|
|
29
|
+
const THROTTLE_WINDOW_MINUTES = 10;
|
|
30
|
+
const THROTTLE_MAX_REPLIES = 10;
|
|
31
|
+
const UNKNOWN_IP_BUCKET = 'unknown';
|
|
32
|
+
|
|
33
|
+
export interface ThreadReplyState {
|
|
34
|
+
success: boolean;
|
|
35
|
+
messageKey: 'thread.sent' | 'thread.error' | 'thread.throttled' | 'thread.closed' | '';
|
|
36
|
+
message?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function firstHop(headerValue: string | null): string | null {
|
|
40
|
+
const [first] = (headerValue ?? '').split(',');
|
|
41
|
+
const trimmed = first?.trim() ?? '';
|
|
42
|
+
return trimmed || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function maskIp(ip: string | null): string {
|
|
46
|
+
if (!ip) return UNKNOWN_IP_BUCKET;
|
|
47
|
+
if (ip.includes(':')) {
|
|
48
|
+
const hextets = ip.split(':').filter(Boolean);
|
|
49
|
+
return `${hextets.slice(0, 3).join(':')}::x`;
|
|
50
|
+
}
|
|
51
|
+
const octets = ip.split('.');
|
|
52
|
+
if (octets.length === 4) {
|
|
53
|
+
octets[3] = 'x';
|
|
54
|
+
return octets.join('.');
|
|
55
|
+
}
|
|
56
|
+
return UNKNOWN_IP_BUCKET;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function submitThreadReply(
|
|
60
|
+
_prevState: unknown,
|
|
61
|
+
formData: FormData
|
|
62
|
+
): Promise<ThreadReplyState> {
|
|
63
|
+
const verification = await verifyBotProtection(formData);
|
|
64
|
+
if (!verification.ok) {
|
|
65
|
+
if (verification.reason === 'honeypot') {
|
|
66
|
+
return { success: true, messageKey: 'thread.sent' };
|
|
67
|
+
}
|
|
68
|
+
return { success: false, messageKey: '', message: verification.message };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rawBody = formData.get('body');
|
|
72
|
+
const body = typeof rawBody === 'string' ? rawBody.trim().slice(0, MAX_BODY_LENGTH) : '';
|
|
73
|
+
if (!body) return { success: false, messageKey: 'thread.error' };
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const supabase = getServiceRoleSupabaseClient();
|
|
77
|
+
|
|
78
|
+
const token = await getCookieValue(THREAD_COOKIE);
|
|
79
|
+
const check = await verifyThreadToken(supabase, token);
|
|
80
|
+
if (!check.valid) return { success: false, messageKey: 'thread.error' };
|
|
81
|
+
|
|
82
|
+
const thread = check.thread;
|
|
83
|
+
if (thread.status === 'closed') return { success: false, messageKey: 'thread.closed' };
|
|
84
|
+
|
|
85
|
+
const requestHeaders = await headers();
|
|
86
|
+
const ipMasked = maskIp(
|
|
87
|
+
firstHop(requestHeaders.get('x-vercel-forwarded-for')) ??
|
|
88
|
+
firstHop(requestHeaders.get('x-real-ip')) ??
|
|
89
|
+
firstHop(requestHeaders.get('x-forwarded-for'))
|
|
90
|
+
);
|
|
91
|
+
const userAgent = requestHeaders.get('user-agent');
|
|
92
|
+
|
|
93
|
+
const since = new Date(Date.now() - THROTTLE_WINDOW_MINUTES * 60_000).toISOString();
|
|
94
|
+
const { count } = await supabase
|
|
95
|
+
.from('thread_messages')
|
|
96
|
+
.select('id', { count: 'exact', head: true })
|
|
97
|
+
.eq('ip_masked', ipMasked)
|
|
98
|
+
.gte('created_at', since);
|
|
99
|
+
if ((count ?? 0) >= THROTTLE_MAX_REPLIES) {
|
|
100
|
+
return { success: false, messageKey: 'thread.throttled' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { data: message, error } = await supabase
|
|
104
|
+
.from('thread_messages')
|
|
105
|
+
.insert({
|
|
106
|
+
thread_id: thread.id,
|
|
107
|
+
direction: 'inbound',
|
|
108
|
+
body,
|
|
109
|
+
author_name: thread.sender_name,
|
|
110
|
+
ip_masked: ipMasked,
|
|
111
|
+
})
|
|
112
|
+
.select('id')
|
|
113
|
+
.single();
|
|
114
|
+
|
|
115
|
+
if (error || !message) {
|
|
116
|
+
console.error('[thread] Could not store a visitor reply:', error?.message);
|
|
117
|
+
return { success: false, messageKey: 'thread.error' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await supabase
|
|
121
|
+
.from('message_threads')
|
|
122
|
+
.update({
|
|
123
|
+
last_message_at: new Date().toISOString(),
|
|
124
|
+
unread_for_admin: true,
|
|
125
|
+
unread_for_visitor: false,
|
|
126
|
+
// A visitor writing back REOPENS the conversation. Marking it handled was a
|
|
127
|
+
// statement about the last exchange, not a permanent verdict — leaving it closed
|
|
128
|
+
// would drop their reply out of the inbox's default view, which is precisely
|
|
129
|
+
// where someone waiting on an answer must not go.
|
|
130
|
+
status: 'open',
|
|
131
|
+
user_agent: userAgent ? userAgent.slice(0, MAX_USER_AGENT_LENGTH) : null,
|
|
132
|
+
})
|
|
133
|
+
.eq('id', thread.id);
|
|
134
|
+
|
|
135
|
+
after(async () => {
|
|
136
|
+
// Route the notice the same way the original submission was routed, so a
|
|
137
|
+
// contact-form conversation keeps reaching that form's address.
|
|
138
|
+
const recipient =
|
|
139
|
+
thread.source === 'contact_form'
|
|
140
|
+
? await resolveFormRecipient(
|
|
141
|
+
// Load the ORIGINATING form's endpoint. Passing null here would skip the
|
|
142
|
+
// per-form address entirely and quietly deliver a careers-form follow-up
|
|
143
|
+
// to the general mailbox.
|
|
144
|
+
thread.form_key ? await getFormEndpoint(thread.form_key) : null
|
|
145
|
+
)
|
|
146
|
+
: (await resolveSellerContactEmail()).email;
|
|
147
|
+
|
|
148
|
+
await notifyAdminOfMessage({
|
|
149
|
+
threadId: thread.id,
|
|
150
|
+
source: thread.source as 'product_inquiry' | 'contact_form',
|
|
151
|
+
messageId: message.id,
|
|
152
|
+
subjectLabel: thread.subject_label,
|
|
153
|
+
senderName: thread.sender_name,
|
|
154
|
+
senderEmail: thread.sender_email,
|
|
155
|
+
message: body,
|
|
156
|
+
recipient,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
revalidatePath('/thread');
|
|
161
|
+
return { success: true, messageKey: 'thread.sent' };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[thread] Visitor reply failed:', error);
|
|
164
|
+
return { success: false, messageKey: 'thread.error' };
|
|
165
|
+
}
|
|
166
|
+
}
|