create-nextblock 0.9.0 → 0.9.6
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/bin/create-nextblock.js +40 -60
- package/docker-template/.env.docker.example +5 -4
- package/docker-template/Dockerfile +20 -3
- package/docker-template/docker-compose.yml +5 -1
- package/docker-template/scripts/docker-setup.mjs +19 -4
- package/package.json +1 -1
- package/templates/nextblock-template/Dockerfile +20 -3
- package/templates/nextblock-template/app/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/actions.ts +58 -8
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +83 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +13 -9
- package/templates/nextblock-template/app/article/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +19 -13
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +30 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +69 -0
- package/templates/nextblock-template/app/layout.tsx +49 -3
- package/templates/nextblock-template/app/lib/site-settings.ts +22 -7
- package/templates/nextblock-template/app/page.tsx +6 -0
- package/templates/nextblock-template/app/product/[slug]/page.tsx +5 -0
- package/templates/nextblock-template/app/providers.tsx +1 -1
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +773 -0
- package/templates/nextblock-template/app/setup/layout.tsx +13 -0
- package/templates/nextblock-template/app/setup/page.tsx +103 -0
- package/templates/nextblock-template/components/AppShell.tsx +12 -0
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -0
- package/templates/nextblock-template/components/header-auth.tsx +24 -62
- package/templates/nextblock-template/docker-compose.yml +5 -1
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -0
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +67 -0
- package/templates/nextblock-template/docs/README.md +2 -0
- package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +1 -1
- package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +1 -1
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +9 -1
- package/templates/nextblock-template/lib/setup/actions.ts +370 -0
- package/templates/nextblock-template/lib/setup/env-status.ts +86 -0
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -0
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -0
- package/templates/nextblock-template/lib/setup/schema-apply.ts +392 -0
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -0
- package/templates/nextblock-template/lib/setup/types.ts +18 -0
- package/templates/nextblock-template/next.config.js +13 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +143 -49
- package/templates/nextblock-template/scripts/docker-setup.mjs +19 -4
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
revokeTrustedDeviceAction,
|
|
29
29
|
sendEmailEnrollmentCode,
|
|
30
30
|
startTotpEnrollment,
|
|
31
|
+
updateAutoAcceptSignups,
|
|
31
32
|
updateGlobalSecuritySettings,
|
|
32
33
|
verifyEmailEnrollment,
|
|
33
34
|
verifyTotpEnrollment,
|
|
@@ -369,10 +370,78 @@ export default function SecurityPanel({ data }: { data: SecurityPanelData }) {
|
|
|
369
370
|
onSave={(formData) => run(() => updateGlobalSecuritySettings(formData))}
|
|
370
371
|
/>
|
|
371
372
|
)}
|
|
373
|
+
|
|
374
|
+
{/* Sign-up policy (admin only) */}
|
|
375
|
+
{data.isAdmin && (
|
|
376
|
+
<SignupPolicyCard
|
|
377
|
+
initial={data.autoAcceptSignups}
|
|
378
|
+
isPending={isPending}
|
|
379
|
+
onSave={(formData) => run(() => updateAutoAcceptSignups(formData))}
|
|
380
|
+
/>
|
|
381
|
+
)}
|
|
372
382
|
</>
|
|
373
383
|
);
|
|
374
384
|
}
|
|
375
385
|
|
|
386
|
+
function SignupPolicyCard({
|
|
387
|
+
initial,
|
|
388
|
+
isPending,
|
|
389
|
+
onSave,
|
|
390
|
+
}: {
|
|
391
|
+
initial: boolean;
|
|
392
|
+
isPending: boolean;
|
|
393
|
+
onSave: (formData: FormData) => void;
|
|
394
|
+
}) {
|
|
395
|
+
const [enabled, setEnabled] = useState(initial);
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<Card>
|
|
399
|
+
<CardHeader>
|
|
400
|
+
<CardTitle>Sign-up Policy (Admin)</CardTitle>
|
|
401
|
+
<CardDescription>
|
|
402
|
+
Controls how new public registrations are handled across the whole site.
|
|
403
|
+
</CardDescription>
|
|
404
|
+
</CardHeader>
|
|
405
|
+
<CardContent>
|
|
406
|
+
<form
|
|
407
|
+
onSubmit={(e) => {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
const formData = new FormData();
|
|
410
|
+
formData.append('auto_accept_signups', String(enabled));
|
|
411
|
+
onSave(formData);
|
|
412
|
+
}}
|
|
413
|
+
className="space-y-6"
|
|
414
|
+
>
|
|
415
|
+
<div className="flex items-start gap-3">
|
|
416
|
+
<Checkbox
|
|
417
|
+
id="auto_accept_signups"
|
|
418
|
+
checked={enabled}
|
|
419
|
+
onCheckedChange={(checked) => setEnabled(checked === true)}
|
|
420
|
+
className="mt-1"
|
|
421
|
+
/>
|
|
422
|
+
<div className="space-y-1">
|
|
423
|
+
<Label htmlFor="auto_accept_signups">
|
|
424
|
+
Auto-approve registrations (skip outbound email verification)
|
|
425
|
+
</Label>
|
|
426
|
+
<p className="text-xs text-slate-500">
|
|
427
|
+
New accounts become active immediately, even without SMTP configured. Convenient
|
|
428
|
+
for local / self-hosted use; leave off for public production sites.
|
|
429
|
+
</p>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<Separator />
|
|
434
|
+
|
|
435
|
+
<Button type="submit" disabled={isPending}>
|
|
436
|
+
{isPending ? <Spinner className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
437
|
+
Save Policy
|
|
438
|
+
</Button>
|
|
439
|
+
</form>
|
|
440
|
+
</CardContent>
|
|
441
|
+
</Card>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
376
445
|
function AdminPolicyCard({
|
|
377
446
|
initial,
|
|
378
447
|
isPending,
|
|
@@ -8,6 +8,7 @@ import { DeferredCartDrawer } from '../components/DeferredCartDrawer';
|
|
|
8
8
|
import { CURRENCY_COOKIE_NAME } from '@nextblock-cms/ecommerce/currency-constants';
|
|
9
9
|
import { ToasterProvider } from './ToasterProvider';
|
|
10
10
|
import { AppShell } from '../components/AppShell';
|
|
11
|
+
import { PublicEnvBootstrap } from '../components/PublicEnvBootstrap';
|
|
11
12
|
import { ConsentGatedAnalytics } from '../components/privacy/ConsentGatedAnalytics';
|
|
12
13
|
import { ConsentBanner } from '../components/privacy/ConsentBanner';
|
|
13
14
|
import { getPrivacySettings } from '../lib/privacy/settings';
|
|
@@ -25,6 +26,7 @@ import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
|
25
26
|
import { unstable_cache } from 'next/cache';
|
|
26
27
|
import { createStaticSupabaseClient, getSiteSettings } from './lib/site-settings';
|
|
27
28
|
import { DEFAULT_OG_IMAGE } from './lib/seo';
|
|
29
|
+
import { isSupabaseConfigured } from '../lib/setup/env-status';
|
|
28
30
|
|
|
29
31
|
const defaultUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
|
|
30
32
|
|
|
@@ -230,11 +232,40 @@ const getCachedActiveLogo = unstable_cache(
|
|
|
230
232
|
);
|
|
231
233
|
|
|
232
234
|
async function loadLayoutData() {
|
|
233
|
-
const supabase = createSupabaseServerClient();
|
|
234
|
-
|
|
235
235
|
const headerList = await headers();
|
|
236
|
-
const cookieStore = await cookies();
|
|
237
236
|
const nonce = headerList.get('x-nonce') || '';
|
|
237
|
+
const requestPath = headerList.get('x-nextblock-path') || '';
|
|
238
|
+
|
|
239
|
+
// Skip the public-chrome data loading when there's nothing to render it on: an
|
|
240
|
+
// unconfigured instance, OR the standalone /setup wizard. On /setup, AppShell shows no
|
|
241
|
+
// header/footer anyway, and the DB schema may not exist yet (configured-but-pre-migrate)
|
|
242
|
+
// — querying it just produces noisy "table not found" errors for data nobody displays.
|
|
243
|
+
if (!isSupabaseConfigured() || requestPath.startsWith('/setup')) {
|
|
244
|
+
return {
|
|
245
|
+
user: null,
|
|
246
|
+
profile: null,
|
|
247
|
+
serverDeterminedLocale: DEFAULT_LOCALE_FOR_LAYOUT,
|
|
248
|
+
availableCurrencies: [] as StoreCurrency[],
|
|
249
|
+
serverCurrencyCode: null,
|
|
250
|
+
availableLanguages: [] as Language[],
|
|
251
|
+
defaultLanguage: null,
|
|
252
|
+
translations: [] as Awaited<ReturnType<typeof getCachedTranslations>>,
|
|
253
|
+
copyrightText: '',
|
|
254
|
+
nonce,
|
|
255
|
+
hasSupabaseEnv: false,
|
|
256
|
+
headerNavItems: [] as NavigationItem[],
|
|
257
|
+
footerNavItems: [] as NavigationItem[],
|
|
258
|
+
logo: null as HeaderLogo | null,
|
|
259
|
+
canAccessCms: false,
|
|
260
|
+
siteTitle: 'NextBlock',
|
|
261
|
+
isEcommerceActive: false,
|
|
262
|
+
globalCss: '',
|
|
263
|
+
privacySettings: DEFAULT_PRIVACY_SETTINGS,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const supabase = createSupabaseServerClient();
|
|
268
|
+
const cookieStore = await cookies();
|
|
238
269
|
|
|
239
270
|
const xUserLocaleHeader = headerList.get('x-user-locale');
|
|
240
271
|
const nextUserLocaleCookie = cookieStore.get('NEXT_USER_LOCALE')?.value;
|
|
@@ -417,6 +448,14 @@ export default async function RootLayout({
|
|
|
417
448
|
? (await import('@vercel/toolbar/next')).VercelToolbar
|
|
418
449
|
: null;
|
|
419
450
|
|
|
451
|
+
// Expose the PUBLIC Supabase values (url + anon key — both safe to ship to the browser)
|
|
452
|
+
// to the client at runtime via <PublicEnvBootstrap>. In production the client uses the
|
|
453
|
+
// build-time-inlined NEXT_PUBLIC_* and these just match; it only matters in local dev,
|
|
454
|
+
// where the wizard writes those vars at runtime and the loaded bundle would otherwise
|
|
455
|
+
// hold stale empties until a dev-server restart. Read from server process.env (fresh).
|
|
456
|
+
const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
|
|
457
|
+
const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
|
|
458
|
+
|
|
420
459
|
return (
|
|
421
460
|
<html lang={serverDeterminedLocale} suppressHydrationWarning>
|
|
422
461
|
<head>
|
|
@@ -424,6 +463,13 @@ export default async function RootLayout({
|
|
|
424
463
|
{globalCss && <style dangerouslySetInnerHTML={{ __html: globalCss }} />}
|
|
425
464
|
</head>
|
|
426
465
|
<body className="min-h-screen">
|
|
466
|
+
{/* Sets window.__NEXTBLOCK_PUBLIC_ENV__ synchronously during render, before any
|
|
467
|
+
descendant calls the browser Supabase client — the local-dev runtime fallback. */}
|
|
468
|
+
<PublicEnvBootstrap
|
|
469
|
+
url={publicSupabaseUrl}
|
|
470
|
+
anonKey={publicSupabaseAnonKey}
|
|
471
|
+
r2Base={process.env.NEXT_PUBLIC_R2_BASE_URL || ''}
|
|
472
|
+
/>
|
|
427
473
|
{/* In development this loads after hydration to avoid browser-hidden nonce comparisons. */}
|
|
428
474
|
<Script
|
|
429
475
|
id="trusted-types-bootstrap"
|
|
@@ -23,13 +23,16 @@ export interface SiteSettings {
|
|
|
23
23
|
* route `generateMetadata` functions.
|
|
24
24
|
*/
|
|
25
25
|
export function createStaticSupabaseClient() {
|
|
26
|
-
|
|
26
|
+
// Fall back to a dummy host when unconfigured (fresh clone, pre-/setup) so the
|
|
27
|
+
// root layout can still render rather than crashing the whole app on boot. Callers
|
|
28
|
+
// (getSiteSettings + the getCached* helpers) already swallow failures and use
|
|
29
|
+
// fallbacks, and loadLayoutData short-circuits before reaching here when there is
|
|
30
|
+
// no Supabase env at all.
|
|
31
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://dummy.supabase.co';
|
|
27
32
|
const supabaseKey =
|
|
28
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new Error('Missing Supabase environment variables for public layout data');
|
|
32
|
-
}
|
|
33
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY ||
|
|
34
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
|
|
35
|
+
'dummy-anon-key';
|
|
33
36
|
|
|
34
37
|
return createSupabaseJsClient<Database>(supabaseUrl, supabaseKey, {
|
|
35
38
|
auth: {
|
|
@@ -53,6 +56,13 @@ export const getSiteSettings = unstable_cache(
|
|
|
53
56
|
siteKeywords: DEFAULT_SITE_KEYWORDS,
|
|
54
57
|
};
|
|
55
58
|
|
|
59
|
+
// Unconfigured instance (pre-/setup): the static client would point at the dummy
|
|
60
|
+
// host, so the fetch fails with a noisy DNS error before falling back. Skip it and
|
|
61
|
+
// return SEO defaults directly. (generateMetadata calls this even on /setup.)
|
|
62
|
+
if (!process.env.NEXT_PUBLIC_SUPABASE_URL) {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
try {
|
|
57
67
|
const supabase = createStaticSupabaseClient();
|
|
58
68
|
const { data, error } = await supabase
|
|
@@ -61,7 +71,12 @@ export const getSiteSettings = unstable_cache(
|
|
|
61
71
|
.in('key', ['site_title', 'site_description', 'site_keywords']);
|
|
62
72
|
|
|
63
73
|
if (error || !data) {
|
|
64
|
-
|
|
74
|
+
// PGRST205 = table not found: the schema isn't migrated yet (e.g. mid-setup,
|
|
75
|
+
// right after the connection is saved but before `npm run db:migrate`). That's an
|
|
76
|
+
// expected transient state — don't shout about it. Real errors still log.
|
|
77
|
+
if (error && error.code !== 'PGRST205') {
|
|
78
|
+
console.error('Error fetching cached site settings:', error);
|
|
79
|
+
}
|
|
65
80
|
return fallback;
|
|
66
81
|
}
|
|
67
82
|
|
|
@@ -63,6 +63,12 @@ async function getPreferredLocale() {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export async function generateMetadata(): Promise<Metadata> {
|
|
66
|
+
// Unconfigured instance (pre-/setup): skip all DB work so metadata generation
|
|
67
|
+
// can't crash the boot. The proxy redirects unconfigured traffic to /setup anyway.
|
|
68
|
+
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
69
|
+
return { title: 'NextBlock' };
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
const preferredLocale = await getPreferredLocale();
|
|
67
73
|
const homepageSlug = await getHomepageSlugForLocale(preferredLocale);
|
|
68
74
|
const pageData = await getPageDataBySlug(homepageSlug, preferredLocale);
|
|
@@ -74,6 +74,11 @@ function resolveCategoryName(category: any, languageCode?: string | null) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export async function generateStaticParams() {
|
|
77
|
+
// Unconfigured instance (pre-/setup): no DB to read product slugs from.
|
|
78
|
+
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
const supabase = getSsgSupabaseClient();
|
|
78
83
|
const { data: products } = await getProducts(supabase);
|
|
79
84
|
const productRows = ((products || []) as any[]).filter(
|
|
@@ -65,7 +65,7 @@ export function Providers({ children, ...props }: { children: React.ReactNode;[k
|
|
|
65
65
|
<TranslationBridge translations={translations}>
|
|
66
66
|
<ThemeProvider
|
|
67
67
|
attribute="class"
|
|
68
|
-
defaultTheme="
|
|
68
|
+
defaultTheme="light"
|
|
69
69
|
enableSystem
|
|
70
70
|
disableTransitionOnChange
|
|
71
71
|
nonce={nonce}
|