create-nextblock 0.13.10 → 0.13.12
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/productGridActions.ts +40 -40
- 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/ecommerce/product-picker/route.ts +151 -151
- 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 -5609
- 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/components/MultiEntityPicker.tsx +263 -263
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- 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/EcommerceActiveContext.tsx +27 -27
- 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/interactions/EmailRecipientsInput.tsx +189 -189
- 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 +497 -497
- 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/languages/components/LanguageDetectionPanel.tsx +188 -188
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- 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/CreateUserForm.tsx +217 -217
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
- 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/BlockRenderer.tsx +4 -0
- 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/ProductGridClient.tsx +114 -114
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +3 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +33 -23
- 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 -0
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
- 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 -464
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/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/ProductGridBlock.tsx +78 -78
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
- 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/i18n/country-languages.ts +247 -247
- package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
- package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
- 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 -0
- 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 -82
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/next.config.js +6 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
|
4
|
-
import { X } from "lucide-react";
|
|
5
|
-
import { cn } from "@nextblock-cms/utils";
|
|
6
|
-
|
|
7
|
-
// Same address shape used elsewhere in the app (createUser). Intentionally pragmatic
|
|
8
|
-
// rather than RFC-exhaustive: something@something.tld with no whitespace.
|
|
9
|
-
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
-
|
|
11
|
-
export function isValidEmail(value: string): boolean {
|
|
12
|
-
return EMAIL_RE.test(value.trim().toLowerCase());
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface EmailRecipientsInputHandle {
|
|
16
|
-
/**
|
|
17
|
-
* Commit any text still sitting in the input, then return the full recipient list.
|
|
18
|
-
* Returns `null` (and shows an inline error) when that pending text is not a valid
|
|
19
|
-
* email — the caller should abort the save in that case. Reading the returned array
|
|
20
|
-
* directly (rather than parent state) avoids a stale-closure race on save.
|
|
21
|
-
*/
|
|
22
|
-
flush: () => string[] | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface EmailRecipientsInputProps {
|
|
26
|
-
value: string[];
|
|
27
|
-
onChange: (emails: string[]) => void;
|
|
28
|
-
disabled?: boolean;
|
|
29
|
-
inputId?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const EmailRecipientsInput = forwardRef<EmailRecipientsInputHandle, EmailRecipientsInputProps>(
|
|
33
|
-
function EmailRecipientsInput({ value, onChange, disabled, inputId }, ref) {
|
|
34
|
-
const [inputValue, setInputValue] = useState("");
|
|
35
|
-
const [error, setError] = useState<string | null>(null);
|
|
36
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
-
|
|
38
|
-
const hasEmail = (list: string[], email: string) =>
|
|
39
|
-
list.some((e) => e.toLowerCase() === email);
|
|
40
|
-
|
|
41
|
-
// Commit a single typed token. Returns the resulting list, or null when invalid.
|
|
42
|
-
const commit = (raw: string): string[] | null => {
|
|
43
|
-
const email = raw.trim().toLowerCase();
|
|
44
|
-
if (!email) return value;
|
|
45
|
-
if (!EMAIL_RE.test(email)) {
|
|
46
|
-
setError(`"${raw.trim()}" is not a valid email address.`);
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
if (hasEmail(value, email)) {
|
|
50
|
-
// Already a recipient — harmless. Clear the input without raising an error so
|
|
51
|
-
// it can't collide with the save-success banner when committed via Save/flush.
|
|
52
|
-
setInputValue("");
|
|
53
|
-
setError(null);
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
const next = [...value, email];
|
|
57
|
-
onChange(next);
|
|
58
|
-
setInputValue("");
|
|
59
|
-
setError(null);
|
|
60
|
-
return next;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Add several tokens at once (paste of a comma/space/newline separated list).
|
|
64
|
-
const addMany = (raw: string) => {
|
|
65
|
-
const tokens = raw
|
|
66
|
-
.split(/[,\s;]+/)
|
|
67
|
-
.map((t) => t.trim())
|
|
68
|
-
.filter(Boolean);
|
|
69
|
-
if (tokens.length === 0) return;
|
|
70
|
-
|
|
71
|
-
const next = [...value];
|
|
72
|
-
const invalid: string[] = [];
|
|
73
|
-
for (const token of tokens) {
|
|
74
|
-
const email = token.toLowerCase();
|
|
75
|
-
if (!EMAIL_RE.test(email)) {
|
|
76
|
-
invalid.push(token);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (!hasEmail(next, email)) next.push(email);
|
|
80
|
-
}
|
|
81
|
-
onChange(next);
|
|
82
|
-
setInputValue("");
|
|
83
|
-
setError(
|
|
84
|
-
invalid.length
|
|
85
|
-
? `Skipped ${invalid.length} invalid address${invalid.length > 1 ? "es" : ""}: ${invalid.join(", ")}`
|
|
86
|
-
: null,
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
91
|
-
if (e.key === "Enter" || e.key === ",") {
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
commit(inputValue);
|
|
94
|
-
} else if (e.key === "Backspace" && inputValue === "" && value.length > 0) {
|
|
95
|
-
onChange(value.slice(0, -1));
|
|
96
|
-
setError(null);
|
|
97
|
-
} else if (error) {
|
|
98
|
-
setError(null);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
103
|
-
const text = e.clipboardData.getData("text");
|
|
104
|
-
if (/[,\s;]/.test(text)) {
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
// Prepend whatever is already typed so a pasted "@corp.com, ..." completes the
|
|
107
|
-
// in-progress "support" rather than discarding it.
|
|
108
|
-
addMany(inputValue + text);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const removeAt = (idx: number) => {
|
|
113
|
-
onChange(value.filter((_, i) => i !== idx));
|
|
114
|
-
setError(null);
|
|
115
|
-
inputRef.current?.focus();
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// `commit` closes over the latest inputValue/value, so re-create the handle when
|
|
119
|
-
// either changes to keep flush() reading current state.
|
|
120
|
-
useImperativeHandle(ref, () => ({ flush: () => commit(inputValue) }), [inputValue, value]);
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div className="space-y-1.5">
|
|
124
|
-
<div
|
|
125
|
-
onClick={() => inputRef.current?.focus()}
|
|
126
|
-
className={cn(
|
|
127
|
-
"flex flex-wrap gap-1.5 min-h-[84px] w-full items-start content-start bg-background border rounded-xl px-2.5 py-2 text-sm cursor-text transition-colors",
|
|
128
|
-
"focus-within:ring-1 focus-within:ring-primary focus-within:border-primary",
|
|
129
|
-
error ? "border-destructive" : "border-border",
|
|
130
|
-
disabled && "opacity-60 pointer-events-none",
|
|
131
|
-
)}
|
|
132
|
-
>
|
|
133
|
-
{value.map((email, idx) => (
|
|
134
|
-
<span
|
|
135
|
-
key={email}
|
|
136
|
-
className="inline-flex items-center gap-1 h-7 pl-2.5 pr-1 rounded-full bg-secondary text-secondary-foreground text-xs font-medium"
|
|
137
|
-
>
|
|
138
|
-
{email}
|
|
139
|
-
<button
|
|
140
|
-
type="button"
|
|
141
|
-
onClick={(e) => {
|
|
142
|
-
e.stopPropagation();
|
|
143
|
-
removeAt(idx);
|
|
144
|
-
}}
|
|
145
|
-
className="inline-flex items-center justify-center h-4 w-4 rounded-full text-muted-foreground hover:text-foreground hover:bg-black/10 dark:hover:bg-white/15 focus:outline-none focus-visible:ring-1 focus-visible:ring-primary transition-colors"
|
|
146
|
-
aria-label={`Remove ${email}`}
|
|
147
|
-
>
|
|
148
|
-
<X className="h-3 w-3" />
|
|
149
|
-
</button>
|
|
150
|
-
</span>
|
|
151
|
-
))}
|
|
152
|
-
<input
|
|
153
|
-
id={inputId}
|
|
154
|
-
ref={inputRef}
|
|
155
|
-
type="email"
|
|
156
|
-
value={inputValue}
|
|
157
|
-
onChange={(e) => setInputValue(e.target.value)}
|
|
158
|
-
onKeyDown={handleKeyDown}
|
|
159
|
-
onPaste={handlePaste}
|
|
160
|
-
onBlur={() => commit(inputValue)}
|
|
161
|
-
disabled={disabled}
|
|
162
|
-
aria-invalid={!!error}
|
|
163
|
-
placeholder={value.length === 0 ? "admin@example.com" : "Add another…"}
|
|
164
|
-
className="flex-1 min-w-[160px] h-7 bg-transparent outline-none placeholder:text-muted-foreground"
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div className="flex items-center justify-between gap-3">
|
|
169
|
-
<span className="text-[10px] text-muted-foreground">
|
|
170
|
-
Type an address and press Enter or comma to add it.
|
|
171
|
-
</span>
|
|
172
|
-
{value.length > 0 && (
|
|
173
|
-
<span className="text-[10px] font-medium text-muted-foreground shrink-0">
|
|
174
|
-
{value.length} recipient{value.length > 1 ? "s" : ""}
|
|
175
|
-
</span>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
{error && (
|
|
180
|
-
<p role="alert" className="text-xs font-medium text-destructive">
|
|
181
|
-
{error}
|
|
182
|
-
</p>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
export default EmailRecipientsInput;
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cn } from "@nextblock-cms/utils";
|
|
6
|
+
|
|
7
|
+
// Same address shape used elsewhere in the app (createUser). Intentionally pragmatic
|
|
8
|
+
// rather than RFC-exhaustive: something@something.tld with no whitespace.
|
|
9
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
10
|
+
|
|
11
|
+
export function isValidEmail(value: string): boolean {
|
|
12
|
+
return EMAIL_RE.test(value.trim().toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EmailRecipientsInputHandle {
|
|
16
|
+
/**
|
|
17
|
+
* Commit any text still sitting in the input, then return the full recipient list.
|
|
18
|
+
* Returns `null` (and shows an inline error) when that pending text is not a valid
|
|
19
|
+
* email — the caller should abort the save in that case. Reading the returned array
|
|
20
|
+
* directly (rather than parent state) avoids a stale-closure race on save.
|
|
21
|
+
*/
|
|
22
|
+
flush: () => string[] | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface EmailRecipientsInputProps {
|
|
26
|
+
value: string[];
|
|
27
|
+
onChange: (emails: string[]) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
inputId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EmailRecipientsInput = forwardRef<EmailRecipientsInputHandle, EmailRecipientsInputProps>(
|
|
33
|
+
function EmailRecipientsInput({ value, onChange, disabled, inputId }, ref) {
|
|
34
|
+
const [inputValue, setInputValue] = useState("");
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
37
|
+
|
|
38
|
+
const hasEmail = (list: string[], email: string) =>
|
|
39
|
+
list.some((e) => e.toLowerCase() === email);
|
|
40
|
+
|
|
41
|
+
// Commit a single typed token. Returns the resulting list, or null when invalid.
|
|
42
|
+
const commit = (raw: string): string[] | null => {
|
|
43
|
+
const email = raw.trim().toLowerCase();
|
|
44
|
+
if (!email) return value;
|
|
45
|
+
if (!EMAIL_RE.test(email)) {
|
|
46
|
+
setError(`"${raw.trim()}" is not a valid email address.`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (hasEmail(value, email)) {
|
|
50
|
+
// Already a recipient — harmless. Clear the input without raising an error so
|
|
51
|
+
// it can't collide with the save-success banner when committed via Save/flush.
|
|
52
|
+
setInputValue("");
|
|
53
|
+
setError(null);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
const next = [...value, email];
|
|
57
|
+
onChange(next);
|
|
58
|
+
setInputValue("");
|
|
59
|
+
setError(null);
|
|
60
|
+
return next;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Add several tokens at once (paste of a comma/space/newline separated list).
|
|
64
|
+
const addMany = (raw: string) => {
|
|
65
|
+
const tokens = raw
|
|
66
|
+
.split(/[,\s;]+/)
|
|
67
|
+
.map((t) => t.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
if (tokens.length === 0) return;
|
|
70
|
+
|
|
71
|
+
const next = [...value];
|
|
72
|
+
const invalid: string[] = [];
|
|
73
|
+
for (const token of tokens) {
|
|
74
|
+
const email = token.toLowerCase();
|
|
75
|
+
if (!EMAIL_RE.test(email)) {
|
|
76
|
+
invalid.push(token);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!hasEmail(next, email)) next.push(email);
|
|
80
|
+
}
|
|
81
|
+
onChange(next);
|
|
82
|
+
setInputValue("");
|
|
83
|
+
setError(
|
|
84
|
+
invalid.length
|
|
85
|
+
? `Skipped ${invalid.length} invalid address${invalid.length > 1 ? "es" : ""}: ${invalid.join(", ")}`
|
|
86
|
+
: null,
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
91
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
commit(inputValue);
|
|
94
|
+
} else if (e.key === "Backspace" && inputValue === "" && value.length > 0) {
|
|
95
|
+
onChange(value.slice(0, -1));
|
|
96
|
+
setError(null);
|
|
97
|
+
} else if (error) {
|
|
98
|
+
setError(null);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
103
|
+
const text = e.clipboardData.getData("text");
|
|
104
|
+
if (/[,\s;]/.test(text)) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
// Prepend whatever is already typed so a pasted "@corp.com, ..." completes the
|
|
107
|
+
// in-progress "support" rather than discarding it.
|
|
108
|
+
addMany(inputValue + text);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const removeAt = (idx: number) => {
|
|
113
|
+
onChange(value.filter((_, i) => i !== idx));
|
|
114
|
+
setError(null);
|
|
115
|
+
inputRef.current?.focus();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// `commit` closes over the latest inputValue/value, so re-create the handle when
|
|
119
|
+
// either changes to keep flush() reading current state.
|
|
120
|
+
useImperativeHandle(ref, () => ({ flush: () => commit(inputValue) }), [inputValue, value]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="space-y-1.5">
|
|
124
|
+
<div
|
|
125
|
+
onClick={() => inputRef.current?.focus()}
|
|
126
|
+
className={cn(
|
|
127
|
+
"flex flex-wrap gap-1.5 min-h-[84px] w-full items-start content-start bg-background border rounded-xl px-2.5 py-2 text-sm cursor-text transition-colors",
|
|
128
|
+
"focus-within:ring-1 focus-within:ring-primary focus-within:border-primary",
|
|
129
|
+
error ? "border-destructive" : "border-border",
|
|
130
|
+
disabled && "opacity-60 pointer-events-none",
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
{value.map((email, idx) => (
|
|
134
|
+
<span
|
|
135
|
+
key={email}
|
|
136
|
+
className="inline-flex items-center gap-1 h-7 pl-2.5 pr-1 rounded-full bg-secondary text-secondary-foreground text-xs font-medium"
|
|
137
|
+
>
|
|
138
|
+
{email}
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={(e) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
removeAt(idx);
|
|
144
|
+
}}
|
|
145
|
+
className="inline-flex items-center justify-center h-4 w-4 rounded-full text-muted-foreground hover:text-foreground hover:bg-black/10 dark:hover:bg-white/15 focus:outline-none focus-visible:ring-1 focus-visible:ring-primary transition-colors"
|
|
146
|
+
aria-label={`Remove ${email}`}
|
|
147
|
+
>
|
|
148
|
+
<X className="h-3 w-3" />
|
|
149
|
+
</button>
|
|
150
|
+
</span>
|
|
151
|
+
))}
|
|
152
|
+
<input
|
|
153
|
+
id={inputId}
|
|
154
|
+
ref={inputRef}
|
|
155
|
+
type="email"
|
|
156
|
+
value={inputValue}
|
|
157
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
158
|
+
onKeyDown={handleKeyDown}
|
|
159
|
+
onPaste={handlePaste}
|
|
160
|
+
onBlur={() => commit(inputValue)}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
aria-invalid={!!error}
|
|
163
|
+
placeholder={value.length === 0 ? "admin@example.com" : "Add another…"}
|
|
164
|
+
className="flex-1 min-w-[160px] h-7 bg-transparent outline-none placeholder:text-muted-foreground"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="flex items-center justify-between gap-3">
|
|
169
|
+
<span className="text-[10px] text-muted-foreground">
|
|
170
|
+
Type an address and press Enter or comma to add it.
|
|
171
|
+
</span>
|
|
172
|
+
{value.length > 0 && (
|
|
173
|
+
<span className="text-[10px] font-medium text-muted-foreground shrink-0">
|
|
174
|
+
{value.length} recipient{value.length > 1 ? "s" : ""}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{error && (
|
|
180
|
+
<p role="alert" className="text-xs font-medium text-destructive">
|
|
181
|
+
{error}
|
|
182
|
+
</p>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
export default EmailRecipientsInput;
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import 'katex/dist/katex.min.css';
|
|
2
|
-
import { redirect } from 'next/navigation';
|
|
3
|
-
import { after } from 'next/server';
|
|
4
|
-
import CmsClientLayout from "./CmsClientLayout";
|
|
5
|
-
import { verifyPackageOnline, createClient } from '@nextblock-cms/db/server';
|
|
6
|
-
import { evaluateTwoFactor, getStaffTwoFactorReminder } from '../../lib/auth/twoFactor';
|
|
7
|
-
import { maybeRefreshUpstreamStatus } from '../../lib/updates/check-upstream';
|
|
8
|
-
import type { SystemAlertItem } from './components/SystemAlertsBanner';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Unresolved system alerts for the dashboard banner. Runs as the signed-in user, so the
|
|
12
|
-
* system_alerts SELECT RLS policy returns rows only for ADMINs (WRITERs get an empty
|
|
13
|
-
* list). Best-effort: any failure (e.g. the table not yet migrated) yields no banner.
|
|
14
|
-
*/
|
|
15
|
-
async function getUnresolvedSystemAlerts(): Promise<SystemAlertItem[]> {
|
|
16
|
-
try {
|
|
17
|
-
const supabase = createClient();
|
|
18
|
-
const { data, error } = await supabase
|
|
19
|
-
.from('system_alerts')
|
|
20
|
-
.select('id, alert_type, title, message, metadata')
|
|
21
|
-
.eq('is_resolved', false)
|
|
22
|
-
.in('alert_type', ['merge_conflict', 'runtime_update_available'])
|
|
23
|
-
.order('created_at', { ascending: false })
|
|
24
|
-
.limit(20);
|
|
25
|
-
if (error || !data) return [];
|
|
26
|
-
return data.map((a) => ({
|
|
27
|
-
id: a.id,
|
|
28
|
-
alert_type: a.alert_type,
|
|
29
|
-
title: a.title,
|
|
30
|
-
message: a.message,
|
|
31
|
-
metadata: (a.metadata ?? null) as Record<string, unknown> | null,
|
|
32
|
-
}));
|
|
33
|
-
} catch {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default async function CmsLayout({
|
|
39
|
-
children,
|
|
40
|
-
}: {
|
|
41
|
-
children: React.ReactNode;
|
|
42
|
-
}) {
|
|
43
|
-
// Enforce any outstanding second factor before rendering the CMS. This guards
|
|
44
|
-
// direct navigation to /cms/* with an aal1 (password-only) session.
|
|
45
|
-
const twoFactor = await evaluateTwoFactor();
|
|
46
|
-
if (twoFactor.status === 'totp_required' || twoFactor.status === 'email_required') {
|
|
47
|
-
redirect('/two-factor?redirect_to=/cms/dashboard');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const [isEcommerceActive, isCortexAiActive, showTwoFactorReminder, systemAlerts] =
|
|
51
|
-
await Promise.all([
|
|
52
|
-
verifyPackageOnline('ecommerce'),
|
|
53
|
-
verifyPackageOnline('cortex-ai'),
|
|
54
|
-
getStaffTwoFactorReminder(),
|
|
55
|
-
getUnresolvedSystemAlerts(),
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
// After the response, refresh upstream update/conflict status in the background
|
|
59
|
-
// (throttled to ~6h, see maybeRefreshUpstreamStatus). This keeps the banner current
|
|
60
|
-
// without a cron — so it works on Vercel Hobby (limited crons) and self-hosted alike.
|
|
61
|
-
after(() => maybeRefreshUpstreamStatus());
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<CmsClientLayout
|
|
65
|
-
isCortexAiActive={isCortexAiActive}
|
|
66
|
-
isEcommerceActive={isEcommerceActive}
|
|
67
|
-
showTwoFactorReminder={showTwoFactorReminder}
|
|
68
|
-
systemAlerts={systemAlerts}
|
|
69
|
-
>
|
|
70
|
-
{children}
|
|
71
|
-
</CmsClientLayout>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
1
|
+
import 'katex/dist/katex.min.css';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { after } from 'next/server';
|
|
4
|
+
import CmsClientLayout from "./CmsClientLayout";
|
|
5
|
+
import { verifyPackageOnline, createClient } from '@nextblock-cms/db/server';
|
|
6
|
+
import { evaluateTwoFactor, getStaffTwoFactorReminder } from '../../lib/auth/twoFactor';
|
|
7
|
+
import { maybeRefreshUpstreamStatus } from '../../lib/updates/check-upstream';
|
|
8
|
+
import type { SystemAlertItem } from './components/SystemAlertsBanner';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unresolved system alerts for the dashboard banner. Runs as the signed-in user, so the
|
|
12
|
+
* system_alerts SELECT RLS policy returns rows only for ADMINs (WRITERs get an empty
|
|
13
|
+
* list). Best-effort: any failure (e.g. the table not yet migrated) yields no banner.
|
|
14
|
+
*/
|
|
15
|
+
async function getUnresolvedSystemAlerts(): Promise<SystemAlertItem[]> {
|
|
16
|
+
try {
|
|
17
|
+
const supabase = createClient();
|
|
18
|
+
const { data, error } = await supabase
|
|
19
|
+
.from('system_alerts')
|
|
20
|
+
.select('id, alert_type, title, message, metadata')
|
|
21
|
+
.eq('is_resolved', false)
|
|
22
|
+
.in('alert_type', ['merge_conflict', 'runtime_update_available'])
|
|
23
|
+
.order('created_at', { ascending: false })
|
|
24
|
+
.limit(20);
|
|
25
|
+
if (error || !data) return [];
|
|
26
|
+
return data.map((a) => ({
|
|
27
|
+
id: a.id,
|
|
28
|
+
alert_type: a.alert_type,
|
|
29
|
+
title: a.title,
|
|
30
|
+
message: a.message,
|
|
31
|
+
metadata: (a.metadata ?? null) as Record<string, unknown> | null,
|
|
32
|
+
}));
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function CmsLayout({
|
|
39
|
+
children,
|
|
40
|
+
}: {
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}) {
|
|
43
|
+
// Enforce any outstanding second factor before rendering the CMS. This guards
|
|
44
|
+
// direct navigation to /cms/* with an aal1 (password-only) session.
|
|
45
|
+
const twoFactor = await evaluateTwoFactor();
|
|
46
|
+
if (twoFactor.status === 'totp_required' || twoFactor.status === 'email_required') {
|
|
47
|
+
redirect('/two-factor?redirect_to=/cms/dashboard');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [isEcommerceActive, isCortexAiActive, showTwoFactorReminder, systemAlerts] =
|
|
51
|
+
await Promise.all([
|
|
52
|
+
verifyPackageOnline('ecommerce'),
|
|
53
|
+
verifyPackageOnline('cortex-ai'),
|
|
54
|
+
getStaffTwoFactorReminder(),
|
|
55
|
+
getUnresolvedSystemAlerts(),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// After the response, refresh upstream update/conflict status in the background
|
|
59
|
+
// (throttled to ~6h, see maybeRefreshUpstreamStatus). This keeps the banner current
|
|
60
|
+
// without a cron — so it works on Vercel Hobby (limited crons) and self-hosted alike.
|
|
61
|
+
after(() => maybeRefreshUpstreamStatus());
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<CmsClientLayout
|
|
65
|
+
isCortexAiActive={isCortexAiActive}
|
|
66
|
+
isEcommerceActive={isEcommerceActive}
|
|
67
|
+
showTwoFactorReminder={showTwoFactorReminder}
|
|
68
|
+
systemAlerts={systemAlerts}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</CmsClientLayout>
|
|
72
|
+
);
|
|
73
|
+
}
|