create-nextblock 0.12.9 → 0.12.11
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 +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- 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 +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- 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/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
- package/templates/nextblock-template/app/actions/interactions.ts +373 -373
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- 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/article/[slug]/PostClientContent.tsx +441 -441
- 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/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
- package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- 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/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -149
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
- package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
- 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/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
- 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/actions.ts +64 -64
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
- package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
- 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/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
- package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- 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/actions.ts +222 -222
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
- 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-data.ts +37 -37
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/account-types.ts +11 -11
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/page.tsx +42 -42
- 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/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
- 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 +369 -369
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- 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/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- 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/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
- 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/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/server.ts +585 -585
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- 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/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
- package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import type { Database } from '@nextblock-cms/db';
|
|
2
|
-
|
|
3
|
-
type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
|
|
4
|
-
|
|
5
|
-
const DASHBOARD_PATH = '/cms/dashboard';
|
|
6
|
-
const HOME_PATH = '/';
|
|
7
|
-
const PROFILE_PATH = '/profile';
|
|
8
|
-
const PASSWORD_RESET_PATH = '/profile/password';
|
|
9
|
-
|
|
10
|
-
function isSafeInternalPath(path?: string | null) {
|
|
11
|
-
return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isDashboardPath(path: string) {
|
|
15
|
-
return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function isCustomerProfileComplete(profile: AuthProfile) {
|
|
19
|
-
return Boolean(profile?.full_name?.trim());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function resolvePostAuthRedirect(
|
|
23
|
-
profile: AuthProfile,
|
|
24
|
-
requestedPath?: string | null
|
|
25
|
-
) {
|
|
26
|
-
const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
|
|
27
|
-
const role = profile?.role ?? null;
|
|
28
|
-
|
|
29
|
-
if (safePath === PASSWORD_RESET_PATH) {
|
|
30
|
-
return PASSWORD_RESET_PATH;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (role === 'ADMIN' || role === 'WRITER') {
|
|
34
|
-
return safePath ?? DASHBOARD_PATH;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!isCustomerProfileComplete(profile)) {
|
|
38
|
-
return PROFILE_PATH;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (safePath && !isDashboardPath(safePath)) {
|
|
42
|
-
return safePath;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return HOME_PATH;
|
|
46
|
-
}
|
|
1
|
+
import type { Database } from '@nextblock-cms/db';
|
|
2
|
+
|
|
3
|
+
type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
|
|
4
|
+
|
|
5
|
+
const DASHBOARD_PATH = '/cms/dashboard';
|
|
6
|
+
const HOME_PATH = '/';
|
|
7
|
+
const PROFILE_PATH = '/profile';
|
|
8
|
+
const PASSWORD_RESET_PATH = '/profile/password';
|
|
9
|
+
|
|
10
|
+
function isSafeInternalPath(path?: string | null) {
|
|
11
|
+
return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isDashboardPath(path: string) {
|
|
15
|
+
return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isCustomerProfileComplete(profile: AuthProfile) {
|
|
19
|
+
return Boolean(profile?.full_name?.trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolvePostAuthRedirect(
|
|
23
|
+
profile: AuthProfile,
|
|
24
|
+
requestedPath?: string | null
|
|
25
|
+
) {
|
|
26
|
+
const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
|
|
27
|
+
const role = profile?.role ?? null;
|
|
28
|
+
|
|
29
|
+
if (safePath === PASSWORD_RESET_PATH) {
|
|
30
|
+
return PASSWORD_RESET_PATH;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (role === 'ADMIN' || role === 'WRITER') {
|
|
34
|
+
return safePath ?? DASHBOARD_PATH;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isCustomerProfileComplete(profile)) {
|
|
38
|
+
return PROFILE_PATH;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (safePath && !isDashboardPath(safePath)) {
|
|
42
|
+
return safePath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return HOME_PATH;
|
|
46
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# NextBlock™ Block System
|
|
2
|
-
|
|
3
|
-
The built-in block registry for the app lives in this directory. The current
|
|
4
|
-
source of truth is `blockRegistry.ts`, which defines block types, schemas,
|
|
5
|
-
defaults, and editor or renderer wiring for the CMS.
|
|
6
|
-
|
|
7
|
-
## Read This First
|
|
8
|
-
|
|
9
|
-
- [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
|
|
10
|
-
- [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
|
|
11
|
-
- [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
|
|
12
|
-
|
|
13
|
-
Use those docs instead of maintaining a second long-form block tutorial here.
|
|
1
|
+
# NextBlock™ Block System
|
|
2
|
+
|
|
3
|
+
The built-in block registry for the app lives in this directory. The current
|
|
4
|
+
source of truth is `blockRegistry.ts`, which defines block types, schemas,
|
|
5
|
+
defaults, and editor or renderer wiring for the CMS.
|
|
6
|
+
|
|
7
|
+
## Read This First
|
|
8
|
+
|
|
9
|
+
- [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
|
|
10
|
+
- [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
|
|
11
|
+
- [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
|
|
12
|
+
|
|
13
|
+
Use those docs instead of maintaining a second long-form block tutorial here.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { z } from '../zod-config';
|
|
2
|
-
import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
|
|
3
|
-
import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
|
|
4
|
-
import { availableBlockTypes, type BlockType } from './blockTypes';
|
|
5
|
-
export { availableBlockTypes, type BlockType } from './blockTypes';
|
|
1
|
+
import { z } from '../zod-config';
|
|
2
|
+
import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
|
|
3
|
+
import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
|
|
4
|
+
import { availableBlockTypes, type BlockType } from './blockTypes';
|
|
5
|
+
export { availableBlockTypes, type BlockType } from './blockTypes';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Block Registry System
|
|
@@ -12,7 +12,7 @@ export { availableBlockTypes, type BlockType } from './blockTypes';
|
|
|
12
12
|
* their initial content, editor components, renderer components, and Zod schemas.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
// --- Zod Schemas & Inferred Types ---
|
|
15
|
+
// --- Zod Schemas & Inferred Types ---
|
|
16
16
|
|
|
17
17
|
export const TextBlockSchema = z.object({
|
|
18
18
|
html_content: z.string().describe('Raw HTML content for the text block'),
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export const availableBlockTypes = [
|
|
2
|
-
"text",
|
|
3
|
-
"heading",
|
|
4
|
-
"image",
|
|
5
|
-
"button",
|
|
6
|
-
"posts_grid",
|
|
7
|
-
"video_embed",
|
|
8
|
-
"section",
|
|
9
|
-
"form",
|
|
10
|
-
"testimonial",
|
|
11
|
-
"product_grid",
|
|
12
|
-
"featured_product",
|
|
13
|
-
"cart",
|
|
14
|
-
"checkout",
|
|
15
|
-
"product_details",
|
|
16
|
-
] as const;
|
|
17
|
-
|
|
18
|
-
export type BlockType = (typeof availableBlockTypes)[number];
|
|
1
|
+
export const availableBlockTypes = [
|
|
2
|
+
"text",
|
|
3
|
+
"heading",
|
|
4
|
+
"image",
|
|
5
|
+
"button",
|
|
6
|
+
"posts_grid",
|
|
7
|
+
"video_embed",
|
|
8
|
+
"section",
|
|
9
|
+
"form",
|
|
10
|
+
"testimonial",
|
|
11
|
+
"product_grid",
|
|
12
|
+
"featured_product",
|
|
13
|
+
"cart",
|
|
14
|
+
"checkout",
|
|
15
|
+
"product_details",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type BlockType = (typeof availableBlockTypes)[number];
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
// lib/botProtection/verify.ts
|
|
2
|
-
//
|
|
3
|
-
// Shared server-side bot-protection verification: the honeypot check plus
|
|
4
|
-
// Cloudflare Turnstile / Google reCAPTCHA token verification. This is the single
|
|
5
|
-
// source of truth used by BOTH the page contact-form handler
|
|
6
|
-
// (app/actions/formActions.ts) and the account-signup action (app/actions.ts).
|
|
7
|
-
//
|
|
8
|
-
// Server-only: it reads the RLS-bypassing service-role client, which throws if
|
|
9
|
-
// imported into a Client Component.
|
|
10
|
-
|
|
11
|
-
import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
12
|
-
|
|
13
|
-
export type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
|
|
14
|
-
|
|
15
|
-
// Shared field names — the client widgets emit these, the verifier reads them.
|
|
16
|
-
export const HONEYPOT_FIELD = 'verification_secondary_email';
|
|
17
|
-
export const TURNSTILE_TOKEN_FIELD = 'cf-turnstile-response';
|
|
18
|
-
export const RECAPTCHA_TOKEN_FIELD = 'g-recaptcha-response';
|
|
19
|
-
|
|
20
|
-
export type BotProtectionResult =
|
|
21
|
-
// Passed (or nothing configured beyond the honeypot).
|
|
22
|
-
| { ok: true }
|
|
23
|
-
// The honeypot was filled — almost certainly a bot. Callers should silently
|
|
24
|
-
// discard the submission and fake a success so the bot learns nothing.
|
|
25
|
-
| { ok: false; reason: 'honeypot' }
|
|
26
|
-
// The captcha was missing, failed, or could not be checked. `message` is a
|
|
27
|
-
// human-readable explanation safe to surface to the user.
|
|
28
|
-
| { ok: false; reason: 'captcha'; message: string };
|
|
29
|
-
|
|
30
|
-
type VerifyOptions = {
|
|
31
|
-
// A block/form may pin a specific provider; otherwise the site-wide setting
|
|
32
|
-
// (site_settings.bot_protection_public.provider) is used.
|
|
33
|
-
botProtectionProvider?: BotProtectionProvider;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Verify a submitted FormData against the configured bot protection.
|
|
38
|
-
*
|
|
39
|
-
* Phase 1 (honeypot) needs no network/DB and always runs — it is the always-on
|
|
40
|
-
* baseline. Phase 2 (captcha) reads the global provider + secret from
|
|
41
|
-
* `site_settings` and calls the provider's siteverify endpoint.
|
|
42
|
-
*/
|
|
43
|
-
export async function verifyBotProtection(
|
|
44
|
-
formData: FormData,
|
|
45
|
-
options?: VerifyOptions
|
|
46
|
-
): Promise<BotProtectionResult> {
|
|
47
|
-
// Phase 1: Honeypot validation
|
|
48
|
-
const honeypot = formData.get(HONEYPOT_FIELD);
|
|
49
|
-
if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
|
|
50
|
-
console.warn('[Bot Protection] Honeypot triggered. Discarding submission from bot.');
|
|
51
|
-
return { ok: false, reason: 'honeypot' };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Phase 2: Advanced captcha verification
|
|
55
|
-
try {
|
|
56
|
-
const supabase = getServiceRoleSupabaseClient();
|
|
57
|
-
|
|
58
|
-
const { data: publicSetting } = await supabase
|
|
59
|
-
.from('site_settings')
|
|
60
|
-
.select('value')
|
|
61
|
-
.eq('key', 'bot_protection_public')
|
|
62
|
-
.maybeSingle();
|
|
63
|
-
|
|
64
|
-
const { data: secretSetting } = await supabase
|
|
65
|
-
.from('site_settings')
|
|
66
|
-
.select('value')
|
|
67
|
-
.eq('key', 'bot_protection_secret')
|
|
68
|
-
.maybeSingle();
|
|
69
|
-
|
|
70
|
-
const publicVal = (publicSetting?.value || {}) as Record<string, any>;
|
|
71
|
-
const secretVal = (secretSetting?.value || {}) as Record<string, any>;
|
|
72
|
-
|
|
73
|
-
const pinnedProvider =
|
|
74
|
-
options?.botProtectionProvider === 'turnstile' || options?.botProtectionProvider === 'recaptcha'
|
|
75
|
-
? options.botProtectionProvider
|
|
76
|
-
: undefined;
|
|
77
|
-
const provider: BotProtectionProvider = pinnedProvider || publicVal.provider || 'none';
|
|
78
|
-
const secretKey =
|
|
79
|
-
secretVal.secretKey ||
|
|
80
|
-
(provider === 'turnstile'
|
|
81
|
-
? process.env.TURNSTILE_SECRET_KEY
|
|
82
|
-
: process.env.RECAPTCHA_SECRET_KEY) ||
|
|
83
|
-
'';
|
|
84
|
-
|
|
85
|
-
if (provider === 'turnstile') {
|
|
86
|
-
const token = formData.get(TURNSTILE_TOKEN_FIELD) as string;
|
|
87
|
-
if (!token) {
|
|
88
|
-
return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
|
|
89
|
-
}
|
|
90
|
-
if (!secretKey) {
|
|
91
|
-
console.error('[Bot Protection] Turnstile secret key is not configured.');
|
|
92
|
-
return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
98
|
-
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const outcome = await res.json();
|
|
102
|
-
if (!outcome.success) {
|
|
103
|
-
console.warn('[Bot Protection] Turnstile verification failed:', outcome);
|
|
104
|
-
return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
|
|
105
|
-
}
|
|
106
|
-
} else if (provider === 'recaptcha') {
|
|
107
|
-
const token = formData.get(RECAPTCHA_TOKEN_FIELD) as string;
|
|
108
|
-
if (!token) {
|
|
109
|
-
return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
|
|
110
|
-
}
|
|
111
|
-
if (!secretKey) {
|
|
112
|
-
console.error('[Bot Protection] reCAPTCHA secret key is not configured.');
|
|
113
|
-
return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
|
117
|
-
method: 'POST',
|
|
118
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
119
|
-
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const outcome = await res.json();
|
|
123
|
-
if (!outcome.success || outcome.score < 0.5) {
|
|
124
|
-
console.warn('[Bot Protection] reCAPTCHA verification failed:', outcome);
|
|
125
|
-
return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
console.error('[Bot Protection] Error during validation:', error);
|
|
130
|
-
return { ok: false, reason: 'captcha', message: 'Sorry, security verification could not be completed at this time.' };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { ok: true };
|
|
134
|
-
}
|
|
1
|
+
// lib/botProtection/verify.ts
|
|
2
|
+
//
|
|
3
|
+
// Shared server-side bot-protection verification: the honeypot check plus
|
|
4
|
+
// Cloudflare Turnstile / Google reCAPTCHA token verification. This is the single
|
|
5
|
+
// source of truth used by BOTH the page contact-form handler
|
|
6
|
+
// (app/actions/formActions.ts) and the account-signup action (app/actions.ts).
|
|
7
|
+
//
|
|
8
|
+
// Server-only: it reads the RLS-bypassing service-role client, which throws if
|
|
9
|
+
// imported into a Client Component.
|
|
10
|
+
|
|
11
|
+
import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
12
|
+
|
|
13
|
+
export type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
|
|
14
|
+
|
|
15
|
+
// Shared field names — the client widgets emit these, the verifier reads them.
|
|
16
|
+
export const HONEYPOT_FIELD = 'verification_secondary_email';
|
|
17
|
+
export const TURNSTILE_TOKEN_FIELD = 'cf-turnstile-response';
|
|
18
|
+
export const RECAPTCHA_TOKEN_FIELD = 'g-recaptcha-response';
|
|
19
|
+
|
|
20
|
+
export type BotProtectionResult =
|
|
21
|
+
// Passed (or nothing configured beyond the honeypot).
|
|
22
|
+
| { ok: true }
|
|
23
|
+
// The honeypot was filled — almost certainly a bot. Callers should silently
|
|
24
|
+
// discard the submission and fake a success so the bot learns nothing.
|
|
25
|
+
| { ok: false; reason: 'honeypot' }
|
|
26
|
+
// The captcha was missing, failed, or could not be checked. `message` is a
|
|
27
|
+
// human-readable explanation safe to surface to the user.
|
|
28
|
+
| { ok: false; reason: 'captcha'; message: string };
|
|
29
|
+
|
|
30
|
+
type VerifyOptions = {
|
|
31
|
+
// A block/form may pin a specific provider; otherwise the site-wide setting
|
|
32
|
+
// (site_settings.bot_protection_public.provider) is used.
|
|
33
|
+
botProtectionProvider?: BotProtectionProvider;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify a submitted FormData against the configured bot protection.
|
|
38
|
+
*
|
|
39
|
+
* Phase 1 (honeypot) needs no network/DB and always runs — it is the always-on
|
|
40
|
+
* baseline. Phase 2 (captcha) reads the global provider + secret from
|
|
41
|
+
* `site_settings` and calls the provider's siteverify endpoint.
|
|
42
|
+
*/
|
|
43
|
+
export async function verifyBotProtection(
|
|
44
|
+
formData: FormData,
|
|
45
|
+
options?: VerifyOptions
|
|
46
|
+
): Promise<BotProtectionResult> {
|
|
47
|
+
// Phase 1: Honeypot validation
|
|
48
|
+
const honeypot = formData.get(HONEYPOT_FIELD);
|
|
49
|
+
if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
|
|
50
|
+
console.warn('[Bot Protection] Honeypot triggered. Discarding submission from bot.');
|
|
51
|
+
return { ok: false, reason: 'honeypot' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Phase 2: Advanced captcha verification
|
|
55
|
+
try {
|
|
56
|
+
const supabase = getServiceRoleSupabaseClient();
|
|
57
|
+
|
|
58
|
+
const { data: publicSetting } = await supabase
|
|
59
|
+
.from('site_settings')
|
|
60
|
+
.select('value')
|
|
61
|
+
.eq('key', 'bot_protection_public')
|
|
62
|
+
.maybeSingle();
|
|
63
|
+
|
|
64
|
+
const { data: secretSetting } = await supabase
|
|
65
|
+
.from('site_settings')
|
|
66
|
+
.select('value')
|
|
67
|
+
.eq('key', 'bot_protection_secret')
|
|
68
|
+
.maybeSingle();
|
|
69
|
+
|
|
70
|
+
const publicVal = (publicSetting?.value || {}) as Record<string, any>;
|
|
71
|
+
const secretVal = (secretSetting?.value || {}) as Record<string, any>;
|
|
72
|
+
|
|
73
|
+
const pinnedProvider =
|
|
74
|
+
options?.botProtectionProvider === 'turnstile' || options?.botProtectionProvider === 'recaptcha'
|
|
75
|
+
? options.botProtectionProvider
|
|
76
|
+
: undefined;
|
|
77
|
+
const provider: BotProtectionProvider = pinnedProvider || publicVal.provider || 'none';
|
|
78
|
+
const secretKey =
|
|
79
|
+
secretVal.secretKey ||
|
|
80
|
+
(provider === 'turnstile'
|
|
81
|
+
? process.env.TURNSTILE_SECRET_KEY
|
|
82
|
+
: process.env.RECAPTCHA_SECRET_KEY) ||
|
|
83
|
+
'';
|
|
84
|
+
|
|
85
|
+
if (provider === 'turnstile') {
|
|
86
|
+
const token = formData.get(TURNSTILE_TOKEN_FIELD) as string;
|
|
87
|
+
if (!token) {
|
|
88
|
+
return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
|
|
89
|
+
}
|
|
90
|
+
if (!secretKey) {
|
|
91
|
+
console.error('[Bot Protection] Turnstile secret key is not configured.');
|
|
92
|
+
return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
98
|
+
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const outcome = await res.json();
|
|
102
|
+
if (!outcome.success) {
|
|
103
|
+
console.warn('[Bot Protection] Turnstile verification failed:', outcome);
|
|
104
|
+
return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
|
|
105
|
+
}
|
|
106
|
+
} else if (provider === 'recaptcha') {
|
|
107
|
+
const token = formData.get(RECAPTCHA_TOKEN_FIELD) as string;
|
|
108
|
+
if (!token) {
|
|
109
|
+
return { ok: false, reason: 'captcha', message: 'Security verification token is missing. Please try again.' };
|
|
110
|
+
}
|
|
111
|
+
if (!secretKey) {
|
|
112
|
+
console.error('[Bot Protection] reCAPTCHA secret key is not configured.');
|
|
113
|
+
return { ok: false, reason: 'captcha', message: 'Bot protection is misconfigured. Please contact support.' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
119
|
+
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const outcome = await res.json();
|
|
123
|
+
if (!outcome.success || outcome.score < 0.5) {
|
|
124
|
+
console.warn('[Bot Protection] reCAPTCHA verification failed:', outcome);
|
|
125
|
+
return { ok: false, reason: 'captcha', message: 'Security verification failed. Please try again.' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('[Bot Protection] Error during validation:', error);
|
|
130
|
+
return { ok: false, reason: 'captcha', message: 'Sorry, security verification could not be completed at this time.' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
@@ -1,77 +1,100 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
getCsvHeaders,
|
|
5
|
-
getTemplateCsv,
|
|
6
|
-
majorToMinor,
|
|
7
|
-
makeUniqueSku,
|
|
8
|
-
makeUniqueSlug,
|
|
9
|
-
minorToMajor,
|
|
10
|
-
normalizeBlocksFromFields,
|
|
11
|
-
parseCsv,
|
|
12
|
-
stringifyCsv,
|
|
13
|
-
} from "./csv";
|
|
14
|
-
|
|
15
|
-
describe("cms transfer csv helpers", () => {
|
|
16
|
-
it("builds templates with the expected content columns", () => {
|
|
17
|
-
expect(getCsvHeaders("pages")).toContain("blocks_json");
|
|
18
|
-
expect(getCsvHeaders("products")).toContain("description_html");
|
|
19
|
-
expect(getCsvHeaders("products")).toContain("description_blocks_json");
|
|
20
|
-
expect(getTemplateCsv("products")).toContain("EXAMPLE-SKU");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("round-trips JSON columns through CSV escaping", () => {
|
|
24
|
-
const csv = stringifyCsv(
|
|
25
|
-
[
|
|
26
|
-
{
|
|
27
|
-
language_code: "en",
|
|
28
|
-
title: "Landing",
|
|
29
|
-
blocks_json: JSON.stringify([
|
|
30
|
-
{ block_type: "text", content: { html_content: "<p>Hello, world</p>" }, order: 0 },
|
|
31
|
-
]),
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
["language_code", "title", "blocks_json"]
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const parsed = parseCsv(csv);
|
|
38
|
-
|
|
39
|
-
expect(parsed.errors).toEqual([]);
|
|
40
|
-
expect(parsed.rows[0].blocks_json).toContain("html_content");
|
|
41
|
-
expect(JSON.parse(parsed.rows[0].blocks_json)).toHaveLength(1);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getCsvHeaders,
|
|
5
|
+
getTemplateCsv,
|
|
6
|
+
majorToMinor,
|
|
7
|
+
makeUniqueSku,
|
|
8
|
+
makeUniqueSlug,
|
|
9
|
+
minorToMajor,
|
|
10
|
+
normalizeBlocksFromFields,
|
|
11
|
+
parseCsv,
|
|
12
|
+
stringifyCsv,
|
|
13
|
+
} from "./csv";
|
|
14
|
+
|
|
15
|
+
describe("cms transfer csv helpers", () => {
|
|
16
|
+
it("builds templates with the expected content columns", () => {
|
|
17
|
+
expect(getCsvHeaders("pages")).toContain("blocks_json");
|
|
18
|
+
expect(getCsvHeaders("products")).toContain("description_html");
|
|
19
|
+
expect(getCsvHeaders("products")).toContain("description_blocks_json");
|
|
20
|
+
expect(getTemplateCsv("products")).toContain("EXAMPLE-SKU");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("round-trips JSON columns through CSV escaping", () => {
|
|
24
|
+
const csv = stringifyCsv(
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
language_code: "en",
|
|
28
|
+
title: "Landing",
|
|
29
|
+
blocks_json: JSON.stringify([
|
|
30
|
+
{ block_type: "text", content: { html_content: "<p>Hello, world</p>" }, order: 0 },
|
|
31
|
+
]),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
["language_code", "title", "blocks_json"]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const parsed = parseCsv(csv);
|
|
38
|
+
|
|
39
|
+
expect(parsed.errors).toEqual([]);
|
|
40
|
+
expect(parsed.rows[0].blocks_json).toContain("html_content");
|
|
41
|
+
expect(JSON.parse(parsed.rows[0].blocks_json)).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("round-trips special characters like the ™ symbol", () => {
|
|
45
|
+
const csv = stringifyCsv(
|
|
46
|
+
[{ language_code: "en", title: "NextBlock™ — Café © 2026" }],
|
|
47
|
+
["language_code", "title"]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const parsed = parseCsv(csv);
|
|
51
|
+
|
|
52
|
+
expect(parsed.errors).toEqual([]);
|
|
53
|
+
expect(parsed.rows[0].title).toBe("NextBlock™ — Café © 2026");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("strips a leading UTF-8 BOM so the first header still matches", () => {
|
|
57
|
+
const csv = stringifyCsv([{ id: "1", title: "NextBlock™" }], ["id", "title"]);
|
|
58
|
+
// Spreadsheet apps (and our own export) prefix the file with a BOM.
|
|
59
|
+
const parsed = parseCsv(String.fromCharCode(0xfeff) + csv);
|
|
60
|
+
|
|
61
|
+
expect(parsed.errors).toEqual([]);
|
|
62
|
+
expect(Object.keys(parsed.rows[0])).toContain("id");
|
|
63
|
+
expect(parsed.rows[0].id).toBe("1");
|
|
64
|
+
expect(parsed.rows[0].title).toBe("NextBlock™");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("converts plain HTML into a text block fallback", () => {
|
|
68
|
+
const errors: Array<{ row: number; message: string }> = [];
|
|
69
|
+
const blocks = normalizeBlocksFromFields({
|
|
70
|
+
html: "<p>Imported description</p>",
|
|
71
|
+
languageId: 1,
|
|
72
|
+
rowNumber: 2,
|
|
73
|
+
errors,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(errors).toEqual([]);
|
|
77
|
+
expect(blocks).toEqual([
|
|
78
|
+
{
|
|
79
|
+
language_id: 1,
|
|
80
|
+
block_type: "text",
|
|
81
|
+
content: { html_content: "<p>Imported description</p>" },
|
|
82
|
+
order: 0,
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("generates unique slugs and SKUs for create-new imports", () => {
|
|
88
|
+
expect(makeUniqueSlug("Example Page", new Set(["example-page"]))).toBe(
|
|
89
|
+
"example-page-copy"
|
|
90
|
+
);
|
|
91
|
+
expect(makeUniqueSku("sku-1", new Set(["SKU-1", "SKU-1-COPY"]))).toBe(
|
|
92
|
+
"SKU-1-COPY-2"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("converts product prices between major and minor units", () => {
|
|
97
|
+
expect(majorToMinor(19.99)).toBe(1999);
|
|
98
|
+
expect(minorToMajor(1999)).toBe(19.99);
|
|
99
|
+
});
|
|
100
|
+
});
|