create-nextblock 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.dockerignore +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/page.tsx +46 -46
- 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/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/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 +5749 -5749
- 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 +4 -2
- 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/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/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/actions.ts +226 -226
- 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/navigation/components/NavigationMenuDnd.tsx +3 -3
- 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/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/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
- 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/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/page.tsx +26 -26
- 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 +23 -21
- 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/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
- 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/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/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/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- 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/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.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 +471 -471
- 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/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/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/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-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- 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/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-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/media/youtube.ts +99 -99
- 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/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 +87 -87
- 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/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
import { createHmac } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
// Resolve the app's internal shared secrets WITHOUT requiring the operator to paste
|
|
5
|
-
// random strings at deploy time. When an explicit env var is set it always wins;
|
|
6
|
-
// otherwise the secret is derived deterministically from the always-present Supabase
|
|
7
|
-
// service-role key. This is what lets a one-click Vercel deploy work with zero env
|
|
8
|
-
// input while Draft Mode and on-demand revalidation still function out of the box.
|
|
9
|
-
//
|
|
10
|
-
// HMAC-SHA256 is one-way, so a leaked derived secret never exposes the service-role
|
|
11
|
-
// key. The derived value is stable for a given Supabase project; rotating the
|
|
12
|
-
// service-role key rotates these secrets too (set the explicit env var to pin one
|
|
13
|
-
// independently — e.g. to configure a Supabase revalidation webhook with a fixed
|
|
14
|
-
// token). `npm run setup` still writes explicit values locally, so dev is unchanged.
|
|
15
|
-
//
|
|
16
|
-
// CRON_SECRET is intentionally NOT derived here: Vercel Cron injects
|
|
17
|
-
// `Authorization: Bearer ${process.env.CRON_SECRET}` read from the env at invocation,
|
|
18
|
-
// so a derived value could never match it. The cron routes instead treat CRON_SECRET
|
|
19
|
-
// as optional (enforced only when set) — see app/api/cron/*/route.ts.
|
|
20
|
-
|
|
21
|
-
function deriveFromServiceRole(label: string): string {
|
|
22
|
-
// Accept the Marketplace integration's SUPABASE_SECRET_KEY alias too, so derivation
|
|
23
|
-
// still works on a one-click deploy that only injected the new key name.
|
|
24
|
-
const master = (
|
|
25
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY
|
|
26
|
-
)?.trim();
|
|
27
|
-
if (!master) return '';
|
|
28
|
-
return createHmac('sha256', master).update(`nextblock:${label}`).digest('hex');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Secret for the programmatic Draft Mode preview entry (`/api/draft?secret=`). */
|
|
32
|
-
export function resolveDraftModeSecret(): string {
|
|
33
|
-
return process.env.DRAFT_MODE_SECRET?.trim() || deriveFromServiceRole('draft-mode');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Shared secret a Supabase webhook sends to `/api/revalidate` (`x-revalidate-secret`). */
|
|
37
|
-
export function resolveRevalidateSecret(): string {
|
|
38
|
-
return process.env.REVALIDATE_SECRET_TOKEN?.trim() || deriveFromServiceRole('revalidate');
|
|
39
|
-
}
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { createHmac } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// Resolve the app's internal shared secrets WITHOUT requiring the operator to paste
|
|
5
|
+
// random strings at deploy time. When an explicit env var is set it always wins;
|
|
6
|
+
// otherwise the secret is derived deterministically from the always-present Supabase
|
|
7
|
+
// service-role key. This is what lets a one-click Vercel deploy work with zero env
|
|
8
|
+
// input while Draft Mode and on-demand revalidation still function out of the box.
|
|
9
|
+
//
|
|
10
|
+
// HMAC-SHA256 is one-way, so a leaked derived secret never exposes the service-role
|
|
11
|
+
// key. The derived value is stable for a given Supabase project; rotating the
|
|
12
|
+
// service-role key rotates these secrets too (set the explicit env var to pin one
|
|
13
|
+
// independently — e.g. to configure a Supabase revalidation webhook with a fixed
|
|
14
|
+
// token). `npm run setup` still writes explicit values locally, so dev is unchanged.
|
|
15
|
+
//
|
|
16
|
+
// CRON_SECRET is intentionally NOT derived here: Vercel Cron injects
|
|
17
|
+
// `Authorization: Bearer ${process.env.CRON_SECRET}` read from the env at invocation,
|
|
18
|
+
// so a derived value could never match it. The cron routes instead treat CRON_SECRET
|
|
19
|
+
// as optional (enforced only when set) — see app/api/cron/*/route.ts.
|
|
20
|
+
|
|
21
|
+
function deriveFromServiceRole(label: string): string {
|
|
22
|
+
// Accept the Marketplace integration's SUPABASE_SECRET_KEY alias too, so derivation
|
|
23
|
+
// still works on a one-click deploy that only injected the new key name.
|
|
24
|
+
const master = (
|
|
25
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY
|
|
26
|
+
)?.trim();
|
|
27
|
+
if (!master) return '';
|
|
28
|
+
return createHmac('sha256', master).update(`nextblock:${label}`).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Secret for the programmatic Draft Mode preview entry (`/api/draft?secret=`). */
|
|
32
|
+
export function resolveDraftModeSecret(): string {
|
|
33
|
+
return process.env.DRAFT_MODE_SECRET?.trim() || deriveFromServiceRole('draft-mode');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Shared secret a Supabase webhook sends to `/api/revalidate` (`x-revalidate-secret`). */
|
|
37
|
+
export function resolveRevalidateSecret(): string {
|
|
38
|
+
return process.env.REVALIDATE_SECRET_TOKEN?.trim() || deriveFromServiceRole('revalidate');
|
|
39
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
// Server-only cookie helpers for the 2FA / trusted-device flow.
|
|
2
|
-
// All three cookies are HttpOnly so they are never readable from JS.
|
|
3
|
-
import { cookies } from 'next/headers';
|
|
4
|
-
|
|
5
|
-
/** Long-lived "remember this device" token (raw token; its SHA-256 is in the DB). */
|
|
6
|
-
export const TRUSTED_DEVICE_COOKIE = 'nb_trusted_device';
|
|
7
|
-
/** Signed marker proving the email second factor was satisfied this session. */
|
|
8
|
-
export const TWO_FACTOR_COOKIE = 'nb_2fa_verified';
|
|
9
|
-
/** Short-lived flag carrying the "remember me" checkbox from login to post-2FA. */
|
|
10
|
-
export const REMEMBER_INTENT_COOKIE = 'nb_remember_intent';
|
|
11
|
-
|
|
12
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
13
|
-
|
|
14
|
-
export async function setSecureCookie(
|
|
15
|
-
name: string,
|
|
16
|
-
value: string,
|
|
17
|
-
maxAgeSeconds: number,
|
|
18
|
-
): Promise<void> {
|
|
19
|
-
const store = await cookies();
|
|
20
|
-
store.set({
|
|
21
|
-
name,
|
|
22
|
-
value,
|
|
23
|
-
httpOnly: true,
|
|
24
|
-
secure: isProd,
|
|
25
|
-
sameSite: 'lax',
|
|
26
|
-
path: '/',
|
|
27
|
-
maxAge: maxAgeSeconds,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function getCookieValue(name: string): Promise<string | null> {
|
|
32
|
-
const store = await cookies();
|
|
33
|
-
return store.get(name)?.value ?? null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function clearCookie(name: string): Promise<void> {
|
|
37
|
-
const store = await cookies();
|
|
38
|
-
store.set({
|
|
39
|
-
name,
|
|
40
|
-
value: '',
|
|
41
|
-
httpOnly: true,
|
|
42
|
-
secure: isProd,
|
|
43
|
-
sameSite: 'lax',
|
|
44
|
-
path: '/',
|
|
45
|
-
maxAge: 0,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
1
|
+
// Server-only cookie helpers for the 2FA / trusted-device flow.
|
|
2
|
+
// All three cookies are HttpOnly so they are never readable from JS.
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
|
|
5
|
+
/** Long-lived "remember this device" token (raw token; its SHA-256 is in the DB). */
|
|
6
|
+
export const TRUSTED_DEVICE_COOKIE = 'nb_trusted_device';
|
|
7
|
+
/** Signed marker proving the email second factor was satisfied this session. */
|
|
8
|
+
export const TWO_FACTOR_COOKIE = 'nb_2fa_verified';
|
|
9
|
+
/** Short-lived flag carrying the "remember me" checkbox from login to post-2FA. */
|
|
10
|
+
export const REMEMBER_INTENT_COOKIE = 'nb_remember_intent';
|
|
11
|
+
|
|
12
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
13
|
+
|
|
14
|
+
export async function setSecureCookie(
|
|
15
|
+
name: string,
|
|
16
|
+
value: string,
|
|
17
|
+
maxAgeSeconds: number,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const store = await cookies();
|
|
20
|
+
store.set({
|
|
21
|
+
name,
|
|
22
|
+
value,
|
|
23
|
+
httpOnly: true,
|
|
24
|
+
secure: isProd,
|
|
25
|
+
sameSite: 'lax',
|
|
26
|
+
path: '/',
|
|
27
|
+
maxAge: maxAgeSeconds,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getCookieValue(name: string): Promise<string | null> {
|
|
32
|
+
const store = await cookies();
|
|
33
|
+
return store.get(name)?.value ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function clearCookie(name: string): Promise<void> {
|
|
37
|
+
const store = await cookies();
|
|
38
|
+
store.set({
|
|
39
|
+
name,
|
|
40
|
+
value: '',
|
|
41
|
+
httpOnly: true,
|
|
42
|
+
secure: isProd,
|
|
43
|
+
sameSite: 'lax',
|
|
44
|
+
path: '/',
|
|
45
|
+
maxAge: 0,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
// Server-only crypto helpers for 2FA tokens, trusted-device hashing, and the
|
|
2
|
-
// signed "second factor satisfied" session marker. Uses Node's crypto module.
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
|
|
5
|
-
/** SHA-256 hex digest. Used to store device/code hashes (never the raw value). */
|
|
6
|
-
export function sha256Hex(input: string): string {
|
|
7
|
-
return crypto.createHash('sha256').update(input).digest('hex');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Cryptographically random URL-safe token (default 32 bytes -> 43 chars). */
|
|
11
|
-
export function randomToken(bytes = 32): string {
|
|
12
|
-
return crypto.randomBytes(bytes).toString('base64url');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Random fixed-length numeric code, e.g. a 6-digit email 2FA code. */
|
|
16
|
-
export function generateNumericCode(digits = 6): string {
|
|
17
|
-
const max = 10 ** digits;
|
|
18
|
-
return crypto.randomInt(0, max).toString().padStart(digits, '0');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getSecret(): string {
|
|
22
|
-
// Dedicated secret if provided, otherwise fall back to the service-role key
|
|
23
|
-
// (server-only, never shipped to the client).
|
|
24
|
-
const secret =
|
|
25
|
-
process.env.NB_2FA_SECRET ||
|
|
26
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
27
|
-
process.env.SUPABASE_SECRET_KEY;
|
|
28
|
-
if (!secret) {
|
|
29
|
-
throw new Error('Missing NB_2FA_SECRET / SUPABASE_SERVICE_ROLE_KEY for 2FA signing.');
|
|
30
|
-
}
|
|
31
|
-
return secret;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** HMAC-SHA256 signature (base64url) of an arbitrary payload string. */
|
|
35
|
-
export function hmacSign(payload: string): string {
|
|
36
|
-
return crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Constant-time string comparison that never throws on length mismatch. */
|
|
40
|
-
export function safeEqual(a: string, b: string): boolean {
|
|
41
|
-
const ab = Buffer.from(a);
|
|
42
|
-
const bb = Buffer.from(b);
|
|
43
|
-
if (ab.length !== bb.length) return false;
|
|
44
|
-
return crypto.timingSafeEqual(ab, bb);
|
|
45
|
-
}
|
|
1
|
+
// Server-only crypto helpers for 2FA tokens, trusted-device hashing, and the
|
|
2
|
+
// signed "second factor satisfied" session marker. Uses Node's crypto module.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
/** SHA-256 hex digest. Used to store device/code hashes (never the raw value). */
|
|
6
|
+
export function sha256Hex(input: string): string {
|
|
7
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Cryptographically random URL-safe token (default 32 bytes -> 43 chars). */
|
|
11
|
+
export function randomToken(bytes = 32): string {
|
|
12
|
+
return crypto.randomBytes(bytes).toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Random fixed-length numeric code, e.g. a 6-digit email 2FA code. */
|
|
16
|
+
export function generateNumericCode(digits = 6): string {
|
|
17
|
+
const max = 10 ** digits;
|
|
18
|
+
return crypto.randomInt(0, max).toString().padStart(digits, '0');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getSecret(): string {
|
|
22
|
+
// Dedicated secret if provided, otherwise fall back to the service-role key
|
|
23
|
+
// (server-only, never shipped to the client).
|
|
24
|
+
const secret =
|
|
25
|
+
process.env.NB_2FA_SECRET ||
|
|
26
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
27
|
+
process.env.SUPABASE_SECRET_KEY;
|
|
28
|
+
if (!secret) {
|
|
29
|
+
throw new Error('Missing NB_2FA_SECRET / SUPABASE_SERVICE_ROLE_KEY for 2FA signing.');
|
|
30
|
+
}
|
|
31
|
+
return secret;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** HMAC-SHA256 signature (base64url) of an arbitrary payload string. */
|
|
35
|
+
export function hmacSign(payload: string): string {
|
|
36
|
+
return crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Constant-time string comparison that never throws on length mismatch. */
|
|
40
|
+
export function safeEqual(a: string, b: string): boolean {
|
|
41
|
+
const ab = Buffer.from(a);
|
|
42
|
+
const bb = Buffer.from(b);
|
|
43
|
+
if (ab.length !== bb.length) return false;
|
|
44
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
45
|
+
}
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
// Server-only trusted-device ("Remember this device") helpers.
|
|
2
|
-
//
|
|
3
|
-
// Trust is server-validated: the cookie holds only a random token, and a bypass
|
|
4
|
-
// is honoured ONLY when a non-expired user_trusted_devices row matches its
|
|
5
|
-
// SHA-256. Deleting the row instantly revokes trust, regardless of the cookie.
|
|
6
|
-
import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
7
|
-
import { getSecuritySettings } from '../privacy/settings';
|
|
8
|
-
import { randomToken, sha256Hex } from './crypto';
|
|
9
|
-
import {
|
|
10
|
-
TRUSTED_DEVICE_COOKIE,
|
|
11
|
-
clearCookie,
|
|
12
|
-
getCookieValue,
|
|
13
|
-
setSecureCookie,
|
|
14
|
-
} from './cookies';
|
|
15
|
-
|
|
16
|
-
export interface TrustedDeviceRow {
|
|
17
|
-
id: string;
|
|
18
|
-
browser_metadata: string | null;
|
|
19
|
-
created_at: string;
|
|
20
|
-
expires_at: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Mint a trusted-device token, persist its hash, and set the cookie. */
|
|
24
|
-
export async function issueTrustedDevice(
|
|
25
|
-
userId: string,
|
|
26
|
-
browserMetadata: string | null,
|
|
27
|
-
): Promise<void> {
|
|
28
|
-
const { trusted_device_days } = await getSecuritySettings();
|
|
29
|
-
const maxAgeSeconds = trusted_device_days * 24 * 60 * 60;
|
|
30
|
-
const token = randomToken(32);
|
|
31
|
-
const expiresAt = new Date(Date.now() + maxAgeSeconds * 1000).toISOString();
|
|
32
|
-
|
|
33
|
-
const svc = getServiceRoleSupabaseClient();
|
|
34
|
-
const { error } = await svc.from('user_trusted_devices').insert({
|
|
35
|
-
user_id: userId,
|
|
36
|
-
device_hash: sha256Hex(token),
|
|
37
|
-
browser_metadata: browserMetadata ? browserMetadata.slice(0, 500) : null,
|
|
38
|
-
expires_at: expiresAt,
|
|
39
|
-
});
|
|
40
|
-
if (error) {
|
|
41
|
-
console.error('Failed to persist trusted device:', error.message);
|
|
42
|
-
return; // Non-fatal: the user simply isn't remembered.
|
|
43
|
-
}
|
|
44
|
-
await setSecureCookie(TRUSTED_DEVICE_COOKIE, token, maxAgeSeconds);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** True when the current request carries a cookie matching a live trusted row. */
|
|
48
|
-
export async function hasValidTrustedDevice(userId: string): Promise<boolean> {
|
|
49
|
-
const token = await getCookieValue(TRUSTED_DEVICE_COOKIE);
|
|
50
|
-
if (!token) return false;
|
|
51
|
-
const svc = getServiceRoleSupabaseClient();
|
|
52
|
-
const { data } = await svc
|
|
53
|
-
.from('user_trusted_devices')
|
|
54
|
-
.select('id')
|
|
55
|
-
.eq('user_id', userId)
|
|
56
|
-
.eq('device_hash', sha256Hex(token))
|
|
57
|
-
.gt('expires_at', new Date().toISOString())
|
|
58
|
-
.maybeSingle();
|
|
59
|
-
return Boolean(data);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** List the signed-in user's trusted devices (RLS scopes to their own rows). */
|
|
63
|
-
export async function listTrustedDevices(userId: string): Promise<TrustedDeviceRow[]> {
|
|
64
|
-
const supabase = createClient();
|
|
65
|
-
const { data } = await supabase
|
|
66
|
-
.from('user_trusted_devices')
|
|
67
|
-
.select('id, browser_metadata, created_at, expires_at')
|
|
68
|
-
.eq('user_id', userId)
|
|
69
|
-
.order('created_at', { ascending: false });
|
|
70
|
-
return data ?? [];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Revoke one trusted device the user owns (RLS prevents touching others'). */
|
|
74
|
-
export async function revokeTrustedDevice(userId: string, id: string): Promise<void> {
|
|
75
|
-
const supabase = createClient();
|
|
76
|
-
const { error } = await supabase
|
|
77
|
-
.from('user_trusted_devices')
|
|
78
|
-
.delete()
|
|
79
|
-
.eq('id', id)
|
|
80
|
-
.eq('user_id', userId);
|
|
81
|
-
if (error) {
|
|
82
|
-
console.error('Failed to revoke trusted device:', error.message);
|
|
83
|
-
throw new Error('Failed to revoke device.');
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Drop all of a user's trusted devices and the local cookie (e.g. on MFA disable). */
|
|
88
|
-
export async function revokeAllTrustedDevices(userId: string): Promise<void> {
|
|
89
|
-
const svc = getServiceRoleSupabaseClient();
|
|
90
|
-
await svc.from('user_trusted_devices').delete().eq('user_id', userId);
|
|
91
|
-
await clearCookie(TRUSTED_DEVICE_COOKIE);
|
|
92
|
-
}
|
|
1
|
+
// Server-only trusted-device ("Remember this device") helpers.
|
|
2
|
+
//
|
|
3
|
+
// Trust is server-validated: the cookie holds only a random token, and a bypass
|
|
4
|
+
// is honoured ONLY when a non-expired user_trusted_devices row matches its
|
|
5
|
+
// SHA-256. Deleting the row instantly revokes trust, regardless of the cookie.
|
|
6
|
+
import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
|
|
7
|
+
import { getSecuritySettings } from '../privacy/settings';
|
|
8
|
+
import { randomToken, sha256Hex } from './crypto';
|
|
9
|
+
import {
|
|
10
|
+
TRUSTED_DEVICE_COOKIE,
|
|
11
|
+
clearCookie,
|
|
12
|
+
getCookieValue,
|
|
13
|
+
setSecureCookie,
|
|
14
|
+
} from './cookies';
|
|
15
|
+
|
|
16
|
+
export interface TrustedDeviceRow {
|
|
17
|
+
id: string;
|
|
18
|
+
browser_metadata: string | null;
|
|
19
|
+
created_at: string;
|
|
20
|
+
expires_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Mint a trusted-device token, persist its hash, and set the cookie. */
|
|
24
|
+
export async function issueTrustedDevice(
|
|
25
|
+
userId: string,
|
|
26
|
+
browserMetadata: string | null,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const { trusted_device_days } = await getSecuritySettings();
|
|
29
|
+
const maxAgeSeconds = trusted_device_days * 24 * 60 * 60;
|
|
30
|
+
const token = randomToken(32);
|
|
31
|
+
const expiresAt = new Date(Date.now() + maxAgeSeconds * 1000).toISOString();
|
|
32
|
+
|
|
33
|
+
const svc = getServiceRoleSupabaseClient();
|
|
34
|
+
const { error } = await svc.from('user_trusted_devices').insert({
|
|
35
|
+
user_id: userId,
|
|
36
|
+
device_hash: sha256Hex(token),
|
|
37
|
+
browser_metadata: browserMetadata ? browserMetadata.slice(0, 500) : null,
|
|
38
|
+
expires_at: expiresAt,
|
|
39
|
+
});
|
|
40
|
+
if (error) {
|
|
41
|
+
console.error('Failed to persist trusted device:', error.message);
|
|
42
|
+
return; // Non-fatal: the user simply isn't remembered.
|
|
43
|
+
}
|
|
44
|
+
await setSecureCookie(TRUSTED_DEVICE_COOKIE, token, maxAgeSeconds);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when the current request carries a cookie matching a live trusted row. */
|
|
48
|
+
export async function hasValidTrustedDevice(userId: string): Promise<boolean> {
|
|
49
|
+
const token = await getCookieValue(TRUSTED_DEVICE_COOKIE);
|
|
50
|
+
if (!token) return false;
|
|
51
|
+
const svc = getServiceRoleSupabaseClient();
|
|
52
|
+
const { data } = await svc
|
|
53
|
+
.from('user_trusted_devices')
|
|
54
|
+
.select('id')
|
|
55
|
+
.eq('user_id', userId)
|
|
56
|
+
.eq('device_hash', sha256Hex(token))
|
|
57
|
+
.gt('expires_at', new Date().toISOString())
|
|
58
|
+
.maybeSingle();
|
|
59
|
+
return Boolean(data);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** List the signed-in user's trusted devices (RLS scopes to their own rows). */
|
|
63
|
+
export async function listTrustedDevices(userId: string): Promise<TrustedDeviceRow[]> {
|
|
64
|
+
const supabase = createClient();
|
|
65
|
+
const { data } = await supabase
|
|
66
|
+
.from('user_trusted_devices')
|
|
67
|
+
.select('id, browser_metadata, created_at, expires_at')
|
|
68
|
+
.eq('user_id', userId)
|
|
69
|
+
.order('created_at', { ascending: false });
|
|
70
|
+
return data ?? [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Revoke one trusted device the user owns (RLS prevents touching others'). */
|
|
74
|
+
export async function revokeTrustedDevice(userId: string, id: string): Promise<void> {
|
|
75
|
+
const supabase = createClient();
|
|
76
|
+
const { error } = await supabase
|
|
77
|
+
.from('user_trusted_devices')
|
|
78
|
+
.delete()
|
|
79
|
+
.eq('id', id)
|
|
80
|
+
.eq('user_id', userId);
|
|
81
|
+
if (error) {
|
|
82
|
+
console.error('Failed to revoke trusted device:', error.message);
|
|
83
|
+
throw new Error('Failed to revoke device.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drop all of a user's trusted devices and the local cookie (e.g. on MFA disable). */
|
|
88
|
+
export async function revokeAllTrustedDevices(userId: string): Promise<void> {
|
|
89
|
+
const svc = getServiceRoleSupabaseClient();
|
|
90
|
+
await svc.from('user_trusted_devices').delete().eq('user_id', userId);
|
|
91
|
+
await clearCookie(TRUSTED_DEVICE_COOKIE);
|
|
92
|
+
}
|
|
@@ -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.
|