@silexlabs/silex 3.0.1-alpha.0 → 3.0.2

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 (323) hide show
  1. package/.env.default +20 -0
  2. package/.silex.js +67 -8
  3. package/LICENSE +661 -203
  4. package/README.md +37 -35
  5. package/dist/client/client/api.d.ts +90 -0
  6. package/dist/client/client/assetUrl.d.ts +54 -0
  7. package/dist/client/client/config.d.ts +102 -0
  8. package/dist/client/client/events.d.ts +23 -0
  9. package/dist/client/client/expose.d.ts +8 -0
  10. package/dist/client/client/grapesjs/LoginDialog.d.ts +15 -0
  11. package/dist/client/client/grapesjs/PublicationManager.d.ts +73 -0
  12. package/dist/client/client/grapesjs/PublicationUi.d.ts +59 -0
  13. package/dist/client/client/grapesjs/blocks.d.ts +1 -0
  14. package/dist/client/client/grapesjs/breadcrumbs.d.ts +1 -0
  15. package/dist/client/client/grapesjs/css-props.d.ts +6 -0
  16. package/dist/client/client/grapesjs/footer.d.ts +2 -0
  17. package/dist/client/client/grapesjs/img.d.ts +6 -0
  18. package/dist/client/client/grapesjs/index.d.ts +9 -0
  19. package/dist/client/client/grapesjs/internal-links.d.ts +1 -0
  20. package/dist/client/client/grapesjs/keymaps.d.ts +123 -0
  21. package/dist/client/client/grapesjs/new-page-dialog.d.ts +2 -0
  22. package/dist/client/client/grapesjs/openImport.d.ts +9 -0
  23. package/dist/client/client/grapesjs/page-panel.d.ts +9 -0
  24. package/dist/client/client/grapesjs/project-bar.d.ts +21 -0
  25. package/dist/client/client/grapesjs/resize-panel.d.ts +1 -0
  26. package/dist/client/client/grapesjs/rich-text.d.ts +4 -0
  27. package/dist/client/client/grapesjs/semantic.d.ts +1 -0
  28. package/dist/client/client/grapesjs/settings-sections.d.ts +16 -0
  29. package/dist/client/client/grapesjs/settings.d.ts +4 -0
  30. package/dist/client/client/grapesjs/storage.d.ts +2 -0
  31. package/dist/client/client/grapesjs/symbolDialogs.d.ts +3 -0
  32. package/dist/client/client/grapesjs/view-buttons.d.ts +1 -0
  33. package/dist/client/client/index.d.ts +10 -0
  34. package/dist/client/client/publication-transformers.d.ts +42 -0
  35. package/dist/client/client/publish-custom-code-block.d.ts +4 -0
  36. package/dist/client/client/publish-fonts.d.ts +4 -0
  37. package/dist/client/client/utils.d.ts +22 -0
  38. package/dist/client/constants.d.ts +32 -0
  39. package/dist/client/css/admin.css +1114 -328
  40. package/dist/client/css/admin.css.map +1 -0
  41. package/dist/client/grapesjs-filter-styles/dist/index.d.ts +4 -0
  42. package/dist/client/grapesjs-fonts/dist/index.d.ts +4 -0
  43. package/dist/client/grapesjs-fonts/src/commands.d.ts +3 -0
  44. package/dist/client/grapesjs-fonts/src/fonts.d.ts +4 -0
  45. package/dist/client/grapesjs-storage-rate-limit/dist/index.d.ts +4 -0
  46. package/dist/client/grapesjs-ui-suggest-classes/dist/index.d.ts +4 -0
  47. package/dist/client/index.html +3 -2
  48. package/dist/client/js/main.js +356 -131
  49. package/dist/client/js/main.js.LICENSE.txt +15 -1
  50. package/dist/client/page.d.ts +2 -0
  51. package/dist/client/silex-lib/src/ts/client/api.d.ts +90 -0
  52. package/dist/client/silex-lib/src/ts/client/assetUrl.d.ts +54 -0
  53. package/dist/client/silex-lib/src/ts/client/config.d.ts +102 -0
  54. package/dist/client/silex-lib/src/ts/client/events.d.ts +23 -0
  55. package/dist/client/silex-lib/src/ts/client/expose.d.ts +8 -0
  56. package/dist/client/silex-lib/src/ts/client/grapesjs/LoginDialog.d.ts +15 -0
  57. package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationManager.d.ts +73 -0
  58. package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationUi.d.ts +59 -0
  59. package/dist/client/silex-lib/src/ts/client/grapesjs/blocks.d.ts +1 -0
  60. package/dist/client/silex-lib/src/ts/client/grapesjs/breadcrumbs.d.ts +1 -0
  61. package/dist/client/silex-lib/src/ts/client/grapesjs/css-props.d.ts +6 -0
  62. package/dist/client/silex-lib/src/ts/client/grapesjs/footer.d.ts +2 -0
  63. package/dist/client/silex-lib/src/ts/client/grapesjs/img.d.ts +6 -0
  64. package/dist/client/silex-lib/src/ts/client/grapesjs/index.d.ts +9 -0
  65. package/dist/client/silex-lib/src/ts/client/grapesjs/internal-links.d.ts +1 -0
  66. package/dist/client/silex-lib/src/ts/client/grapesjs/keymaps.d.ts +123 -0
  67. package/dist/client/silex-lib/src/ts/client/grapesjs/new-page-dialog.d.ts +2 -0
  68. package/dist/client/silex-lib/src/ts/client/grapesjs/openImport.d.ts +9 -0
  69. package/dist/client/silex-lib/src/ts/client/grapesjs/page-panel.d.ts +9 -0
  70. package/dist/client/silex-lib/src/ts/client/grapesjs/project-bar.d.ts +21 -0
  71. package/dist/client/silex-lib/src/ts/client/grapesjs/resize-panel.d.ts +1 -0
  72. package/dist/client/silex-lib/src/ts/client/grapesjs/rich-text.d.ts +4 -0
  73. package/dist/client/silex-lib/src/ts/client/grapesjs/semantic.d.ts +1 -0
  74. package/dist/client/silex-lib/src/ts/client/grapesjs/settings-sections.d.ts +16 -0
  75. package/dist/client/silex-lib/src/ts/client/grapesjs/settings.d.ts +4 -0
  76. package/dist/client/silex-lib/src/ts/client/grapesjs/storage.d.ts +2 -0
  77. package/dist/client/silex-lib/src/ts/client/grapesjs/symbolDialogs.d.ts +3 -0
  78. package/dist/client/silex-lib/src/ts/client/grapesjs/view-buttons.d.ts +1 -0
  79. package/dist/client/silex-lib/src/ts/client/index.d.ts +10 -0
  80. package/dist/client/silex-lib/src/ts/client/publication-transformers.d.ts +42 -0
  81. package/dist/client/silex-lib/src/ts/client/publish-custom-code-block.d.ts +4 -0
  82. package/dist/client/silex-lib/src/ts/client/publish-fonts.d.ts +4 -0
  83. package/dist/client/silex-lib/src/ts/client/utils.d.ts +22 -0
  84. package/dist/client/silex-lib/src/ts/constants.d.ts +32 -0
  85. package/dist/client/silex-lib/src/ts/page.d.ts +2 -0
  86. package/dist/client/silex-lib/src/ts/types.d.ts +339 -0
  87. package/dist/client/types.d.ts +339 -0
  88. package/dist/plugins/client/client/events.d.ts +23 -0
  89. package/dist/plugins/client/client/events.js +43 -0
  90. package/dist/plugins/client/client/events.js.map +1 -0
  91. package/dist/plugins/client/plugins/client/template.d.ts +2 -0
  92. package/dist/plugins/client/plugins/client/template.js +234 -0
  93. package/dist/plugins/client/plugins/client/template.js.map +1 -0
  94. package/dist/plugins/client/plugins/client/website-info.d.ts +1 -0
  95. package/dist/plugins/client/plugins/client/website-info.js +72 -0
  96. package/dist/plugins/client/plugins/client/website-info.js.map +1 -0
  97. package/dist/plugins/client/types.d.ts +339 -0
  98. package/dist/plugins/client/types.js +80 -0
  99. package/dist/plugins/client/types.js.map +1 -0
  100. package/dist/plugins/server/constants.d.ts +32 -0
  101. package/dist/plugins/server/constants.js +76 -0
  102. package/dist/plugins/server/constants.js.map +1 -0
  103. package/dist/plugins/server/plugins/server/Cache.d.ts +2 -0
  104. package/dist/plugins/server/plugins/server/Cache.js +36 -0
  105. package/dist/plugins/server/plugins/server/Cache.js.map +1 -0
  106. package/dist/plugins/server/plugins/server/DownloadConnector.d.ts +29 -0
  107. package/dist/plugins/server/plugins/server/DownloadConnector.js +127 -0
  108. package/dist/plugins/server/plugins/server/DownloadConnector.js.map +1 -0
  109. package/dist/plugins/server/plugins/server/FtpConnector.d.ts +86 -0
  110. package/dist/plugins/server/plugins/server/FtpConnector.js +536 -0
  111. package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -0
  112. package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +135 -0
  113. package/dist/plugins/server/plugins/server/GitlabConnector.js +649 -0
  114. package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -0
  115. package/dist/plugins/server/plugins/server/GitlabHostingConnector.d.ts +16 -0
  116. package/dist/plugins/server/plugins/server/GitlabHostingConnector.js +209 -0
  117. package/dist/plugins/server/plugins/server/GitlabHostingConnector.js.map +1 -0
  118. package/dist/plugins/server/plugins/server/PublicationOptimizer.d.ts +0 -0
  119. package/dist/plugins/server/plugins/server/PublicationOptimizer.js +14 -0
  120. package/dist/plugins/server/plugins/server/PublicationOptimizer.js.map +1 -0
  121. package/dist/plugins/server/plugins/server/SslPlugin.d.ts +1 -0
  122. package/dist/plugins/server/plugins/server/SslPlugin.js +86 -0
  123. package/dist/plugins/server/plugins/server/SslPlugin.js.map +1 -0
  124. package/dist/plugins/server/plugins/server/StaticPlugin.d.ts +8 -0
  125. package/dist/plugins/server/plugins/server/StaticPlugin.js +45 -0
  126. package/dist/plugins/server/plugins/server/StaticPlugin.js.map +1 -0
  127. package/dist/plugins/server/server/config.d.ts +59 -0
  128. package/dist/plugins/server/server/config.js +181 -0
  129. package/dist/plugins/server/server/config.js.map +1 -0
  130. package/dist/plugins/server/server/connectors/FsHosting.d.ts +14 -0
  131. package/dist/plugins/server/server/connectors/FsHosting.js +66 -0
  132. package/dist/plugins/server/server/connectors/FsHosting.js.map +1 -0
  133. package/dist/plugins/server/server/connectors/FsStorage.d.ts +44 -0
  134. package/dist/plugins/server/server/connectors/FsStorage.js +261 -0
  135. package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -0
  136. package/dist/plugins/server/server/connectors/connectors.d.ts +86 -0
  137. package/dist/plugins/server/server/connectors/connectors.js +126 -0
  138. package/dist/plugins/server/server/connectors/connectors.js.map +1 -0
  139. package/dist/plugins/server/server/events.d.ts +26 -0
  140. package/dist/plugins/server/server/events.js +31 -0
  141. package/dist/plugins/server/server/events.js.map +1 -0
  142. package/dist/plugins/server/server/jobs.d.ts +12 -0
  143. package/dist/plugins/server/server/jobs.js +79 -0
  144. package/dist/plugins/server/server/jobs.js.map +1 -0
  145. package/dist/plugins/server/server/utils/validation.d.ts +7 -0
  146. package/dist/plugins/server/server/utils/validation.js +37 -0
  147. package/dist/plugins/server/server/utils/validation.js.map +1 -0
  148. package/dist/plugins/server/types.d.ts +339 -0
  149. package/dist/plugins/server/types.js +84 -0
  150. package/dist/plugins/server/types.js.map +1 -0
  151. package/dist/server/constants.js +55 -0
  152. package/dist/server/plugins/server/Cache.js +19 -0
  153. package/dist/server/server/api/api.js +20 -0
  154. package/dist/server/server/api/connectorApi.js +334 -0
  155. package/dist/server/server/api/publicationApi.js +106 -0
  156. package/dist/server/server/api/websiteApi.js +315 -0
  157. package/dist/server/server/cli.js +71 -0
  158. package/dist/server/server/config.js +120 -0
  159. package/dist/server/server/connectors/FsHosting.js +47 -0
  160. package/dist/server/server/connectors/FsStorage.js +228 -0
  161. package/dist/server/server/connectors/connectors.js +88 -0
  162. package/dist/server/server/events.js +14 -0
  163. package/dist/server/server/expose.js +27 -0
  164. package/dist/server/server/express.js +46 -0
  165. package/dist/server/server/index.js +92 -0
  166. package/dist/server/server/jobs.js +50 -0
  167. package/dist/server/server/utils/validation.js +14 -0
  168. package/dist/server/types.js +40 -0
  169. package/package.json +99 -49
  170. package/public/assets/download.png +0 -0
  171. package/public/assets/favicon.ico +0 -0
  172. package/public/assets/favicon.png +0 -0
  173. package/public/assets/ftp.png +0 -0
  174. package/public/assets/gitlab.png +0 -0
  175. package/public/assets/gitlab2.png +0 -0
  176. package/public/assets/laptop.png +0 -0
  177. package/public/assets/logo-silex-small.png +0 -0
  178. package/public/silex.js +22 -23
  179. package/src/css/_variables.scss +24 -20
  180. package/src/css/dialog.scss +35 -2
  181. package/src/css/fonts.scss +31 -0
  182. package/src/css/footer.scss +12 -0
  183. package/src/css/grapesjs-light.scss +180 -0
  184. package/src/css/grapesjs-plugins.scss +227 -0
  185. package/src/css/grapesjs.scss +159 -1
  186. package/src/css/index.scss +12 -1
  187. package/src/css/light.scss +7 -0
  188. package/src/css/loader.scss +2 -0
  189. package/src/css/project-bar.scss +32 -16
  190. package/src/css/silex.scss +79 -5
  191. package/src/css/website-info.scss +14 -0
  192. package/src/html/index.pug +3 -3
  193. package/src/html/templates/includes.pug +0 -1
  194. package/src/ts/client/api.ts +156 -0
  195. package/src/ts/client/assetUrl.test.ts +164 -0
  196. package/src/ts/client/assetUrl.ts +254 -0
  197. package/src/ts/client/config.ts +175 -13
  198. package/src/ts/client/events.ts +43 -0
  199. package/src/ts/client/expose.ts +27 -1
  200. package/src/ts/client/grapesjs/LoginDialog.ts +204 -0
  201. package/src/ts/client/grapesjs/PublicationManager.ts +433 -0
  202. package/src/ts/client/grapesjs/PublicationUi.ts +366 -0
  203. package/src/ts/client/grapesjs/blocks.ts +17 -0
  204. package/src/ts/client/grapesjs/breadcrumbs.ts +79 -0
  205. package/src/ts/client/grapesjs/css-props.ts +910 -0
  206. package/src/ts/client/grapesjs/eleventy.ts +30 -7
  207. package/src/ts/client/grapesjs/footer.ts +42 -0
  208. package/src/ts/client/grapesjs/img.ts +147 -0
  209. package/src/ts/client/grapesjs/index.ts +311 -122
  210. package/src/ts/client/grapesjs/internal-links.test.ts +19 -1
  211. package/src/ts/client/grapesjs/internal-links.ts +31 -7
  212. package/src/ts/client/grapesjs/keymaps.ts +207 -0
  213. package/src/ts/client/grapesjs/new-page-dialog.ts +19 -3
  214. package/src/ts/client/grapesjs/openImport.ts +22 -0
  215. package/src/ts/client/grapesjs/page-panel.ts +109 -7
  216. package/src/ts/client/grapesjs/project-bar.ts +133 -72
  217. package/src/ts/client/grapesjs/resize-panel.ts +20 -0
  218. package/src/ts/client/grapesjs/rich-text.ts +38 -13
  219. package/src/ts/client/grapesjs/semantic.ts +20 -3
  220. package/src/ts/client/grapesjs/settings-sections.ts +117 -0
  221. package/src/ts/client/grapesjs/settings.ts +191 -96
  222. package/src/ts/client/grapesjs/storage.ts +187 -0
  223. package/src/ts/client/grapesjs/symbolDialogs.ts +21 -5
  224. package/src/ts/client/grapesjs/view-buttons.ts +42 -0
  225. package/src/ts/client/index.ts +80 -21
  226. package/src/ts/client/publication-transformers.test.ts +207 -0
  227. package/src/ts/client/publication-transformers.ts +280 -0
  228. package/src/ts/client/publish-custom-code-block.ts +31 -0
  229. package/src/ts/client/publish-fonts.ts +36 -0
  230. package/src/ts/client/utils.ts +70 -1
  231. package/src/ts/constants.ts +78 -0
  232. package/src/ts/externs.ts +17 -0
  233. package/src/ts/page.ts +20 -3
  234. package/src/ts/plugins/client/template.ts +246 -0
  235. package/src/ts/plugins/client/website-info.ts +84 -0
  236. package/src/ts/plugins/server/Cache.ts +32 -0
  237. package/src/ts/plugins/server/DownloadConnector.ts +146 -0
  238. package/src/ts/plugins/server/FtpConnector.test.ts +82 -0
  239. package/src/ts/plugins/server/FtpConnector.ts +621 -0
  240. package/src/ts/plugins/server/GitlabConnector.ts +749 -0
  241. package/src/ts/plugins/server/GitlabHostingConnector.ts +215 -0
  242. package/src/ts/plugins/server/PublicationOptimizer.ts +14 -0
  243. package/src/ts/plugins/server/SslPlugin.ts +84 -0
  244. package/src/ts/plugins/server/StaticPlugin.ts +45 -0
  245. package/src/ts/server/api/api.ts +19 -0
  246. package/src/ts/server/api/connectorApi.ts +464 -0
  247. package/src/ts/server/api/publicationApi.ts +159 -0
  248. package/src/ts/server/api/websiteApi.ts +462 -0
  249. package/src/ts/server/cli.ts +48 -14
  250. package/src/ts/server/config.ts +158 -60
  251. package/src/ts/server/connectors/FsHosting.ts +66 -0
  252. package/src/ts/server/connectors/FsStorage.test.ts +51 -0
  253. package/src/ts/server/connectors/FsStorage.ts +290 -0
  254. package/src/ts/server/connectors/connectors.ts +203 -0
  255. package/src/ts/server/events.ts +37 -0
  256. package/src/ts/server/expose.ts +19 -0
  257. package/src/ts/server/express.ts +57 -5
  258. package/src/ts/server/index.ts +80 -19
  259. package/src/ts/server/jobs.test.ts +75 -0
  260. package/src/ts/server/jobs.ts +89 -0
  261. package/src/ts/server/utils/BackwardCompat.ts +546 -528
  262. package/src/ts/server/utils/validation.ts +34 -0
  263. package/src/ts/types.ts +295 -85
  264. package/.dockerignore +0 -5
  265. package/.editorconfig +0 -14
  266. package/.eslintrc.json +0 -43
  267. package/.github/FUNDING.yml +0 -13
  268. package/.github/workflows/docker-hub-publish.yml +0 -36
  269. package/.github/workflows/npm-publish.yml +0 -19
  270. package/.github/workflows/test.yml +0 -26
  271. package/.nvmrc +0 -1
  272. package/.silex/default/.silex.data.json +0 -1
  273. package/CODE_OF_CONDUCT.md +0 -128
  274. package/CONTRIBUTING.md +0 -131
  275. package/Dockerfile +0 -15
  276. package/directus.silex.js +0 -7
  277. package/dist/constants.js +0 -4
  278. package/dist/events.js +0 -5
  279. package/dist/page.js +0 -15
  280. package/dist/server/cli.js +0 -43
  281. package/dist/server/config.js +0 -70
  282. package/dist/server/express.js +0 -18
  283. package/dist/server/index.js +0 -58
  284. package/dist/types.js +0 -34
  285. package/jest.config.js +0 -10
  286. package/logos/logo_by_ceubri/silex-icon-2018@200px.png +0 -0
  287. package/logos/logo_by_ceubri/silex-icon-2018@4x.png +0 -0
  288. package/logos/logo_by_ceubri/silex-logo-2018-white-transp.pdf +0 -0
  289. package/logos/logo_by_ceubri/silex-logo-2018-white.jpg +0 -0
  290. package/logos/logo_by_ceubri/silex-logo-2018-white.pdf +0 -0
  291. package/logos/logo_by_ceubri/silex-logo-2018-white.png +0 -0
  292. package/logos/logo_by_ceubri/silex-logo-2018-white@0.5x.png +0 -0
  293. package/logos/logo_by_ceubri/silex-logo-2018-white@0.75x.png +0 -0
  294. package/logos/logo_by_ceubri/silex-logo-2018-white@4x.jpg +0 -0
  295. package/logos/logo_by_ceubri/silex-logo-2018-white@4x.png +0 -0
  296. package/logos/logo_by_ceubri/silex-logo-2018.ai +5 -2521
  297. package/logos/logo_by_ceubri/silex-logo-2018.jpg +0 -0
  298. package/logos/logo_by_ceubri/silex-logo-2018.pdf +0 -0
  299. package/logos/logo_by_ceubri/silex-logo-2018.png +0 -0
  300. package/logos/logo_by_ceubri/silex-logo-2018@0.5x.png +0 -0
  301. package/logos/logo_by_ceubri/silex-logo-2018@0.75x.png +0 -0
  302. package/logos/logo_by_ceubri/silex-logo-2018@4x.jpg +0 -0
  303. package/logos/logo_by_ceubri/silex-logo-2018@4x.png +0 -0
  304. package/logos/silexlabs.png +0 -0
  305. package/src/example-client-config.js +0 -47
  306. package/src/plugins/AuthPlugin.js +0 -21
  307. package/src/plugins/Cache.js +0 -11
  308. package/src/plugins/DefaultBackend.js +0 -149
  309. package/src/plugins/ExpressPlugin.js +0 -39
  310. package/src/plugins/HooksPlugin.js +0 -58
  311. package/src/plugins/PublishPlugin.js +0 -69
  312. package/src/plugins/SslPlugin.js +0 -66
  313. package/src/plugins/StaticPlugin.js +0 -48
  314. package/src/plugins/WebsitePlugin.js +0 -207
  315. package/src/ts/client/grapesjs/publish.ts +0 -386
  316. package/src/ts/client/grapesjs/template.ts +0 -166
  317. package/src/ts/events.ts +0 -2
  318. package/tsconfig-server-debug.json +0 -19
  319. package/tsconfig-server-release.json +0 -19
  320. package/tsconfig.jest.json +0 -9
  321. package/tsconfig.json +0 -26
  322. package/webpack.config.js +0 -28
  323. package/webpack.debug.config.js +0 -8
@@ -0,0 +1,749 @@
1
+ /*
2
+ * Silex website builder, free/libre no-code tool for makers.
3
+ * Copyright (c) 2023 lexoyo and Silex Labs foundation
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { API_CONNECTOR_LOGIN_CALLBACK, API_CONNECTOR_PATH, API_PATH, WEBSITE_DATA_FILE } from '../../constants'
19
+ import { ServerConfig } from '../../server/config'
20
+ import { ConnectorFile, ConnectorFileContent, StatusCallback, StorageConnector, contentToBuffer, contentToString, toConnectorData } from '../../server/connectors/connectors'
21
+ import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent, JobStatus } from '../../types'
22
+ import fetch from 'node-fetch'
23
+ import crypto, { createHash } from 'crypto'
24
+ import { join } from 'path'
25
+
26
+ /**
27
+ * Gitlab connector
28
+ * @fileoverview Gitlab connector for Silex, connect to the user's Gitlab account to store websites
29
+ * @see https://docs.gitlab.com/ee/api/oauth2.html
30
+ */
31
+
32
+ export interface GitlabOptions {
33
+ clientId: string
34
+ clientSecret: string
35
+ branch: string
36
+ assetsFolder: string
37
+ //metaRepo: string
38
+ //metaRepoFile: string
39
+ repoPrefix: string
40
+ scope: string
41
+ domain: string
42
+ timeOut: number
43
+ }
44
+
45
+ export interface GitlabToken {
46
+ state?: string
47
+ codeVerifier?: string
48
+ codeChallenge?: string
49
+ token?: {
50
+ access_token: string
51
+ token_type: string
52
+ expires_in: number
53
+ refresh_token: string
54
+ created_at: number
55
+ id_token: string
56
+ scope: string
57
+ }
58
+ userId?: number
59
+ username?: string
60
+ }
61
+
62
+ export type GitlabSession = Record<string, GitlabToken>
63
+
64
+ interface GitlabAction {
65
+ action: 'create' | 'delete' | 'move' | 'update' | 'cherry-pick'
66
+ file_path?: string
67
+ content?: string
68
+ commit_id?: string
69
+ }
70
+
71
+ interface GitlabWriteFile {
72
+ branch: string
73
+ commit_message: string
74
+ id?: string
75
+ actions?: GitlabAction[]
76
+ content?: string
77
+ file_path?: string
78
+ encoding?: 'base64' | 'text'
79
+ }
80
+
81
+ interface GitlabGetToken {
82
+ grant_type: 'authorization_code'
83
+ client_id: string
84
+ client_secret: string
85
+ code: string
86
+ redirect_uri: string
87
+ code_verifier: string
88
+ }
89
+
90
+ interface GitlabWebsiteName {
91
+ name: string
92
+ }
93
+
94
+ interface GitlabCreateBranch {
95
+ branch: string
96
+ ref: string
97
+ }
98
+
99
+ interface GitlabGetTags {
100
+ per_page?: number
101
+ }
102
+
103
+ interface GitlabCreateTag {
104
+ tag_name: string
105
+ ref: string
106
+ message: string
107
+ }
108
+
109
+ interface GitlabFetchCommits {
110
+ ref_name: string
111
+ since: string
112
+ }
113
+
114
+ interface MetaRepoFileContent {
115
+ websites: {
116
+ [websiteId: string]: {
117
+ meta: WebsiteMetaFileContent,
118
+ createdAt: string,
119
+ updatedAt: string,
120
+ }
121
+ }
122
+ }
123
+
124
+ const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
125
+ <svg
126
+ xmlns:svg="http://www.w3.org/2000/svg"
127
+ xmlns="http://www.w3.org/2000/svg"
128
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
129
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
130
+ width="1000"
131
+ height="963.197"
132
+ viewBox="0 0 1000 963.197"
133
+ version="1.1"
134
+ id="svg85">
135
+ <sodipodi:namedview
136
+ id="namedview87"
137
+ pagecolor="#ffffff"
138
+ bordercolor="#666666"
139
+ borderopacity="1.0"
140
+ inkscape:pageshadow="2"
141
+ inkscape:pageopacity="0.0"
142
+ inkscape:pagecheckerboard="0"
143
+ showgrid="false"
144
+ inkscape:zoom="1"
145
+ inkscape:cx="991.5"
146
+ inkscape:cy="964.5"
147
+ inkscape:window-width="1126"
148
+ inkscape:window-height="895"
149
+ inkscape:window-x="774"
150
+ inkscape:window-y="12"
151
+ inkscape:window-maximized="0"
152
+ inkscape:current-layer="svg85" />
153
+ <defs
154
+ id="defs74">
155
+ <style
156
+ id="style72">.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style>
157
+ </defs>
158
+ <g
159
+ id="LOGO"
160
+ transform="matrix(5.2068817,0,0,5.2068817,-489.30756,-507.76085)">
161
+ <path
162
+ class="cls-1"
163
+ d="m 282.83,170.73 -0.27,-0.69 -26.14,-68.22 a 6.81,6.81 0 0 0 -2.69,-3.24 7,7 0 0 0 -8,0.43 7,7 0 0 0 -2.32,3.52 l -17.65,54 h -71.47 l -17.65,-54 a 6.86,6.86 0 0 0 -2.32,-3.53 7,7 0 0 0 -8,-0.43 6.87,6.87 0 0 0 -2.69,3.24 L 97.44,170 l -0.26,0.69 a 48.54,48.54 0 0 0 16.1,56.1 l 0.09,0.07 0.24,0.17 39.82,29.82 19.7,14.91 12,9.06 a 8.07,8.07 0 0 0 9.76,0 l 12,-9.06 19.7,-14.91 40.06,-30 0.1,-0.08 a 48.56,48.56 0 0 0 16.08,-56.04 z"
164
+ id="path76" />
165
+ <path
166
+ class="cls-2"
167
+ d="m 282.83,170.73 -0.27,-0.69 a 88.3,88.3 0 0 0 -35.15,15.8 L 190,229.25 c 19.55,14.79 36.57,27.64 36.57,27.64 l 40.06,-30 0.1,-0.08 a 48.56,48.56 0 0 0 16.1,-56.08 z"
168
+ id="path78" />
169
+ <path
170
+ class="cls-3"
171
+ d="m 153.43,256.89 19.7,14.91 12,9.06 a 8.07,8.07 0 0 0 9.76,0 l 12,-9.06 19.7,-14.91 c 0,0 -17.04,-12.89 -36.59,-27.64 -19.55,14.75 -36.57,27.64 -36.57,27.64 z"
172
+ id="path80" />
173
+ <path
174
+ class="cls-2"
175
+ d="M 132.58,185.84 A 88.19,88.19 0 0 0 97.44,170 l -0.26,0.69 a 48.54,48.54 0 0 0 16.1,56.1 l 0.09,0.07 0.24,0.17 39.82,29.82 c 0,0 17,-12.85 36.57,-27.64 z"
176
+ id="path82" />
177
+ </g>
178
+ </svg>`
179
+ const encodedSvg = encodeURIComponent(svg)
180
+ const ICON = '/assets/gitlab.png'
181
+
182
+ export default class GitlabConnector implements StorageConnector {
183
+ connectorId = 'gitlab'
184
+ connectorType = ConnectorType.STORAGE
185
+ displayName = 'GitLab'
186
+ icon = ICON
187
+ disableLogout = false
188
+ color = '#2B1B63'
189
+ background = 'rgba(252, 109, 38, 0.2)'
190
+ options: GitlabOptions
191
+
192
+ constructor(private config: ServerConfig, opts: Partial<GitlabOptions>) {
193
+ this.options = {
194
+ branch: 'main',
195
+ assetsFolder: 'assets',
196
+ //metaRepo: 'silex-meta',
197
+ //metaRepoFile: 'websites.json',
198
+ repoPrefix: 'silex_',
199
+ scope: 'api', // 'api+read_api+read_user+read_repository+write_repository+email+sudo+profile+openid'
200
+ ...opts,
201
+ } as GitlabOptions
202
+ if(!this.options.clientId) throw new Error('Missing Gitlab client ID')
203
+ if(!this.options.clientSecret) throw new Error('Missing Gitlab client secret')
204
+ if(!this.options.domain) throw new Error('Missing Gitlab domain')
205
+ if(!this.options.timeOut) this.options.timeOut = 15000 /* default value */
206
+ }
207
+
208
+ // **
209
+ // Convenience methods for the Gitlab API
210
+ private getAssetPath(path: string): string {
211
+ return encodeURIComponent(join(this.options.assetsFolder, path))
212
+ }
213
+
214
+ isUsingOfficialInstance(): boolean {
215
+ const gitlabDomainRegexp = /(^|\b)(gitlab\.com)($|\b)/
216
+ return gitlabDomainRegexp.test(this.options.domain)
217
+ }
218
+
219
+ async createFile(session: GitlabSession, websiteId: WebsiteId, path: string, content: string, isBase64 = false): Promise<void> {
220
+ // Remove leading slash
221
+ const safePath = path.replace(/^\//, '')
222
+ const encodePath = decodeURIComponent(path)
223
+ return this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${safePath}`, 'POST', {
224
+ id: websiteId,
225
+ branch: this.options.branch,
226
+ content,
227
+ commit_message: `Create file ${encodePath} from Silex`,
228
+ encoding: isBase64 ? 'base64' : undefined,
229
+ })
230
+ }
231
+
232
+ async updateFile(session: GitlabSession, websiteId: WebsiteId, path: string, content: string, isBase64 = false): Promise<void> {
233
+ // Remove leading slash
234
+ const safePath = path.replace(/^\//, '')
235
+ const encodePath = decodeURIComponent(path)
236
+ return this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${safePath}`, 'PUT', {
237
+ id: websiteId,
238
+ branch: this.options.branch,
239
+ content: await contentToString(content),
240
+ commit_message: `Update website asset ${encodePath} from Silex`,
241
+ encoding: isBase64 ? 'base64' : undefined,
242
+ })
243
+ }
244
+
245
+ async readFile(session: GitlabSession, websiteId: string, fileName: string): Promise<Buffer> {
246
+ const safePath = fileName.replace(/^\//, '')
247
+ // Call the API
248
+ const url = `${this.options.domain}/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${this.getSessionToken(session).token?.access_token}`
249
+ const response = await fetch(url, {
250
+ method: 'GET',
251
+ headers: {
252
+ 'Content-Type': 'application/json',
253
+ },
254
+ })
255
+ const json = await response.json()
256
+ if(!response.ok) throw new ApiError(`Error reading file "${fileName}" from Gitlab: ${json?.message ?? json?.error ?? response.statusText}`, response.status)
257
+ // From base64 string to buffer
258
+ const buf = Buffer.from(json.content, 'base64')
259
+ // Return the image bytes
260
+ return buf
261
+ }
262
+
263
+ /*
264
+ * Get the meta repo path for the current user
265
+ * The meta repo contains a JSON file which contains the list of websites
266
+ */
267
+ //private getMetaRepoPath(session: GitlabSession): string {
268
+ // if(!this.getSessionToken(session).username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
269
+ // return encodeURIComponent(`${this.getSessionToken(session).username}/${this.options.metaRepo}`)
270
+ //}
271
+
272
+ ///**
273
+ // * Initialize the storage with a meta repo
274
+ // */
275
+ //private async initStorage(session: GitlabSession): Promise<void> {
276
+ // // Create the meta repo
277
+ // try {
278
+ // const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
279
+ // name: this.options.metaRepo,
280
+ // }) as any
281
+ // return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
282
+ // websites: {}
283
+ // } as MetaRepoFileContent))
284
+ // } catch (e) {
285
+ // console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
286
+ // throw e
287
+ // }
288
+ //}
289
+
290
+ /**
291
+ * Call the Gitlab API with the user's token and handle errors
292
+ */
293
+ async callApi(session: GitlabSession, path: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET', body: GitlabWriteFile | GitlabGetToken | GitlabWebsiteName | GitlabCreateBranch | GitlabGetTags | GitlabCreateTag | GitlabFetchCommits | null = null, params: any = {}): Promise<any> {
294
+ const token = this.getSessionToken(session).token
295
+ const tokenParam = token ? `access_token=${token.access_token}&` : ''
296
+ const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent((v as any).toString())}`).join('&')
297
+ const url = `${this.options.domain}/${path}?${tokenParam}${paramsStr}`
298
+ const headers = {
299
+ 'Content-Type': 'application/json',
300
+ }
301
+ if (method === 'GET' && body) {
302
+ console.error('Gitlab API error (4) - GET request with body', {url, method, body, params})
303
+ }
304
+ // With or without body
305
+ let response
306
+ try {
307
+ response = await fetch(url, body && method !== 'GET' ? {
308
+ method,
309
+ headers,
310
+ body: body ? JSON.stringify(body) : undefined
311
+ } : {
312
+ method,
313
+ headers,
314
+ })
315
+ } catch (e) {
316
+ console.error('Gitlab API error (0)', e)
317
+ throw new ApiError(`Gitlab API error (0): ${e.message} ${e.code} ${e.name} ${e.type}`, 500)
318
+ }
319
+ let json: { message: string, error: string } | any
320
+ // Handle the case when the server returns a non-JSON response (e.g. 400 Bad Request)
321
+ const text = await response.text()
322
+ if (!response.ok) {
323
+ if (text.includes('A file with this name doesn\'t exist')) {
324
+ throw new ApiError('Gitlab API error (5): Not Found', 404)
325
+ } else if (response.status === 401 && this.getSessionToken(session).token?.refresh_token) {
326
+ // Refresh the token
327
+ const token = this.getSessionToken(session).token
328
+ const body = {
329
+ grant_type: 'refresh_token',
330
+ refresh_token: token?.refresh_token,
331
+ client_id: this.options.clientId,
332
+ client_secret: this.options.clientSecret,
333
+ }
334
+ const response = await fetch(this.options.domain + '/oauth/token', {
335
+ method: 'POST',
336
+ headers: {
337
+ 'Content-Type': 'application/json',
338
+ },
339
+ body: JSON.stringify(body)
340
+ })
341
+ const refreshJson = await response.json()
342
+ if (response.ok) {
343
+ this.setSessionToken(session, {
344
+ token: {
345
+ ...token,
346
+ ...refreshJson,
347
+ },
348
+ } as GitlabToken)
349
+ return await this.callApi(session, path, method, body as any, params)
350
+ } else {
351
+ const message = typeof refreshJson?.message === 'object' ? Object.entries(refreshJson.message).map(entry => entry.join(' ')).join(' ') : refreshJson?.message ?? refreshJson?.error ?? response.statusText
352
+ console.error('Gitlab API error (2) - could not refresh token', response.status, response.statusText, {message}, 'refresh_token:', token?.refresh_token)
353
+ // Workaround for when the token is invalid
354
+ // It happens often which is not normal (refresh token should last 6 months)
355
+ this.logout(session)
356
+ // Notify the user
357
+ throw new ApiError(`Gitlab API error (2): ${message}`, response.status)
358
+ }
359
+ } else {
360
+ const message = typeof json?.message === 'object' ? Object.entries(json.message).map(entry => entry.join(' ')).join(' ') : json?.message ?? json?.error ?? response.statusText
361
+ console.error('Gitlab API error (1)', response.status, response.statusText, {url, method, body, params, text, message})
362
+ throw new ApiError(`Gitlab API error (1): ${message}`, response.status)
363
+ }
364
+ }
365
+ try {
366
+ json = JSON.parse(text)
367
+ } catch (e) {
368
+ if(!response.ok) {
369
+ // A real error
370
+ throw e
371
+ } else {
372
+ // Useless error linked to the fact that the response is not JSON
373
+ console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, {url, method, body, params, text})
374
+ return text
375
+ }
376
+ }
377
+ return json
378
+ }
379
+
380
+ private generateCodeVerifier() {
381
+ return crypto.randomBytes(64).toString('base64')
382
+ .replace(/\+/g, '-')
383
+ .replace(/\//g, '_')
384
+ .replace(/=/g, '')
385
+ .substr(0, 128)
386
+ }
387
+
388
+ private async generateCodeChallenge(verifier) {
389
+ const hashed = createHash('sha256').update(verifier).digest()
390
+ let base64Url = hashed.toString('base64')
391
+ // Replace '+' with '-', '/' with '_', and remove '='
392
+ base64Url = base64Url.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
393
+ return base64Url
394
+ }
395
+
396
+
397
+ private getRedirect() {
398
+ const params = `connectorId=${this.connectorId}&type=${this.connectorType}`
399
+ return `${this.config.url}${API_PATH}${API_CONNECTOR_PATH}${API_CONNECTOR_LOGIN_CALLBACK}?${params}`
400
+ }
401
+
402
+ /**
403
+ * Get the OAuth URL to redirect the user to
404
+ * The URL should look like
405
+ * https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
406
+ * OAuth2 Step #1 from https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-with-proof-key-for-code-exchange-pkce
407
+ */
408
+ async getOAuthUrl(session: GitlabSession): Promise<string> {
409
+ const redirect_uri = encodeURIComponent(this.getRedirect())
410
+
411
+ const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
412
+ const codeVerifier = this.generateCodeVerifier()
413
+
414
+ // Create the code challenge
415
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier)
416
+
417
+ // Store the code verifier and code challenge in the session
418
+ this.setSessionToken(session, {
419
+ ...this.getSessionToken(session),
420
+ state,
421
+ codeVerifier,
422
+ codeChallenge,
423
+ })
424
+ return `${this.options.domain}/oauth/authorize?client_id=${this.options.clientId}&redirect_uri=${redirect_uri}&response_type=code&state=${this.getSessionToken(session).state}&scope=${this.options.scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`
425
+ }
426
+
427
+ getSessionToken(session: GitlabSession | undefined): GitlabToken {
428
+ return (session ?? {})[this.connectorId] ?? {}
429
+ }
430
+ setSessionToken(session: GitlabSession, token: GitlabToken): void {
431
+ session[this.connectorId] = token
432
+ }
433
+ resetSessionToken(session: GitlabSession): void {
434
+ delete session[this.connectorId]
435
+ }
436
+
437
+ getOptions(formData: object): object {
438
+ return {} // FIXME: store branch
439
+ }
440
+
441
+ async getLoginForm(session: GitlabSession, redirectTo: string): Promise<null> {
442
+ return null
443
+ }
444
+
445
+ async getSettingsForm(session: GitlabSession, redirectTo: string): Promise<null> {
446
+ return null
447
+ }
448
+
449
+ async isLoggedIn(session: GitlabSession): Promise<boolean> {
450
+ return !!this.getSessionToken(session).token
451
+ }
452
+
453
+ /**
454
+ * Get the token from return code
455
+ * Set the token in the session
456
+ * OAuth2 Step #2 from https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-with-proof-key-for-code-exchange-pkce
457
+ */
458
+ async setToken(session: GitlabSession, loginResult: any): Promise<void> {
459
+ const sessionToken = this.getSessionToken(session)
460
+ if(!loginResult.state || loginResult.state !== sessionToken?.state) {
461
+ this.logout(session)
462
+ throw new ApiError('Invalid state', 401)
463
+ }
464
+ if(!sessionToken?.codeVerifier) {
465
+ this.logout(session)
466
+ throw new ApiError('Missing code verifier', 401)
467
+ }
468
+ if(!sessionToken?.codeChallenge) {
469
+ this.logout(session)
470
+ throw new ApiError('Missing code challenge', 401)
471
+ }
472
+
473
+ const response = await fetch(this.options.domain + '/oauth/token', {
474
+ method: 'POST',
475
+ headers: {
476
+ 'Content-Type': 'application/json',
477
+ },
478
+ body: JSON.stringify({
479
+ client_id: this.options.clientId,
480
+ client_secret: this.options.clientSecret,
481
+ code: loginResult.code,
482
+ grant_type: 'authorization_code',
483
+ redirect_uri: this.getRedirect(),
484
+ code_verifier: sessionToken.codeVerifier,
485
+ }),
486
+ })
487
+
488
+ const token = await response.json()
489
+
490
+ // Store the token in the session
491
+ this.setSessionToken(session, {
492
+ token,
493
+ ...this.getSessionToken(session),
494
+ })
495
+
496
+ // We need to get the user ID for listWebsites
497
+ const user = await this.callApi(session, 'api/v4/user') as any
498
+
499
+ // Store the user details in the session
500
+ this.setSessionToken(session, {
501
+ ...this.getSessionToken(session),
502
+ userId: user.id,
503
+ username: user.username,
504
+ })
505
+ }
506
+
507
+ async logout(session: GitlabSession): Promise<void> {
508
+ this.resetSessionToken(session)
509
+ }
510
+
511
+ async getUser(session: GitlabSession): Promise<ConnectorUser> {
512
+ const user = await this.callApi(session, 'api/v4/user') as any
513
+ return {
514
+ name: user.name,
515
+ email: user.email,
516
+ picture: user.avatar_url,
517
+ storage: await toConnectorData(session, this as StorageConnector),
518
+ }
519
+ }
520
+
521
+ async listWebsites(session: GitlabSession): Promise<WebsiteMeta[]> {
522
+ //try {
523
+ // const result = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
524
+ // ref: this.options.branch,
525
+ // })
526
+ // const { content } = result
527
+ // const contentDecoded = Buffer.from(content, 'base64').toString('utf8')
528
+ // const websites = (JSON.parse(contentDecoded) as MetaRepoFileContent).websites
529
+ // return Object.entries(websites).map(([websiteId, {meta, createdAt, updatedAt}]) => ({
530
+ // websiteId,
531
+ // createdAt: new Date(createdAt),
532
+ // updatedAt: new Date(updatedAt),
533
+ // ...meta,
534
+ // }))
535
+ //} catch (e) {
536
+ // console.error('Could not list websites', e.statusCode, e.httpStatusCode, e.code)
537
+ // if (e.statusCode === 404 || e.httpStatusCode === 404) {
538
+ // await this.initStorage(session)
539
+ // return []
540
+ // } else {
541
+ // throw e
542
+ // }
543
+ //}
544
+ const userId = this.getSessionToken(session).userId
545
+ if(!userId) {
546
+ this.logout(session)
547
+ throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
548
+ }
549
+ const projects = await this.callApi(session, `api/v4/users/${userId}/projects`) as any[]
550
+ return projects
551
+ .filter(p => p.name.startsWith(this.options.repoPrefix))
552
+ .map(p => ({
553
+ websiteId: p.id,
554
+ name: p.name.replace(this.options.repoPrefix, ''),
555
+ createdAt: p.created_at,
556
+ updatedAt: p.last_activity_at,
557
+ connectorUserSettings: {},
558
+ }))
559
+ }
560
+
561
+ async readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData> {
562
+ const result = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_DATA_FILE}`, 'GET', null, {
563
+ ref: this.options.branch,
564
+ }) as any
565
+ const { content } = result
566
+ const contentDecoded = Buffer.from(content, 'base64').toString('utf8')
567
+ const websiteData = JSON.parse(contentDecoded) as WebsiteData
568
+ return websiteData
569
+ }
570
+
571
+ async createWebsite(session: GitlabSession, websiteMeta: WebsiteMetaFileContent): Promise<WebsiteId> {
572
+ const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
573
+ name: this.options.repoPrefix + websiteMeta.name,
574
+ }) as any
575
+ await this.createFile(session, project.id, WEBSITE_DATA_FILE, JSON.stringify({} as WebsiteData))
576
+ //await this.createFile(session, project.id, WEBSITE_META_DATA_FILE, JSON.stringify(websiteMeta))
577
+ //await this.updateWebsite(session, project.id, {} as WebsiteData)
578
+ //await this.setWebsiteMeta(session, project.id, websiteMeta)
579
+ return project.id
580
+ }
581
+
582
+ async updateWebsite(session: GitlabSession, websiteId: WebsiteId, websiteData: WebsiteData): Promise<void> {
583
+ const project = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_DATA_FILE}`, 'PUT', {
584
+ branch: this.options.branch,
585
+ commit_message: 'Update website data from Silex',
586
+ content: JSON.stringify(websiteData),
587
+ id: websiteId,
588
+ })
589
+ }
590
+
591
+ async deleteWebsite(session: GitlabSession, websiteId: WebsiteId): Promise<void> {
592
+ // Delete repo
593
+ await this.callApi(session, `api/v4/projects/${websiteId}`, 'DELETE')
594
+ //// Load the meta repo data
595
+ //const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
596
+ // ref: this.options.branch,
597
+ //})
598
+ //const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
599
+ //const data = metaRepo.websites[websiteId]
600
+ //if(!data) throw new ApiError(`Website ${websiteId} not found`, 404)
601
+ //// Update or create the website meta data
602
+ //delete metaRepo.websites[websiteId]
603
+ //// Save the meta repo data
604
+ //const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
605
+ // branch: this.options.branch,
606
+ // commit_message: `Delete meta data of ${data.meta.name} (${websiteId}) from Silex`,
607
+ // content: JSON.stringify(metaRepo),
608
+ // file_path: this.options.metaRepoFile,
609
+ //})
610
+ }
611
+
612
+ async duplicateWebsite(session: GitlabSession, websiteId: string): Promise<void> {
613
+ // Get the repo meta data
614
+ const meta = await this.getWebsiteMeta(session, websiteId)
615
+ // List all the repository files
616
+ const blobs = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
617
+ recursive: true,
618
+ })
619
+ const files = blobs
620
+ .filter(item => item.type === 'blob')
621
+ .map(item => item.path)
622
+ // Create a new repo
623
+ const newId = await this.createWebsite(session, {
624
+ ...meta,
625
+ name: meta.name + ' Copy ' + new Date().toISOString().replace(/T.*/, '') + ' ' + Math.random().toString(36).substring(2, 4),
626
+ })
627
+ // Upload all files
628
+ for(const file of files) {
629
+ const path = encodeURIComponent(file)
630
+ const content = await this.readFile(session, websiteId, path)
631
+ // From buffer to string
632
+ const contentStr = content.toString('base64')
633
+ switch(file) {
634
+ case WEBSITE_DATA_FILE:
635
+ await this.updateFile(session, newId, path, contentStr, true)
636
+ break
637
+ default:
638
+ await this.createFile(session, newId, path, contentStr, true)
639
+ }
640
+ }
641
+ }
642
+
643
+ async getWebsiteMeta(session: GitlabSession, websiteId: WebsiteId): Promise<WebsiteMeta> {
644
+ //const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
645
+ // ref: this.options.branch,
646
+ //})
647
+ //const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
648
+ //if(!metaRepo.websites[websiteId]) throw new ApiError(`Website ${websiteId} not found`, 404)
649
+ //return {
650
+ // websiteId,
651
+ // createdAt: new Date(metaRepo.websites[websiteId].createdAt),
652
+ // updatedAt: new Date(metaRepo.websites[websiteId].updatedAt),
653
+ // ...metaRepo.websites[websiteId].meta,
654
+ //}
655
+ // const response = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'GET', null, {
656
+ // ref: this.options.branch,
657
+ // }) as any
658
+ // Base64 to string to JSON
659
+ // const contentDecoded = Buffer.from(response.content, 'base64').toString('utf8')
660
+ // const websiteMeta = JSON.parse(contentDecoded) as WebsiteMetaFileContent
661
+ const project = await this.callApi(session, `api/v4/projects/${websiteId}`)
662
+ return {
663
+ websiteId,
664
+ name: project.name.replace(this.options.repoPrefix, ''),
665
+ imageUrl: project.avatar_url,
666
+ createdAt: project.created_at,
667
+ updatedAt: project.last_activity_at,
668
+ connectorUserSettings: {},
669
+ }
670
+ }
671
+
672
+ async setWebsiteMeta(session: GitlabSession, websiteId: WebsiteId, websiteMeta: WebsiteMetaFileContent): Promise<void> {
673
+ //// Load the meta repo data
674
+ //const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
675
+ // ref: this.options.branch,
676
+ //})
677
+ //const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
678
+ //// Update or create the website meta data
679
+ //metaRepo.websites[websiteId] = {
680
+ // updatedAt: new Date().toISOString(),
681
+ // createdAt: metaRepo.websites[websiteId]?.createdAt ?? new Date().toISOString(),
682
+ // meta: websiteMeta,
683
+ //}
684
+ //// Save the meta repo data
685
+ //const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
686
+ // branch: this.options.branch,
687
+ // commit_message: `Update website meta data of ${websiteMeta.name} (${websiteId}) from Silex`,
688
+ // content: JSON.stringify(metaRepo),
689
+ // file_path: this.options.metaRepoFile,
690
+ //})
691
+
692
+ // Rename the repo if needed
693
+ const oldMeta = await this.getWebsiteMeta(session, websiteId)
694
+ if(websiteMeta.name !== oldMeta.name) {
695
+ await this.callApi(session, `api/v4/projects/${websiteId}`, 'PUT', {
696
+ name: this.options.repoPrefix + websiteMeta.name,
697
+ })
698
+ }
699
+ //// Update the metadata file
700
+ //await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'PUT', {
701
+ // branch: this.options.branch,
702
+ // commit_message: 'Update website meta data from Silex',
703
+ // content: JSON.stringify(websiteMeta),
704
+ // file_path: WEBSITE_META_DATA_FILE,
705
+ // id: websiteId,
706
+ //})
707
+ }
708
+
709
+ async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback | undefined): Promise<void> {
710
+ status && await status({ message: 'in progress...', status: JobStatus.IN_PROGRESS })
711
+ // For each file
712
+ for (const file of files) {
713
+ // Convert to base64
714
+ const content = (await contentToBuffer(file.content)).toString('base64')
715
+ const path = this.getAssetPath(file.path)
716
+
717
+ try {
718
+ await this.updateFile(session, websiteId, path, content, true)
719
+ } catch (e) {
720
+ // If the file does not exist, create it
721
+ if (e.statusCode === 404 || e.httpStatusCode === 404 || e.message.endsWith('A file with this name doesn\'t exist')) {
722
+ await this.createFile(session, websiteId, path, content, true)
723
+ } else {
724
+ status && await status({ message: 'Error', status: JobStatus.ERROR })
725
+ throw e
726
+ }
727
+ }
728
+ }
729
+ status && await status({ message: 'Successfull', status: JobStatus.SUCCESS })
730
+ }
731
+
732
+ async readAsset(session: GitlabSession, websiteId: string, fileName: string): Promise<ConnectorFileContent> {
733
+ // Remove leading slash
734
+ const finalPath = this.getAssetPath(fileName)
735
+ return this.readFile(session, websiteId, finalPath)
736
+ }
737
+
738
+ async deleteAssets(session: GitlabSession, websiteId: string, fileNames: string[]): Promise<void> {
739
+ return this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', {
740
+ id: websiteId,
741
+ branch: this.options.branch,
742
+ commit_message: `Delete assets from Silex: ${fileNames.join(', ')}`,
743
+ actions: fileNames.map(f => ({
744
+ action: 'delete',
745
+ file_path: this.getAssetPath(f),
746
+ })),
747
+ })
748
+ }
749
+ }