create-nextblock 0.10.2 → 0.10.4
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/package.json +1 -1
- package/templates/nextblock-template/app/actions/email.ts +4 -3
- package/templates/nextblock-template/app/actions/formActions.ts +51 -42
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +281 -0
- package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +4 -0
- package/templates/nextblock-template/app/checkout/success/actions.ts +2 -1
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +64 -20
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -0
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +118 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +6 -11
- package/templates/nextblock-template/app/cms/layout.tsx +8 -3
- package/templates/nextblock-template/app/cms/settings/email/actions.ts +60 -0
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -0
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +5 -6
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +0 -48
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +4 -3
- package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -0
- package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -0
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -0
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +3 -0
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +2 -2
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +20 -0
- package/templates/nextblock-template/app/layout.tsx +5 -1
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +15 -158
- package/templates/nextblock-template/app/setup/page.tsx +0 -8
- package/templates/nextblock-template/components/AppShell.tsx +0 -7
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -1
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +25 -20
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +13 -2
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +11 -0
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +59 -8
- package/templates/nextblock-template/docs/README.md +3 -0
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +11 -13
- package/templates/nextblock-template/lib/auth/twoFactor.ts +41 -0
- package/templates/nextblock-template/lib/config/email-settings.ts +217 -0
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -0
- package/templates/nextblock-template/lib/onboarding/status.ts +136 -0
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -0
- package/templates/nextblock-template/lib/privacy/settings.ts +12 -0
- package/templates/nextblock-template/lib/privacy/types.ts +3 -1
- package/templates/nextblock-template/lib/setup/actions.ts +6 -21
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +10 -0
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -227,11 +227,8 @@ export async function recheckStatus(): Promise<ProvisioningStatus & { writable:
|
|
|
227
227
|
|
|
228
228
|
export interface CompleteSetupInput {
|
|
229
229
|
admin: { email: string; password: string; fullName: string };
|
|
230
|
-
|
|
231
|
-
/** Local-only extra env (storage / SMTP) collected by the wizard. */
|
|
230
|
+
/** Local-only extra env (media storage) collected by the wizard. */
|
|
232
231
|
envValues?: Record<string, string>;
|
|
233
|
-
/** Bot protection — stored in the DB so it works on read-only channels too. */
|
|
234
|
-
turnstile?: { provider: 'none' | 'turnstile'; siteKey: string; secretKey: string };
|
|
235
232
|
/** "Start from a clean database" — wipe before installing (local dev only, server-gated). */
|
|
236
233
|
resetFirst?: boolean;
|
|
237
234
|
}
|
|
@@ -286,11 +283,8 @@ export async function completeSetup(input: CompleteSetupInput): Promise<ActionRe
|
|
|
286
283
|
if (password.length < 8) {
|
|
287
284
|
return { ok: false, error: 'Use an administrator password of at least 8 characters.' };
|
|
288
285
|
}
|
|
289
|
-
if (input.turnstile?.provider === 'turnstile' && !input.turnstile.secretKey?.trim()) {
|
|
290
|
-
return { ok: false, error: 'Enter a Turnstile secret key, or disable bot protection.' };
|
|
291
|
-
}
|
|
292
286
|
|
|
293
|
-
// 1) Persist any local env extras (storage
|
|
287
|
+
// 1) Persist any local env extras (media storage) for Profile B.
|
|
294
288
|
if (
|
|
295
289
|
input.envValues &&
|
|
296
290
|
Object.keys(input.envValues).length > 0 &&
|
|
@@ -377,20 +371,11 @@ export async function completeSetup(input: CompleteSetupInput): Promise<ActionRe
|
|
|
377
371
|
}
|
|
378
372
|
|
|
379
373
|
// 4) Persist DB-backed settings (service role bypasses RLS — no admin exists yet).
|
|
374
|
+
// Sign-up policy, bot protection, email, and payments are no longer collected by the
|
|
375
|
+
// wizard; they are configured later from the CMS. New sign-ups default to requiring
|
|
376
|
+
// email verification (auto_accept_signups = false) as a safe default.
|
|
380
377
|
try {
|
|
381
|
-
|
|
382
|
-
await admin.from('site_settings').upsert({
|
|
383
|
-
key: 'bot_protection_public',
|
|
384
|
-
value: { provider: input.turnstile.provider, siteKey: input.turnstile.siteKey },
|
|
385
|
-
});
|
|
386
|
-
await admin.from('site_settings').upsert({
|
|
387
|
-
key: 'bot_protection_secret',
|
|
388
|
-
value: { secretKey: input.turnstile.secretKey },
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
await setSystemConfigurationServiceRole({
|
|
392
|
-
auto_accept_signups: Boolean(input.autoAcceptSignups),
|
|
393
|
-
});
|
|
378
|
+
await setSystemConfigurationServiceRole({ auto_accept_signups: false });
|
|
394
379
|
} catch (caught) {
|
|
395
380
|
return {
|
|
396
381
|
ok: false,
|
|
@@ -168,5 +168,15 @@ export const MIGRATIONS_BUNDLE: BundledMigration[] = [
|
|
|
168
168
|
"version": "00000000000030",
|
|
169
169
|
"name": "00000000000030_setup_system_configuration.sql",
|
|
170
170
|
"sql": "-- 00000000000030_setup_system_configuration.sql\n-- First-Boot Setup Wizard: global system configuration.\n--\n-- Adds a dedicated, RLS-locked `system_configuration` table that holds settings the\n-- browser /setup wizard manages and that don't belong in the public key-value\n-- `site_settings` store. It is a singleton (exactly one row, id = 1).\n--\n-- Shape:\n-- auto_accept_signups boolean -- when true, new public sign-ups skip outbound email\n-- verification (the signup route uses a service-role\n-- admin.createUser({ email_confirm: true }) path).\n-- settings jsonb -- forward-compatible catch-all for future feature\n-- toggles ({} by default). Do NOT store true secrets\n-- here (Turnstile/AI secrets keep living in their\n-- existing site_settings sensitive keys).\n--\n-- Access is locked to the ADMIN role for normal clients (NextBlock has no separate\n-- \"super-admin\" tier — ADMIN is the top level). The service_role retains full access\n-- so the wizard can seed/read it before any admin exists.\n\nCREATE TABLE IF NOT EXISTS public.system_configuration (\n id integer PRIMARY KEY DEFAULT 1,\n auto_accept_signups boolean NOT NULL DEFAULT false,\n settings jsonb NOT NULL DEFAULT '{}'::jsonb,\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT system_configuration_singleton CHECK (id = 1)\n);\n\nCOMMENT ON TABLE public.system_configuration IS\n 'Singleton (id = 1) of global setup-wizard configuration. ADMIN-only via RLS; never store secrets in settings.';\n\n-- Seed the single row so reads always find it.\nINSERT INTO public.system_configuration (id, auto_accept_signups, settings)\nVALUES (1, false, '{}'::jsonb)\nON CONFLICT (id) DO NOTHING;\n\nALTER TABLE public.system_configuration ENABLE ROW LEVEL SECURITY;\n\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.system_configuration TO authenticated;\nGRANT ALL ON public.system_configuration TO service_role;\n\n-- ADMIN-only for every operation by authenticated clients.\nDROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;\nCREATE POLICY system_configuration_admin_select\n ON public.system_configuration\n FOR SELECT\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;\nCREATE POLICY system_configuration_admin_insert\n ON public.system_configuration\n FOR INSERT\n TO authenticated\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;\nCREATE POLICY system_configuration_admin_update\n ON public.system_configuration\n FOR UPDATE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN')\n WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN');\n\nDROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;\nCREATE POLICY system_configuration_admin_delete\n ON public.system_configuration\n FOR DELETE\n TO authenticated\n USING ((SELECT public.get_current_user_role()) = 'ADMIN');\n\n-- Service role bypasses the ADMIN checks (used by the wizard before an admin exists,\n-- and by the signup route to read auto_accept_signups as an anonymous visitor).\nDROP POLICY IF EXISTS system_configuration_service_role_all ON public.system_configuration;\nCREATE POLICY system_configuration_service_role_all\n ON public.system_configuration\n FOR ALL\n TO service_role\n USING (true)\n WITH CHECK (true);\n"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"version": "00000000000031",
|
|
174
|
+
"name": "00000000000031_seed_footer_navigation.sql",
|
|
175
|
+
"sql": "-- 00000000000031_seed_footer_navigation.sql\n-- Seed editable FOOTER navigation items (EN + FR).\n--\n-- The public footer used to hard-code its \"Privacy Policy\" and \"Terms of Service\"\n-- links in apps/nextblock/components/AppShell.tsx. This migration turns them into\n-- real navigation_items rows under the FOOTER menu so they are editable from the\n-- CMS (/cms/navigation) like any other menu, and so the French footer points at\n-- the localized page slugs.\n--\n-- Targets the legal pages seeded by 00000000000027_setup_privacy_and_mfa.sql:\n-- EN /privacy-policy FR /politique-de-confidentialite\n-- EN /terms-of-service FR /conditions-utilisation\n--\n-- Idempotent: re-running replaces the FOOTER rows it owns (matched by URL) and\n-- reuses their translation_group_id so EN/FR stay linked across re-runs.\nDO $seed_footer$\nDECLARE\n v_en bigint;\n v_fr bigint;\n v_privacy_group uuid;\n v_terms_group uuid;\n v_en_privacy_page bigint;\n v_fr_privacy_page bigint;\n v_en_terms_page bigint;\n v_fr_terms_page bigint;\nBEGIN\n SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;\n SELECT id INTO v_fr FROM public.languages WHERE code = 'fr' LIMIT 1;\n\n IF v_en IS NULL THEN\n RAISE NOTICE 'Default language \"en\" not found; skipping footer navigation seed.';\n RETURN;\n END IF;\n\n -- Reuse existing translation groups if these footer items were seeded before,\n -- so EN <-> FR stay paired and re-runs do not orphan translations.\n SELECT translation_group_id INTO v_privacy_group\n FROM public.navigation_items\n WHERE menu_key = 'FOOTER' AND url = '/privacy-policy' LIMIT 1;\n IF v_privacy_group IS NULL THEN v_privacy_group := gen_random_uuid(); END IF;\n\n SELECT translation_group_id INTO v_terms_group\n FROM public.navigation_items\n WHERE menu_key = 'FOOTER' AND url = '/terms-of-service' LIMIT 1;\n IF v_terms_group IS NULL THEN v_terms_group := gen_random_uuid(); END IF;\n\n -- Best-effort link each item to its underlying page (ON DELETE SET NULL keeps\n -- the link working even if a page is later removed; url remains the source of truth).\n SELECT id INTO v_en_privacy_page\n FROM public.pages WHERE slug = 'privacy-policy' AND language_id = v_en LIMIT 1;\n SELECT id INTO v_en_terms_page\n FROM public.pages WHERE slug = 'terms-of-service' AND language_id = v_en LIMIT 1;\n\n -- Remove any prior copies of the footer links we own, then re-insert cleanly.\n DELETE FROM public.navigation_items\n WHERE menu_key = 'FOOTER'\n AND url IN (\n '/privacy-policy', '/terms-of-service',\n '/politique-de-confidentialite', '/conditions-utilisation'\n );\n\n -- ----- English footer -----\n INSERT INTO public.navigation_items\n (language_id, menu_key, label, url, \"order\", page_id, translation_group_id)\n VALUES\n (v_en, 'FOOTER', 'Privacy Policy', '/privacy-policy', 0, v_en_privacy_page, v_privacy_group),\n (v_en, 'FOOTER', 'Terms of Service', '/terms-of-service', 1, v_en_terms_page, v_terms_group);\n\n -- ----- French footer (only if the French language exists) -----\n IF v_fr IS NOT NULL THEN\n SELECT id INTO v_fr_privacy_page\n FROM public.pages WHERE slug = 'politique-de-confidentialite' AND language_id = v_fr LIMIT 1;\n SELECT id INTO v_fr_terms_page\n FROM public.pages WHERE slug = 'conditions-utilisation' AND language_id = v_fr LIMIT 1;\n\n INSERT INTO public.navigation_items\n (language_id, menu_key, label, url, \"order\", page_id, translation_group_id)\n VALUES\n (v_fr, 'FOOTER', 'Politique de confidentialité', '/politique-de-confidentialite', 0, v_fr_privacy_page, v_privacy_group),\n (v_fr, 'FOOTER', 'Conditions d''utilisation', '/conditions-utilisation', 1, v_fr_terms_page, v_terms_group);\n END IF;\n\n RAISE NOTICE 'Seeded FOOTER navigation items (Privacy Policy, Terms of Service).';\nEND;\n$seed_footer$;\n"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"version": "00000000000032",
|
|
179
|
+
"name": "00000000000032_neutralize_seeded_contact_emails.sql",
|
|
180
|
+
"sql": "-- 00000000000032_neutralize_seeded_contact_emails.sql\n-- Remove the original authors' contact addresses from seeded content so a\n-- downloaded / self-hosted copy of NextBlock never routes mail to us.\n--\n-- Earlier migrations baked real addresses into block content:\n-- * 00000000000027 seeded `privacy@nextblock.dev` across the Privacy Policy and\n-- Terms pages (EN + FR), as visible text and `mailto:` links.\n-- * 00000000000010 seeded a `mailto:info@nextblock.dev` CTA on the French home\n-- page (the English page correctly links to /contact), and `foo@bar.com` as\n-- the contact form recipient.\n--\n-- Migrations are append-only, so this is a forward-only data fix rather than an\n-- edit of those files. Each statement is idempotent (a no-op once applied).\n--\n-- The `{{privacy_email}}` token is resolved at render time by the app\n-- (apps/nextblock/lib/privacy/contact-emails.ts): admin \"Support email\" setting\n-- -> SANDBOX_PRIVACY_EMAIL env (sandbox only) -> privacy@example.com fallback.\n\n-- 1. Privacy / Terms legal pages: swap the hard-coded address for a merge tag.\nUPDATE public.blocks\nSET content = replace(content::text, 'privacy@nextblock.dev', '{{privacy_email}}')::jsonb\nWHERE content::text LIKE '%privacy@nextblock.dev%';\n\n-- 2. French home \"Nous contacter\" CTA: point at the contact form like the English\n-- page instead of a mailto to our inbox.\nUPDATE public.blocks\nSET content = replace(content::text, 'mailto:info@nextblock.dev', '/contact')::jsonb\nWHERE content::text LIKE '%mailto:info@nextblock.dev%';\n\n-- 3. Contact form default recipient: use a neutral placeholder. In sandbox the\n-- app overrides this with SANDBOX_CONTACT_EMAIL at submit time.\nUPDATE public.blocks\nSET content = replace(content::text, 'foo@bar.com', 'contact@example.com')::jsonb\nWHERE content::text LIKE '%foo@bar.com%';\n"
|
|
171
181
|
}
|
|
172
182
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|