create-nextblock 0.12.8 → 0.12.11

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.
Files changed (293) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
  19. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  20. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
  21. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
  22. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  23. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  24. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  25. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  26. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
  27. package/templates/nextblock-template/app/actions/interactions.ts +373 -373
  28. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  29. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  30. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  31. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  32. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
  33. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  34. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  35. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  36. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  37. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  38. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
  39. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  40. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  41. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  42. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  46. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  47. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  48. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  49. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  50. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  53. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  54. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  55. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  56. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
  57. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  58. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  59. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  60. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  61. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  62. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  63. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  66. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  67. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  68. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  69. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  70. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  71. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  72. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
  73. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  74. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
  75. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
  76. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  77. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  78. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  79. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  80. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  81. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -149
  82. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
  83. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  84. package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
  85. package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
  86. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  87. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  88. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  89. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  90. package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
  91. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  92. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  93. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  94. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  95. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  98. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  99. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  100. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
  101. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
  102. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  103. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  104. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
  105. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
  106. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
  107. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  108. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  109. package/templates/nextblock-template/app/cms/settings/email/actions.ts +64 -64
  110. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
  111. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  112. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  113. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  114. package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
  115. package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
  116. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  117. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
  118. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  119. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  120. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  121. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  122. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  123. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  124. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  125. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  126. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
  127. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
  128. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  129. package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
  130. package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
  131. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  132. package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
  133. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
  134. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  135. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  136. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  137. package/templates/nextblock-template/app/cms/users/actions.ts +222 -222
  138. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  139. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
  140. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  141. package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
  142. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
  143. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  144. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  145. package/templates/nextblock-template/app/profile/account-data.ts +37 -37
  146. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  147. package/templates/nextblock-template/app/profile/account-types.ts +11 -11
  148. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  149. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
  150. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
  151. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  152. package/templates/nextblock-template/app/profile/page.tsx +42 -42
  153. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  154. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  155. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  156. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  157. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  158. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  159. package/templates/nextblock-template/app/sitemap.ts +130 -130
  160. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  161. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  162. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  163. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  164. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  165. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  166. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  167. package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
  168. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  169. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  170. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -369
  171. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
  172. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  173. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  174. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  175. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  176. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  177. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  178. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  179. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  180. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  181. package/templates/nextblock-template/components/form-message.tsx +32 -32
  182. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  183. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  184. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  185. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  186. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  189. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  192. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  193. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  194. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  195. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  196. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  197. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  198. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  199. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  200. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  201. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  202. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
  203. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  204. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  205. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  206. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  207. package/templates/nextblock-template/docs/README.md +39 -39
  208. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
  209. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  210. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  211. package/templates/nextblock-template/index.d.ts +7 -7
  212. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  213. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  214. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  215. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  216. package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
  217. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  218. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  219. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
  220. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
  221. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  222. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
  223. package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
  224. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  225. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  226. package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
  227. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  228. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  229. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  230. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  231. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
  232. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  233. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  234. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  235. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  236. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  237. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  238. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  239. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  240. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  241. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  242. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  243. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  244. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  245. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  246. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  247. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  248. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  249. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  250. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  251. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  252. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  253. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  254. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  255. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  256. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  257. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  258. package/templates/nextblock-template/lib/search/server.ts +585 -585
  259. package/templates/nextblock-template/lib/search/types.ts +27 -27
  260. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  261. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  262. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  263. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
  264. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  265. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  266. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  267. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  268. package/templates/nextblock-template/lib/site-url.ts +48 -48
  269. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  270. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  271. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  272. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  273. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  274. package/templates/nextblock-template/next.config.js +6 -0
  275. package/templates/nextblock-template/package.json +1 -1
  276. package/templates/nextblock-template/postcss.config.js +6 -6
  277. package/templates/nextblock-template/scripts/backup.js +115 -115
  278. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  279. package/templates/nextblock-template/scripts/restore.js +385 -385
  280. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  281. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  282. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  283. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  284. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  285. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  286. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  287. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  288. package/templates/nextblock-template/tailwind.config.js +25 -25
  289. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  290. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  291. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  292. package/tsconfig.base.json +3 -3
  293. package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
@@ -1,522 +1,522 @@
1
- 'use client';
2
-
3
- import { useRef, useState, useTransition } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import {
6
- Alert,
7
- AlertDescription,
8
- Badge,
9
- Button,
10
- Card,
11
- CardContent,
12
- CardDescription,
13
- CardHeader,
14
- CardTitle,
15
- Checkbox,
16
- Input,
17
- Label,
18
- Separator,
19
- Spinner,
20
- } from '@nextblock-cms/ui';
21
- import { Message } from '../../../../../components/form-message';
22
- import {
23
- MAX_TRUSTED_DEVICE_DAYS,
24
- MIN_TRUSTED_DEVICE_DAYS,
25
- } from '../../../../../lib/privacy/types';
26
- import {
27
- disableMfa,
28
- revokeTrustedDeviceAction,
29
- sendEmailEnrollmentCode,
30
- startTotpEnrollment,
31
- updateAutoAcceptSignups,
32
- updateGlobalSecuritySettings,
33
- verifyEmailEnrollment,
34
- verifyTotpEnrollment,
35
- type SecurityPanelData,
36
- } from '../actions';
37
-
38
- type EnrollMode = 'idle' | 'totp' | 'email';
39
-
40
- function formatDate(value: string): string {
41
- try {
42
- return new Date(value).toLocaleDateString(undefined, {
43
- year: 'numeric',
44
- month: 'short',
45
- day: 'numeric',
46
- });
47
- } catch {
48
- return value;
49
- }
50
- }
51
-
52
- export default function SecurityPanel({ data }: { data: SecurityPanelData }) {
53
- const router = useRouter();
54
- const [isPending, startTransition] = useTransition();
55
- const [message, setMessage] = useState<Message | null>(null);
56
-
57
- const [mode, setMode] = useState<EnrollMode>('idle');
58
- const [totp, setTotp] = useState<{ factorId: string; qrCode: string; secret: string } | null>(
59
- null,
60
- );
61
- const [emailSent, setEmailSent] = useState(false);
62
- const [code, setCode] = useState('');
63
-
64
- const run = (fn: () => Promise<unknown>, after?: () => void) => {
65
- setMessage(null);
66
- startTransition(async () => {
67
- try {
68
- const result = (await fn()) as { message?: string } | undefined;
69
- if (result?.message) setMessage({ success: result.message });
70
- after?.();
71
- router.refresh();
72
- } catch (error) {
73
- setMessage({
74
- error: error instanceof Error ? error.message : 'Something went wrong.',
75
- });
76
- }
77
- });
78
- };
79
-
80
- const resetEnrollment = () => {
81
- setMode('idle');
82
- setTotp(null);
83
- setEmailSent(false);
84
- setCode('');
85
- };
86
-
87
- // --- Enrollment handlers ------------------------------------------------------
88
-
89
- const beginTotp = () => {
90
- setMessage(null);
91
- setMode('totp');
92
- setEmailSent(false);
93
- startTransition(async () => {
94
- const result = await startTotpEnrollment();
95
- if (result.ok) {
96
- setTotp({ factorId: result.factorId, qrCode: result.qrCode, secret: result.secret });
97
- } else {
98
- setMessage({ error: result.error });
99
- setMode('idle');
100
- }
101
- });
102
- };
103
-
104
- const submitTotpCode = () => {
105
- if (!totp) return;
106
- const formData = new FormData();
107
- formData.append('factorId', totp.factorId);
108
- formData.append('code', code);
109
- run(() => verifyTotpEnrollment(formData), resetEnrollment);
110
- };
111
-
112
- const beginEmail = () => {
113
- setMode('email');
114
- setTotp(null);
115
- run(async () => {
116
- const result = await sendEmailEnrollmentCode();
117
- setEmailSent(true);
118
- return result;
119
- });
120
- };
121
-
122
- const submitEmailCode = () => {
123
- const formData = new FormData();
124
- formData.append('code', code);
125
- run(() => verifyEmailEnrollment(formData), resetEnrollment);
126
- };
127
-
128
- const messageAlert = message && (
129
- <Alert
130
- variant={'success' in message ? 'success' : 'destructive'}
131
- className="py-2 px-4"
132
- >
133
- <AlertDescription>
134
- {'success' in message
135
- ? message.success
136
- : 'error' in message
137
- ? message.error
138
- : ''}
139
- </AlertDescription>
140
- </Alert>
141
- );
142
-
143
- return (
144
- <>
145
- {/* Status + per-user MFA */}
146
- <Card>
147
- <CardHeader>
148
- <CardTitle className="flex items-center gap-2">
149
- Two-Factor Authentication
150
- {data.mfaEnabled ? (
151
- <Badge variant="default">
152
- Enabled · {data.mfaType === 'totp' ? 'Authenticator app' : 'Email code'}
153
- </Badge>
154
- ) : (
155
- <Badge variant="secondary">Off</Badge>
156
- )}
157
- </CardTitle>
158
- <CardDescription>
159
- Add a second step when you sign in to the CMS. You can use an authenticator
160
- app (TOTP) or a one-time code sent to <strong>{data.email}</strong>.
161
- </CardDescription>
162
- </CardHeader>
163
- <CardContent className="space-y-6">
164
- {messageAlert}
165
-
166
- {data.mfaEnabled ? (
167
- <div className="flex items-center justify-between rounded-lg border p-4">
168
- <div className="text-sm">
169
- <p className="font-medium">
170
- {data.mfaType === 'totp'
171
- ? 'Authenticator app is protecting your account.'
172
- : 'Email verification is protecting your account.'}
173
- </p>
174
- <p className="text-slate-500">
175
- Disabling 2FA also forgets all of your trusted devices.
176
- </p>
177
- </div>
178
- <Button
179
- variant="destructive"
180
- disabled={isPending}
181
- onClick={() => run(() => disableMfa(), resetEnrollment)}
182
- >
183
- {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Disable 2FA'}
184
- </Button>
185
- </div>
186
- ) : (
187
- <div className="space-y-4">
188
- {/* Method chooser */}
189
- <div className="grid gap-3 sm:grid-cols-2">
190
- <button
191
- type="button"
192
- onClick={beginTotp}
193
- className={`rounded-lg border p-4 text-left transition hover:border-primary ${
194
- mode === 'totp' ? 'border-primary ring-1 ring-primary' : ''
195
- }`}
196
- >
197
- <p className="font-medium text-sm">Authenticator App (TOTP)</p>
198
- <p className="text-xs text-slate-500">
199
- Scan a QR code with Google Authenticator, 1Password, Authy, etc.
200
- </p>
201
- </button>
202
- <button
203
- type="button"
204
- onClick={beginEmail}
205
- className={`rounded-lg border p-4 text-left transition hover:border-primary ${
206
- mode === 'email' ? 'border-primary ring-1 ring-primary' : ''
207
- }`}
208
- >
209
- <p className="font-medium text-sm">Secure Email Code</p>
210
- <p className="text-xs text-slate-500">
211
- Receive a 6-digit code at your account email each time you sign in.
212
- </p>
213
- </button>
214
- </div>
215
-
216
- {/* TOTP enrollment */}
217
- {mode === 'totp' && (
218
- <div className="rounded-lg border p-4 space-y-4">
219
- {totp ? (
220
- <>
221
- <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-start">
222
- {/* eslint-disable-next-line @next/next/no-img-element */}
223
- <img
224
- src={totp.qrCode}
225
- alt="Authenticator setup QR code"
226
- className="h-44 w-44 rounded bg-white p-2"
227
- />
228
- <div className="text-sm space-y-2">
229
- <p>1. Scan this QR code with your authenticator app.</p>
230
- <p>
231
- 2. Or enter this key manually:
232
- <br />
233
- <code className="break-all text-xs bg-muted px-1 py-0.5 rounded">
234
- {totp.secret}
235
- </code>
236
- </p>
237
- <p>3. Enter the current 6-digit code below.</p>
238
- </div>
239
- </div>
240
- <div className="flex items-end gap-3">
241
- <div className="space-y-1">
242
- <Label htmlFor="totp_code">6-digit code</Label>
243
- <Input
244
- id="totp_code"
245
- inputMode="numeric"
246
- autoComplete="one-time-code"
247
- maxLength={6}
248
- value={code}
249
- onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
250
- className="w-32 tracking-widest"
251
- placeholder="000000"
252
- />
253
- </div>
254
- <Button onClick={submitTotpCode} disabled={isPending || code.length !== 6}>
255
- {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
256
- </Button>
257
- <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
258
- Cancel
259
- </Button>
260
- </div>
261
- </>
262
- ) : (
263
- <div className="flex items-center gap-2 text-sm text-slate-500">
264
- <Spinner className="h-4 w-4 animate-spin" /> Generating your QR code…
265
- </div>
266
- )}
267
- </div>
268
- )}
269
-
270
- {/* Email enrollment */}
271
- {mode === 'email' && (
272
- <div className="rounded-lg border p-4 space-y-4">
273
- <p className="text-sm">
274
- {emailSent
275
- ? `Enter the 6-digit code we sent to ${data.email}.`
276
- : 'Sending a verification code to your email…'}
277
- </p>
278
- <div className="flex items-end gap-3">
279
- <div className="space-y-1">
280
- <Label htmlFor="email_code">6-digit code</Label>
281
- <Input
282
- id="email_code"
283
- inputMode="numeric"
284
- autoComplete="one-time-code"
285
- maxLength={6}
286
- value={code}
287
- onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
288
- className="w-32 tracking-widest"
289
- placeholder="000000"
290
- disabled={!emailSent}
291
- />
292
- </div>
293
- <Button
294
- onClick={submitEmailCode}
295
- disabled={isPending || code.length !== 6 || !emailSent}
296
- >
297
- {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
298
- </Button>
299
- <Button
300
- variant="ghost"
301
- onClick={beginEmail}
302
- disabled={isPending}
303
- title="Send a new code"
304
- >
305
- Resend
306
- </Button>
307
- <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
308
- Cancel
309
- </Button>
310
- </div>
311
- </div>
312
- )}
313
- </div>
314
- )}
315
- </CardContent>
316
- </Card>
317
-
318
- {/* Trusted devices */}
319
- <Card>
320
- <CardHeader>
321
- <CardTitle>Trusted Devices</CardTitle>
322
- <CardDescription>
323
- Devices where you chose &ldquo;Remember this device&rdquo; skip 2FA until the
324
- trust expires. Revoke any you no longer recognize.
325
- </CardDescription>
326
- </CardHeader>
327
- <CardContent>
328
- {data.trustedDevices.length === 0 ? (
329
- <p className="text-sm text-slate-500">No trusted devices.</p>
330
- ) : (
331
- <ul className="divide-y rounded-lg border">
332
- {data.trustedDevices.map((device) => (
333
- <li
334
- key={device.id}
335
- className="flex items-center justify-between gap-4 p-3 text-sm"
336
- >
337
- <div className="min-w-0">
338
- <p className="truncate font-medium">
339
- {device.browser_metadata || 'Unknown device'}
340
- </p>
341
- <p className="text-xs text-slate-500">
342
- Trusted {formatDate(device.created_at)} · expires{' '}
343
- {formatDate(device.expires_at)}
344
- </p>
345
- </div>
346
- <Button
347
- variant="outline"
348
- size="sm"
349
- disabled={isPending}
350
- onClick={() => {
351
- const formData = new FormData();
352
- formData.append('id', device.id);
353
- run(() => revokeTrustedDeviceAction(formData));
354
- }}
355
- >
356
- Revoke
357
- </Button>
358
- </li>
359
- ))}
360
- </ul>
361
- )}
362
- </CardContent>
363
- </Card>
364
-
365
- {/* Admin global policy */}
366
- {data.isAdmin && (
367
- <AdminPolicyCard
368
- initial={data.globalSettings}
369
- isPending={isPending}
370
- onSave={(formData) => run(() => updateGlobalSecuritySettings(formData))}
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
- )}
382
- </>
383
- );
384
- }
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
-
445
- function AdminPolicyCard({
446
- initial,
447
- isPending,
448
- onSave,
449
- }: {
450
- initial: SecurityPanelData['globalSettings'];
451
- isPending: boolean;
452
- onSave: (formData: FormData) => void;
453
- }) {
454
- const [days, setDays] = useState(String(initial.trusted_device_days));
455
- const [enforce, setEnforce] = useState(initial.enforce_staff_2fa);
456
- const formRef = useRef<HTMLFormElement>(null);
457
-
458
- return (
459
- <Card>
460
- <CardHeader>
461
- <CardTitle>Organization Policy (Admin)</CardTitle>
462
- <CardDescription>
463
- Applies to every staff member. Trusted-device trust is server-validated and
464
- revocable at any time from this page.
465
- </CardDescription>
466
- </CardHeader>
467
- <CardContent>
468
- <form
469
- ref={formRef}
470
- onSubmit={(e) => {
471
- e.preventDefault();
472
- const formData = new FormData();
473
- formData.append('trusted_device_days', days);
474
- formData.append('enforce_staff_2fa', String(enforce));
475
- onSave(formData);
476
- }}
477
- className="space-y-6"
478
- >
479
- <div className="space-y-2">
480
- <Label htmlFor="trusted_device_days">Remember-device duration (days)</Label>
481
- <Input
482
- id="trusted_device_days"
483
- type="number"
484
- min={MIN_TRUSTED_DEVICE_DAYS}
485
- max={MAX_TRUSTED_DEVICE_DAYS}
486
- value={days}
487
- onChange={(e) => setDays(e.target.value)}
488
- className="w-40"
489
- />
490
- <p className="text-xs text-slate-500">
491
- How long a &ldquo;Remember this device&rdquo; choice lasts before 2FA is
492
- required again. Default 30 days, up to {MAX_TRUSTED_DEVICE_DAYS} (10 years).
493
- </p>
494
- </div>
495
-
496
- <div className="flex items-start gap-3">
497
- <Checkbox
498
- id="enforce_staff_2fa"
499
- checked={enforce}
500
- onCheckedChange={(checked) => setEnforce(checked === true)}
501
- className="mt-1"
502
- />
503
- <div className="space-y-1">
504
- <Label htmlFor="enforce_staff_2fa">Encourage staff to enable 2FA</Label>
505
- <p className="text-xs text-slate-500">
506
- Shows a reminder banner across the CMS to ADMIN/WRITER accounts that
507
- haven&rsquo;t set up a second factor. On by default.
508
- </p>
509
- </div>
510
- </div>
511
-
512
- <Separator />
513
-
514
- <Button type="submit" disabled={isPending}>
515
- {isPending ? <Spinner className="mr-2 h-4 w-4 animate-spin" /> : null}
516
- Save Policy
517
- </Button>
518
- </form>
519
- </CardContent>
520
- </Card>
521
- );
522
- }
1
+ 'use client';
2
+
3
+ import { useRef, useState, useTransition } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ Alert,
7
+ AlertDescription,
8
+ Badge,
9
+ Button,
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardHeader,
14
+ CardTitle,
15
+ Checkbox,
16
+ Input,
17
+ Label,
18
+ Separator,
19
+ Spinner,
20
+ } from '@nextblock-cms/ui';
21
+ import { Message } from '../../../../../components/form-message';
22
+ import {
23
+ MAX_TRUSTED_DEVICE_DAYS,
24
+ MIN_TRUSTED_DEVICE_DAYS,
25
+ } from '../../../../../lib/privacy/types';
26
+ import {
27
+ disableMfa,
28
+ revokeTrustedDeviceAction,
29
+ sendEmailEnrollmentCode,
30
+ startTotpEnrollment,
31
+ updateAutoAcceptSignups,
32
+ updateGlobalSecuritySettings,
33
+ verifyEmailEnrollment,
34
+ verifyTotpEnrollment,
35
+ type SecurityPanelData,
36
+ } from '../actions';
37
+
38
+ type EnrollMode = 'idle' | 'totp' | 'email';
39
+
40
+ function formatDate(value: string): string {
41
+ try {
42
+ return new Date(value).toLocaleDateString(undefined, {
43
+ year: 'numeric',
44
+ month: 'short',
45
+ day: 'numeric',
46
+ });
47
+ } catch {
48
+ return value;
49
+ }
50
+ }
51
+
52
+ export default function SecurityPanel({ data }: { data: SecurityPanelData }) {
53
+ const router = useRouter();
54
+ const [isPending, startTransition] = useTransition();
55
+ const [message, setMessage] = useState<Message | null>(null);
56
+
57
+ const [mode, setMode] = useState<EnrollMode>('idle');
58
+ const [totp, setTotp] = useState<{ factorId: string; qrCode: string; secret: string } | null>(
59
+ null,
60
+ );
61
+ const [emailSent, setEmailSent] = useState(false);
62
+ const [code, setCode] = useState('');
63
+
64
+ const run = (fn: () => Promise<unknown>, after?: () => void) => {
65
+ setMessage(null);
66
+ startTransition(async () => {
67
+ try {
68
+ const result = (await fn()) as { message?: string } | undefined;
69
+ if (result?.message) setMessage({ success: result.message });
70
+ after?.();
71
+ router.refresh();
72
+ } catch (error) {
73
+ setMessage({
74
+ error: error instanceof Error ? error.message : 'Something went wrong.',
75
+ });
76
+ }
77
+ });
78
+ };
79
+
80
+ const resetEnrollment = () => {
81
+ setMode('idle');
82
+ setTotp(null);
83
+ setEmailSent(false);
84
+ setCode('');
85
+ };
86
+
87
+ // --- Enrollment handlers ------------------------------------------------------
88
+
89
+ const beginTotp = () => {
90
+ setMessage(null);
91
+ setMode('totp');
92
+ setEmailSent(false);
93
+ startTransition(async () => {
94
+ const result = await startTotpEnrollment();
95
+ if (result.ok) {
96
+ setTotp({ factorId: result.factorId, qrCode: result.qrCode, secret: result.secret });
97
+ } else {
98
+ setMessage({ error: result.error });
99
+ setMode('idle');
100
+ }
101
+ });
102
+ };
103
+
104
+ const submitTotpCode = () => {
105
+ if (!totp) return;
106
+ const formData = new FormData();
107
+ formData.append('factorId', totp.factorId);
108
+ formData.append('code', code);
109
+ run(() => verifyTotpEnrollment(formData), resetEnrollment);
110
+ };
111
+
112
+ const beginEmail = () => {
113
+ setMode('email');
114
+ setTotp(null);
115
+ run(async () => {
116
+ const result = await sendEmailEnrollmentCode();
117
+ setEmailSent(true);
118
+ return result;
119
+ });
120
+ };
121
+
122
+ const submitEmailCode = () => {
123
+ const formData = new FormData();
124
+ formData.append('code', code);
125
+ run(() => verifyEmailEnrollment(formData), resetEnrollment);
126
+ };
127
+
128
+ const messageAlert = message && (
129
+ <Alert
130
+ variant={'success' in message ? 'success' : 'destructive'}
131
+ className="py-2 px-4"
132
+ >
133
+ <AlertDescription>
134
+ {'success' in message
135
+ ? message.success
136
+ : 'error' in message
137
+ ? message.error
138
+ : ''}
139
+ </AlertDescription>
140
+ </Alert>
141
+ );
142
+
143
+ return (
144
+ <>
145
+ {/* Status + per-user MFA */}
146
+ <Card>
147
+ <CardHeader>
148
+ <CardTitle className="flex items-center gap-2">
149
+ Two-Factor Authentication
150
+ {data.mfaEnabled ? (
151
+ <Badge variant="default">
152
+ Enabled · {data.mfaType === 'totp' ? 'Authenticator app' : 'Email code'}
153
+ </Badge>
154
+ ) : (
155
+ <Badge variant="secondary">Off</Badge>
156
+ )}
157
+ </CardTitle>
158
+ <CardDescription>
159
+ Add a second step when you sign in to the CMS. You can use an authenticator
160
+ app (TOTP) or a one-time code sent to <strong>{data.email}</strong>.
161
+ </CardDescription>
162
+ </CardHeader>
163
+ <CardContent className="space-y-6">
164
+ {messageAlert}
165
+
166
+ {data.mfaEnabled ? (
167
+ <div className="flex items-center justify-between rounded-lg border p-4">
168
+ <div className="text-sm">
169
+ <p className="font-medium">
170
+ {data.mfaType === 'totp'
171
+ ? 'Authenticator app is protecting your account.'
172
+ : 'Email verification is protecting your account.'}
173
+ </p>
174
+ <p className="text-slate-500">
175
+ Disabling 2FA also forgets all of your trusted devices.
176
+ </p>
177
+ </div>
178
+ <Button
179
+ variant="destructive"
180
+ disabled={isPending}
181
+ onClick={() => run(() => disableMfa(), resetEnrollment)}
182
+ >
183
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Disable 2FA'}
184
+ </Button>
185
+ </div>
186
+ ) : (
187
+ <div className="space-y-4">
188
+ {/* Method chooser */}
189
+ <div className="grid gap-3 sm:grid-cols-2">
190
+ <button
191
+ type="button"
192
+ onClick={beginTotp}
193
+ className={`rounded-lg border p-4 text-left transition hover:border-primary ${
194
+ mode === 'totp' ? 'border-primary ring-1 ring-primary' : ''
195
+ }`}
196
+ >
197
+ <p className="font-medium text-sm">Authenticator App (TOTP)</p>
198
+ <p className="text-xs text-slate-500">
199
+ Scan a QR code with Google Authenticator, 1Password, Authy, etc.
200
+ </p>
201
+ </button>
202
+ <button
203
+ type="button"
204
+ onClick={beginEmail}
205
+ className={`rounded-lg border p-4 text-left transition hover:border-primary ${
206
+ mode === 'email' ? 'border-primary ring-1 ring-primary' : ''
207
+ }`}
208
+ >
209
+ <p className="font-medium text-sm">Secure Email Code</p>
210
+ <p className="text-xs text-slate-500">
211
+ Receive a 6-digit code at your account email each time you sign in.
212
+ </p>
213
+ </button>
214
+ </div>
215
+
216
+ {/* TOTP enrollment */}
217
+ {mode === 'totp' && (
218
+ <div className="rounded-lg border p-4 space-y-4">
219
+ {totp ? (
220
+ <>
221
+ <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-start">
222
+ {/* eslint-disable-next-line @next/next/no-img-element */}
223
+ <img
224
+ src={totp.qrCode}
225
+ alt="Authenticator setup QR code"
226
+ className="h-44 w-44 rounded bg-white p-2"
227
+ />
228
+ <div className="text-sm space-y-2">
229
+ <p>1. Scan this QR code with your authenticator app.</p>
230
+ <p>
231
+ 2. Or enter this key manually:
232
+ <br />
233
+ <code className="break-all text-xs bg-muted px-1 py-0.5 rounded">
234
+ {totp.secret}
235
+ </code>
236
+ </p>
237
+ <p>3. Enter the current 6-digit code below.</p>
238
+ </div>
239
+ </div>
240
+ <div className="flex items-end gap-3">
241
+ <div className="space-y-1">
242
+ <Label htmlFor="totp_code">6-digit code</Label>
243
+ <Input
244
+ id="totp_code"
245
+ inputMode="numeric"
246
+ autoComplete="one-time-code"
247
+ maxLength={6}
248
+ value={code}
249
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
250
+ className="w-32 tracking-widest"
251
+ placeholder="000000"
252
+ />
253
+ </div>
254
+ <Button onClick={submitTotpCode} disabled={isPending || code.length !== 6}>
255
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
256
+ </Button>
257
+ <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
258
+ Cancel
259
+ </Button>
260
+ </div>
261
+ </>
262
+ ) : (
263
+ <div className="flex items-center gap-2 text-sm text-slate-500">
264
+ <Spinner className="h-4 w-4 animate-spin" /> Generating your QR code…
265
+ </div>
266
+ )}
267
+ </div>
268
+ )}
269
+
270
+ {/* Email enrollment */}
271
+ {mode === 'email' && (
272
+ <div className="rounded-lg border p-4 space-y-4">
273
+ <p className="text-sm">
274
+ {emailSent
275
+ ? `Enter the 6-digit code we sent to ${data.email}.`
276
+ : 'Sending a verification code to your email…'}
277
+ </p>
278
+ <div className="flex items-end gap-3">
279
+ <div className="space-y-1">
280
+ <Label htmlFor="email_code">6-digit code</Label>
281
+ <Input
282
+ id="email_code"
283
+ inputMode="numeric"
284
+ autoComplete="one-time-code"
285
+ maxLength={6}
286
+ value={code}
287
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
288
+ className="w-32 tracking-widest"
289
+ placeholder="000000"
290
+ disabled={!emailSent}
291
+ />
292
+ </div>
293
+ <Button
294
+ onClick={submitEmailCode}
295
+ disabled={isPending || code.length !== 6 || !emailSent}
296
+ >
297
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
298
+ </Button>
299
+ <Button
300
+ variant="ghost"
301
+ onClick={beginEmail}
302
+ disabled={isPending}
303
+ title="Send a new code"
304
+ >
305
+ Resend
306
+ </Button>
307
+ <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
308
+ Cancel
309
+ </Button>
310
+ </div>
311
+ </div>
312
+ )}
313
+ </div>
314
+ )}
315
+ </CardContent>
316
+ </Card>
317
+
318
+ {/* Trusted devices */}
319
+ <Card>
320
+ <CardHeader>
321
+ <CardTitle>Trusted Devices</CardTitle>
322
+ <CardDescription>
323
+ Devices where you chose &ldquo;Remember this device&rdquo; skip 2FA until the
324
+ trust expires. Revoke any you no longer recognize.
325
+ </CardDescription>
326
+ </CardHeader>
327
+ <CardContent>
328
+ {data.trustedDevices.length === 0 ? (
329
+ <p className="text-sm text-slate-500">No trusted devices.</p>
330
+ ) : (
331
+ <ul className="divide-y rounded-lg border">
332
+ {data.trustedDevices.map((device) => (
333
+ <li
334
+ key={device.id}
335
+ className="flex items-center justify-between gap-4 p-3 text-sm"
336
+ >
337
+ <div className="min-w-0">
338
+ <p className="truncate font-medium">
339
+ {device.browser_metadata || 'Unknown device'}
340
+ </p>
341
+ <p className="text-xs text-slate-500">
342
+ Trusted {formatDate(device.created_at)} · expires{' '}
343
+ {formatDate(device.expires_at)}
344
+ </p>
345
+ </div>
346
+ <Button
347
+ variant="outline"
348
+ size="sm"
349
+ disabled={isPending}
350
+ onClick={() => {
351
+ const formData = new FormData();
352
+ formData.append('id', device.id);
353
+ run(() => revokeTrustedDeviceAction(formData));
354
+ }}
355
+ >
356
+ Revoke
357
+ </Button>
358
+ </li>
359
+ ))}
360
+ </ul>
361
+ )}
362
+ </CardContent>
363
+ </Card>
364
+
365
+ {/* Admin global policy */}
366
+ {data.isAdmin && (
367
+ <AdminPolicyCard
368
+ initial={data.globalSettings}
369
+ isPending={isPending}
370
+ onSave={(formData) => run(() => updateGlobalSecuritySettings(formData))}
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
+ )}
382
+ </>
383
+ );
384
+ }
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
+
445
+ function AdminPolicyCard({
446
+ initial,
447
+ isPending,
448
+ onSave,
449
+ }: {
450
+ initial: SecurityPanelData['globalSettings'];
451
+ isPending: boolean;
452
+ onSave: (formData: FormData) => void;
453
+ }) {
454
+ const [days, setDays] = useState(String(initial.trusted_device_days));
455
+ const [enforce, setEnforce] = useState(initial.enforce_staff_2fa);
456
+ const formRef = useRef<HTMLFormElement>(null);
457
+
458
+ return (
459
+ <Card>
460
+ <CardHeader>
461
+ <CardTitle>Organization Policy (Admin)</CardTitle>
462
+ <CardDescription>
463
+ Applies to every staff member. Trusted-device trust is server-validated and
464
+ revocable at any time from this page.
465
+ </CardDescription>
466
+ </CardHeader>
467
+ <CardContent>
468
+ <form
469
+ ref={formRef}
470
+ onSubmit={(e) => {
471
+ e.preventDefault();
472
+ const formData = new FormData();
473
+ formData.append('trusted_device_days', days);
474
+ formData.append('enforce_staff_2fa', String(enforce));
475
+ onSave(formData);
476
+ }}
477
+ className="space-y-6"
478
+ >
479
+ <div className="space-y-2">
480
+ <Label htmlFor="trusted_device_days">Remember-device duration (days)</Label>
481
+ <Input
482
+ id="trusted_device_days"
483
+ type="number"
484
+ min={MIN_TRUSTED_DEVICE_DAYS}
485
+ max={MAX_TRUSTED_DEVICE_DAYS}
486
+ value={days}
487
+ onChange={(e) => setDays(e.target.value)}
488
+ className="w-40"
489
+ />
490
+ <p className="text-xs text-slate-500">
491
+ How long a &ldquo;Remember this device&rdquo; choice lasts before 2FA is
492
+ required again. Default 30 days, up to {MAX_TRUSTED_DEVICE_DAYS} (10 years).
493
+ </p>
494
+ </div>
495
+
496
+ <div className="flex items-start gap-3">
497
+ <Checkbox
498
+ id="enforce_staff_2fa"
499
+ checked={enforce}
500
+ onCheckedChange={(checked) => setEnforce(checked === true)}
501
+ className="mt-1"
502
+ />
503
+ <div className="space-y-1">
504
+ <Label htmlFor="enforce_staff_2fa">Encourage staff to enable 2FA</Label>
505
+ <p className="text-xs text-slate-500">
506
+ Shows a reminder banner across the CMS to ADMIN/WRITER accounts that
507
+ haven&rsquo;t set up a second factor. On by default.
508
+ </p>
509
+ </div>
510
+ </div>
511
+
512
+ <Separator />
513
+
514
+ <Button type="submit" disabled={isPending}>
515
+ {isPending ? <Spinner className="mr-2 h-4 w-4 animate-spin" /> : null}
516
+ Save Policy
517
+ </Button>
518
+ </form>
519
+ </CardContent>
520
+ </Card>
521
+ );
522
+ }