create-snapify-app 0.0.0-stage → 1.1.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 (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +33 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +100 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /**
8
+ * Signature NOVALABS des versions approuvées : les sites n'installent que des paquets signés
9
+ * par une clé de confiance. La clé privée (SNAPIFY_SIGNING_KEY, chemin PEM) ne quitte jamais
10
+ * ce serveur et n'est jamais versionnée.
11
+ */
12
+ import fs from 'fs/promises';
13
+ import os from 'os';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { eq } from 'drizzle-orm';
17
+ import { db } from '../db/client';
18
+ import { mpProducts, mpVersions } from '../db/schema';
19
+ import { HttpError } from '../http';
20
+ import { decodePackage, encodePackage, sign, verify } from '../../sdk/package';
21
+ import type { LicensePayload, SignedLicense } from '../../sdk/index';
22
+ import { TRUSTED_KEYS } from '../modules/trust';
23
+ import { validateMarketplacePackage, type PackageValidationResult } from './packageValidate';
24
+
25
+ type Version = typeof mpVersions.$inferSelect;
26
+
27
+ export const PACKAGES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../data/marketplace-packages');
28
+
29
+ async function signingKey(): Promise<{ pem: string; kid: string } | null> {
30
+ const raw = process.env.SNAPIFY_SIGNING_KEY;
31
+ const kid = process.env.SNAPIFY_SIGNING_KID || 'novalabs-2026';
32
+ if (!raw) return null;
33
+ if (!TRUSTED_KEYS[kid]) throw new HttpError(503, `Clé de signature « ${kid} » absente de la liste des clés de confiance`);
34
+ const file = raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
35
+ return { pem: await fs.readFile(file, 'utf8'), kid };
36
+ }
37
+
38
+ function freeLicense(moduleId: string, key: { pem: string; kid: string }): SignedLicense {
39
+ const payload: LicensePayload = {
40
+ v: 1,
41
+ licenseId: `free-${moduleId}`,
42
+ moduleId,
43
+ licensee: 'Licence gratuite',
44
+ plan: 'free',
45
+ domains: ['*'],
46
+ issuedAt: new Date().toISOString(),
47
+ expiresAt: null,
48
+ };
49
+ return { payload, signature: sign(payload, key.pem, key.kid) };
50
+ }
51
+
52
+ /**
53
+ * Signe les versions non signées d'un produit au moment de l'approbation. Chaque paquet est
54
+ * revalidé avant signature ; une version gratuite reçoit la licence gratuite intégrée.
55
+ */
56
+ export async function signProductVersions(versions: Version[], pkgDir: string, free: boolean): Promise<Version[]> {
57
+ const pending = versions.filter((v) => !v.signed);
58
+ if (!pending.length) return [];
59
+ const key = await signingKey();
60
+ if (!key) throw new HttpError(503, 'Signature impossible : SNAPIFY_SIGNING_KEY n’est pas configurée sur le serveur de la marketplace.');
61
+
62
+ const signed: Version[] = [];
63
+ for (const ver of pending) {
64
+ const abs = path.resolve(pkgDir, path.basename(ver.packagePath));
65
+ if (!abs.startsWith(pkgDir + path.sep)) throw new HttpError(400, 'Chemin de paquet invalide');
66
+ const buf = await fs.readFile(abs).catch(() => {
67
+ throw new HttpError(404, `Fichier de la version ${ver.version} introuvable`);
68
+ });
69
+
70
+ const before = await validateMarketplacePackage(buf);
71
+ if (!before.ok || !before.manifest) {
72
+ const errors = before.checks.filter((c) => c.status === 'fail').map((c) => c.detail);
73
+ throw new HttpError(422, `Version ${ver.version} non conforme : ${errors.join(' ; ') || 'paquet invalide'}`);
74
+ }
75
+ if (ver.moduleId && before.manifest.id !== ver.moduleId) throw new HttpError(422, `Version ${ver.version} : identifiant inattendu (${before.manifest.id})`);
76
+
77
+ const { pkg } = decodePackage(buf);
78
+ pkg.signature = sign(pkg.manifest, key.pem, key.kid);
79
+ if (free) pkg.license = freeLicense(pkg.manifest.id, key);
80
+ else delete pkg.license;
81
+ if (!verify(pkg.manifest, pkg.signature, TRUSTED_KEYS)) throw new HttpError(500, 'La signature produite ne correspond à aucune clé de confiance');
82
+
83
+ const out = encodePackage(pkg);
84
+ const report: PackageValidationResult = await validateMarketplacePackage(out);
85
+ if (!report.ok || !report.signed) throw new HttpError(500, `Version ${ver.version} : le paquet signé ne passe pas la validation`);
86
+
87
+ const tmp = `${abs}.signing`;
88
+ await fs.writeFile(tmp, out);
89
+ await fs.rename(tmp, abs);
90
+ const [row] = await db
91
+ .update(mpVersions)
92
+ .set({ signed: true, sha256: report.sha256, size: report.size, validationReport: report as unknown as Record<string, unknown> })
93
+ .where(eq(mpVersions.id, ver.id))
94
+ .returning();
95
+ signed.push(row);
96
+ }
97
+ return signed;
98
+ }
99
+
100
+ /**
101
+ * Approbation d'un produit (nouveau ou mise à jour d'un produit déjà publié) : signe toutes ses
102
+ * versions en attente puis le publie. Seul chemin de publication, pour l'admin marketplace comme pour le superadmin.
103
+ */
104
+ export async function approveProduct(productId: string) {
105
+ const [p] = await db.select().from(mpProducts).where(eq(mpProducts.id, productId)).limit(1);
106
+ if (!p) throw new HttpError(404, 'Introuvable');
107
+ const versions = await db.select().from(mpVersions).where(eq(mpVersions.productId, p.id));
108
+ if (!versions.length) throw new HttpError(422, 'Aucune version à publier');
109
+ const signed = await signProductVersions(versions, PACKAGES_DIR, p.priceMillimes === 0);
110
+ const [updated] = await db
111
+ .update(mpProducts)
112
+ .set({ status: 'published', rejectionReason: null, updatedAt: new Date() })
113
+ .where(eq(mpProducts.id, p.id))
114
+ .returning();
115
+ return { product: updated, signed: signed.map((v) => v.version) };
116
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /**
8
+ * Synchronise la marketplace (DB + paquets) depuis marketplace/catalog.json —
9
+ * modules et thèmes officiels Snapify/NOVALABS. Aucun produit mock.
10
+ */
11
+ import crypto from 'crypto';
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { and, eq, inArray, like, or } from 'drizzle-orm';
16
+ import { db } from '../db/client';
17
+ import { mpCarts, mpOrderItems, mpProducts, mpPublishers, mpReviews, mpVersions, platformUsers } from '../db/schema';
18
+ import { hashPassword } from '../platform/passwords';
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const MARKETPLACE_ROOT = path.resolve(__dirname, '../../marketplace');
22
+ const PKG_DIR = path.resolve(__dirname, '../../data/marketplace-packages');
23
+ const UPLOAD_DIR = path.resolve(__dirname, '../../public/uploads/marketplace');
24
+
25
+ type CatalogEntry = {
26
+ id: string;
27
+ kind?: 'module' | 'theme';
28
+ name: string;
29
+ version: string;
30
+ description: string;
31
+ author?: { name?: string; email?: string; url?: string };
32
+ pricing?: 'free' | 'paid';
33
+ category?: string;
34
+ icon?: string;
35
+ permissions?: string[];
36
+ engines?: { snapify: string };
37
+ screenshot?: string;
38
+ gallery?: string[];
39
+ package: string;
40
+ sha256: string;
41
+ size: number;
42
+ signed?: boolean;
43
+ };
44
+
45
+ async function ensurePublisherFor(author?: CatalogEntry['author']): Promise<typeof mpPublishers.$inferSelect> {
46
+ const email = (author?.email || 'dev@snapify.tn').trim().toLowerCase();
47
+ const displayName = (author?.name || (email === 'dev@snapify.tn' ? 'NOVALABS WEB DESIGN' : email.split('@')[0])).trim();
48
+ const website = (author?.url || 'https://novalabs.tn').trim();
49
+
50
+ let [pubUser] = await db.select().from(platformUsers).where(eq(platformUsers.email, email)).limit(1);
51
+ if (!pubUser) {
52
+ const { hash, salt } = hashPassword(email === 'dev@snapify.tn' ? 'Developer1' : crypto.randomBytes(18).toString('base64url'));
53
+ const id = crypto.randomUUID();
54
+ [pubUser] = await db
55
+ .insert(platformUsers)
56
+ .values({
57
+ id,
58
+ email,
59
+ displayName,
60
+ passwordHash: hash,
61
+ salt,
62
+ role: 'publisher',
63
+ })
64
+ .returning();
65
+ } else if (pubUser.role === 'member') {
66
+ await db.update(platformUsers).set({ role: 'publisher', displayName: pubUser.displayName || displayName }).where(eq(platformUsers.id, pubUser.id));
67
+ }
68
+
69
+ let [publisher] = await db.select().from(mpPublishers).where(eq(mpPublishers.userId, pubUser.id)).limit(1);
70
+ if (!publisher) {
71
+ [publisher] = await db
72
+ .insert(mpPublishers)
73
+ .values({
74
+ id: crypto.randomUUID(),
75
+ userId: pubUser.id,
76
+ displayName,
77
+ website,
78
+ status: 'active',
79
+ paidAt: new Date(),
80
+ })
81
+ .returning();
82
+ } else if (publisher.status !== 'active' || publisher.displayName !== displayName) {
83
+ [publisher] = await db
84
+ .update(mpPublishers)
85
+ .set({
86
+ status: 'active',
87
+ paidAt: publisher.paidAt || new Date(),
88
+ displayName,
89
+ website: website || publisher.website,
90
+ })
91
+ .where(eq(mpPublishers.id, publisher.id))
92
+ .returning();
93
+ }
94
+ return publisher;
95
+ }
96
+
97
+ async function removeMockProducts() {
98
+ const mocks = await db
99
+ .select()
100
+ .from(mpProducts)
101
+ .where(
102
+ or(
103
+ like(mpProducts.slug, 'novalabs.demo-%'),
104
+ like(mpProducts.slug, 'demo.%'),
105
+ eq(mpProducts.slug, 'pack-seo-pro'),
106
+ eq(mpProducts.slug, 'novalabs.demo-seo'),
107
+ eq(mpProducts.slug, 'novalabs.demo-theme-lite')
108
+ )
109
+ );
110
+ if (!mocks.length) return;
111
+ const ids = mocks.map((p) => p.id);
112
+ await db.delete(mpVersions).where(inArray(mpVersions.productId, ids));
113
+ await db.delete(mpReviews).where(inArray(mpReviews.productId, ids));
114
+ await db.delete(mpOrderItems).where(inArray(mpOrderItems.productId, ids));
115
+ // paniers : retirer l’id mock de productIds
116
+ const carts = await db.select().from(mpCarts);
117
+ for (const c of carts) {
118
+ const next = (c.productIds || []).filter((id) => !ids.includes(id));
119
+ if (next.length !== (c.productIds || []).length) {
120
+ await db.update(mpCarts).set({ productIds: next, updatedAt: new Date() }).where(eq(mpCarts.userId, c.userId));
121
+ }
122
+ }
123
+ await db.delete(mpProducts).where(inArray(mpProducts.id, ids));
124
+ for (const p of mocks) console.log(`[marketplace] mock retiré : ${p.slug}`);
125
+ }
126
+
127
+ async function copyImage(rel: string, name: string): Promise<string | null> {
128
+ const src = path.resolve(MARKETPLACE_ROOT, rel);
129
+ if (!src.startsWith(MARKETPLACE_ROOT + path.sep)) return null;
130
+ try {
131
+ const buf = await fs.readFile(src);
132
+ await fs.mkdir(UPLOAD_DIR, { recursive: true });
133
+ const file = `${name}${path.extname(src).toLowerCase() || '.jpg'}`;
134
+ await fs.writeFile(path.join(UPLOAD_DIR, file), buf);
135
+ return `/uploads/marketplace/${file}`;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /** Couverture (capture du paquet) puis galerie `marketplace/gallery/<id>/`. */
142
+ async function copyScreenshots(entry: CatalogEntry): Promise<string[]> {
143
+ const base = entry.id.replace(/\./g, '-');
144
+ const urls: (string | null)[] = [];
145
+ if (entry.screenshot) urls.push(await copyImage(entry.screenshot, `${base}-shot`));
146
+ for (const [i, rel] of (Array.isArray(entry.gallery) ? entry.gallery.slice(0, 8) : []).entries()) {
147
+ if (typeof rel === 'string') urls.push(await copyImage(rel, `${base}-gallery-${i + 1}`));
148
+ }
149
+ return urls.filter((u): u is string => !!u);
150
+ }
151
+
152
+ export async function syncOfficialMarketplace(): Promise<{ synced: number }> {
153
+ await fs.mkdir(PKG_DIR, { recursive: true });
154
+ await fs.mkdir(UPLOAD_DIR, { recursive: true });
155
+ await removeMockProducts();
156
+
157
+ let catalog: { modules?: CatalogEntry[] };
158
+ try {
159
+ catalog = JSON.parse(await fs.readFile(path.join(MARKETPLACE_ROOT, 'catalog.json'), 'utf8'));
160
+ } catch (e) {
161
+ console.error('[marketplace] catalog.json introuvable — sync annulée', e);
162
+ return { synced: 0 };
163
+ }
164
+
165
+ const entries = Array.isArray(catalog.modules) ? catalog.modules : [];
166
+ if (!entries.length) {
167
+ console.warn('[marketplace] catalogue vide');
168
+ return { synced: 0 };
169
+ }
170
+
171
+ const publisherCache = new Map<string, typeof mpPublishers.$inferSelect>();
172
+ let synced = 0;
173
+
174
+ for (const entry of entries) {
175
+ if (!entry?.id || !entry.version || !entry.package) continue;
176
+ const kind = entry.kind === 'theme' ? 'theme' : 'module';
177
+ const pkgSrc = path.resolve(MARKETPLACE_ROOT, entry.package);
178
+ if (!pkgSrc.startsWith(MARKETPLACE_ROOT + path.sep)) continue;
179
+
180
+ let pkgBuf: Buffer;
181
+ try {
182
+ pkgBuf = await fs.readFile(pkgSrc);
183
+ } catch {
184
+ console.error(`[marketplace] paquet manquant : ${entry.package}`);
185
+ continue;
186
+ }
187
+
188
+ const filename = path.basename(pkgSrc);
189
+ await fs.writeFile(path.join(PKG_DIR, filename), pkgBuf);
190
+
191
+ const screenshots = await copyScreenshots(entry);
192
+ const shortDesc = (entry.description || '').slice(0, 280);
193
+ const longDesc = `<p>${entry.description || ''}</p><p>Éditeur : ${entry.author?.name || 'NOVALABS WEB DESIGN'}.</p>`;
194
+ const priceMillimes = entry.pricing === 'paid' ? 49_000 : 0;
195
+ const authorKey = (entry.author?.email || 'dev@snapify.tn').trim().toLowerCase();
196
+ let publisher = publisherCache.get(authorKey);
197
+ if (!publisher) {
198
+ publisher = await ensurePublisherFor(entry.author);
199
+ publisherCache.set(authorKey, publisher);
200
+ }
201
+
202
+ let [product] = await db.select().from(mpProducts).where(eq(mpProducts.slug, entry.id)).limit(1);
203
+ if (!product) {
204
+ const id = crypto.randomUUID();
205
+ [product] = await db
206
+ .insert(mpProducts)
207
+ .values({
208
+ id,
209
+ publisherId: publisher.id,
210
+ slug: entry.id,
211
+ name: entry.name,
212
+ kind,
213
+ shortDesc,
214
+ longDesc,
215
+ category: entry.category || (kind === 'theme' ? 'Apparence' : 'Autres'),
216
+ priceMillimes,
217
+ icon: entry.icon || (kind === 'theme' ? 'layout' : 'package'),
218
+ screenshots,
219
+ engines: entry.engines || { snapify: '^1.0.0' },
220
+ permissions: entry.permissions || [],
221
+ status: 'published',
222
+ })
223
+ .returning();
224
+ } else {
225
+ [product] = await db
226
+ .update(mpProducts)
227
+ .set({
228
+ publisherId: publisher.id,
229
+ name: entry.name,
230
+ kind,
231
+ shortDesc,
232
+ longDesc,
233
+ category: entry.category || product.category,
234
+ priceMillimes,
235
+ icon: screenshots[0] || entry.icon || product.icon,
236
+ screenshots: screenshots.length ? screenshots : product.screenshots,
237
+ engines: entry.engines || product.engines,
238
+ permissions: entry.permissions || product.permissions,
239
+ status: 'published',
240
+ updatedAt: new Date(),
241
+ })
242
+ .where(eq(mpProducts.id, product.id))
243
+ .returning();
244
+ }
245
+
246
+ const [existingVer] = await db
247
+ .select()
248
+ .from(mpVersions)
249
+ .where(and(eq(mpVersions.productId, product.id), eq(mpVersions.version, entry.version)))
250
+ .limit(1);
251
+
252
+ if (!existingVer) {
253
+ await db.insert(mpVersions).values({
254
+ id: crypto.randomUUID(),
255
+ productId: product.id,
256
+ version: entry.version,
257
+ changelog: `Version ${entry.version} (catalogue officiel Snapify)`,
258
+ packagePath: filename,
259
+ sha256: entry.sha256,
260
+ size: entry.size || pkgBuf.length,
261
+ signed: entry.signed !== false,
262
+ moduleId: entry.id,
263
+ });
264
+ } else {
265
+ await db
266
+ .update(mpVersions)
267
+ .set({
268
+ packagePath: filename,
269
+ sha256: entry.sha256,
270
+ size: entry.size || pkgBuf.length,
271
+ signed: entry.signed !== false,
272
+ moduleId: entry.id,
273
+ })
274
+ .where(eq(mpVersions.id, existingVer.id));
275
+ }
276
+
277
+ synced += 1;
278
+ }
279
+
280
+ console.log(`[marketplace] sync officiel : ${synced} produit(s) depuis catalog.json`);
281
+ return { synced };
282
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /** Images envoyées en base64 (photos de profil…) : contrôle du format réel, enregistrement et suppression. */
8
+ import crypto from 'crypto';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { getDataPaths } from '../cms/store';
12
+
13
+ const FORMATS = {
14
+ 'image/jpeg': { ext: 'jpg', magic: (b: Buffer) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
15
+ 'image/png': { ext: 'png', magic: (b: Buffer) => b.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) },
16
+ 'image/webp': { ext: 'webp', magic: (b: Buffer) => b.subarray(0, 4).toString('latin1') === 'RIFF' && b.subarray(8, 12).toString('latin1') === 'WEBP' },
17
+ } as const;
18
+
19
+ export type ImageMime = keyof typeof FORMATS;
20
+
21
+ export class ImageError extends Error {
22
+ constructor(
23
+ public status: number,
24
+ message: string
25
+ ) {
26
+ super(message);
27
+ }
28
+ }
29
+
30
+ /** Décode et vérifie une image : le type annoncé doit correspondre à la signature du fichier. */
31
+ export function decodeImage(input: { dataBase64?: unknown; mimeType?: unknown }, maxBytes: number): { buf: Buffer; ext: string } {
32
+ const mime = String(input.mimeType || '') as ImageMime;
33
+ const format = FORMATS[mime];
34
+ if (!format) throw new ImageError(415, 'Format non pris en charge : JPG, PNG ou WebP uniquement.');
35
+ const raw = String(input.dataBase64 || '').replace(/^data:[^;]+;base64,/, '');
36
+ if (!raw || !/^[A-Za-z0-9+/=\s]+$/.test(raw)) throw new ImageError(400, 'Image illisible.');
37
+ const buf = Buffer.from(raw, 'base64');
38
+ if (buf.length > maxBytes) throw new ImageError(413, `Image trop lourde (${Math.round(maxBytes / 1024)} Ko max.).`);
39
+ if (buf.length < 12 || !format.magic(buf)) throw new ImageError(415, 'Le fichier n’est pas une image valide.');
40
+ return { buf, ext: format.ext };
41
+ }
42
+
43
+ const SUBDIR_PATTERN = /^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*$/;
44
+
45
+ /** Enregistre l'image sous public/uploads/<subdir>/ avec un nom imprévisible ; renvoie son URL publique. */
46
+ export async function saveImage(subdir: string, prefix: string, buf: Buffer, ext: string): Promise<string> {
47
+ if (!SUBDIR_PATTERN.test(subdir)) throw new Error(`Dossier d'images invalide : ${subdir}`);
48
+ const { UPLOADS_DIR } = getDataPaths();
49
+ const dir = path.join(UPLOADS_DIR, subdir);
50
+ await fs.mkdir(dir, { recursive: true });
51
+ const safePrefix = prefix.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 40) || 'image';
52
+ const name = `${safePrefix}-${crypto.randomBytes(8).toString('hex')}.${ext}`;
53
+ await fs.writeFile(path.join(dir, name), buf, { mode: 0o644 });
54
+ return `/uploads/${subdir}/${name}`;
55
+ }
56
+
57
+ /** Supprime une image précédemment enregistrée dans ce dossier ; ignore toute autre URL. */
58
+ export async function removeImage(subdir: string, url: string | undefined | null): Promise<void> {
59
+ if (!url || !SUBDIR_PATTERN.test(subdir) || !url.startsWith(`/uploads/${subdir}/`)) return;
60
+ const name = url.slice(`/uploads/${subdir}/`.length);
61
+ if (!/^[a-z0-9-]+\.(jpg|png|webp)$/.test(name)) return;
62
+ await fs.rm(path.join(getDataPaths().UPLOADS_DIR, subdir, name), { force: true }).catch(() => undefined);
63
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ import type { Request, Response, NextFunction } from 'express';
8
+ import { passwordExpired, tokenMatchesUser, verifyToken } from '../cms/auth';
9
+ import { loadSnapshot } from '../cms/store';
10
+ import type { AdminUser, CmsSnapshot } from '../cms/types';
11
+ import { rolePermissions, SUPERADMIN_ROLE, type Permission } from '../cms/permissions';
12
+ import { HttpError } from '../http';
13
+ import { getSession, isSessionActive, touchSession } from '../security/sessions';
14
+ import { clientIp, ipAllowed } from '../security/ip';
15
+ import { hit, rateLimitError } from '../security/rateLimit';
16
+
17
+ export interface AdminContext {
18
+ sub: string;
19
+ email: string;
20
+ name: string;
21
+ role: string;
22
+ sid: string;
23
+ isSuper: boolean;
24
+ permissions: Permission[];
25
+ }
26
+
27
+ export interface AuthedRequest extends Request {
28
+ admin?: AdminContext;
29
+ }
30
+
31
+ export function permissionsOf(s: CmsSnapshot, user: AdminUser): Permission[] {
32
+ return rolePermissions(s.roles, user.role);
33
+ }
34
+
35
+ /** Étapes obligatoires avant d'accéder à l'administration. */
36
+ export function pendingRequirements(s: CmsSnapshot, user: AdminUser) {
37
+ const policy = s.system.security;
38
+ const twoFactorRequired = policy.twoFactor === 'all' || (policy.twoFactor === 'superadmins' && user.role === SUPERADMIN_ROLE);
39
+ return {
40
+ mustChangePassword: !!user.mustChangePassword || passwordExpired(user, policy.passwordExpiryDays),
41
+ mustSetup2fa: twoFactorRequired && !user.totpEnabled,
42
+ twoFactorRequired,
43
+ };
44
+ }
45
+
46
+ const unauthorized = (message = 'Session expirée') => new HttpError(401, message, 'SESSION_EXPIRED');
47
+
48
+ async function authenticate(req: AuthedRequest, res: Response): Promise<{ user: AdminUser; snap: CmsSnapshot }> {
49
+ const header = req.headers.authorization;
50
+ const token = header?.startsWith('Bearer ') ? header.slice(7) : undefined;
51
+ if (!token) throw new HttpError(401, 'Non authentifié', 'SESSION_EXPIRED');
52
+
53
+ const payload = verifyToken(token);
54
+ if (!payload) throw unauthorized();
55
+
56
+ const snap = await loadSnapshot();
57
+ const user = snap.users.find((u) => u.id === payload.sub);
58
+ if (!user || !tokenMatchesUser(payload, user)) throw unauthorized();
59
+ if (user.status !== 'active') throw unauthorized(user.status === 'suspended' ? 'Compte suspendu' : 'Session expirée');
60
+
61
+ const session = await getSession(payload.sid);
62
+ if (!session || session.userId !== user.id || !isSessionActive(session, snap.system.security.idleMinutes)) {
63
+ throw unauthorized(session && !session.revokedAt && session.expiresAt.getTime() > Date.now() ? 'Session fermée après inactivité' : 'Session expirée');
64
+ }
65
+ touchSession(session.id, clientIp(req));
66
+
67
+ const quota = hit(`admin:${user.id}`, snap.system.limits.adminApiPerMinute, 60_000);
68
+ if (!quota.ok) throw rateLimitError(res, quota, 'Trop de requêtes');
69
+
70
+ req.admin = {
71
+ sub: user.id,
72
+ email: user.email,
73
+ name: user.name,
74
+ role: user.role,
75
+ sid: session.id,
76
+ isSuper: user.role === SUPERADMIN_ROLE,
77
+ permissions: permissionsOf(snap, user),
78
+ };
79
+ return { user, snap };
80
+ }
81
+
82
+ /**
83
+ * Toute personne connectée. Tant qu'un changement de mot de passe ou l'activation de la 2FA est exigé,
84
+ * seules les routes marquées `allowPending` (profil, déconnexion) répondent.
85
+ */
86
+ export function requireAuth(opts: { allowPending?: boolean } = {}) {
87
+ return async (req: AuthedRequest, res: Response, next: NextFunction) => {
88
+ try {
89
+ const { user, snap } = await authenticate(req, res);
90
+ if (!opts.allowPending) {
91
+ const pending = pendingRequirements(snap, user);
92
+ if (pending.mustChangePassword) throw new HttpError(403, 'Vous devez changer votre mot de passe', 'PASSWORD_CHANGE_REQUIRED');
93
+ if (pending.mustSetup2fa) throw new HttpError(403, 'Activez la double authentification pour continuer', 'TWO_FACTOR_REQUIRED');
94
+ }
95
+ next();
96
+ } catch (e) {
97
+ next(e);
98
+ }
99
+ };
100
+ }
101
+
102
+ /** Au moins une des permissions demandées (le super administrateur les a toutes). */
103
+ export function requirePerm(...perms: Permission[]) {
104
+ const auth = requireAuth();
105
+ return (req: AuthedRequest, res: Response, next: NextFunction) =>
106
+ auth(req, res, (err?: unknown) => {
107
+ if (err) return next(err);
108
+ if (!perms.some((p) => req.admin!.permissions.includes(p))) return next(new HttpError(403, 'Droits insuffisants', 'FORBIDDEN'));
109
+ next();
110
+ });
111
+ }
112
+
113
+ export const requireAdmin = () => requireAuth();
114
+
115
+ export const hasPerm = (req: AuthedRequest, ...perms: Permission[]) => perms.some((p) => !!req.admin?.permissions.includes(p));
116
+
117
+ export function assertPerm(req: AuthedRequest, perm: Permission, message = 'Droits insuffisants') {
118
+ if (!hasPerm(req, perm)) throw new HttpError(403, message, 'FORBIDDEN');
119
+ }
120
+
121
+ /** Restreint toute l'API d'administration aux adresses autorisées (contournement : SNAPIFY_ADMIN_IP_BYPASS=true). */
122
+ export async function adminIpGuard(req: Request, _res: Response, next: NextFunction) {
123
+ try {
124
+ if (process.env.SNAPIFY_ADMIN_IP_BYPASS === 'true') return next();
125
+ const s = await loadSnapshot();
126
+ const ip = clientIp(req);
127
+ if (!ipAllowed(ip, s.system.security.adminIpAllowlist)) {
128
+ return next(new HttpError(403, `Administration inaccessible depuis votre adresse IP (${ip})`, 'IP_BLOCKED'));
129
+ }
130
+ next();
131
+ } catch (e) {
132
+ next(e);
133
+ }
134
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ /**
8
+ * Modules installés d'office au premier démarrage, avec reprise des réglages de l'époque
9
+ * où WhatsApp et l'assistant IA faisaient partie du cœur du CMS (clés `whatsapp` et `ai`
10
+ * de site_settings, supprimées après migration).
11
+ */
12
+ type Legacy = Record<string, unknown> | undefined;
13
+
14
+ export interface OfficialModule {
15
+ id: string;
16
+ legacyKey: string;
17
+ settings: (legacy: Legacy) => Record<string, unknown>;
18
+ enabled: (legacy: Legacy) => boolean;
19
+ }
20
+
21
+ const pick = (src: Legacy, keys: string[]) => Object.fromEntries(keys.filter((k) => src && k in src).map((k) => [k, src![k]]));
22
+
23
+ export const OFFICIAL_MODULES: OfficialModule[] = [
24
+ {
25
+ id: 'novalabs.whatsapp',
26
+ legacyKey: 'whatsapp',
27
+ settings: (l) => pick(l, ['number', 'defaultMessage', 'showOnMobile', 'showOnDesktop', 'position']),
28
+ enabled: (l) => l?.enabled !== false,
29
+ },
30
+ {
31
+ id: 'novalabs.ai-chat',
32
+ legacyKey: 'ai',
33
+ settings: (l) => {
34
+ const out: Record<string, unknown> = pick(l, ['model', 'temperature', 'maxMessagesPerSession', 'maxSessionsPerDayPerIp', 'maxTokensPerDayGlobal']);
35
+ const override = typeof l?.systemInstructionOverride === 'string' ? l.systemInstructionOverride.trim() : '';
36
+ if (override) out.systemInstruction = override;
37
+ if (Array.isArray(l?.showOnPages)) out.showOnPages = l.showOnPages;
38
+ return out;
39
+ },
40
+ enabled: (l) => l?.enabled !== false,
41
+ },
42
+ ];
43
+
44
+ export const LEGACY_KEYS = OFFICIAL_MODULES.map((m) => m.legacyKey);