create-nextblock 0.8.11 → 0.9.5
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 +101 -35
- package/docker-template/.dockerignore +23 -0
- package/docker-template/.env.docker.example +56 -0
- package/docker-template/Dockerfile +85 -0
- package/docker-template/docker/db/init/99-jwt.sql +6 -0
- package/docker-template/docker/db/init/99-roles.sql +25 -0
- package/docker-template/docker/kong/kong.yml +112 -0
- package/docker-template/docker/migrate/run-migrations.sh +51 -0
- package/docker-template/docker-compose.yml +219 -0
- package/docker-template/scripts/docker-setup.mjs +242 -0
- package/package.json +1 -1
- package/scripts/sync-template.js +29 -0
- package/templates/nextblock-template/.dockerignore +23 -0
- package/templates/nextblock-template/Dockerfile +85 -0
- 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/upload/presigned-url/route.ts +9 -9
- package/templates/nextblock-template/app/article/[slug]/page.tsx +5 -0
- 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 +57 -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/setup/SetupWizard.tsx +771 -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/header-auth.tsx +24 -62
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -0
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -0
- package/templates/nextblock-template/docker/kong/kong.yml +112 -0
- package/templates/nextblock-template/docker/migrate/run-migrations.sh +51 -0
- package/templates/nextblock-template/docker-compose.yml +219 -0
- 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/custom-block-r2-upload.test.ts +5 -5
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +2 -2
- 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 +379 -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 +9 -0
- package/templates/nextblock-template/package.json +6 -2
- package/templates/nextblock-template/proxy.ts +143 -49
- package/templates/nextblock-template/scripts/docker-setup.mjs +242 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
3
|
import { encodedRedirect } from "@nextblock-cms/utils/server";
|
|
4
|
-
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import { createClient, getServiceRoleSupabaseClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import { headers } from "next/headers";
|
|
6
6
|
import { redirect } from "next/navigation";
|
|
7
7
|
import { resolvePostAuthRedirect } from "../lib/auth-redirects";
|
|
8
8
|
import { createEmailChallenge, evaluateTwoFactor } from "../lib/auth/twoFactor";
|
|
9
9
|
import { REMEMBER_INTENT_COOKIE, setSecureCookie } from "../lib/auth/cookies";
|
|
10
10
|
import { sendTwoFactorCodeEmail } from "./actions/twoFactorEmail";
|
|
11
|
+
import { getSystemConfiguration } from "../lib/setup/system-config";
|
|
11
12
|
|
|
12
13
|
export const signUpAction = async (formData: FormData) => {
|
|
13
14
|
const email = formData.get("email")?.toString();
|
|
@@ -29,7 +30,48 @@ export const signUpAction = async (formData: FormData) => {
|
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Auto-accept mode (system_configuration.auto_accept_signups): create an already-
|
|
34
|
+
// confirmed account via the service role so the user is active immediately, with no
|
|
35
|
+
// outbound verification email — regardless of SMTP / project email-confirmation
|
|
36
|
+
// settings. Falls through to the standard signUp flow if the service role isn't
|
|
37
|
+
// available. NOTE: keep supabase.auth calls outside any try/catch so the redirect
|
|
38
|
+
// they (and encodedRedirect) throw internally is never swallowed.
|
|
39
|
+
const { auto_accept_signups: autoAcceptSignups } = await getSystemConfiguration();
|
|
40
|
+
if (autoAcceptSignups) {
|
|
41
|
+
let admin: ReturnType<typeof getServiceRoleSupabaseClient> | null = null;
|
|
42
|
+
try {
|
|
43
|
+
admin = getServiceRoleSupabaseClient();
|
|
44
|
+
} catch {
|
|
45
|
+
admin = null; // service role missing — use the standard flow below
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (admin) {
|
|
49
|
+
const { error: createError } = await admin.auth.admin.createUser({
|
|
50
|
+
email,
|
|
51
|
+
password,
|
|
52
|
+
email_confirm: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (createError) {
|
|
56
|
+
if (createError.message.toLowerCase().includes("already")) {
|
|
57
|
+
return encodedRedirect("error", "/sign-up", "auth.signup_existing_account_hint");
|
|
58
|
+
}
|
|
59
|
+
return encodedRedirect("error", "/sign-up", createError.message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { error: signInError } = await supabase.auth.signInWithPassword({
|
|
63
|
+
email,
|
|
64
|
+
password,
|
|
65
|
+
});
|
|
66
|
+
if (signInError) {
|
|
67
|
+
return encodedRedirect("error", "/sign-in", signInError.message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return redirect("/post-sign-in");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { data, error } = await supabase.auth.signUp({
|
|
33
75
|
email,
|
|
34
76
|
password,
|
|
35
77
|
options: {
|
|
@@ -57,13 +99,21 @@ export const signUpAction = async (formData: FormData) => {
|
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
return encodedRedirect("error", "/sign-up", error.message);
|
|
60
|
-
} else {
|
|
61
|
-
return encodedRedirect(
|
|
62
|
-
"success",
|
|
63
|
-
"/sign-up",
|
|
64
|
-
"auth.signup_check_email_profile",
|
|
65
|
-
);
|
|
66
102
|
}
|
|
103
|
+
|
|
104
|
+
// When sign-up returns a session, the account is already confirmed — self-hosted GoTrue with
|
|
105
|
+
// autoconfirm (no SMTP), or any project without email confirmation. The user is already signed
|
|
106
|
+
// in, so send them into the app instead of telling them to check an email that was never sent.
|
|
107
|
+
// (The first account becomes ADMIN and lands in the dashboard; everyone else routes normally.)
|
|
108
|
+
if (data.session) {
|
|
109
|
+
return redirect("/post-sign-in");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return encodedRedirect(
|
|
113
|
+
"success",
|
|
114
|
+
"/sign-up",
|
|
115
|
+
"auth.signup_check_email_profile",
|
|
116
|
+
);
|
|
67
117
|
};
|
|
68
118
|
|
|
69
119
|
export const signInAction = async (formData: FormData) => {
|
|
@@ -7816,6 +7816,89 @@ npm run dev</code></pre>
|
|
|
7816
7816
|
), 0 FROM target_posts tp WHERE tp.slug = 'comment-configurer-nextblock';
|
|
7817
7817
|
|
|
7818
7818
|
|
|
7819
|
+
-- >>> FROM: 00000000000030_setup_system_configuration.sql <<<
|
|
7820
|
+
-- 00000000000030_setup_system_configuration.sql
|
|
7821
|
+
-- First-Boot Setup Wizard: global system configuration.
|
|
7822
|
+
--
|
|
7823
|
+
-- Adds a dedicated, RLS-locked \`system_configuration\` table that holds settings the
|
|
7824
|
+
-- browser /setup wizard manages and that don't belong in the public key-value
|
|
7825
|
+
-- \`site_settings\` store. It is a singleton (exactly one row, id = 1).
|
|
7826
|
+
--
|
|
7827
|
+
-- Shape:
|
|
7828
|
+
-- auto_accept_signups boolean -- when true, new public sign-ups skip outbound email
|
|
7829
|
+
-- verification (the signup route uses a service-role
|
|
7830
|
+
-- admin.createUser({ email_confirm: true }) path).
|
|
7831
|
+
-- settings jsonb -- forward-compatible catch-all for future feature
|
|
7832
|
+
-- toggles ({} by default). Do NOT store true secrets
|
|
7833
|
+
-- here (Turnstile/AI secrets keep living in their
|
|
7834
|
+
-- existing site_settings sensitive keys).
|
|
7835
|
+
--
|
|
7836
|
+
-- Access is locked to the ADMIN role for normal clients (NextBlock has no separate
|
|
7837
|
+
-- "super-admin" tier — ADMIN is the top level). The service_role retains full access
|
|
7838
|
+
-- so the wizard can seed/read it before any admin exists.
|
|
7839
|
+
|
|
7840
|
+
CREATE TABLE IF NOT EXISTS public.system_configuration (
|
|
7841
|
+
id integer PRIMARY KEY DEFAULT 1,
|
|
7842
|
+
auto_accept_signups boolean NOT NULL DEFAULT false,
|
|
7843
|
+
settings jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
7844
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
7845
|
+
CONSTRAINT system_configuration_singleton CHECK (id = 1)
|
|
7846
|
+
);
|
|
7847
|
+
|
|
7848
|
+
COMMENT ON TABLE public.system_configuration IS
|
|
7849
|
+
'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';
|
|
7850
|
+
|
|
7851
|
+
-- Seed the single row so reads always find it.
|
|
7852
|
+
INSERT INTO public.system_configuration (id, auto_accept_signups, settings)
|
|
7853
|
+
VALUES (1, false, '{}'::jsonb)
|
|
7854
|
+
ON CONFLICT (id) DO NOTHING;
|
|
7855
|
+
|
|
7856
|
+
ALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;
|
|
7857
|
+
|
|
7858
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.system_configuration TO authenticated;
|
|
7859
|
+
GRANT ALL ON public.system_configuration TO service_role;
|
|
7860
|
+
|
|
7861
|
+
-- ADMIN-only for every operation by authenticated clients.
|
|
7862
|
+
DROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;
|
|
7863
|
+
CREATE POLICY system_configuration_admin_select
|
|
7864
|
+
ON public.system_configuration
|
|
7865
|
+
FOR SELECT
|
|
7866
|
+
TO authenticated
|
|
7867
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN');
|
|
7868
|
+
|
|
7869
|
+
DROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;
|
|
7870
|
+
CREATE POLICY system_configuration_admin_insert
|
|
7871
|
+
ON public.system_configuration
|
|
7872
|
+
FOR INSERT
|
|
7873
|
+
TO authenticated
|
|
7874
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');
|
|
7875
|
+
|
|
7876
|
+
DROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;
|
|
7877
|
+
CREATE POLICY system_configuration_admin_update
|
|
7878
|
+
ON public.system_configuration
|
|
7879
|
+
FOR UPDATE
|
|
7880
|
+
TO authenticated
|
|
7881
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN')
|
|
7882
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');
|
|
7883
|
+
|
|
7884
|
+
DROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;
|
|
7885
|
+
CREATE POLICY system_configuration_admin_delete
|
|
7886
|
+
ON public.system_configuration
|
|
7887
|
+
FOR DELETE
|
|
7888
|
+
TO authenticated
|
|
7889
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN');
|
|
7890
|
+
|
|
7891
|
+
-- Service role bypasses the ADMIN checks (used by the wizard before an admin exists,
|
|
7892
|
+
-- and by the signup route to read auto_accept_signups as an anonymous visitor).
|
|
7893
|
+
DROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;
|
|
7894
|
+
CREATE POLICY system_configuration_service_role_all
|
|
7895
|
+
ON public.system_configuration
|
|
7896
|
+
FOR ALL
|
|
7897
|
+
TO service_role
|
|
7898
|
+
USING (true)
|
|
7899
|
+
WITH CHECK (true);
|
|
7900
|
+
|
|
7901
|
+
|
|
7819
7902
|
-- Step D: Anchor preserved profiles
|
|
7820
7903
|
INSERT INTO public.profiles (id, updated_at, full_name, avatar_url, website, role)
|
|
7821
7904
|
SELECT preserved_user.id, NULL, NULL, NULL, NULL, 'ADMIN'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app/api/upload/presigned-url/route.ts
|
|
2
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
-
import { createClient } from "@nextblock-cms/db/server"; // Server client for auth
|
|
4
|
-
import {
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server"; // Server client for auth
|
|
4
|
+
import { getS3PresignClient } from "@nextblock-cms/utils/server";
|
|
5
5
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|
6
6
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
7
7
|
|
|
@@ -32,13 +32,13 @@ export async function POST(request: NextRequest) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
|
-
const s3Client = await
|
|
36
|
-
if (!s3Client) {
|
|
37
|
-
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
38
|
-
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { filename, contentType, size, folder: rawFolder } = await request.json();
|
|
35
|
+
const s3Client = await getS3PresignClient();
|
|
36
|
+
if (!s3Client) {
|
|
37
|
+
console.error('R2 client is not configured. Check your R2 environment variables.');
|
|
38
|
+
return NextResponse.json({ error: 'File uploads are not configured on this server.' }, { status: 500 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { filename, contentType, size, folder: rawFolder } = await request.json();
|
|
42
42
|
|
|
43
43
|
if (!filename || !contentType || !size) {
|
|
44
44
|
return NextResponse.json({ error: "Missing filename, contentType, or size" }, { status: 400 });
|
|
@@ -25,6 +25,11 @@ import { getRequestOrigin } from '../../../lib/visual-editing/edit-info';
|
|
|
25
25
|
|
|
26
26
|
export const dynamicParams = true;
|
|
27
27
|
export const revalidate = 3600;
|
|
28
|
+
// Render per-request: the shared root layout reads cookies()/headers()/draftMode() (auth + locale),
|
|
29
|
+
// so attempting a statically-cached render throws DYNAMIC_SERVER_USAGE (500). Matches the sibling
|
|
30
|
+
// content routes /[slug] and /product/[slug], which already force dynamic for the same reason.
|
|
31
|
+
export const dynamic = 'force-dynamic';
|
|
32
|
+
export const fetchCache = 'force-no-store';
|
|
28
33
|
|
|
29
34
|
interface ResolvedPostParams {
|
|
30
35
|
slug: string;
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
revokeTrustedDevice,
|
|
25
25
|
type TrustedDeviceRow,
|
|
26
26
|
} from '../../../../lib/auth/trustedDevices';
|
|
27
|
+
import {
|
|
28
|
+
getSystemConfiguration,
|
|
29
|
+
updateSystemConfiguration,
|
|
30
|
+
} from '../../../../lib/setup/system-config';
|
|
27
31
|
|
|
28
32
|
export interface SecurityPanelData {
|
|
29
33
|
email: string;
|
|
@@ -33,6 +37,7 @@ export interface SecurityPanelData {
|
|
|
33
37
|
isAdmin: boolean;
|
|
34
38
|
globalSettings: SecuritySettings;
|
|
35
39
|
trustedDevices: TrustedDeviceRow[];
|
|
40
|
+
autoAcceptSignups: boolean;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
async function requireUser() {
|
|
@@ -70,6 +75,31 @@ export async function getSecurityPanelData(): Promise<SecurityPanelData> {
|
|
|
70
75
|
isAdmin: profile?.role === 'ADMIN',
|
|
71
76
|
globalSettings: await readSecuritySettings(),
|
|
72
77
|
trustedDevices: await listTrustedDevices(user.id),
|
|
78
|
+
autoAcceptSignups: (await getSystemConfiguration()).auto_accept_signups,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Sign-up policy (admin only) ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export async function updateAutoAcceptSignups(formData: FormData) {
|
|
85
|
+
const { supabase, user } = await requireUser();
|
|
86
|
+
const { data: profile } = await supabase
|
|
87
|
+
.from('profiles')
|
|
88
|
+
.select('role')
|
|
89
|
+
.eq('id', user.id)
|
|
90
|
+
.single();
|
|
91
|
+
if (profile?.role !== 'ADMIN') {
|
|
92
|
+
throw new Error('Only administrators can change the sign-up policy.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const enabled = formData.get('auto_accept_signups') === 'true';
|
|
96
|
+
await updateSystemConfiguration({ auto_accept_signups: enabled });
|
|
97
|
+
revalidatePath('/cms/settings/security');
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
message: enabled
|
|
101
|
+
? 'New sign-ups will be auto-approved without email verification.'
|
|
102
|
+
: 'New sign-ups now require email verification.',
|
|
73
103
|
};
|
|
74
104
|
}
|
|
75
105
|
|
|
@@ -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,
|
|
@@ -25,6 +25,7 @@ import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
|
25
25
|
import { unstable_cache } from 'next/cache';
|
|
26
26
|
import { createStaticSupabaseClient, getSiteSettings } from './lib/site-settings';
|
|
27
27
|
import { DEFAULT_OG_IMAGE } from './lib/seo';
|
|
28
|
+
import { isSupabaseConfigured } from '../lib/setup/env-status';
|
|
28
29
|
|
|
29
30
|
const defaultUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';
|
|
30
31
|
|
|
@@ -230,11 +231,40 @@ const getCachedActiveLogo = unstable_cache(
|
|
|
230
231
|
);
|
|
231
232
|
|
|
232
233
|
async function loadLayoutData() {
|
|
233
|
-
const supabase = createSupabaseServerClient();
|
|
234
|
-
|
|
235
234
|
const headerList = await headers();
|
|
236
|
-
const cookieStore = await cookies();
|
|
237
235
|
const nonce = headerList.get('x-nonce') || '';
|
|
236
|
+
const requestPath = headerList.get('x-nextblock-path') || '';
|
|
237
|
+
|
|
238
|
+
// Skip the public-chrome data loading when there's nothing to render it on: an
|
|
239
|
+
// unconfigured instance, OR the standalone /setup wizard. On /setup, AppShell shows no
|
|
240
|
+
// header/footer anyway, and the DB schema may not exist yet (configured-but-pre-migrate)
|
|
241
|
+
// — querying it just produces noisy "table not found" errors for data nobody displays.
|
|
242
|
+
if (!isSupabaseConfigured() || requestPath.startsWith('/setup')) {
|
|
243
|
+
return {
|
|
244
|
+
user: null,
|
|
245
|
+
profile: null,
|
|
246
|
+
serverDeterminedLocale: DEFAULT_LOCALE_FOR_LAYOUT,
|
|
247
|
+
availableCurrencies: [] as StoreCurrency[],
|
|
248
|
+
serverCurrencyCode: null,
|
|
249
|
+
availableLanguages: [] as Language[],
|
|
250
|
+
defaultLanguage: null,
|
|
251
|
+
translations: [] as Awaited<ReturnType<typeof getCachedTranslations>>,
|
|
252
|
+
copyrightText: '',
|
|
253
|
+
nonce,
|
|
254
|
+
hasSupabaseEnv: false,
|
|
255
|
+
headerNavItems: [] as NavigationItem[],
|
|
256
|
+
footerNavItems: [] as NavigationItem[],
|
|
257
|
+
logo: null as HeaderLogo | null,
|
|
258
|
+
canAccessCms: false,
|
|
259
|
+
siteTitle: 'NextBlock',
|
|
260
|
+
isEcommerceActive: false,
|
|
261
|
+
globalCss: '',
|
|
262
|
+
privacySettings: DEFAULT_PRIVACY_SETTINGS,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const supabase = createSupabaseServerClient();
|
|
267
|
+
const cookieStore = await cookies();
|
|
238
268
|
|
|
239
269
|
const xUserLocaleHeader = headerList.get('x-user-locale');
|
|
240
270
|
const nextUserLocaleCookie = cookieStore.get('NEXT_USER_LOCALE')?.value;
|
|
@@ -417,6 +447,22 @@ export default async function RootLayout({
|
|
|
417
447
|
? (await import('@vercel/toolbar/next')).VercelToolbar
|
|
418
448
|
: null;
|
|
419
449
|
|
|
450
|
+
// Expose the PUBLIC Supabase values (url + anon key — both safe to ship to the
|
|
451
|
+
// browser) at runtime. In production the client uses the build-time-inlined
|
|
452
|
+
// NEXT_PUBLIC_* and ignores this; it only matters in local dev, where the wizard
|
|
453
|
+
// writes those vars at runtime and the already-loaded browser bundle would otherwise
|
|
454
|
+
// hold stale empties until a dev-server restart. Read from server process.env here,
|
|
455
|
+
// so it's always fresh.
|
|
456
|
+
const publicEnvBootstrap = (() => {
|
|
457
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
|
|
458
|
+
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
|
|
459
|
+
if (!url || !anonKey) return '';
|
|
460
|
+
return `window.__NEXTBLOCK_PUBLIC_ENV__=${JSON.stringify({ url, anonKey }).replace(
|
|
461
|
+
/</g,
|
|
462
|
+
'\\u003c',
|
|
463
|
+
)};`;
|
|
464
|
+
})();
|
|
465
|
+
|
|
420
466
|
return (
|
|
421
467
|
<html lang={serverDeterminedLocale} suppressHydrationWarning>
|
|
422
468
|
<head>
|
|
@@ -424,6 +470,14 @@ export default async function RootLayout({
|
|
|
424
470
|
{globalCss && <style dangerouslySetInnerHTML={{ __html: globalCss }} />}
|
|
425
471
|
</head>
|
|
426
472
|
<body className="min-h-screen">
|
|
473
|
+
{publicEnvBootstrap && (
|
|
474
|
+
<Script
|
|
475
|
+
id="nextblock-public-env"
|
|
476
|
+
strategy={TRUSTED_TYPES_SCRIPT_STRATEGY}
|
|
477
|
+
nonce={nonce}
|
|
478
|
+
dangerouslySetInnerHTML={{ __html: publicEnvBootstrap }}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
427
481
|
{/* In development this loads after hydration to avoid browser-hidden nonce comparisons. */}
|
|
428
482
|
<Script
|
|
429
483
|
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(
|