create-nextblock 0.12.8 → 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/next.config.js +6 -0
- 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,284 +1,284 @@
|
|
|
1
|
-
'use server';
|
|
2
|
-
|
|
3
|
-
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
-
import { revalidatePath } from 'next/cache';
|
|
5
|
-
import { sendTwoFactorCodeEmail } from '../../../actions/twoFactorEmail';
|
|
6
|
-
import {
|
|
7
|
-
getSecuritySettings as readSecuritySettings,
|
|
8
|
-
saveSecuritySettings,
|
|
9
|
-
} from '../../../../lib/privacy/settings';
|
|
10
|
-
import {
|
|
11
|
-
MAX_TRUSTED_DEVICE_DAYS,
|
|
12
|
-
MIN_TRUSTED_DEVICE_DAYS,
|
|
13
|
-
type SecuritySettings,
|
|
14
|
-
} from '../../../../lib/privacy/types';
|
|
15
|
-
import {
|
|
16
|
-
createEmailChallenge,
|
|
17
|
-
issueTwoFactorVerifiedCookie,
|
|
18
|
-
clearTwoFactorVerifiedCookie,
|
|
19
|
-
verifyEmailChallenge,
|
|
20
|
-
} from '../../../../lib/auth/twoFactor';
|
|
21
|
-
import {
|
|
22
|
-
listTrustedDevices,
|
|
23
|
-
revokeAllTrustedDevices,
|
|
24
|
-
revokeTrustedDevice,
|
|
25
|
-
type TrustedDeviceRow,
|
|
26
|
-
} from '../../../../lib/auth/trustedDevices';
|
|
27
|
-
import {
|
|
28
|
-
getSystemConfiguration,
|
|
29
|
-
updateSystemConfiguration,
|
|
30
|
-
} from '../../../../lib/setup/system-config';
|
|
31
|
-
|
|
32
|
-
export interface SecurityPanelData {
|
|
33
|
-
email: string;
|
|
34
|
-
mfaEnabled: boolean;
|
|
35
|
-
mfaType: 'totp' | 'email' | null;
|
|
36
|
-
hasVerifiedTotp: boolean;
|
|
37
|
-
isAdmin: boolean;
|
|
38
|
-
globalSettings: SecuritySettings;
|
|
39
|
-
trustedDevices: TrustedDeviceRow[];
|
|
40
|
-
autoAcceptSignups: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function requireUser() {
|
|
44
|
-
const supabase = createClient();
|
|
45
|
-
const {
|
|
46
|
-
data: { user },
|
|
47
|
-
} = await supabase.auth.getUser();
|
|
48
|
-
if (!user) {
|
|
49
|
-
throw new Error('You must be signed in.');
|
|
50
|
-
}
|
|
51
|
-
return { supabase, user };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function getSecurityPanelData(): Promise<SecurityPanelData> {
|
|
55
|
-
const { supabase, user } = await requireUser();
|
|
56
|
-
|
|
57
|
-
const [{ data: settings }, { data: profile }, { data: factors }] = await Promise.all([
|
|
58
|
-
supabase
|
|
59
|
-
.from('user_security_settings')
|
|
60
|
-
.select('mfa_enabled, mfa_type')
|
|
61
|
-
.eq('user_id', user.id)
|
|
62
|
-
.maybeSingle(),
|
|
63
|
-
supabase.from('profiles').select('role').eq('id', user.id).single(),
|
|
64
|
-
supabase.auth.mfa.listFactors(),
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
// listFactors().totp only contains verified TOTP factors.
|
|
68
|
-
const hasVerifiedTotp = Boolean(factors?.totp && factors.totp.length > 0);
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
email: user.email ?? '',
|
|
72
|
-
mfaEnabled: Boolean(settings?.mfa_enabled),
|
|
73
|
-
mfaType: (settings?.mfa_type as 'totp' | 'email' | null) ?? null,
|
|
74
|
-
hasVerifiedTotp,
|
|
75
|
-
isAdmin: profile?.role === 'ADMIN',
|
|
76
|
-
globalSettings: await readSecuritySettings(),
|
|
77
|
-
trustedDevices: await listTrustedDevices(user.id),
|
|
78
|
-
autoAcceptSignups: (await getSystemConfiguration()).auto_accept_signups,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// --- Sign-up policy (admin only) ------------------------------------------------
|
|
83
|
-
|
|
84
|
-
export async function updateAutoAcceptSignups(formData: FormData) {
|
|
85
|
-
const { supabase, user } = await requireUser();
|
|
86
|
-
const { data: profile } = await supabase
|
|
87
|
-
.from('profiles')
|
|
88
|
-
.select('role')
|
|
89
|
-
.eq('id', user.id)
|
|
90
|
-
.single();
|
|
91
|
-
if (profile?.role !== 'ADMIN') {
|
|
92
|
-
throw new Error('Only administrators can change the sign-up policy.');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const enabled = formData.get('auto_accept_signups') === 'true';
|
|
96
|
-
await updateSystemConfiguration({ auto_accept_signups: enabled });
|
|
97
|
-
revalidatePath('/cms/settings/security');
|
|
98
|
-
return {
|
|
99
|
-
success: true,
|
|
100
|
-
message: enabled
|
|
101
|
-
? 'New sign-ups will be auto-approved without email verification.'
|
|
102
|
-
: 'New sign-ups now require email verification.',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// --- Global policy (admin only) -------------------------------------------------
|
|
107
|
-
|
|
108
|
-
export async function updateGlobalSecuritySettings(formData: FormData) {
|
|
109
|
-
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
110
|
-
throw new Error('Security settings are disabled in the sandbox environment.');
|
|
111
|
-
}
|
|
112
|
-
const { supabase, user } = await requireUser();
|
|
113
|
-
const { data: profile } = await supabase
|
|
114
|
-
.from('profiles')
|
|
115
|
-
.select('role')
|
|
116
|
-
.eq('id', user.id)
|
|
117
|
-
.single();
|
|
118
|
-
if (profile?.role !== 'ADMIN') {
|
|
119
|
-
throw new Error('Only administrators can change the global security policy.');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const days = Number.parseInt(formData.get('trusted_device_days')?.toString() ?? '', 10);
|
|
123
|
-
const settings: SecuritySettings = {
|
|
124
|
-
trusted_device_days: Number.isFinite(days)
|
|
125
|
-
? Math.min(MAX_TRUSTED_DEVICE_DAYS, Math.max(MIN_TRUSTED_DEVICE_DAYS, days))
|
|
126
|
-
: 30,
|
|
127
|
-
enforce_staff_2fa: formData.get('enforce_staff_2fa') === 'true',
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
await saveSecuritySettings(settings);
|
|
131
|
-
revalidatePath('/cms/settings/security');
|
|
132
|
-
return { success: true, message: 'Security policy saved.' };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --- TOTP enrollment ------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
export type EnrollTotpResult =
|
|
138
|
-
| { ok: true; factorId: string; qrCode: string; secret: string }
|
|
139
|
-
| { ok: false; error: string };
|
|
140
|
-
|
|
141
|
-
export async function startTotpEnrollment(): Promise<EnrollTotpResult> {
|
|
142
|
-
const { supabase } = await requireUser();
|
|
143
|
-
|
|
144
|
-
// Clear any half-finished factors so re-enrolling never hits a name clash.
|
|
145
|
-
// `listFactors().totp` only returns verified factors, so check `all`.
|
|
146
|
-
const { data: existing } = await supabase.auth.mfa.listFactors();
|
|
147
|
-
for (const factor of existing?.all ?? []) {
|
|
148
|
-
if (factor.factor_type === 'totp' && factor.status === 'unverified') {
|
|
149
|
-
await supabase.auth.mfa.unenroll({ factorId: factor.id });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const { data, error } = await supabase.auth.mfa.enroll({
|
|
154
|
-
factorType: 'totp',
|
|
155
|
-
friendlyName: `nextblock-totp-${Date.now()}`,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (error || !data) {
|
|
159
|
-
return {
|
|
160
|
-
ok: false,
|
|
161
|
-
error:
|
|
162
|
-
error?.message ??
|
|
163
|
-
'Could not start authenticator setup. Ensure TOTP MFA is enabled for this Supabase project.',
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
ok: true,
|
|
169
|
-
factorId: data.id,
|
|
170
|
-
qrCode: data.totp.qr_code,
|
|
171
|
-
secret: data.totp.secret,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export async function verifyTotpEnrollment(formData: FormData) {
|
|
176
|
-
const { supabase, user } = await requireUser();
|
|
177
|
-
const factorId = formData.get('factorId')?.toString() ?? '';
|
|
178
|
-
const code = (formData.get('code')?.toString() ?? '').trim();
|
|
179
|
-
|
|
180
|
-
if (!factorId || !/^\d{6}$/.test(code)) {
|
|
181
|
-
throw new Error('Enter the 6-digit code from your authenticator app.');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const { data: challenge, error: challengeError } = await supabase.auth.mfa.challenge({
|
|
185
|
-
factorId,
|
|
186
|
-
});
|
|
187
|
-
if (challengeError || !challenge) {
|
|
188
|
-
throw new Error(challengeError?.message ?? 'Could not start verification.');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const { error: verifyError } = await supabase.auth.mfa.verify({
|
|
192
|
-
factorId,
|
|
193
|
-
challengeId: challenge.id,
|
|
194
|
-
code,
|
|
195
|
-
});
|
|
196
|
-
if (verifyError) {
|
|
197
|
-
throw new Error('That code was not valid. Please try again.');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const { error: upsertError } = await supabase.from('user_security_settings').upsert({
|
|
201
|
-
user_id: user.id,
|
|
202
|
-
mfa_enabled: true,
|
|
203
|
-
mfa_type: 'totp',
|
|
204
|
-
updated_at: new Date().toISOString(),
|
|
205
|
-
});
|
|
206
|
-
if (upsertError) {
|
|
207
|
-
throw new Error('Verified, but failed to save your preference. Please retry.');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
revalidatePath('/cms/settings/security');
|
|
211
|
-
return { success: true, message: 'Authenticator app enabled.' };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// --- Email-code enrollment ------------------------------------------------------
|
|
215
|
-
|
|
216
|
-
export async function sendEmailEnrollmentCode() {
|
|
217
|
-
const { user } = await requireUser();
|
|
218
|
-
if (!user.email) {
|
|
219
|
-
throw new Error('Your account has no email address on file.');
|
|
220
|
-
}
|
|
221
|
-
const code = await createEmailChallenge(user.id);
|
|
222
|
-
await sendTwoFactorCodeEmail(user.email, code, 'enable email two-factor authentication');
|
|
223
|
-
return { success: true, message: `We sent a 6-digit code to ${user.email}.` };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export async function verifyEmailEnrollment(formData: FormData) {
|
|
227
|
-
const { supabase, user } = await requireUser();
|
|
228
|
-
const code = (formData.get('code')?.toString() ?? '').trim();
|
|
229
|
-
|
|
230
|
-
const ok = await verifyEmailChallenge(user.id, code);
|
|
231
|
-
if (!ok) {
|
|
232
|
-
throw new Error('That code was incorrect or expired. Request a new one.');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const { error } = await supabase.from('user_security_settings').upsert({
|
|
236
|
-
user_id: user.id,
|
|
237
|
-
mfa_enabled: true,
|
|
238
|
-
mfa_type: 'email',
|
|
239
|
-
updated_at: new Date().toISOString(),
|
|
240
|
-
});
|
|
241
|
-
if (error) {
|
|
242
|
-
throw new Error('Verified, but failed to save your preference. Please retry.');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// The user just proved control of their inbox, so this session is satisfied.
|
|
246
|
-
await issueTwoFactorVerifiedCookie(user.id);
|
|
247
|
-
revalidatePath('/cms/settings/security');
|
|
248
|
-
return { success: true, message: 'Email verification enabled.' };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// --- Disable / device management ------------------------------------------------
|
|
252
|
-
|
|
253
|
-
export async function disableMfa() {
|
|
254
|
-
const { supabase, user } = await requireUser();
|
|
255
|
-
|
|
256
|
-
const { data: factors } = await supabase.auth.mfa.listFactors();
|
|
257
|
-
for (const factor of factors?.all ?? []) {
|
|
258
|
-
await supabase.auth.mfa.unenroll({ factorId: factor.id });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
await supabase.from('user_security_settings').upsert({
|
|
262
|
-
user_id: user.id,
|
|
263
|
-
mfa_enabled: false,
|
|
264
|
-
mfa_type: null,
|
|
265
|
-
updated_at: new Date().toISOString(),
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
await revokeAllTrustedDevices(user.id);
|
|
269
|
-
await clearTwoFactorVerifiedCookie();
|
|
270
|
-
|
|
271
|
-
revalidatePath('/cms/settings/security');
|
|
272
|
-
return { success: true, message: 'Two-factor authentication disabled.' };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export async function revokeTrustedDeviceAction(formData: FormData) {
|
|
276
|
-
const { user } = await requireUser();
|
|
277
|
-
const id = formData.get('id')?.toString() ?? '';
|
|
278
|
-
if (!id) {
|
|
279
|
-
throw new Error('Missing device id.');
|
|
280
|
-
}
|
|
281
|
-
await revokeTrustedDevice(user.id, id);
|
|
282
|
-
revalidatePath('/cms/settings/security');
|
|
283
|
-
return { success: true, message: 'Device revoked.' };
|
|
284
|
-
}
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server';
|
|
4
|
+
import { revalidatePath } from 'next/cache';
|
|
5
|
+
import { sendTwoFactorCodeEmail } from '../../../actions/twoFactorEmail';
|
|
6
|
+
import {
|
|
7
|
+
getSecuritySettings as readSecuritySettings,
|
|
8
|
+
saveSecuritySettings,
|
|
9
|
+
} from '../../../../lib/privacy/settings';
|
|
10
|
+
import {
|
|
11
|
+
MAX_TRUSTED_DEVICE_DAYS,
|
|
12
|
+
MIN_TRUSTED_DEVICE_DAYS,
|
|
13
|
+
type SecuritySettings,
|
|
14
|
+
} from '../../../../lib/privacy/types';
|
|
15
|
+
import {
|
|
16
|
+
createEmailChallenge,
|
|
17
|
+
issueTwoFactorVerifiedCookie,
|
|
18
|
+
clearTwoFactorVerifiedCookie,
|
|
19
|
+
verifyEmailChallenge,
|
|
20
|
+
} from '../../../../lib/auth/twoFactor';
|
|
21
|
+
import {
|
|
22
|
+
listTrustedDevices,
|
|
23
|
+
revokeAllTrustedDevices,
|
|
24
|
+
revokeTrustedDevice,
|
|
25
|
+
type TrustedDeviceRow,
|
|
26
|
+
} from '../../../../lib/auth/trustedDevices';
|
|
27
|
+
import {
|
|
28
|
+
getSystemConfiguration,
|
|
29
|
+
updateSystemConfiguration,
|
|
30
|
+
} from '../../../../lib/setup/system-config';
|
|
31
|
+
|
|
32
|
+
export interface SecurityPanelData {
|
|
33
|
+
email: string;
|
|
34
|
+
mfaEnabled: boolean;
|
|
35
|
+
mfaType: 'totp' | 'email' | null;
|
|
36
|
+
hasVerifiedTotp: boolean;
|
|
37
|
+
isAdmin: boolean;
|
|
38
|
+
globalSettings: SecuritySettings;
|
|
39
|
+
trustedDevices: TrustedDeviceRow[];
|
|
40
|
+
autoAcceptSignups: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function requireUser() {
|
|
44
|
+
const supabase = createClient();
|
|
45
|
+
const {
|
|
46
|
+
data: { user },
|
|
47
|
+
} = await supabase.auth.getUser();
|
|
48
|
+
if (!user) {
|
|
49
|
+
throw new Error('You must be signed in.');
|
|
50
|
+
}
|
|
51
|
+
return { supabase, user };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function getSecurityPanelData(): Promise<SecurityPanelData> {
|
|
55
|
+
const { supabase, user } = await requireUser();
|
|
56
|
+
|
|
57
|
+
const [{ data: settings }, { data: profile }, { data: factors }] = await Promise.all([
|
|
58
|
+
supabase
|
|
59
|
+
.from('user_security_settings')
|
|
60
|
+
.select('mfa_enabled, mfa_type')
|
|
61
|
+
.eq('user_id', user.id)
|
|
62
|
+
.maybeSingle(),
|
|
63
|
+
supabase.from('profiles').select('role').eq('id', user.id).single(),
|
|
64
|
+
supabase.auth.mfa.listFactors(),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// listFactors().totp only contains verified TOTP factors.
|
|
68
|
+
const hasVerifiedTotp = Boolean(factors?.totp && factors.totp.length > 0);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
email: user.email ?? '',
|
|
72
|
+
mfaEnabled: Boolean(settings?.mfa_enabled),
|
|
73
|
+
mfaType: (settings?.mfa_type as 'totp' | 'email' | null) ?? null,
|
|
74
|
+
hasVerifiedTotp,
|
|
75
|
+
isAdmin: profile?.role === 'ADMIN',
|
|
76
|
+
globalSettings: await readSecuritySettings(),
|
|
77
|
+
trustedDevices: await listTrustedDevices(user.id),
|
|
78
|
+
autoAcceptSignups: (await getSystemConfiguration()).auto_accept_signups,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Sign-up policy (admin only) ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export async function updateAutoAcceptSignups(formData: FormData) {
|
|
85
|
+
const { supabase, user } = await requireUser();
|
|
86
|
+
const { data: profile } = await supabase
|
|
87
|
+
.from('profiles')
|
|
88
|
+
.select('role')
|
|
89
|
+
.eq('id', user.id)
|
|
90
|
+
.single();
|
|
91
|
+
if (profile?.role !== 'ADMIN') {
|
|
92
|
+
throw new Error('Only administrators can change the sign-up policy.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const enabled = formData.get('auto_accept_signups') === 'true';
|
|
96
|
+
await updateSystemConfiguration({ auto_accept_signups: enabled });
|
|
97
|
+
revalidatePath('/cms/settings/security');
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
message: enabled
|
|
101
|
+
? 'New sign-ups will be auto-approved without email verification.'
|
|
102
|
+
: 'New sign-ups now require email verification.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Global policy (admin only) -------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export async function updateGlobalSecuritySettings(formData: FormData) {
|
|
109
|
+
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
|
|
110
|
+
throw new Error('Security settings are disabled in the sandbox environment.');
|
|
111
|
+
}
|
|
112
|
+
const { supabase, user } = await requireUser();
|
|
113
|
+
const { data: profile } = await supabase
|
|
114
|
+
.from('profiles')
|
|
115
|
+
.select('role')
|
|
116
|
+
.eq('id', user.id)
|
|
117
|
+
.single();
|
|
118
|
+
if (profile?.role !== 'ADMIN') {
|
|
119
|
+
throw new Error('Only administrators can change the global security policy.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const days = Number.parseInt(formData.get('trusted_device_days')?.toString() ?? '', 10);
|
|
123
|
+
const settings: SecuritySettings = {
|
|
124
|
+
trusted_device_days: Number.isFinite(days)
|
|
125
|
+
? Math.min(MAX_TRUSTED_DEVICE_DAYS, Math.max(MIN_TRUSTED_DEVICE_DAYS, days))
|
|
126
|
+
: 30,
|
|
127
|
+
enforce_staff_2fa: formData.get('enforce_staff_2fa') === 'true',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
await saveSecuritySettings(settings);
|
|
131
|
+
revalidatePath('/cms/settings/security');
|
|
132
|
+
return { success: true, message: 'Security policy saved.' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- TOTP enrollment ------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export type EnrollTotpResult =
|
|
138
|
+
| { ok: true; factorId: string; qrCode: string; secret: string }
|
|
139
|
+
| { ok: false; error: string };
|
|
140
|
+
|
|
141
|
+
export async function startTotpEnrollment(): Promise<EnrollTotpResult> {
|
|
142
|
+
const { supabase } = await requireUser();
|
|
143
|
+
|
|
144
|
+
// Clear any half-finished factors so re-enrolling never hits a name clash.
|
|
145
|
+
// `listFactors().totp` only returns verified factors, so check `all`.
|
|
146
|
+
const { data: existing } = await supabase.auth.mfa.listFactors();
|
|
147
|
+
for (const factor of existing?.all ?? []) {
|
|
148
|
+
if (factor.factor_type === 'totp' && factor.status === 'unverified') {
|
|
149
|
+
await supabase.auth.mfa.unenroll({ factorId: factor.id });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { data, error } = await supabase.auth.mfa.enroll({
|
|
154
|
+
factorType: 'totp',
|
|
155
|
+
friendlyName: `nextblock-totp-${Date.now()}`,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (error || !data) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
error:
|
|
162
|
+
error?.message ??
|
|
163
|
+
'Could not start authenticator setup. Ensure TOTP MFA is enabled for this Supabase project.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
factorId: data.id,
|
|
170
|
+
qrCode: data.totp.qr_code,
|
|
171
|
+
secret: data.totp.secret,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function verifyTotpEnrollment(formData: FormData) {
|
|
176
|
+
const { supabase, user } = await requireUser();
|
|
177
|
+
const factorId = formData.get('factorId')?.toString() ?? '';
|
|
178
|
+
const code = (formData.get('code')?.toString() ?? '').trim();
|
|
179
|
+
|
|
180
|
+
if (!factorId || !/^\d{6}$/.test(code)) {
|
|
181
|
+
throw new Error('Enter the 6-digit code from your authenticator app.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { data: challenge, error: challengeError } = await supabase.auth.mfa.challenge({
|
|
185
|
+
factorId,
|
|
186
|
+
});
|
|
187
|
+
if (challengeError || !challenge) {
|
|
188
|
+
throw new Error(challengeError?.message ?? 'Could not start verification.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { error: verifyError } = await supabase.auth.mfa.verify({
|
|
192
|
+
factorId,
|
|
193
|
+
challengeId: challenge.id,
|
|
194
|
+
code,
|
|
195
|
+
});
|
|
196
|
+
if (verifyError) {
|
|
197
|
+
throw new Error('That code was not valid. Please try again.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { error: upsertError } = await supabase.from('user_security_settings').upsert({
|
|
201
|
+
user_id: user.id,
|
|
202
|
+
mfa_enabled: true,
|
|
203
|
+
mfa_type: 'totp',
|
|
204
|
+
updated_at: new Date().toISOString(),
|
|
205
|
+
});
|
|
206
|
+
if (upsertError) {
|
|
207
|
+
throw new Error('Verified, but failed to save your preference. Please retry.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
revalidatePath('/cms/settings/security');
|
|
211
|
+
return { success: true, message: 'Authenticator app enabled.' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Email-code enrollment ------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export async function sendEmailEnrollmentCode() {
|
|
217
|
+
const { user } = await requireUser();
|
|
218
|
+
if (!user.email) {
|
|
219
|
+
throw new Error('Your account has no email address on file.');
|
|
220
|
+
}
|
|
221
|
+
const code = await createEmailChallenge(user.id);
|
|
222
|
+
await sendTwoFactorCodeEmail(user.email, code, 'enable email two-factor authentication');
|
|
223
|
+
return { success: true, message: `We sent a 6-digit code to ${user.email}.` };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function verifyEmailEnrollment(formData: FormData) {
|
|
227
|
+
const { supabase, user } = await requireUser();
|
|
228
|
+
const code = (formData.get('code')?.toString() ?? '').trim();
|
|
229
|
+
|
|
230
|
+
const ok = await verifyEmailChallenge(user.id, code);
|
|
231
|
+
if (!ok) {
|
|
232
|
+
throw new Error('That code was incorrect or expired. Request a new one.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { error } = await supabase.from('user_security_settings').upsert({
|
|
236
|
+
user_id: user.id,
|
|
237
|
+
mfa_enabled: true,
|
|
238
|
+
mfa_type: 'email',
|
|
239
|
+
updated_at: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
if (error) {
|
|
242
|
+
throw new Error('Verified, but failed to save your preference. Please retry.');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The user just proved control of their inbox, so this session is satisfied.
|
|
246
|
+
await issueTwoFactorVerifiedCookie(user.id);
|
|
247
|
+
revalidatePath('/cms/settings/security');
|
|
248
|
+
return { success: true, message: 'Email verification enabled.' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Disable / device management ------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export async function disableMfa() {
|
|
254
|
+
const { supabase, user } = await requireUser();
|
|
255
|
+
|
|
256
|
+
const { data: factors } = await supabase.auth.mfa.listFactors();
|
|
257
|
+
for (const factor of factors?.all ?? []) {
|
|
258
|
+
await supabase.auth.mfa.unenroll({ factorId: factor.id });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await supabase.from('user_security_settings').upsert({
|
|
262
|
+
user_id: user.id,
|
|
263
|
+
mfa_enabled: false,
|
|
264
|
+
mfa_type: null,
|
|
265
|
+
updated_at: new Date().toISOString(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await revokeAllTrustedDevices(user.id);
|
|
269
|
+
await clearTwoFactorVerifiedCookie();
|
|
270
|
+
|
|
271
|
+
revalidatePath('/cms/settings/security');
|
|
272
|
+
return { success: true, message: 'Two-factor authentication disabled.' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function revokeTrustedDeviceAction(formData: FormData) {
|
|
276
|
+
const { user } = await requireUser();
|
|
277
|
+
const id = formData.get('id')?.toString() ?? '';
|
|
278
|
+
if (!id) {
|
|
279
|
+
throw new Error('Missing device id.');
|
|
280
|
+
}
|
|
281
|
+
await revokeTrustedDevice(user.id, id);
|
|
282
|
+
revalidatePath('/cms/settings/security');
|
|
283
|
+
return { success: true, message: 'Device revoked.' };
|
|
284
|
+
}
|