@silexlabs/silex 3.0.0-alpha.39 → 3.0.0-alpha.41

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 (110) hide show
  1. package/.silex.js +38 -8
  2. package/dist/client/client/config.d.ts +11 -41
  3. package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
  4. package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
  5. package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
  6. package/dist/client/client/grapesjs/index.d.ts +18 -44
  7. package/dist/client/client/grapesjs/storage.d.ts +1 -0
  8. package/dist/client/client/services.d.ts +35 -0
  9. package/dist/client/constants.d.ts +17 -0
  10. package/dist/client/css/admin.css +86 -3
  11. package/dist/client/js/main.js +201 -90
  12. package/dist/client/js/main.js.LICENSE.txt +6 -0
  13. package/dist/client/types.d.ts +200 -29
  14. package/dist/constants.d.ts +21 -0
  15. package/dist/constants.js +38 -1
  16. package/dist/constants.js.map +1 -0
  17. package/dist/events.d.ts +2 -0
  18. package/dist/events.js +17 -0
  19. package/dist/events.js.map +1 -0
  20. package/dist/plugins/AuthPlugin.d.ts +1 -0
  21. package/dist/plugins/AuthPlugin.js +38 -0
  22. package/dist/plugins/AuthPlugin.js.map +1 -0
  23. package/dist/plugins/Cache.d.ts +2 -0
  24. package/dist/plugins/Cache.js +32 -0
  25. package/dist/plugins/Cache.js.map +1 -0
  26. package/dist/plugins/FtpBackend.d.ts +92 -0
  27. package/dist/plugins/FtpBackend.js +604 -0
  28. package/dist/plugins/FtpBackend.js.map +1 -0
  29. package/dist/plugins/HooksPlugin.d.ts +7 -0
  30. package/dist/plugins/HooksPlugin.js +76 -0
  31. package/dist/plugins/HooksPlugin.js.map +1 -0
  32. package/dist/plugins/PublicationPlugin.d.ts +17 -0
  33. package/dist/plugins/PublicationPlugin.js +170 -0
  34. package/dist/plugins/PublicationPlugin.js.map +1 -0
  35. package/dist/plugins/SslPlugin.d.ts +1 -0
  36. package/dist/plugins/SslPlugin.js +86 -0
  37. package/dist/plugins/SslPlugin.js.map +1 -0
  38. package/dist/plugins/StaticPlugin.d.ts +1 -0
  39. package/dist/plugins/StaticPlugin.js +66 -0
  40. package/dist/plugins/StaticPlugin.js.map +1 -0
  41. package/dist/plugins/WebsitePlugin.d.ts +14 -0
  42. package/dist/plugins/WebsitePlugin.js +302 -0
  43. package/dist/plugins/WebsitePlugin.js.map +1 -0
  44. package/dist/server/backends/FsBackend.d.ts +42 -0
  45. package/dist/server/backends/FsBackend.js +269 -0
  46. package/dist/server/backends/FsBackend.js.map +1 -0
  47. package/dist/server/backends/index.d.ts +95 -0
  48. package/dist/server/backends/index.js +255 -0
  49. package/dist/server/backends/index.js.map +1 -0
  50. package/dist/server/cli.js +0 -1
  51. package/dist/server/config.d.ts +58 -0
  52. package/dist/server/config.js +128 -24
  53. package/dist/server/config.js.map +1 -0
  54. package/dist/server/express.js +31 -3
  55. package/dist/server/index.js +9 -20
  56. package/dist/server/jobs.d.ts +6 -0
  57. package/dist/server/jobs.js +75 -0
  58. package/dist/server/jobs.js.map +1 -0
  59. package/dist/server/utils/validation.d.ts +7 -0
  60. package/dist/server/utils/validation.js +37 -0
  61. package/dist/server/utils/validation.js.map +1 -0
  62. package/dist/types.d.ts +270 -0
  63. package/dist/types.js +35 -28
  64. package/dist/types.js.map +1 -0
  65. package/package.json +19 -8
  66. package/public/silex.js +17 -0
  67. package/src/css/dialog.scss +9 -2
  68. package/src/css/grapesjs.scss +79 -0
  69. package/src/css/project-bar.scss +2 -2
  70. package/src/example-client-config.js +17 -0
  71. package/src/html/templates/includes.pug +0 -1
  72. package/src/ts/client/config.ts +7 -6
  73. package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
  74. package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
  75. package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
  76. package/src/ts/client/grapesjs/index.ts +88 -85
  77. package/src/ts/client/grapesjs/storage.ts +110 -0
  78. package/src/ts/client/index.ts +5 -7
  79. package/src/ts/client/services.ts +129 -0
  80. package/src/ts/constants.ts +21 -0
  81. package/src/ts/plugins/AuthPlugin.ts +38 -0
  82. package/src/ts/plugins/Cache.ts +28 -0
  83. package/src/ts/plugins/FtpBackend.ts +689 -0
  84. package/src/ts/plugins/HooksPlugin.ts +79 -0
  85. package/src/ts/plugins/PublicationPlugin.ts +170 -0
  86. package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
  87. package/src/ts/plugins/StaticPlugin.ts +68 -0
  88. package/src/ts/plugins/WebsitePlugin.ts +318 -0
  89. package/src/ts/server/backends/FsBackend.ts +266 -0
  90. package/src/ts/server/backends/index.ts +309 -0
  91. package/src/ts/server/cli.ts +0 -2
  92. package/src/ts/server/config.ts +103 -33
  93. package/src/ts/server/express.ts +37 -3
  94. package/src/ts/server/index.ts +24 -14
  95. package/src/ts/server/jobs.test.ts +74 -0
  96. package/src/ts/server/jobs.ts +77 -0
  97. package/src/ts/server/utils/validation.ts +33 -0
  98. package/src/ts/types.ts +167 -59
  99. package/dist/client/client/grapesjs/publish.d.ts +0 -18
  100. package/dist/page.js +0 -15
  101. package/src/plugins/AuthPlugin.js +0 -21
  102. package/src/plugins/Cache.js +0 -11
  103. package/src/plugins/DefaultBackend.js +0 -149
  104. package/src/plugins/ExpressPlugin.js +0 -39
  105. package/src/plugins/HooksPlugin.js +0 -58
  106. package/src/plugins/PublishPlugin.js +0 -69
  107. package/src/plugins/StaticPlugin.js +0 -48
  108. package/src/plugins/WebsitePlugin.js +0 -207
  109. package/src/ts/client/grapesjs/publish.ts +0 -403
  110. /package/.silex/default/{.silex.data.json → website.json} +0 -0
@@ -0,0 +1,79 @@
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 fetch from 'node-fetch'
19
+ import { ServerConfig } from '../server/config'
20
+
21
+ type HooksOptions = {
22
+ gitUrl?: string
23
+ buildUrl?: string
24
+ }
25
+
26
+ const rootPath = process.env.DATA_FOLDER
27
+
28
+ // **********
29
+ throw new Error('TODO: implement hooks plugin')
30
+ const EVENT_ASSET_WRITE_END = 'silex:asset:write:end'
31
+ const EVENT_PUBLISH_END = 'silex:publish:end'
32
+ const EVENT_WRITE_END = 'silex:write:end'
33
+ function projectPath(projectId: string) { return `${rootPath}/${projectId}` }
34
+ // **********
35
+
36
+ export default async function(config: ServerConfig, opts: HooksOptions = {}) {
37
+ // Options with defaults
38
+ const options = {
39
+ gitUrl: process.env.SILEX_HOOK_GIT,
40
+ buildUrl: process.env.SILEX_HOOK_BUILD,
41
+ ...opts
42
+ }
43
+ config.on(EVENT_WRITE_END, async ({ res, req, projectId, data }) => {
44
+ if(options.gitUrl) {
45
+ await hook(options.gitUrl, {
46
+ path: projectPath(projectId),
47
+ message: 'Change from Silex',
48
+ })
49
+ }
50
+ })
51
+ config.on(EVENT_ASSET_WRITE_END, async ({ res, req, projectId, uploadDir, form, data }) => {
52
+ if(options.gitUrl) {
53
+ await hook(options.gitUrl, {
54
+ path: projectPath(projectId),
55
+ message: 'Change from Silex',
56
+ })
57
+ }
58
+ })
59
+ config.on(EVENT_PUBLISH_END, async ({ projectId, files, req, res }) => {
60
+ if(options.buildUrl) {
61
+ await hook(options.buildUrl)
62
+ }
63
+ })
64
+ }
65
+
66
+ async function hook(url, params = {}) {
67
+ if(url) {
68
+ const urlObj = new URL(url)
69
+ console.log('Calling hook', {url})
70
+ try {
71
+ Object.keys(params)
72
+ .forEach(key => urlObj.searchParams.append(key, params[key]))
73
+ return fetch(urlObj.toString())
74
+ } catch(error) {
75
+ console.log('Hook call failed', {url, error})
76
+ return null
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,170 @@
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 express from 'express'
19
+ import { noCache } from './Cache'
20
+ import { minify } from 'html-minifier'
21
+ import { API_PUBLICATION_PUBLISH, API_PUBLICATION_STATUS } from '../constants'
22
+ import { getJob } from '../server/jobs'
23
+ import { ApiPublicationPublishRequestBody, ApiPublicationPublishRequestQuery, ApiPublicationPublishResponse, ApiPublicationStatusRequestQuery, ApiPublicationStatusResponse, ApiResponseError, JobData, JobId, PublicationSettings, PublicationData as PublicationData, BackendType} from '../types'
24
+ import { HostingProvider, getBackend } from '../server/backends'
25
+ import { ServerConfig } from '../server/config'
26
+ import { requiredParam } from '../server/utils/validation'
27
+ import { EVENT_STARTUP_START } from '../events'
28
+
29
+ /**
30
+ * @fileoverview Publication plugin for Silex
31
+ * Adds a publication API to Silex server
32
+ */
33
+
34
+ const defaultPublication: PublicationSettings = {
35
+ url: '',
36
+ autoHomePage: true,
37
+ assets: { path: 'assets', url: '/assets' },
38
+ html: { path: '' },
39
+ css: { path: 'css', url: '/css' },
40
+ }
41
+
42
+ /**
43
+ * Error thrown by the publication API
44
+ * @param message error message
45
+ * @param code http status code
46
+ */
47
+ export class PublicationError extends Error {
48
+ constructor(message: string, public code: number) {
49
+ super(message)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get the desired backend
55
+ * Can be the default backend or a specific one
56
+ */
57
+ export async function getHostingProvider(session: any, config: ServerConfig, backendId?: string): Promise<HostingProvider> {
58
+ const hostingProvider = await getBackend(config, session, BackendType.HOSTING, backendId) // ?? config.getHostingProviders()[0]
59
+
60
+ if (!hostingProvider) {
61
+ throw new PublicationError('No hosting provider found', 404)
62
+ }
63
+
64
+ if (!await hostingProvider.isLoggedIn(session)) {
65
+ throw new PublicationError('Not logged in', 401)
66
+ }
67
+
68
+ return hostingProvider as HostingProvider
69
+ }
70
+
71
+ export default async function(config: ServerConfig, opts = {}) {
72
+ const options = {
73
+ // Defaults
74
+ statusUrl: process.env.SILEX_PUBLICATION_STATUS_URL,
75
+ backend: 'src/plugins/DefaultBackend.js',
76
+ // Options
77
+ ...opts,
78
+ }
79
+
80
+ config.on(EVENT_STARTUP_START, ({app}) => {
81
+ const router = express.Router()
82
+
83
+ // Get publication status
84
+ router.get(`/${API_PUBLICATION_STATUS}`, async function (req, res) {
85
+ const query: ApiPublicationStatusRequestQuery = req.query as any
86
+ const jobId: JobId = query.jobId as string
87
+ const job = jobId && getJob(jobId)
88
+ if (!job) {
89
+ console.error(`Error: job not found with id ${jobId}`)
90
+ res.status(404).json({
91
+ message: 'Error: job not found.',
92
+ } as ApiResponseError)
93
+ return
94
+ }
95
+ res.json({
96
+ ...job,
97
+ _timeout: undefined,
98
+ } as ApiPublicationStatusResponse)
99
+ })
100
+
101
+ // Publish website
102
+ router.post(`/${API_PUBLICATION_PUBLISH}`, async function (req, res) {
103
+ try {
104
+ const query: ApiPublicationPublishRequestQuery = req.query as any
105
+ const body: ApiPublicationPublishRequestBody = req.body
106
+ const id = requiredParam<string>(query.id as string, 'id in query')
107
+ const session = requiredParam(req['session'], 'session on express request')
108
+
109
+ // Check params
110
+ const { files, publication } = body as PublicationData
111
+ const publicationSettings: PublicationSettings = {
112
+ ...defaultPublication,
113
+ ...publication,
114
+ }
115
+ const backend = requiredParam(publicationSettings.backend, 'backend object in publicationSettings')
116
+ const backendId = requiredParam(backend.backendId, 'backendId in publicationSettings')
117
+ if (!files) throw new PublicationError('Error in the request, files not found', 400)
118
+
119
+ // Get hosting provider and make sure the user is logged in
120
+ const hostingProvider = await getHostingProvider(session, config, backendId)
121
+ if (!hostingProvider) throw new PublicationError('Error in the request, hosting provider not found', 400)
122
+ if (!hostingProvider.isLoggedIn(session)) throw new PublicationError(`You must be logged in with ${hostingProvider.displayName} to publish`, 401)
123
+
124
+ // Optim HTML
125
+ files.map(file => ({
126
+ ...file,
127
+ html: minify(file.html, {
128
+ continueOnParseError: true,
129
+ collapseInlineTagWhitespace: true,
130
+ collapseWhitespace: true,
131
+ minifyCSS: true,
132
+ minifyJS: true,
133
+ removeScriptTypeAttributes: true,
134
+ removeStyleLinkTypeAttributes: true,
135
+ }).trim(),
136
+ }))
137
+ // Publication
138
+ const filesList = files.flatMap(file => ([{
139
+ path: file.htmlPath,
140
+ content: file.html,
141
+ }, {
142
+ path: file.cssPath,
143
+ content: file.css,
144
+ }]))
145
+ try {
146
+ res.json({
147
+ url: await hostingProvider.getWebsiteUrl(session, id),
148
+ job: await hostingProvider.publish(session, id, backend, filesList),
149
+ } as ApiPublicationPublishResponse)
150
+ } catch (err) {
151
+ console.error('Error publishing the website', err)
152
+ res.status(500).json({
153
+ message: `Error publishing the website. ${err.message}`
154
+ } as ApiResponseError)
155
+ return
156
+ }
157
+ } catch (err) {
158
+ console.error('Error publishing the website', err)
159
+ res
160
+ .status(err.code ?? 500)
161
+ .json({
162
+ message: `Error publishing the website. ${err.message}`
163
+ } as ApiResponseError)
164
+ return
165
+ }
166
+ })
167
+
168
+ app.use(noCache, router)
169
+ })
170
+ }
@@ -1,7 +1,25 @@
1
- const express = require('express')
2
- const { readFileSync } = require('fs')
3
- const { createServer } = require('https')
4
- const fromorceSSL = require('express-force-ssl')
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 express from 'express'
19
+ import { readFileSync } from 'fs'
20
+ import { createServer } from 'https'
21
+ import forceSSL from 'express-force-ssl'
22
+ import { EVENT_STARTUP_START } from '../events'
5
23
 
6
24
  // interface SslOptions {
7
25
  // forceHttps?: boolean
@@ -11,7 +29,7 @@ const fromorceSSL = require('express-force-ssl')
11
29
  // sslPort?: string
12
30
  // }
13
31
 
14
- module.exports = async function(config, opts = {}) {
32
+ export default async function(config, opts = {}) {
15
33
  // Options with defaults
16
34
  const options = {
17
35
  forceHttps: process.env.SILEX_FORCE_HTTPS === 'true',
@@ -23,7 +41,7 @@ module.exports = async function(config, opts = {}) {
23
41
  }
24
42
 
25
43
  // Add routes on silex startup
26
- config.on('silex:startup:start', ({app}) => {
44
+ config.on(EVENT_STARTUP_START, ({app}) => {
27
45
  const router = express.Router()
28
46
 
29
47
  // SSL
@@ -0,0 +1,68 @@
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 express from 'express'
19
+ import nodeModules from 'node_modules-path'
20
+ import serveStatic from 'serve-static'
21
+ import { withCache } from './Cache'
22
+ import { join } from 'path'
23
+ import { EVENT_STARTUP_START } from '../events'
24
+
25
+ type StaticOptions = {
26
+ routes: {
27
+ path: string
28
+ route: string
29
+ }[]
30
+ }
31
+
32
+ export default async function(config, opts = {}) {
33
+ // Options with defaults
34
+ const options: StaticOptions = {
35
+ routes: [
36
+ {
37
+ route: '/',
38
+ path: 'public',
39
+ }, {
40
+ route: '/css/',
41
+ path: nodeModules('@fortawesome/fontawesome-free') + '/@fortawesome/fontawesome-free/css/',
42
+ }, {
43
+ route: '/webfonts/',
44
+ path: nodeModules('@fortawesome/fontawesome-free') + '/@fortawesome/fontawesome-free/webfonts/',
45
+ }, {
46
+ route: '/',
47
+ path: 'dist/client',
48
+ },
49
+ ]
50
+ // add project route for source maps
51
+ .concat(config.debug ? [{
52
+ route: '/',
53
+ path: './',
54
+ }] : []),
55
+ ...opts,
56
+ }
57
+
58
+ config.on(EVENT_STARTUP_START, ({app}) => {
59
+ const router = express.Router()
60
+ options.routes
61
+ .forEach(folder => {
62
+ if (!folder.route) throw new Error('The config for static module has no `route` attribute')
63
+ if (!folder.path) throw new Error('The config for static module has no `path` attribute')
64
+ router.use(folder.route, serveStatic(folder.path))
65
+ })
66
+ app.use(withCache, router)
67
+ })
68
+ }
@@ -0,0 +1,318 @@
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 { Router } from 'express'
19
+ import formidable, { File as FormidableFile } from 'formidable'
20
+ import { noCache } from './Cache'
21
+ import { API_WEBSITE_ASSETS_READ, API_WEBSITE_ASSETS_WRITE, API_WEBSITE_READ, API_WEBSITE_WRITE, API_WEBSITE_DELETE, WEBSITE_DATA_FILE_NAME } from '../constants'
22
+ import { createReadStream } from 'fs'
23
+ import { ApiResponseError, ApiWebsiteAssetsReadRequestParams, ApiWebsiteAssetsReadRequestQuery, ApiWebsiteAssetsReadResponse, ApiWebsiteAssetsWriteRequestQuery, ApiWebsiteAssetsWriteResponse, ApiWebsiteDeleteRequestQuery, ApiWebsiteReadRequestQuery, ApiWebsiteReadResponse, ApiWebsiteWriteRequestBody, ApiWebsiteWriteRequestQuery, ApiWebsiteWriteResponse, BackendId, BackendType, WebsiteMeta, WebsiteData, WebsiteId } from '../types'
24
+ import { File, StorageProvider, getBackend } from '../server/backends'
25
+ import { Readable } from 'stream'
26
+ import { requiredParam } from '../server/utils/validation'
27
+ import { basename } from 'path'
28
+ import { EVENT_STARTUP_START } from '../events'
29
+
30
+ /**
31
+ * @fileoverview Website plugin for Silex
32
+ * This plugin provides the website API to Silex server
33
+ */
34
+
35
+ /**
36
+ * Error thrown by the website API
37
+ * @param message error message
38
+ * @param code http status code
39
+ */
40
+ export class WebsiteError extends Error {
41
+ constructor(message: string, public code: number) {
42
+ super(message)
43
+ }
44
+ }
45
+
46
+ export default async function(config, opts = {}) {
47
+ // Options with defaults
48
+ const options = {
49
+ // Default constants
50
+ assetsPath: '/assets',
51
+ // Options
52
+ ...opts
53
+ }
54
+
55
+ config.on(EVENT_STARTUP_START, ({app}) => {
56
+ // Create a new router
57
+ const router = Router()
58
+ app.use(noCache, router)
59
+
60
+ // Load website data
61
+ router.get(`/${API_WEBSITE_READ}`, async (req, res) => {
62
+ const query: ApiWebsiteReadRequestQuery = req.query
63
+ const { id, backendId } = query
64
+ try {
65
+ if(id) {
66
+ // Get website data
67
+ const websiteData: WebsiteData | Readable = await readWebsite(
68
+ req['session'],
69
+ requiredParam(id, 'Website id'),
70
+ backendId as string | undefined,
71
+ )
72
+ if (websiteData instanceof Readable) {
73
+ websiteData.pipe(res.type('application/json'))
74
+ } else {
75
+ res.json(websiteData as ApiWebsiteReadResponse)
76
+ }
77
+ } else {
78
+ // List websites
79
+ const websites = await listWebsites(req['session'], query.backendId as string | undefined)
80
+ res.json(websites as ApiWebsiteReadResponse)
81
+ }
82
+ } catch(e) {
83
+ console.error('Error getting website data', e)
84
+ if(e instanceof WebsiteError) {
85
+ res.status(e.code).json({ message: e.message } as ApiResponseError)
86
+ } else {
87
+ res.status(500).json({ message: e.message } as ApiResponseError)
88
+ }
89
+ }
90
+ })
91
+
92
+ // Save website data
93
+ router.post(`/${API_WEBSITE_WRITE}`, async (req, res) => {
94
+ try {
95
+ const query: ApiWebsiteWriteRequestQuery = req.query as any
96
+ const body: ApiWebsiteWriteRequestBody = req.body
97
+ const id = requiredParam<WebsiteId>(query.id, 'Website id')
98
+ const websiteData = requiredParam<WebsiteData>(body, 'Website data') as WebsiteData
99
+ await writeWebsite(
100
+ req['session'],
101
+ id,
102
+ websiteData,
103
+ query.backendId,
104
+ )
105
+ res.status(200).json({ message: 'Website saved' } as ApiResponseError)
106
+ } catch(e) {
107
+ console.error('Error saving website data', e)
108
+ if(e instanceof WebsiteError) {
109
+ res.status(e.code).json({ message: e.message } as ApiResponseError)
110
+ } else {
111
+ res.status(500).json({ message: e.message } as ApiResponseError)
112
+ }
113
+ }
114
+ })
115
+
116
+ // Delete website
117
+ router.delete(`/${API_WEBSITE_DELETE}`, async (req, res) => {
118
+ try {
119
+ const query: ApiWebsiteDeleteRequestQuery = req.query as any
120
+ const id = requiredParam<WebsiteId>(query.id, 'Website id')
121
+ await deleteWebsite(req['session'], id, query.backendId)
122
+ res.status(200).json({ message: 'Website deleted' } as ApiResponseError)
123
+ } catch(e) {
124
+ console.error('Error deleting website data', e)
125
+ if(e instanceof WebsiteError) {
126
+ res.status(e.code).json({ message: e.message } as ApiResponseError)
127
+ } else {
128
+ res.status(500).json({ message: e.message } as ApiResponseError)
129
+ }
130
+ }
131
+ })
132
+
133
+ // Load assets
134
+ router.get(`/${API_WEBSITE_ASSETS_READ}`, async (req, res) => {{
135
+ try {
136
+ const query: ApiWebsiteAssetsReadRequestQuery = req.query as any
137
+ const params: ApiWebsiteAssetsReadRequestParams = req.params as any
138
+ const id = requiredParam<WebsiteId>(query.id, 'Website id')
139
+ const path = requiredParam<string>(params.path, 'path')
140
+ const asset: File = await readAsset(req['session'], id, path, query.backendId)
141
+ // Set content type
142
+ res.contentType(basename(asset.path))
143
+ // Send the file
144
+ if(asset.content instanceof Readable) {
145
+ // Stream
146
+ asset.content.pipe(res)
147
+ } else {
148
+ // Buffer or string
149
+ res.send(asset.content as ApiWebsiteAssetsReadResponse)
150
+ }
151
+ } catch(e) {
152
+ console.error('Error getting asset', e)
153
+ if(e instanceof WebsiteError) {
154
+ res.status(e.code).json({ message: e.message } as ApiResponseError)
155
+ } else {
156
+ res.status(500).json({ message: e.message } as ApiResponseError)
157
+ }
158
+ }
159
+ }})
160
+
161
+ // Upload assets
162
+ router.post(`/${API_WEBSITE_ASSETS_WRITE}`, async (req, res) => {
163
+ try {
164
+ const query: ApiWebsiteAssetsWriteRequestQuery = req.query as any
165
+ const id = requiredParam<WebsiteId>(query.id as WebsiteId, 'Website id')
166
+
167
+ // Get the file data from the request
168
+ const form = formidable({
169
+ filename: (name, ext, part, _form) => `${name}${ext}`,
170
+ multiples: true,
171
+ keepExtensions: true,
172
+ })
173
+ const files: File[] = await new Promise<File[]>((resolve, reject) => {
174
+ form.parse(req, async (err, fields, _files) => {
175
+ if (err) {
176
+ console.error('Error parsing upload data', err)
177
+ reject(new WebsiteError('Error parsing upload data: ' + err.message, 400))
178
+ } else {
179
+ const files = [].concat(_files['files[]'])
180
+ .map((file: FormidableFile) => ({
181
+ path: `${options.assetsPath}/${file.originalFilename}`,
182
+ content: createReadStream(file.filepath),
183
+ }))
184
+ resolve(files)
185
+ }
186
+ })
187
+ })
188
+
189
+ // Write the files
190
+ console.log('Uploading assets', files)
191
+ const data = await writeAssets(req['session'], id, files, query.backendId)
192
+ console.log('Uploaded assets', data)
193
+
194
+ // Return the file URLs to insert in the website
195
+ res.json({
196
+ data, // As expected by grapesjs (https://grapesjs.com/docs/modules/Assets.html#uploading-assets)
197
+ } as ApiWebsiteAssetsWriteResponse)
198
+
199
+ } catch(e) {
200
+ console.error('Error uploading assets', e)
201
+ if(e instanceof WebsiteError) {
202
+ res.status(e.code).json({ message: e.message } as ApiResponseError)
203
+ } else {
204
+ res.status(500).json({ message: e.message } as ApiResponseError)
205
+ }
206
+ }
207
+ })
208
+ })
209
+
210
+ /**
211
+ * Get the desired backend
212
+ * Can be the default backend or a specific one
213
+ */
214
+ async function getStorageProvider(session: any, backendId?: string): Promise<StorageProvider> {
215
+ const storageProvider = await getBackend(config, session, BackendType.STORAGE, backendId) // ?? config.getStorageProviders()[0]
216
+
217
+ if(!storageProvider) {
218
+ throw new WebsiteError('No storage provider found', 404)
219
+ }
220
+
221
+ if(!await storageProvider.isLoggedIn(session)) {
222
+ throw new WebsiteError('Not logged in', 401)
223
+ }
224
+
225
+ return storageProvider as StorageProvider
226
+ }
227
+
228
+ /**
229
+ * Website a website data or list all websites
230
+ */
231
+ async function readWebsite(session: any, id: string, backendId?: string): Promise<WebsiteData | Readable> {
232
+ // Get the desired backend
233
+ const storageProvider = await getStorageProvider(session, backendId)
234
+
235
+ // List websites or get a website
236
+ // Get a website data
237
+ const file = await storageProvider.readFile(session, id, WEBSITE_DATA_FILE_NAME)
238
+ if(typeof file.content === 'string') return JSON.parse(file.content)
239
+ else if(file.content instanceof Buffer) return JSON.parse(file.content.toString())
240
+ else if(file.content instanceof Readable) return file.content
241
+ throw new WebsiteError('Invalid website data', 500)
242
+ }
243
+
244
+ /**
245
+ * List existing websites
246
+ */
247
+ async function listWebsites(session: any, backendId?: string): Promise<WebsiteMeta[]> {
248
+ // Get the desired backend
249
+ const storageProvider = await getStorageProvider(session, backendId)
250
+
251
+ // List websites
252
+ return storageProvider.listWebsites(session)
253
+ }
254
+
255
+ /**
256
+ * Write the website data to the backend
257
+ */
258
+ async function writeWebsite(session: any, id: WebsiteId, websiteData: WebsiteData, backendId?: BackendId): Promise<void> {
259
+ // Get the desired backend
260
+ const storageProvider = await getStorageProvider(session, backendId)
261
+
262
+ // Init the storage for this website (create the folder if it does not exist)
263
+ // FIXME: put this after login
264
+ // await storageProvider.init(session, id)
265
+
266
+ // Write the website data
267
+ await storageProvider.writeFiles(session, id, [{
268
+ path: WEBSITE_DATA_FILE_NAME,
269
+ content: JSON.stringify(websiteData),
270
+ }])
271
+ }
272
+
273
+ /**
274
+ * Delete a website
275
+ */
276
+ async function deleteWebsite(session: any, id: string, backendId?: string) {
277
+ // Get the desired backend
278
+ const storageProvider = await getStorageProvider(session, backendId)
279
+
280
+ // Delete the website
281
+ return await storageProvider.deleteDir(session, id, '/')
282
+ }
283
+
284
+ /**
285
+ * Read an asset
286
+ */
287
+ async function readAsset(session: any, id: string, fileName: string, backendId?: string): Promise<File> {
288
+ //const { session } = req
289
+ //const id = req.query.id
290
+ //const fileName = req.params[0]
291
+ //const uploadDir = await assetsDir(id)
292
+ //res.sendFile(`${uploadDir}/${fileName}`)
293
+
294
+ // Get the desired backend
295
+ const storageProvider = await getStorageProvider(session, backendId)
296
+
297
+ // Read the asset from the backend
298
+ return await storageProvider.readFile(session, id, `/${options.assetsPath}/${fileName}`)
299
+ }
300
+
301
+ /**
302
+ * Write an asset to the backend
303
+ */
304
+ async function writeAssets(session: any, id: string, files: File[], backendId?: string): Promise<string[]> {
305
+ // Get the desired backend
306
+ const storageProvider = await getStorageProvider(session, backendId)
307
+
308
+ // Write the asset to the backend
309
+ await storageProvider.writeFiles(
310
+ session,
311
+ id,
312
+ files
313
+ )
314
+
315
+ // Return the files URLs with the website id
316
+ return files.map(({ path }) => `${path}?id=${id}`)
317
+ }
318
+ }