create-nextblock 0.2.78 → 0.8.0

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 (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,251 @@
1
+ 'use server';
2
+
3
+ import { createClient } from '@nextblock-cms/db/server';
4
+ import { revalidatePath } from 'next/cache';
5
+ import { sendTwoFactorCodeEmail } from '../../../actions/twoFactorEmail';
6
+ import {
7
+ getSecuritySettings as readSecuritySettings,
8
+ saveSecuritySettings,
9
+ } from '../../../../lib/privacy/settings';
10
+ import {
11
+ MAX_TRUSTED_DEVICE_DAYS,
12
+ MIN_TRUSTED_DEVICE_DAYS,
13
+ type SecuritySettings,
14
+ } from '../../../../lib/privacy/types';
15
+ import {
16
+ createEmailChallenge,
17
+ issueTwoFactorVerifiedCookie,
18
+ clearTwoFactorVerifiedCookie,
19
+ verifyEmailChallenge,
20
+ } from '../../../../lib/auth/twoFactor';
21
+ import {
22
+ listTrustedDevices,
23
+ revokeAllTrustedDevices,
24
+ revokeTrustedDevice,
25
+ type TrustedDeviceRow,
26
+ } from '../../../../lib/auth/trustedDevices';
27
+
28
+ export interface SecurityPanelData {
29
+ email: string;
30
+ mfaEnabled: boolean;
31
+ mfaType: 'totp' | 'email' | null;
32
+ hasVerifiedTotp: boolean;
33
+ isAdmin: boolean;
34
+ globalSettings: SecuritySettings;
35
+ trustedDevices: TrustedDeviceRow[];
36
+ }
37
+
38
+ async function requireUser() {
39
+ const supabase = createClient();
40
+ const {
41
+ data: { user },
42
+ } = await supabase.auth.getUser();
43
+ if (!user) {
44
+ throw new Error('You must be signed in.');
45
+ }
46
+ return { supabase, user };
47
+ }
48
+
49
+ export async function getSecurityPanelData(): Promise<SecurityPanelData> {
50
+ const { supabase, user } = await requireUser();
51
+
52
+ const [{ data: settings }, { data: profile }, { data: factors }] = await Promise.all([
53
+ supabase
54
+ .from('user_security_settings')
55
+ .select('mfa_enabled, mfa_type')
56
+ .eq('user_id', user.id)
57
+ .maybeSingle(),
58
+ supabase.from('profiles').select('role').eq('id', user.id).single(),
59
+ supabase.auth.mfa.listFactors(),
60
+ ]);
61
+
62
+ // listFactors().totp only contains verified TOTP factors.
63
+ const hasVerifiedTotp = Boolean(factors?.totp && factors.totp.length > 0);
64
+
65
+ return {
66
+ email: user.email ?? '',
67
+ mfaEnabled: Boolean(settings?.mfa_enabled),
68
+ mfaType: (settings?.mfa_type as 'totp' | 'email' | null) ?? null,
69
+ hasVerifiedTotp,
70
+ isAdmin: profile?.role === 'ADMIN',
71
+ globalSettings: await readSecuritySettings(),
72
+ trustedDevices: await listTrustedDevices(user.id),
73
+ };
74
+ }
75
+
76
+ // --- Global policy (admin only) -------------------------------------------------
77
+
78
+ export async function updateGlobalSecuritySettings(formData: FormData) {
79
+ const { supabase, user } = await requireUser();
80
+ const { data: profile } = await supabase
81
+ .from('profiles')
82
+ .select('role')
83
+ .eq('id', user.id)
84
+ .single();
85
+ if (profile?.role !== 'ADMIN') {
86
+ throw new Error('Only administrators can change the global security policy.');
87
+ }
88
+
89
+ const days = Number.parseInt(formData.get('trusted_device_days')?.toString() ?? '', 10);
90
+ const settings: SecuritySettings = {
91
+ trusted_device_days: Number.isFinite(days)
92
+ ? Math.min(MAX_TRUSTED_DEVICE_DAYS, Math.max(MIN_TRUSTED_DEVICE_DAYS, days))
93
+ : 30,
94
+ enforce_staff_2fa: formData.get('enforce_staff_2fa') === 'true',
95
+ };
96
+
97
+ await saveSecuritySettings(settings);
98
+ revalidatePath('/cms/settings/security');
99
+ return { success: true, message: 'Security policy saved.' };
100
+ }
101
+
102
+ // --- TOTP enrollment ------------------------------------------------------------
103
+
104
+ export type EnrollTotpResult =
105
+ | { ok: true; factorId: string; qrCode: string; secret: string }
106
+ | { ok: false; error: string };
107
+
108
+ export async function startTotpEnrollment(): Promise<EnrollTotpResult> {
109
+ const { supabase } = await requireUser();
110
+
111
+ // Clear any half-finished factors so re-enrolling never hits a name clash.
112
+ // `listFactors().totp` only returns verified factors, so check `all`.
113
+ const { data: existing } = await supabase.auth.mfa.listFactors();
114
+ for (const factor of existing?.all ?? []) {
115
+ if (factor.factor_type === 'totp' && factor.status === 'unverified') {
116
+ await supabase.auth.mfa.unenroll({ factorId: factor.id });
117
+ }
118
+ }
119
+
120
+ const { data, error } = await supabase.auth.mfa.enroll({
121
+ factorType: 'totp',
122
+ friendlyName: `nextblock-totp-${Date.now()}`,
123
+ });
124
+
125
+ if (error || !data) {
126
+ return {
127
+ ok: false,
128
+ error:
129
+ error?.message ??
130
+ 'Could not start authenticator setup. Ensure TOTP MFA is enabled for this Supabase project.',
131
+ };
132
+ }
133
+
134
+ return {
135
+ ok: true,
136
+ factorId: data.id,
137
+ qrCode: data.totp.qr_code,
138
+ secret: data.totp.secret,
139
+ };
140
+ }
141
+
142
+ export async function verifyTotpEnrollment(formData: FormData) {
143
+ const { supabase, user } = await requireUser();
144
+ const factorId = formData.get('factorId')?.toString() ?? '';
145
+ const code = (formData.get('code')?.toString() ?? '').trim();
146
+
147
+ if (!factorId || !/^\d{6}$/.test(code)) {
148
+ throw new Error('Enter the 6-digit code from your authenticator app.');
149
+ }
150
+
151
+ const { data: challenge, error: challengeError } = await supabase.auth.mfa.challenge({
152
+ factorId,
153
+ });
154
+ if (challengeError || !challenge) {
155
+ throw new Error(challengeError?.message ?? 'Could not start verification.');
156
+ }
157
+
158
+ const { error: verifyError } = await supabase.auth.mfa.verify({
159
+ factorId,
160
+ challengeId: challenge.id,
161
+ code,
162
+ });
163
+ if (verifyError) {
164
+ throw new Error('That code was not valid. Please try again.');
165
+ }
166
+
167
+ const { error: upsertError } = await supabase.from('user_security_settings').upsert({
168
+ user_id: user.id,
169
+ mfa_enabled: true,
170
+ mfa_type: 'totp',
171
+ updated_at: new Date().toISOString(),
172
+ });
173
+ if (upsertError) {
174
+ throw new Error('Verified, but failed to save your preference. Please retry.');
175
+ }
176
+
177
+ revalidatePath('/cms/settings/security');
178
+ return { success: true, message: 'Authenticator app enabled.' };
179
+ }
180
+
181
+ // --- Email-code enrollment ------------------------------------------------------
182
+
183
+ export async function sendEmailEnrollmentCode() {
184
+ const { user } = await requireUser();
185
+ if (!user.email) {
186
+ throw new Error('Your account has no email address on file.');
187
+ }
188
+ const code = await createEmailChallenge(user.id);
189
+ await sendTwoFactorCodeEmail(user.email, code, 'enable email two-factor authentication');
190
+ return { success: true, message: `We sent a 6-digit code to ${user.email}.` };
191
+ }
192
+
193
+ export async function verifyEmailEnrollment(formData: FormData) {
194
+ const { supabase, user } = await requireUser();
195
+ const code = (formData.get('code')?.toString() ?? '').trim();
196
+
197
+ const ok = await verifyEmailChallenge(user.id, code);
198
+ if (!ok) {
199
+ throw new Error('That code was incorrect or expired. Request a new one.');
200
+ }
201
+
202
+ const { error } = await supabase.from('user_security_settings').upsert({
203
+ user_id: user.id,
204
+ mfa_enabled: true,
205
+ mfa_type: 'email',
206
+ updated_at: new Date().toISOString(),
207
+ });
208
+ if (error) {
209
+ throw new Error('Verified, but failed to save your preference. Please retry.');
210
+ }
211
+
212
+ // The user just proved control of their inbox, so this session is satisfied.
213
+ await issueTwoFactorVerifiedCookie(user.id);
214
+ revalidatePath('/cms/settings/security');
215
+ return { success: true, message: 'Email verification enabled.' };
216
+ }
217
+
218
+ // --- Disable / device management ------------------------------------------------
219
+
220
+ export async function disableMfa() {
221
+ const { supabase, user } = await requireUser();
222
+
223
+ const { data: factors } = await supabase.auth.mfa.listFactors();
224
+ for (const factor of factors?.all ?? []) {
225
+ await supabase.auth.mfa.unenroll({ factorId: factor.id });
226
+ }
227
+
228
+ await supabase.from('user_security_settings').upsert({
229
+ user_id: user.id,
230
+ mfa_enabled: false,
231
+ mfa_type: null,
232
+ updated_at: new Date().toISOString(),
233
+ });
234
+
235
+ await revokeAllTrustedDevices(user.id);
236
+ await clearTwoFactorVerifiedCookie();
237
+
238
+ revalidatePath('/cms/settings/security');
239
+ return { success: true, message: 'Two-factor authentication disabled.' };
240
+ }
241
+
242
+ export async function revokeTrustedDeviceAction(formData: FormData) {
243
+ const { user } = await requireUser();
244
+ const id = formData.get('id')?.toString() ?? '';
245
+ if (!id) {
246
+ throw new Error('Missing device id.');
247
+ }
248
+ await revokeTrustedDevice(user.id, id);
249
+ revalidatePath('/cms/settings/security');
250
+ return { success: true, message: 'Device revoked.' };
251
+ }
@@ -0,0 +1,453 @@
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
+ updateGlobalSecuritySettings,
32
+ verifyEmailEnrollment,
33
+ verifyTotpEnrollment,
34
+ type SecurityPanelData,
35
+ } from '../actions';
36
+
37
+ type EnrollMode = 'idle' | 'totp' | 'email';
38
+
39
+ function formatDate(value: string): string {
40
+ try {
41
+ return new Date(value).toLocaleDateString(undefined, {
42
+ year: 'numeric',
43
+ month: 'short',
44
+ day: 'numeric',
45
+ });
46
+ } catch {
47
+ return value;
48
+ }
49
+ }
50
+
51
+ export default function SecurityPanel({ data }: { data: SecurityPanelData }) {
52
+ const router = useRouter();
53
+ const [isPending, startTransition] = useTransition();
54
+ const [message, setMessage] = useState<Message | null>(null);
55
+
56
+ const [mode, setMode] = useState<EnrollMode>('idle');
57
+ const [totp, setTotp] = useState<{ factorId: string; qrCode: string; secret: string } | null>(
58
+ null,
59
+ );
60
+ const [emailSent, setEmailSent] = useState(false);
61
+ const [code, setCode] = useState('');
62
+
63
+ const run = (fn: () => Promise<unknown>, after?: () => void) => {
64
+ setMessage(null);
65
+ startTransition(async () => {
66
+ try {
67
+ const result = (await fn()) as { message?: string } | undefined;
68
+ if (result?.message) setMessage({ success: result.message });
69
+ after?.();
70
+ router.refresh();
71
+ } catch (error) {
72
+ setMessage({
73
+ error: error instanceof Error ? error.message : 'Something went wrong.',
74
+ });
75
+ }
76
+ });
77
+ };
78
+
79
+ const resetEnrollment = () => {
80
+ setMode('idle');
81
+ setTotp(null);
82
+ setEmailSent(false);
83
+ setCode('');
84
+ };
85
+
86
+ // --- Enrollment handlers ------------------------------------------------------
87
+
88
+ const beginTotp = () => {
89
+ setMessage(null);
90
+ setMode('totp');
91
+ setEmailSent(false);
92
+ startTransition(async () => {
93
+ const result = await startTotpEnrollment();
94
+ if (result.ok) {
95
+ setTotp({ factorId: result.factorId, qrCode: result.qrCode, secret: result.secret });
96
+ } else {
97
+ setMessage({ error: result.error });
98
+ setMode('idle');
99
+ }
100
+ });
101
+ };
102
+
103
+ const submitTotpCode = () => {
104
+ if (!totp) return;
105
+ const formData = new FormData();
106
+ formData.append('factorId', totp.factorId);
107
+ formData.append('code', code);
108
+ run(() => verifyTotpEnrollment(formData), resetEnrollment);
109
+ };
110
+
111
+ const beginEmail = () => {
112
+ setMode('email');
113
+ setTotp(null);
114
+ run(async () => {
115
+ const result = await sendEmailEnrollmentCode();
116
+ setEmailSent(true);
117
+ return result;
118
+ });
119
+ };
120
+
121
+ const submitEmailCode = () => {
122
+ const formData = new FormData();
123
+ formData.append('code', code);
124
+ run(() => verifyEmailEnrollment(formData), resetEnrollment);
125
+ };
126
+
127
+ const messageAlert = message && (
128
+ <Alert
129
+ variant={'success' in message ? 'success' : 'destructive'}
130
+ className="py-2 px-4"
131
+ >
132
+ <AlertDescription>
133
+ {'success' in message
134
+ ? message.success
135
+ : 'error' in message
136
+ ? message.error
137
+ : ''}
138
+ </AlertDescription>
139
+ </Alert>
140
+ );
141
+
142
+ return (
143
+ <>
144
+ {/* Status + per-user MFA */}
145
+ <Card>
146
+ <CardHeader>
147
+ <CardTitle className="flex items-center gap-2">
148
+ Two-Factor Authentication
149
+ {data.mfaEnabled ? (
150
+ <Badge variant="default">
151
+ Enabled · {data.mfaType === 'totp' ? 'Authenticator app' : 'Email code'}
152
+ </Badge>
153
+ ) : (
154
+ <Badge variant="secondary">Off</Badge>
155
+ )}
156
+ </CardTitle>
157
+ <CardDescription>
158
+ Add a second step when you sign in to the CMS. You can use an authenticator
159
+ app (TOTP) or a one-time code sent to <strong>{data.email}</strong>.
160
+ </CardDescription>
161
+ </CardHeader>
162
+ <CardContent className="space-y-6">
163
+ {messageAlert}
164
+
165
+ {data.mfaEnabled ? (
166
+ <div className="flex items-center justify-between rounded-lg border p-4">
167
+ <div className="text-sm">
168
+ <p className="font-medium">
169
+ {data.mfaType === 'totp'
170
+ ? 'Authenticator app is protecting your account.'
171
+ : 'Email verification is protecting your account.'}
172
+ </p>
173
+ <p className="text-slate-500">
174
+ Disabling 2FA also forgets all of your trusted devices.
175
+ </p>
176
+ </div>
177
+ <Button
178
+ variant="destructive"
179
+ disabled={isPending}
180
+ onClick={() => run(() => disableMfa(), resetEnrollment)}
181
+ >
182
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Disable 2FA'}
183
+ </Button>
184
+ </div>
185
+ ) : (
186
+ <div className="space-y-4">
187
+ {/* Method chooser */}
188
+ <div className="grid gap-3 sm:grid-cols-2">
189
+ <button
190
+ type="button"
191
+ onClick={beginTotp}
192
+ className={`rounded-lg border p-4 text-left transition hover:border-primary ${
193
+ mode === 'totp' ? 'border-primary ring-1 ring-primary' : ''
194
+ }`}
195
+ >
196
+ <p className="font-medium text-sm">Authenticator App (TOTP)</p>
197
+ <p className="text-xs text-slate-500">
198
+ Scan a QR code with Google Authenticator, 1Password, Authy, etc.
199
+ </p>
200
+ </button>
201
+ <button
202
+ type="button"
203
+ onClick={beginEmail}
204
+ className={`rounded-lg border p-4 text-left transition hover:border-primary ${
205
+ mode === 'email' ? 'border-primary ring-1 ring-primary' : ''
206
+ }`}
207
+ >
208
+ <p className="font-medium text-sm">Secure Email Code</p>
209
+ <p className="text-xs text-slate-500">
210
+ Receive a 6-digit code at your account email each time you sign in.
211
+ </p>
212
+ </button>
213
+ </div>
214
+
215
+ {/* TOTP enrollment */}
216
+ {mode === 'totp' && (
217
+ <div className="rounded-lg border p-4 space-y-4">
218
+ {totp ? (
219
+ <>
220
+ <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-start">
221
+ {/* eslint-disable-next-line @next/next/no-img-element */}
222
+ <img
223
+ src={totp.qrCode}
224
+ alt="Authenticator setup QR code"
225
+ className="h-44 w-44 rounded bg-white p-2"
226
+ />
227
+ <div className="text-sm space-y-2">
228
+ <p>1. Scan this QR code with your authenticator app.</p>
229
+ <p>
230
+ 2. Or enter this key manually:
231
+ <br />
232
+ <code className="break-all text-xs bg-muted px-1 py-0.5 rounded">
233
+ {totp.secret}
234
+ </code>
235
+ </p>
236
+ <p>3. Enter the current 6-digit code below.</p>
237
+ </div>
238
+ </div>
239
+ <div className="flex items-end gap-3">
240
+ <div className="space-y-1">
241
+ <Label htmlFor="totp_code">6-digit code</Label>
242
+ <Input
243
+ id="totp_code"
244
+ inputMode="numeric"
245
+ autoComplete="one-time-code"
246
+ maxLength={6}
247
+ value={code}
248
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
249
+ className="w-32 tracking-widest"
250
+ placeholder="000000"
251
+ />
252
+ </div>
253
+ <Button onClick={submitTotpCode} disabled={isPending || code.length !== 6}>
254
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
255
+ </Button>
256
+ <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
257
+ Cancel
258
+ </Button>
259
+ </div>
260
+ </>
261
+ ) : (
262
+ <div className="flex items-center gap-2 text-sm text-slate-500">
263
+ <Spinner className="h-4 w-4 animate-spin" /> Generating your QR code…
264
+ </div>
265
+ )}
266
+ </div>
267
+ )}
268
+
269
+ {/* Email enrollment */}
270
+ {mode === 'email' && (
271
+ <div className="rounded-lg border p-4 space-y-4">
272
+ <p className="text-sm">
273
+ {emailSent
274
+ ? `Enter the 6-digit code we sent to ${data.email}.`
275
+ : 'Sending a verification code to your email…'}
276
+ </p>
277
+ <div className="flex items-end gap-3">
278
+ <div className="space-y-1">
279
+ <Label htmlFor="email_code">6-digit code</Label>
280
+ <Input
281
+ id="email_code"
282
+ inputMode="numeric"
283
+ autoComplete="one-time-code"
284
+ maxLength={6}
285
+ value={code}
286
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
287
+ className="w-32 tracking-widest"
288
+ placeholder="000000"
289
+ disabled={!emailSent}
290
+ />
291
+ </div>
292
+ <Button
293
+ onClick={submitEmailCode}
294
+ disabled={isPending || code.length !== 6 || !emailSent}
295
+ >
296
+ {isPending ? <Spinner className="h-4 w-4 animate-spin" /> : 'Verify & enable'}
297
+ </Button>
298
+ <Button
299
+ variant="ghost"
300
+ onClick={beginEmail}
301
+ disabled={isPending}
302
+ title="Send a new code"
303
+ >
304
+ Resend
305
+ </Button>
306
+ <Button variant="ghost" onClick={resetEnrollment} disabled={isPending}>
307
+ Cancel
308
+ </Button>
309
+ </div>
310
+ </div>
311
+ )}
312
+ </div>
313
+ )}
314
+ </CardContent>
315
+ </Card>
316
+
317
+ {/* Trusted devices */}
318
+ <Card>
319
+ <CardHeader>
320
+ <CardTitle>Trusted Devices</CardTitle>
321
+ <CardDescription>
322
+ Devices where you chose &ldquo;Remember this device&rdquo; skip 2FA until the
323
+ trust expires. Revoke any you no longer recognize.
324
+ </CardDescription>
325
+ </CardHeader>
326
+ <CardContent>
327
+ {data.trustedDevices.length === 0 ? (
328
+ <p className="text-sm text-slate-500">No trusted devices.</p>
329
+ ) : (
330
+ <ul className="divide-y rounded-lg border">
331
+ {data.trustedDevices.map((device) => (
332
+ <li
333
+ key={device.id}
334
+ className="flex items-center justify-between gap-4 p-3 text-sm"
335
+ >
336
+ <div className="min-w-0">
337
+ <p className="truncate font-medium">
338
+ {device.browser_metadata || 'Unknown device'}
339
+ </p>
340
+ <p className="text-xs text-slate-500">
341
+ Trusted {formatDate(device.created_at)} · expires{' '}
342
+ {formatDate(device.expires_at)}
343
+ </p>
344
+ </div>
345
+ <Button
346
+ variant="outline"
347
+ size="sm"
348
+ disabled={isPending}
349
+ onClick={() => {
350
+ const formData = new FormData();
351
+ formData.append('id', device.id);
352
+ run(() => revokeTrustedDeviceAction(formData));
353
+ }}
354
+ >
355
+ Revoke
356
+ </Button>
357
+ </li>
358
+ ))}
359
+ </ul>
360
+ )}
361
+ </CardContent>
362
+ </Card>
363
+
364
+ {/* Admin global policy */}
365
+ {data.isAdmin && (
366
+ <AdminPolicyCard
367
+ initial={data.globalSettings}
368
+ isPending={isPending}
369
+ onSave={(formData) => run(() => updateGlobalSecuritySettings(formData))}
370
+ />
371
+ )}
372
+ </>
373
+ );
374
+ }
375
+
376
+ function AdminPolicyCard({
377
+ initial,
378
+ isPending,
379
+ onSave,
380
+ }: {
381
+ initial: SecurityPanelData['globalSettings'];
382
+ isPending: boolean;
383
+ onSave: (formData: FormData) => void;
384
+ }) {
385
+ const [days, setDays] = useState(String(initial.trusted_device_days));
386
+ const [enforce, setEnforce] = useState(initial.enforce_staff_2fa);
387
+ const formRef = useRef<HTMLFormElement>(null);
388
+
389
+ return (
390
+ <Card>
391
+ <CardHeader>
392
+ <CardTitle>Organization Policy (Admin)</CardTitle>
393
+ <CardDescription>
394
+ Applies to every staff member. Trusted-device trust is server-validated and
395
+ revocable at any time from this page.
396
+ </CardDescription>
397
+ </CardHeader>
398
+ <CardContent>
399
+ <form
400
+ ref={formRef}
401
+ onSubmit={(e) => {
402
+ e.preventDefault();
403
+ const formData = new FormData();
404
+ formData.append('trusted_device_days', days);
405
+ formData.append('enforce_staff_2fa', String(enforce));
406
+ onSave(formData);
407
+ }}
408
+ className="space-y-6"
409
+ >
410
+ <div className="space-y-2">
411
+ <Label htmlFor="trusted_device_days">Remember-device duration (days)</Label>
412
+ <Input
413
+ id="trusted_device_days"
414
+ type="number"
415
+ min={MIN_TRUSTED_DEVICE_DAYS}
416
+ max={MAX_TRUSTED_DEVICE_DAYS}
417
+ value={days}
418
+ onChange={(e) => setDays(e.target.value)}
419
+ className="w-40"
420
+ />
421
+ <p className="text-xs text-slate-500">
422
+ How long a &ldquo;Remember this device&rdquo; choice lasts before 2FA is
423
+ required again. Default 30 days, up to {MAX_TRUSTED_DEVICE_DAYS} (10 years).
424
+ </p>
425
+ </div>
426
+
427
+ <div className="flex items-start gap-3">
428
+ <Checkbox
429
+ id="enforce_staff_2fa"
430
+ checked={enforce}
431
+ onCheckedChange={(checked) => setEnforce(checked === true)}
432
+ className="mt-1"
433
+ />
434
+ <div className="space-y-1">
435
+ <Label htmlFor="enforce_staff_2fa">Encourage staff to enable 2FA</Label>
436
+ <p className="text-xs text-slate-500">
437
+ Surfaces a reminder for ADMIN/WRITER accounts that haven&rsquo;t set up a
438
+ second factor.
439
+ </p>
440
+ </div>
441
+ </div>
442
+
443
+ <Separator />
444
+
445
+ <Button type="submit" disabled={isPending}>
446
+ {isPending ? <Spinner className="mr-2 h-4 w-4 animate-spin" /> : null}
447
+ Save Policy
448
+ </Button>
449
+ </form>
450
+ </CardContent>
451
+ </Card>
452
+ );
453
+ }