create-nextblock 0.15.10 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -0
- package/templates/nextblock-template/app/sitemap.ts +1 -1
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +0 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/proxy.ts +240 -20
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
- package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
|
2
2
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
|
-
import type
|
|
3
|
+
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
4
4
|
import type { Database } from '@nextblock-cms/db';
|
|
5
|
+
import {
|
|
6
|
+
buildRedirectIndex,
|
|
7
|
+
matchRedirect,
|
|
8
|
+
type RedirectRule,
|
|
9
|
+
} from '@nextblock-cms/utils/seo';
|
|
5
10
|
import { resolveSupabaseAnonKey, resolveSupabaseUrl } from './lib/setup/env-status';
|
|
11
|
+
import {
|
|
12
|
+
fetchActiveRedirects,
|
|
13
|
+
isSchemaMissingError,
|
|
14
|
+
isSelfRedirect,
|
|
15
|
+
resolveRedirectTarget,
|
|
16
|
+
shouldSkipRedirectLookup,
|
|
17
|
+
} from './lib/seo/redirect-store';
|
|
6
18
|
import {
|
|
7
19
|
LANGUAGE_DETECTION_SETTING_KEY,
|
|
8
20
|
DEFAULT_LANGUAGE_DETECTION_SETTINGS,
|
|
@@ -86,24 +98,13 @@ function isSetupAllowlisted(pathname: string): boolean {
|
|
|
86
98
|
// modules persist across requests in a worker, so this avoids a per-request DB hit.
|
|
87
99
|
let provisionedAdminCache: { value: boolean; expires: number } | null = null;
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
function isSchemaMissingError(error: { code?: string; message?: string } | null): boolean {
|
|
97
|
-
if (!error) return false;
|
|
98
|
-
const code = error.code ?? '';
|
|
99
|
-
if (code === '42P01' || code === 'PGRST205') return true;
|
|
100
|
-
const message = (error.message ?? '').toLowerCase();
|
|
101
|
-
return (
|
|
102
|
-
message.includes('does not exist') ||
|
|
103
|
-
message.includes('schema cache') ||
|
|
104
|
-
message.includes('could not find the table')
|
|
105
|
-
);
|
|
106
|
-
}
|
|
101
|
+
// `isSchemaMissingError` used to be defined right here, as a private copy. It now
|
|
102
|
+
// lives in ./lib/seo/redirect-store and is imported above, because the redirect
|
|
103
|
+
// lookup needs exactly the same test — "did this error mean the table is absent,
|
|
104
|
+
// or merely that the database is having a bad minute?" — and a predicate that
|
|
105
|
+
// decides whether a whole site funnels its traffic to /setup must have one
|
|
106
|
+
// definition. The reasoning behind the specific codes it looks for (42P01,
|
|
107
|
+
// PGRST205, and the message sniffing underneath) is documented there.
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Returns true once the system has a first admin (site_settings.is_admin_created).
|
|
@@ -231,6 +232,116 @@ function getLocaleRuntimeConfig(
|
|
|
231
232
|
return localeConfigInflight;
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Managed redirects
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
//
|
|
239
|
+
// Operator-authored 301/302 rules live in public.cms_redirects and are resolved
|
|
240
|
+
// here rather than in next.config.js, because a redirect is content: an editor who
|
|
241
|
+
// renames a slug needs the old URL to keep working immediately, without a redeploy.
|
|
242
|
+
// The trade is that every request now potentially needs a database answer, so the
|
|
243
|
+
// caching below is not an optimisation — it is the thing that makes the feature
|
|
244
|
+
// affordable at all.
|
|
245
|
+
//
|
|
246
|
+
// The cache mirrors localeConfigCache/localeConfigInflight deliberately, right down
|
|
247
|
+
// to the shape of the value and the `.finally()` that clears the in-flight handle.
|
|
248
|
+
// Proxy modules persist across requests inside a worker, so a module-level entry is
|
|
249
|
+
// shared by every request that worker serves; the in-flight promise is what stops a
|
|
250
|
+
// burst of concurrent misses (a cold worker taking a page's document plus its
|
|
251
|
+
// subresources) from turning into a burst of identical queries.
|
|
252
|
+
//
|
|
253
|
+
// Two TTLs, because the two outcomes mean different things. A successful read —
|
|
254
|
+
// including a successful read of an EMPTY table, which is what almost every site
|
|
255
|
+
// will have — is cached for 60s: rules change at human speed and a minute of
|
|
256
|
+
// staleness after an operator saves one is acceptable. A failure is cached for only
|
|
257
|
+
// 10s, because the most likely cause is an install that has pulled this code but
|
|
258
|
+
// not yet run `npm run db:migrate`, and after that migration lands redirects should
|
|
259
|
+
// start working within seconds rather than after a full minute of a poisoned cache.
|
|
260
|
+
const REDIRECT_CACHE_TTL_MS = 60_000;
|
|
261
|
+
const REDIRECT_NEGATIVE_CACHE_TTL_MS = 10_000;
|
|
262
|
+
|
|
263
|
+
// `null` here means "we could not read the table", which is not the same as an
|
|
264
|
+
// empty Map ("we read it and there are no rules"). Only the empty Map earns the
|
|
265
|
+
// long TTL.
|
|
266
|
+
let redirectIndexCache: { value: Map<string, RedirectRule> | null; expires: number } | null = null;
|
|
267
|
+
let redirectIndexInflight: Promise<Map<string, RedirectRule> | null> | null = null;
|
|
268
|
+
|
|
269
|
+
// The cookie-bound client built inside `proxy()` cannot be used for this lookup: it
|
|
270
|
+
// is constructed further down, after the redirect check has already had to decide.
|
|
271
|
+
// So this is a plain anon client, mirroring libs/db/src/lib/supabase/ssg-client.ts —
|
|
272
|
+
// no cookies, no session persistence, no token refresh, because reading a public
|
|
273
|
+
// table needs none of that and every one of those features would add work to a path
|
|
274
|
+
// that runs in front of the whole site. It is memoised for the life of the worker
|
|
275
|
+
// alongside the credentials it was built from, so a cache hit constructs nothing at
|
|
276
|
+
// all and a credential change (a redeploy with new env) still rebuilds it.
|
|
277
|
+
let redirectLookupClient: { client: SupabaseClient; key: string; url: string } | null = null;
|
|
278
|
+
|
|
279
|
+
function getRedirectLookupClient(supabaseUrl: string, supabaseAnonKey: string): SupabaseClient {
|
|
280
|
+
if (
|
|
281
|
+
redirectLookupClient &&
|
|
282
|
+
redirectLookupClient.url === supabaseUrl &&
|
|
283
|
+
redirectLookupClient.key === supabaseAnonKey
|
|
284
|
+
) {
|
|
285
|
+
return redirectLookupClient.client;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const client = createClient(supabaseUrl, supabaseAnonKey, {
|
|
289
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
290
|
+
});
|
|
291
|
+
redirectLookupClient = { client, key: supabaseAnonKey, url: supabaseUrl };
|
|
292
|
+
return client;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function loadRedirectIndex(
|
|
296
|
+
supabaseUrl: string,
|
|
297
|
+
supabaseAnonKey: string,
|
|
298
|
+
): Promise<Map<string, RedirectRule> | null> {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
try {
|
|
301
|
+
const rules = await fetchActiveRedirects(getRedirectLookupClient(supabaseUrl, supabaseAnonKey));
|
|
302
|
+
|
|
303
|
+
// fetchActiveRedirects never throws and returns null only for "could not read".
|
|
304
|
+
// That includes the table not existing yet, which is why the negative TTL is
|
|
305
|
+
// short: this is the state a site sits in between deploying the code and
|
|
306
|
+
// running the migration.
|
|
307
|
+
if (rules === null) {
|
|
308
|
+
redirectIndexCache = { value: null, expires: now + REDIRECT_NEGATIVE_CACHE_TTL_MS };
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const index = buildRedirectIndex(rules);
|
|
313
|
+
redirectIndexCache = { value: index, expires: now + REDIRECT_CACHE_TTL_MS };
|
|
314
|
+
return index;
|
|
315
|
+
} catch {
|
|
316
|
+
redirectIndexCache = { value: null, expires: now + REDIRECT_NEGATIVE_CACHE_TTL_MS };
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns the redirect lookup index, or null when the table could not be read.
|
|
323
|
+
*
|
|
324
|
+
* Never rejects: `loadRedirectIndex` swallows everything, so a caller may await this
|
|
325
|
+
* without a try/catch and still be sure a database outage cannot escape into the
|
|
326
|
+
* request pipeline. The caller wraps it anyway, on the principle that a throw
|
|
327
|
+
* anywhere inside the proxy does not break a redirect — it returns a 500 for every
|
|
328
|
+
* page, asset and API route on the site simultaneously.
|
|
329
|
+
*/
|
|
330
|
+
function getRedirectIndex(
|
|
331
|
+
supabaseUrl: string,
|
|
332
|
+
supabaseAnonKey: string,
|
|
333
|
+
): Promise<Map<string, RedirectRule> | null> {
|
|
334
|
+
if (redirectIndexCache && redirectIndexCache.expires > Date.now()) {
|
|
335
|
+
return Promise.resolve(redirectIndexCache.value);
|
|
336
|
+
}
|
|
337
|
+
if (!redirectIndexInflight) {
|
|
338
|
+
redirectIndexInflight = loadRedirectIndex(supabaseUrl, supabaseAnonKey).finally(() => {
|
|
339
|
+
redirectIndexInflight = null;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return redirectIndexInflight;
|
|
343
|
+
}
|
|
344
|
+
|
|
234
345
|
function getHttpOrigin(value: string | undefined): string | null {
|
|
235
346
|
if (!value) {
|
|
236
347
|
return null;
|
|
@@ -292,11 +403,30 @@ function applySecurityHeaders(
|
|
|
292
403
|
return response;
|
|
293
404
|
}
|
|
294
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Every redirect this proxy issues goes through here, so that none of them can
|
|
408
|
+
* accidentally ship without the seven security headers and the CSP. A bare
|
|
409
|
+
* `NextResponse.redirect()` returns a response with none of them.
|
|
410
|
+
*
|
|
411
|
+
* The default status is 307, which is what `NextResponse.redirect()` produces on its
|
|
412
|
+
* own — so adding this parameter changed the behaviour of exactly zero existing call
|
|
413
|
+
* sites. 307 is also the right default for the gates below it: they are all
|
|
414
|
+
* "you cannot be here *right now*" decisions (unprovisioned, signed out, wrong role),
|
|
415
|
+
* and a browser that cached a permanent redirect for those would keep bouncing the
|
|
416
|
+
* user long after they had signed in.
|
|
417
|
+
*
|
|
418
|
+
* Managed content redirects are the exception, and the reason the parameter exists:
|
|
419
|
+
* a 301 is the whole point of "this page moved", because it is the code search
|
|
420
|
+
* engines act on. Only {301, 302, 303, 307, 308} are permitted — Next throws
|
|
421
|
+
* RangeError E529 for anything else — which is also why `RedirectStatusCode` is
|
|
422
|
+
* narrowed to 301 | 302 upstream rather than being an open `number`.
|
|
423
|
+
*/
|
|
295
424
|
function createRedirectResponse(
|
|
296
425
|
url: URL,
|
|
297
426
|
contentSecurityPolicy?: string | null,
|
|
427
|
+
status = 307,
|
|
298
428
|
): NextResponse {
|
|
299
|
-
return applySecurityHeaders(NextResponse.redirect(url), contentSecurityPolicy);
|
|
429
|
+
return applySecurityHeaders(NextResponse.redirect(url, status), contentSecurityPolicy);
|
|
300
430
|
}
|
|
301
431
|
|
|
302
432
|
function createContentSecurityPolicy(nonceValue: string, supabaseUrl: string | undefined): string {
|
|
@@ -486,6 +616,96 @@ export async function proxy(request: NextRequest) {
|
|
|
486
616
|
return createRedirectResponse(new URL('/setup', request.url), contentSecurityPolicy);
|
|
487
617
|
}
|
|
488
618
|
|
|
619
|
+
// Managed redirects, resolved before anything else touches Supabase.
|
|
620
|
+
//
|
|
621
|
+
// The position of this block is the point. It sits after the unconfigured gate
|
|
622
|
+
// (there is no database to ask before that) and before `supabase.auth.getSession()`,
|
|
623
|
+
// so a visitor following a dead link is answered with a 301 without the proxy
|
|
624
|
+
// having spent a session round-trip and a user lookup on a request whose response
|
|
625
|
+
// body is empty. It also sits before the CMS role checks, which is harmless
|
|
626
|
+
// because `shouldSkipRedirectLookup` refuses to consider /cms at all.
|
|
627
|
+
//
|
|
628
|
+
// The whole block is wrapped in try/catch and fails open. There is no such thing
|
|
629
|
+
// as a redirect important enough to justify a throw here: the proxy runs in front
|
|
630
|
+
// of every route in the app, so an exception escaping this block would return a
|
|
631
|
+
// 500 for the entire site rather than for one stale URL.
|
|
632
|
+
if (!shouldSkipRedirectLookup(pathname)) {
|
|
633
|
+
try {
|
|
634
|
+
const redirectIndex = await getRedirectIndex(
|
|
635
|
+
supabaseUrl as string,
|
|
636
|
+
supabaseAnonKey as string,
|
|
637
|
+
);
|
|
638
|
+
const rule = redirectIndex ? matchRedirect(redirectIndex, pathname) : null;
|
|
639
|
+
|
|
640
|
+
if (rule) {
|
|
641
|
+
// A destination is either a site-relative path or an absolute https URL —
|
|
642
|
+
// the database CHECK constraint allows nothing else. Resolving the relative
|
|
643
|
+
// form against `request.url` preserves the incoming scheme and host, so a
|
|
644
|
+
// rule works identically on localhost, on a preview deployment and in
|
|
645
|
+
// production without the operator ever writing an origin. The resolution
|
|
646
|
+
// (including whether the visitor's query string travels with them, and what
|
|
647
|
+
// to do about a destination the URL parser cannot make sense of) lives in
|
|
648
|
+
// `resolveRedirectTarget` because it is ordinary logic that deserves ordinary
|
|
649
|
+
// tests, and this function body cannot be given any.
|
|
650
|
+
const target = resolveRedirectTarget(rule.destinationPath, {
|
|
651
|
+
search: request.nextUrl.search,
|
|
652
|
+
url: request.url,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// `null` means the stored destination does not parse as a URL at all, which
|
|
656
|
+
// is a broken rule rather than a broken request: skip it and let the page
|
|
657
|
+
// render, the same outcome the catch below produces.
|
|
658
|
+
if (target) {
|
|
659
|
+
// Runtime loop guard. `validateRedirectRule` already rejects cycles when a
|
|
660
|
+
// rule is saved and the table forbids source = destination, but neither
|
|
661
|
+
// catches a pair that differs only by normalisation (`/about` -> `/about/`),
|
|
662
|
+
// and neither can see a rule that was inserted straight into the database.
|
|
663
|
+
// A same-path redirect is an infinite loop in the visitor's browser, so it
|
|
664
|
+
// is checked here, against the resolved URL, every single time. Only paths
|
|
665
|
+
// on this origin can loop; an off-site destination is somebody else's
|
|
666
|
+
// problem by definition.
|
|
667
|
+
//
|
|
668
|
+
// Both sides of this comparison are pathnames, which is why carrying the
|
|
669
|
+
// query onto `target` cannot change its answer: `URL.pathname` never
|
|
670
|
+
// contains a query string. Comparing anything wider here — the href, or a
|
|
671
|
+
// path with its search appended — would let a rule that only adds
|
|
672
|
+
// parameters slip past the guard and loop.
|
|
673
|
+
const isSameOrigin = target.origin === request.nextUrl.origin;
|
|
674
|
+
if (!isSameOrigin || !isSelfRedirect(pathname, target.pathname)) {
|
|
675
|
+
const redirectResponse = createRedirectResponse(
|
|
676
|
+
target,
|
|
677
|
+
contentSecurityPolicy,
|
|
678
|
+
rule.statusCode,
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// The proxy sets no Cache-Control on any of its other redirects, which is
|
|
682
|
+
// fine for the auth gates (they must be re-evaluated per request) but wrong
|
|
683
|
+
// here, so this one is explicit. A 301 is permanent by definition and every
|
|
684
|
+
// hop it saves is a request that never reaches the origin, so an hour of
|
|
685
|
+
// shared caching is free value. A 302 gets `no-cache` because a temporary
|
|
686
|
+
// redirect that a browser has cached is an operational trap: the operator
|
|
687
|
+
// deletes the rule, the site behaves correctly for everyone new, and the
|
|
688
|
+
// people who already hit it keep being redirected with no way to tell them
|
|
689
|
+
// apart or fix it.
|
|
690
|
+
//
|
|
691
|
+
// Caching a Location that now varies with the incoming query is safe
|
|
692
|
+
// because caches key on the full request URL: `/old?utm=a` and `/old?utm=b`
|
|
693
|
+
// are separate entries, so neither can be served the other's destination.
|
|
694
|
+
redirectResponse.headers.set(
|
|
695
|
+
'Cache-Control',
|
|
696
|
+
rule.statusCode === 301 ? 'public, max-age=3600' : 'no-cache',
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return redirectResponse;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
// Fail open: log it and let the request render normally.
|
|
705
|
+
console.error(`Proxy: redirect lookup failed for ${pathname}; serving the page.`, error);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
489
709
|
let response = NextResponse.next({
|
|
490
710
|
request: {
|
|
491
711
|
headers: requestHeaders,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// apps/nextblock/scripts/backup.js
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const readline = require('readline');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const readline = require('readline');
|
|
6
6
|
|
|
7
7
|
// Load environment (database connection string should be in .env.local or .env)
|
|
8
8
|
require('dotenv').config({ path: '.env.local' }); // adjust path if needed
|
|
@@ -35,114 +35,114 @@ const port = connectionUrl.port || '5432';
|
|
|
35
35
|
const dbName = connectionUrl.pathname.replace(/^\//, ''); // strip leading '/'
|
|
36
36
|
const user = connectionUrl.username;
|
|
37
37
|
const password = connectionUrl.password;
|
|
38
|
-
const sslMode = connectionUrl.searchParams.get('sslmode') || 'require'; // default to require SSL
|
|
39
|
-
|
|
40
|
-
// Prepare backup directory with timestamp name
|
|
41
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // replace colon and dot with hyphen for filename safety
|
|
42
|
-
|
|
43
|
-
function sanitizeName(rawName) {
|
|
44
|
-
if (!rawName) {
|
|
45
|
-
return '';
|
|
46
|
-
}
|
|
47
|
-
return rawName
|
|
48
|
-
.trim()
|
|
49
|
-
.replace(/[^\w.-]+/g, '-') // keep letters/numbers/_/./-
|
|
50
|
-
.replace(/-+/g, '-')
|
|
51
|
-
.replace(/^-|-$/g, '');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseNameFromArgs() {
|
|
55
|
-
const args = process.argv.slice(2);
|
|
56
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
57
|
-
const arg = args[i];
|
|
58
|
-
if (arg === '--name' || arg === '-n') {
|
|
59
|
-
return args[i + 1] ?? '';
|
|
60
|
-
}
|
|
61
|
-
const match = arg.match(/^--name=(.*)$/);
|
|
62
|
-
if (match) {
|
|
63
|
-
return match[1];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (typeof process.env.BACKUP_NAME === 'string') {
|
|
67
|
-
return process.env.BACKUP_NAME;
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function promptForName(defaultLabel) {
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
const rl = readline.createInterface({
|
|
75
|
-
input: process.stdin,
|
|
76
|
-
output: process.stdout,
|
|
77
|
-
});
|
|
78
|
-
rl.question(
|
|
79
|
-
`Enter a friendly name for this backup (press Enter to use ${defaultLabel}): `,
|
|
80
|
-
(answer) => {
|
|
81
|
-
rl.close();
|
|
82
|
-
resolve(answer);
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function buildFolderName(baseName, rawName) {
|
|
89
|
-
const sanitized = sanitizeName(rawName);
|
|
90
|
-
if (!sanitized) {
|
|
91
|
-
return baseName;
|
|
92
|
-
}
|
|
93
|
-
return `${baseName}__${sanitized}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function resolveBackupFolderName(baseName) {
|
|
97
|
-
const providedName = parseNameFromArgs();
|
|
98
|
-
if (providedName !== null) {
|
|
99
|
-
return buildFolderName(baseName, providedName);
|
|
100
|
-
}
|
|
101
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
102
|
-
const answer = await promptForName(baseName);
|
|
103
|
-
return buildFolderName(baseName, answer);
|
|
104
|
-
}
|
|
105
|
-
return baseName;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
(async () => {
|
|
109
|
-
let backupFolderName;
|
|
110
|
-
try {
|
|
111
|
-
backupFolderName = await resolveBackupFolderName(timestamp);
|
|
112
|
-
} catch (err) {
|
|
113
|
-
console.error(`❌ Failed to determine backup name: ${err.message}`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const backupDir = path.join(__dirname, '../backups', backupFolderName);
|
|
118
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
119
|
-
const dumpFile = path.join(backupDir, 'dump.sql');
|
|
120
|
-
|
|
121
|
-
console.log(`🔄 Backing up database to ${dumpFile} ...`);
|
|
122
|
-
|
|
123
|
-
// Spawn the pg_dump process
|
|
124
|
-
const dumpArgs = [
|
|
125
|
-
'--clean', '--if-exists', '--quote-all-identifiers', // include DROP statements:contentReference[oaicite:8]{index=8}
|
|
126
|
-
'-h', host,
|
|
127
|
-
'-U', user,
|
|
128
|
-
'-p', port,
|
|
129
|
-
'-d', dbName,
|
|
130
|
-
'-f', dumpFile
|
|
131
|
-
];
|
|
132
|
-
const envVars = { ...process.env, PGPASSWORD: password, PGSSLMODE: sslMode };
|
|
133
|
-
const pgDump = spawn('pg_dump', dumpArgs, { env: envVars });
|
|
134
|
-
|
|
135
|
-
// Forward pg_dump errors to console
|
|
136
|
-
pgDump.stderr.on('data', (data) => {
|
|
137
|
-
process.stderr.write(data);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Handle process exit
|
|
141
|
-
pgDump.on('close', (code) => {
|
|
142
|
-
if (code === 0) {
|
|
143
|
-
console.log("✅ Backup completed successfully.");
|
|
144
|
-
} else {
|
|
145
|
-
console.error(`❌ pg_dump exited with code ${code}. Check error output above for details.`);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
})();
|
|
38
|
+
const sslMode = connectionUrl.searchParams.get('sslmode') || 'require'; // default to require SSL
|
|
39
|
+
|
|
40
|
+
// Prepare backup directory with timestamp name
|
|
41
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // replace colon and dot with hyphen for filename safety
|
|
42
|
+
|
|
43
|
+
function sanitizeName(rawName) {
|
|
44
|
+
if (!rawName) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
return rawName
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/[^\w.-]+/g, '-') // keep letters/numbers/_/./-
|
|
50
|
+
.replace(/-+/g, '-')
|
|
51
|
+
.replace(/^-|-$/g, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseNameFromArgs() {
|
|
55
|
+
const args = process.argv.slice(2);
|
|
56
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
if (arg === '--name' || arg === '-n') {
|
|
59
|
+
return args[i + 1] ?? '';
|
|
60
|
+
}
|
|
61
|
+
const match = arg.match(/^--name=(.*)$/);
|
|
62
|
+
if (match) {
|
|
63
|
+
return match[1];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (typeof process.env.BACKUP_NAME === 'string') {
|
|
67
|
+
return process.env.BACKUP_NAME;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function promptForName(defaultLabel) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const rl = readline.createInterface({
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
output: process.stdout,
|
|
77
|
+
});
|
|
78
|
+
rl.question(
|
|
79
|
+
`Enter a friendly name for this backup (press Enter to use ${defaultLabel}): `,
|
|
80
|
+
(answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
resolve(answer);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildFolderName(baseName, rawName) {
|
|
89
|
+
const sanitized = sanitizeName(rawName);
|
|
90
|
+
if (!sanitized) {
|
|
91
|
+
return baseName;
|
|
92
|
+
}
|
|
93
|
+
return `${baseName}__${sanitized}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function resolveBackupFolderName(baseName) {
|
|
97
|
+
const providedName = parseNameFromArgs();
|
|
98
|
+
if (providedName !== null) {
|
|
99
|
+
return buildFolderName(baseName, providedName);
|
|
100
|
+
}
|
|
101
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
102
|
+
const answer = await promptForName(baseName);
|
|
103
|
+
return buildFolderName(baseName, answer);
|
|
104
|
+
}
|
|
105
|
+
return baseName;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
(async () => {
|
|
109
|
+
let backupFolderName;
|
|
110
|
+
try {
|
|
111
|
+
backupFolderName = await resolveBackupFolderName(timestamp);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`❌ Failed to determine backup name: ${err.message}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const backupDir = path.join(__dirname, '../backups', backupFolderName);
|
|
118
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
119
|
+
const dumpFile = path.join(backupDir, 'dump.sql');
|
|
120
|
+
|
|
121
|
+
console.log(`🔄 Backing up database to ${dumpFile} ...`);
|
|
122
|
+
|
|
123
|
+
// Spawn the pg_dump process
|
|
124
|
+
const dumpArgs = [
|
|
125
|
+
'--clean', '--if-exists', '--quote-all-identifiers', // include DROP statements:contentReference[oaicite:8]{index=8}
|
|
126
|
+
'-h', host,
|
|
127
|
+
'-U', user,
|
|
128
|
+
'-p', port,
|
|
129
|
+
'-d', dbName,
|
|
130
|
+
'-f', dumpFile
|
|
131
|
+
];
|
|
132
|
+
const envVars = { ...process.env, PGPASSWORD: password, PGSSLMODE: sslMode };
|
|
133
|
+
const pgDump = spawn('pg_dump', dumpArgs, { env: envVars });
|
|
134
|
+
|
|
135
|
+
// Forward pg_dump errors to console
|
|
136
|
+
pgDump.stderr.on('data', (data) => {
|
|
137
|
+
process.stderr.write(data);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Handle process exit
|
|
141
|
+
pgDump.on('close', (code) => {
|
|
142
|
+
if (code === 0) {
|
|
143
|
+
console.log("✅ Backup completed successfully.");
|
|
144
|
+
} else {
|
|
145
|
+
console.error(`❌ pg_dump exited with code ${code}. Check error output above for details.`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
})();
|