@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.
- package/.silex.js +38 -8
- package/dist/client/client/config.d.ts +11 -41
- package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
- package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
- package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
- package/dist/client/client/grapesjs/index.d.ts +18 -44
- package/dist/client/client/grapesjs/storage.d.ts +1 -0
- package/dist/client/client/services.d.ts +35 -0
- package/dist/client/constants.d.ts +17 -0
- package/dist/client/css/admin.css +86 -3
- package/dist/client/js/main.js +201 -90
- package/dist/client/js/main.js.LICENSE.txt +6 -0
- package/dist/client/types.d.ts +200 -29
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +38 -1
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -0
- package/dist/plugins/AuthPlugin.d.ts +1 -0
- package/dist/plugins/AuthPlugin.js +38 -0
- package/dist/plugins/AuthPlugin.js.map +1 -0
- package/dist/plugins/Cache.d.ts +2 -0
- package/dist/plugins/Cache.js +32 -0
- package/dist/plugins/Cache.js.map +1 -0
- package/dist/plugins/FtpBackend.d.ts +92 -0
- package/dist/plugins/FtpBackend.js +604 -0
- package/dist/plugins/FtpBackend.js.map +1 -0
- package/dist/plugins/HooksPlugin.d.ts +7 -0
- package/dist/plugins/HooksPlugin.js +76 -0
- package/dist/plugins/HooksPlugin.js.map +1 -0
- package/dist/plugins/PublicationPlugin.d.ts +17 -0
- package/dist/plugins/PublicationPlugin.js +170 -0
- package/dist/plugins/PublicationPlugin.js.map +1 -0
- package/dist/plugins/SslPlugin.d.ts +1 -0
- package/dist/plugins/SslPlugin.js +86 -0
- package/dist/plugins/SslPlugin.js.map +1 -0
- package/dist/plugins/StaticPlugin.d.ts +1 -0
- package/dist/plugins/StaticPlugin.js +66 -0
- package/dist/plugins/StaticPlugin.js.map +1 -0
- package/dist/plugins/WebsitePlugin.d.ts +14 -0
- package/dist/plugins/WebsitePlugin.js +302 -0
- package/dist/plugins/WebsitePlugin.js.map +1 -0
- package/dist/server/backends/FsBackend.d.ts +42 -0
- package/dist/server/backends/FsBackend.js +269 -0
- package/dist/server/backends/FsBackend.js.map +1 -0
- package/dist/server/backends/index.d.ts +95 -0
- package/dist/server/backends/index.js +255 -0
- package/dist/server/backends/index.js.map +1 -0
- package/dist/server/cli.js +0 -1
- package/dist/server/config.d.ts +58 -0
- package/dist/server/config.js +128 -24
- package/dist/server/config.js.map +1 -0
- package/dist/server/express.js +31 -3
- package/dist/server/index.js +9 -20
- package/dist/server/jobs.d.ts +6 -0
- package/dist/server/jobs.js +75 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/utils/validation.d.ts +7 -0
- package/dist/server/utils/validation.js +37 -0
- package/dist/server/utils/validation.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +35 -28
- package/dist/types.js.map +1 -0
- package/package.json +19 -8
- package/public/silex.js +17 -0
- package/src/css/dialog.scss +9 -2
- package/src/css/grapesjs.scss +79 -0
- package/src/css/project-bar.scss +2 -2
- package/src/example-client-config.js +17 -0
- package/src/html/templates/includes.pug +0 -1
- package/src/ts/client/config.ts +7 -6
- package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
- package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
- package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
- package/src/ts/client/grapesjs/index.ts +88 -85
- package/src/ts/client/grapesjs/storage.ts +110 -0
- package/src/ts/client/index.ts +5 -7
- package/src/ts/client/services.ts +129 -0
- package/src/ts/constants.ts +21 -0
- package/src/ts/plugins/AuthPlugin.ts +38 -0
- package/src/ts/plugins/Cache.ts +28 -0
- package/src/ts/plugins/FtpBackend.ts +689 -0
- package/src/ts/plugins/HooksPlugin.ts +79 -0
- package/src/ts/plugins/PublicationPlugin.ts +170 -0
- package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
- package/src/ts/plugins/StaticPlugin.ts +68 -0
- package/src/ts/plugins/WebsitePlugin.ts +318 -0
- package/src/ts/server/backends/FsBackend.ts +266 -0
- package/src/ts/server/backends/index.ts +309 -0
- package/src/ts/server/cli.ts +0 -2
- package/src/ts/server/config.ts +103 -33
- package/src/ts/server/express.ts +37 -3
- package/src/ts/server/index.ts +24 -14
- package/src/ts/server/jobs.test.ts +74 -0
- package/src/ts/server/jobs.ts +77 -0
- package/src/ts/server/utils/validation.ts +33 -0
- package/src/ts/types.ts +167 -59
- package/dist/client/client/grapesjs/publish.d.ts +0 -18
- package/dist/page.js +0 -15
- package/src/plugins/AuthPlugin.js +0 -21
- package/src/plugins/Cache.js +0 -11
- package/src/plugins/DefaultBackend.js +0 -149
- package/src/plugins/ExpressPlugin.js +0 -39
- package/src/plugins/HooksPlugin.js +0 -58
- package/src/plugins/PublishPlugin.js +0 -69
- package/src/plugins/StaticPlugin.js +0 -48
- package/src/plugins/WebsitePlugin.js +0 -207
- package/src/ts/client/grapesjs/publish.ts +0 -403
- /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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|