@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,266 @@
|
|
|
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 fs, { stat } from 'fs/promises'
|
|
19
|
+
import { createWriteStream, mkdir } from 'fs'
|
|
20
|
+
import fsExtra from 'fs-extra'
|
|
21
|
+
import { File, StorageProvider, HostingProvider, StatusCallback, BackendSession} from '.'
|
|
22
|
+
import { join } from 'path'
|
|
23
|
+
import { BackendData, BackendUser, WebsiteMeta, FileMeta, JobData, JobId, JobStatus, WebsiteId, BackendType, PublicationJobData } from '../../types'
|
|
24
|
+
import { jobError, jobSuccess, startJob } from '../jobs'
|
|
25
|
+
import { userInfo } from 'os'
|
|
26
|
+
import { WEBSITE_DATA_FILE_NAME } from '../../constants'
|
|
27
|
+
import { requiredParam } from '../utils/validation'
|
|
28
|
+
import { ServerConfig } from '../config'
|
|
29
|
+
|
|
30
|
+
type FsSession = BackendSession
|
|
31
|
+
|
|
32
|
+
interface FsOptions {
|
|
33
|
+
rootPath?: string
|
|
34
|
+
type: BackendType
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class FsBackend implements StorageProvider<FsSession>, HostingProvider<FsSession> {
|
|
38
|
+
id = 'fs'
|
|
39
|
+
displayName = 'File system'
|
|
40
|
+
icon = 'file'
|
|
41
|
+
rootPath: string
|
|
42
|
+
disableLogout = true
|
|
43
|
+
options: { rootPath: string, type: BackendType }
|
|
44
|
+
type: BackendType
|
|
45
|
+
|
|
46
|
+
constructor(config: ServerConfig | null, opts: FsOptions) {
|
|
47
|
+
this.options = {
|
|
48
|
+
rootPath: __dirname + '/../../../.silex',
|
|
49
|
+
...opts,
|
|
50
|
+
}
|
|
51
|
+
this.type = this.options.type
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ********************
|
|
55
|
+
// Utils methods
|
|
56
|
+
// ********************
|
|
57
|
+
updateStatus(filesStatuses, status, statusCbk) {
|
|
58
|
+
statusCbk && statusCbk({
|
|
59
|
+
message: `<p>Writing files:<ul><li>${filesStatuses.map(({file, message}) => `${file.path}: ${message}`).join('</li><li>')}</li></ul></p>`,
|
|
60
|
+
status,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
initStatus(files) {
|
|
64
|
+
return files.map(file => ({
|
|
65
|
+
file,
|
|
66
|
+
message: 'Waiting',
|
|
67
|
+
status: JobStatus.IN_PROGRESS,
|
|
68
|
+
}))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ********************
|
|
72
|
+
// Backend interface
|
|
73
|
+
// ********************
|
|
74
|
+
async getAdminUrl(session: FsSession, id: WebsiteId): Promise<string> {
|
|
75
|
+
return ''
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getAuthorizeURL(session: FsSession): Promise<string> {
|
|
79
|
+
return ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async isLoggedIn(session: FsSession): Promise<boolean> {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async login(session: FsSession): Promise<void> {}
|
|
87
|
+
|
|
88
|
+
async logout(session: FsSession): Promise<void> {}
|
|
89
|
+
|
|
90
|
+
async getUserData(session: FsSession): Promise<BackendUser> {
|
|
91
|
+
const { username, } = userInfo()
|
|
92
|
+
return {
|
|
93
|
+
name: username,
|
|
94
|
+
picture: this.icon,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getSiteMeta(session: FsSession, id: string): Promise<WebsiteMeta> {
|
|
99
|
+
const websiteId = requiredParam<WebsiteId>(id, 'website id')
|
|
100
|
+
const files = await this.listDir(session, websiteId, '/')
|
|
101
|
+
const file = files.find(f => f.name === WEBSITE_DATA_FILE_NAME)
|
|
102
|
+
if(!file) throw new Error('Website not found')
|
|
103
|
+
return {
|
|
104
|
+
websiteId,
|
|
105
|
+
// url: await this.getFileUrl(session, websiteId, WEBSITE_DATA_FILE_NAME),
|
|
106
|
+
//metadata: {
|
|
107
|
+
// path: session.ftp.path,
|
|
108
|
+
//},
|
|
109
|
+
...file,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async init(session: FsSession, id: WebsiteId): Promise<void> {
|
|
114
|
+
await fs.mkdir(join(this.options.rootPath, id), { recursive: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ********************
|
|
118
|
+
// Storage interface
|
|
119
|
+
// ********************
|
|
120
|
+
async listWebsites(session: any): Promise<WebsiteMeta[]> {
|
|
121
|
+
const list = await fs.readdir(this.options.rootPath)
|
|
122
|
+
return Promise.all(list.map(async fileName => {
|
|
123
|
+
const filePath = join(this.options.rootPath, fileName)
|
|
124
|
+
const fileStat = await stat(filePath)
|
|
125
|
+
return {
|
|
126
|
+
websiteId: fileName,
|
|
127
|
+
createdAt: fileStat.birthtime,
|
|
128
|
+
updatedAt: fileStat.mtime,
|
|
129
|
+
metadata: {
|
|
130
|
+
...fileStat,
|
|
131
|
+
},
|
|
132
|
+
} as WebsiteMeta
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async readFile(session: FsSession, id: WebsiteId, path: string): Promise<File> {
|
|
137
|
+
const fullPath = join(this.options.rootPath, id, path)
|
|
138
|
+
const content = await fs.readFile(fullPath)
|
|
139
|
+
console.log('read file', path, fullPath, content.length)
|
|
140
|
+
return { path, content }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async writeFiles(session: FsSession, id: WebsiteId, files: File[], statusCbk?: StatusCallback): Promise<string[]> {
|
|
144
|
+
const filesStatuses = this.initStatus(files)
|
|
145
|
+
let error = false
|
|
146
|
+
const filePaths = [] as string[]
|
|
147
|
+
for (const fileStatus of filesStatuses) {
|
|
148
|
+
const {file} = fileStatus
|
|
149
|
+
const path = join(this.options.rootPath, id, file.path)
|
|
150
|
+
filePaths.push(path)
|
|
151
|
+
if (typeof file.content === 'string') {
|
|
152
|
+
fileStatus.message = 'Writing'
|
|
153
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
154
|
+
try {
|
|
155
|
+
console.log('write file', path, file.content.length)
|
|
156
|
+
await fs.writeFile(path, file.content)
|
|
157
|
+
} catch(err) {
|
|
158
|
+
fileStatus.message = `Error (${err})`
|
|
159
|
+
console.log('write file stream', path, file.content.length)
|
|
160
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
161
|
+
error = true
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
fileStatus.message = 'Success'
|
|
165
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
166
|
+
} else {
|
|
167
|
+
fileStatus.message = 'Writing'
|
|
168
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
169
|
+
const writeStream = createWriteStream(path)
|
|
170
|
+
file.content.pipe(writeStream)
|
|
171
|
+
await new Promise((resolve) => {
|
|
172
|
+
writeStream.on('finish', () => {
|
|
173
|
+
fileStatus.message = 'Success'
|
|
174
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
175
|
+
resolve(file)
|
|
176
|
+
})
|
|
177
|
+
writeStream.on('error', err => {
|
|
178
|
+
console.error('writeStream error', err)
|
|
179
|
+
fileStatus.message = `Error (${err})`
|
|
180
|
+
this.updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
|
|
181
|
+
error = true
|
|
182
|
+
resolve(file)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.updateStatus(filesStatuses, error ? JobStatus.ERROR : JobStatus.SUCCESS, statusCbk)
|
|
188
|
+
return filePaths
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async deleteFiles(session: FsSession, id: WebsiteId, paths: string[]): Promise<void> {
|
|
192
|
+
for (const path of paths) {
|
|
193
|
+
await fs.unlink(join(this.options.rootPath, id, path))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async listDir(session: FsSession, id: WebsiteId, path: string): Promise<FileMeta[]> {
|
|
198
|
+
const list = await fs.readdir(join(this.options.rootPath, id, path))
|
|
199
|
+
return Promise.all(list.map(async fileName => {
|
|
200
|
+
const filePath = join(this.options.rootPath, id, path, fileName)
|
|
201
|
+
const fileStat = await stat(filePath)
|
|
202
|
+
return {
|
|
203
|
+
name: fileName,
|
|
204
|
+
size: fileStat.size,
|
|
205
|
+
isDir: fileStat.isDirectory(),
|
|
206
|
+
createdAt: fileStat.birthtime,
|
|
207
|
+
updatedAt: fileStat.mtime,
|
|
208
|
+
}
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// async listDir(session: FsSession, id: string, path: string): Promise<FileMeta[]> {
|
|
213
|
+
// const fileNames = await fs.readdir(this.options.rootPath)
|
|
214
|
+
// // Filter directories only
|
|
215
|
+
// // That's a way to do an async filter
|
|
216
|
+
// const withType = await Promise.all(fileNames.map(async fileName => ({
|
|
217
|
+
// fileName,
|
|
218
|
+
// isDir: (await stat(join(this.options.rootPath, fileName))).isDirectory(),
|
|
219
|
+
// })))
|
|
220
|
+
// return withType
|
|
221
|
+
// .filter(({fileName, isDir}) => isDir)
|
|
222
|
+
// .map(({fileName}) => fileName as WebsiteId)
|
|
223
|
+
// }
|
|
224
|
+
|
|
225
|
+
async createDir(session: FsSession, id: WebsiteId, path: string): Promise<void> {
|
|
226
|
+
await fs.mkdir(join(this.options.rootPath, id, path), { recursive: true })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async deleteDir(session: FsSession, id: WebsiteId, path: string): Promise<void> {
|
|
230
|
+
await fsExtra.remove(join(this.options.rootPath, id, path))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getFileUrl(session: FsSession, id: WebsiteId, path: string): Promise<string> {
|
|
234
|
+
const filePath = join(this.options.rootPath, id, path)
|
|
235
|
+
const fileUrl = new URL(filePath, 'file://')
|
|
236
|
+
return fileUrl.toString()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ********************
|
|
240
|
+
// Hosting provider interface
|
|
241
|
+
// ********************
|
|
242
|
+
async publish(session: FsSession, id: WebsiteId, backendData: BackendData, files: File[]): Promise<JobData> {
|
|
243
|
+
const job = startJob(`Publishing to ${this.displayName}`) as PublicationJobData
|
|
244
|
+
job.logs = [[`Publishing to ${this.displayName}`]]
|
|
245
|
+
job.errors = [[]]
|
|
246
|
+
this.writeFiles(session, id, files, async ({status, message}) => {
|
|
247
|
+
// Update the job status
|
|
248
|
+
job.status = status
|
|
249
|
+
job.message = message
|
|
250
|
+
job.logs[0].push(message)
|
|
251
|
+
// Add the website url
|
|
252
|
+
job.url = await this.getFileUrl(session, id, 'index.html')
|
|
253
|
+
if(status === JobStatus.SUCCESS) {
|
|
254
|
+
jobSuccess(job.jobId, message)
|
|
255
|
+
} else if(status === JobStatus.ERROR) {
|
|
256
|
+
job.errors[0].push(message)
|
|
257
|
+
jobError(job.jobId, message)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
return job
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async getWebsiteUrl(session: FsSession, id: WebsiteId): Promise<string> {
|
|
264
|
+
return this.getFileUrl(session, id, '/index.html')
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
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 { Application, Response, Router } from 'express'
|
|
19
|
+
import { Readable } from 'stream'
|
|
20
|
+
import { ServerConfig } from '../config'
|
|
21
|
+
import { JobStatus, JobData, BackendData, BackendId, WebsiteId, ApiBackendListResponse, ApiBackendListRequestQuery, ApiResponseError, ApiBackendLoginRequestQuery, ApiBackendLoggedInPostMessage, ApiBackendLogoutRequestQuery, BackendType, BackendUser, WebsiteMeta, ApiBackendLoginStatusRequestQuery, ApiBackendLoginStatusResponse, FileMeta } from '../../types'
|
|
22
|
+
import { API_BACKEND_LIST, API_BACKEND_LOGIN_CALLBACK, API_BACKEND_LOGIN_STATUS, API_BACKEND_LOGOUT } from '../../constants'
|
|
23
|
+
import { requiredParam } from '../utils/validation'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @fileoverview define types for Silex backends
|
|
27
|
+
* Bakends can provide storage for website data and assets, and/or hosting to publish the website online
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Files are stored in backend as a File object
|
|
32
|
+
* @see types/ClientSideFile
|
|
33
|
+
*/
|
|
34
|
+
export interface File {
|
|
35
|
+
path: string,
|
|
36
|
+
content: Buffer | string | Readable,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Callback to update the publication status
|
|
41
|
+
*/
|
|
42
|
+
export type StatusCallback = ({message, status}: {message: string, status: JobStatus}) => Promise<void>
|
|
43
|
+
|
|
44
|
+
export type BackendSession = any
|
|
45
|
+
/**
|
|
46
|
+
* Backends are the base interface for Storage and Hosting providers
|
|
47
|
+
*/
|
|
48
|
+
export interface Backend<Session extends BackendSession = BackendSession> {
|
|
49
|
+
id: BackendId
|
|
50
|
+
displayName: string
|
|
51
|
+
icon: string
|
|
52
|
+
disableLogout?: boolean
|
|
53
|
+
type: BackendType
|
|
54
|
+
getAdminUrl(session: Session, id: WebsiteId): Promise<string | null>
|
|
55
|
+
getAuthorizeURL(session: Session): Promise<string | null>
|
|
56
|
+
isLoggedIn(session: Session): Promise<boolean>
|
|
57
|
+
//login(session: Session, userData: any): Promise<void>
|
|
58
|
+
logout(session: Session): Promise<void>
|
|
59
|
+
getUserData(session: Session): Promise<BackendUser>
|
|
60
|
+
init(session: Session, id: WebsiteId): Promise<void>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Storage are used to store the website data and assets
|
|
65
|
+
* And possibly rename files and directories, and get the URL of a file
|
|
66
|
+
*
|
|
67
|
+
*/
|
|
68
|
+
export interface StorageProvider<Session = BackendSession> extends Backend<Session> {
|
|
69
|
+
listWebsites(session: Session): Promise<WebsiteMeta[]>
|
|
70
|
+
readFile(session: Session, id: WebsiteId, path: string): Promise<File>
|
|
71
|
+
writeFiles(session: Session, id: WebsiteId, files: File[], status?: StatusCallback): Promise<string[]> // Returns the files paths on storage
|
|
72
|
+
deleteFiles(session: Session, id: WebsiteId, paths: string[]): Promise<void>
|
|
73
|
+
listDir(session: Session, id: WebsiteId, path: string): Promise<FileMeta[]>
|
|
74
|
+
createDir(session: Session, id: WebsiteId, path: string): Promise<void>
|
|
75
|
+
deleteDir(session: Session, id: WebsiteId, path: string): Promise<void>
|
|
76
|
+
getSiteMeta(session: Session, id: WebsiteId): Promise<WebsiteMeta>
|
|
77
|
+
//getFileUrl(session: Session, id: WebsiteId, path: string): Promise<string>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hosting providers are used to publish the website
|
|
82
|
+
*/
|
|
83
|
+
export interface HostingProvider<Session = BackendSession> extends Backend<Session> {
|
|
84
|
+
publish(session: Session, id: WebsiteId, backendData: BackendData, files: File[]): Promise<JobData>
|
|
85
|
+
getWebsiteUrl(session: Session, id: WebsiteId): Promise<string>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function toBackendEnum(type: string): BackendType {
|
|
89
|
+
return BackendType[type.toUpperCase() as keyof typeof BackendType]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get a backend by id or by type
|
|
94
|
+
*/
|
|
95
|
+
export async function getBackend<T extends Backend>(config: ServerConfig, session: any, type: BackendType, backendId?: BackendId): Promise<T | undefined> {
|
|
96
|
+
const backends = config.getBackends<T>(type)
|
|
97
|
+
// Find the backend by id
|
|
98
|
+
if (backendId) return backends.find(s => s.id === backendId && s.type === type)
|
|
99
|
+
// Find the first logged in backend
|
|
100
|
+
for (const backend of backends) {
|
|
101
|
+
if (await backend.isLoggedIn(session)) {
|
|
102
|
+
return backend
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Defaults to the first backend
|
|
106
|
+
return backends[0]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert a backend to a BackendData object to be sent to the frontend
|
|
111
|
+
*/
|
|
112
|
+
export async function toBackendData(session: any, backend: Backend): Promise<BackendData> {
|
|
113
|
+
return {
|
|
114
|
+
backendId: backend.id,
|
|
115
|
+
type: backend.type,
|
|
116
|
+
displayName: backend.displayName,
|
|
117
|
+
icon: backend.icon,
|
|
118
|
+
disableLogout: !!backend.disableLogout,
|
|
119
|
+
isLoggedIn: await backend.isLoggedIn(session),
|
|
120
|
+
authUrl: await backend.getAuthorizeURL(session),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add routes to the express app
|
|
126
|
+
*/
|
|
127
|
+
export function addRoutes(config: ServerConfig, app: Application) {
|
|
128
|
+
// Create the router
|
|
129
|
+
const router = Router()
|
|
130
|
+
app.use(router)
|
|
131
|
+
|
|
132
|
+
// Backend routes
|
|
133
|
+
router.get(`/${API_BACKEND_LOGIN_STATUS}`, routeLoginStatus)
|
|
134
|
+
router.get(`/${API_BACKEND_LIST}`, routeListBackends)
|
|
135
|
+
router.post(`/${API_BACKEND_LOGOUT}`, routeLogout)
|
|
136
|
+
router.get(`/${API_BACKEND_LOGIN_CALLBACK}`, routeLoginSuccess)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Express route to check if the user is logged in
|
|
141
|
+
* Returns user data and backend data
|
|
142
|
+
*/
|
|
143
|
+
export async function routeLoginStatus(req, res) {
|
|
144
|
+
try {
|
|
145
|
+
const config = requiredParam(req.app.get('config') as ServerConfig, 'Config object on express js APP')
|
|
146
|
+
const query = requiredParam(req.query as ApiBackendLoginStatusRequestQuery, 'Query object')
|
|
147
|
+
const session = requiredParam(req['session'], 'Session object')
|
|
148
|
+
const websiteId = query.id
|
|
149
|
+
const backendId = query.backendId
|
|
150
|
+
const type = query.type
|
|
151
|
+
const backend = await getBackend<Backend>(config, session, type, backendId)
|
|
152
|
+
if (!backend) {
|
|
153
|
+
res
|
|
154
|
+
.status(500)
|
|
155
|
+
.json({
|
|
156
|
+
error: true,
|
|
157
|
+
message: 'No backend found',
|
|
158
|
+
} as ApiResponseError)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
if(!await backend.isLoggedIn(session)) {
|
|
162
|
+
res
|
|
163
|
+
.status(401)
|
|
164
|
+
.json({
|
|
165
|
+
error: true,
|
|
166
|
+
message: 'Not logged in',
|
|
167
|
+
} as ApiResponseError)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
const backendData = await toBackendData(session, backend)
|
|
171
|
+
const user = await backend.getUserData(session)
|
|
172
|
+
const websiteMeta = websiteId && backend.type === BackendType.STORAGE && await (backend as StorageProvider).getSiteMeta(session, websiteId)
|
|
173
|
+
res.json({
|
|
174
|
+
backend: backendData,
|
|
175
|
+
user,
|
|
176
|
+
websiteMeta,
|
|
177
|
+
} as ApiBackendLoginStatusResponse)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error in the login status request', error)
|
|
180
|
+
res.status(error?.code ?? 500).json({
|
|
181
|
+
error: true,
|
|
182
|
+
message: error.message,
|
|
183
|
+
} as ApiResponseError)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Express route to list the backends
|
|
189
|
+
*/
|
|
190
|
+
export async function routeListBackends(req, res) {
|
|
191
|
+
try {
|
|
192
|
+
const config = requiredParam(req.app.get('config') as ServerConfig, 'Config object on express js APP')
|
|
193
|
+
const query = req.query as ApiBackendListRequestQuery
|
|
194
|
+
const type = toBackendEnum(requiredParam(query.type, 'Backend type'))
|
|
195
|
+
const backends = config.getBackends<Backend>(type)
|
|
196
|
+
try {
|
|
197
|
+
const list = await Promise.all(backends.map(async backend => toBackendData(req['session'], backend)))
|
|
198
|
+
res.json(list as ApiBackendListResponse)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Error while listing backends', error)
|
|
201
|
+
res.status(error?.code ?? 500).json({
|
|
202
|
+
error: true,
|
|
203
|
+
message: 'Error while listing backends: ' + error.message,
|
|
204
|
+
} as ApiResponseError)
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error in the list backends request', error)
|
|
208
|
+
res.status(error?.code ?? 400).json({
|
|
209
|
+
error: true,
|
|
210
|
+
message: 'Error in the list backends request: ' + error.message,
|
|
211
|
+
} as ApiResponseError)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Utility function to send an HTML page to the browser
|
|
217
|
+
* This page will send a postMessage to the parent window and close itself
|
|
218
|
+
*/
|
|
219
|
+
function sendHtml(res: Response, message: string, backendId?: BackendId, error?: Error, defaultErrorCode?: number) {
|
|
220
|
+
error && console.error('Error while logging in', error)
|
|
221
|
+
// Data for postMessage
|
|
222
|
+
const data = {
|
|
223
|
+
type: 'login', // For postMessage
|
|
224
|
+
error: error ? true : false,
|
|
225
|
+
message,
|
|
226
|
+
backendId,
|
|
227
|
+
} as ApiBackendLoggedInPostMessage
|
|
228
|
+
// HTTP status code
|
|
229
|
+
const status = error ? error['code'] ?? defaultErrorCode ?? 500 : 200
|
|
230
|
+
// Send the HTML
|
|
231
|
+
res
|
|
232
|
+
.status(status)
|
|
233
|
+
.send(`
|
|
234
|
+
<html>
|
|
235
|
+
<head>
|
|
236
|
+
<script>
|
|
237
|
+
window.opener.postMessage(${JSON.stringify(data)}, '*')
|
|
238
|
+
window.close()
|
|
239
|
+
</script>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
<p>${message}</p>
|
|
243
|
+
<p>Close this window</p>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Express route to serve as redirect after a successful login
|
|
251
|
+
* The returned HTML will postMessage data and close the popup window
|
|
252
|
+
*/
|
|
253
|
+
export async function routeLoginSuccess(req, res) {
|
|
254
|
+
try {
|
|
255
|
+
const query = req.query as ApiBackendLoginRequestQuery
|
|
256
|
+
if(query.error) throw new Error(query.error)
|
|
257
|
+
|
|
258
|
+
const backendId = requiredParam(query.backendId, 'Backend id')
|
|
259
|
+
const config = requiredParam(req.app.get('config') as ServerConfig, 'Config object on express js APP')
|
|
260
|
+
const type = toBackendEnum(requiredParam(query.type, 'Backend type'))
|
|
261
|
+
const backend = await getBackend<Backend>(config, req['session'], type, backendId)
|
|
262
|
+
if (!backend) throw new Error('Backend not found ' + backendId)
|
|
263
|
+
try {
|
|
264
|
+
sendHtml(res, 'Logged in', backendId)
|
|
265
|
+
} catch (error) {
|
|
266
|
+
sendHtml(res, 'Error while logging in', undefined, error, 500)
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
sendHtml(res, 'Error in the request ' + error.message, undefined, error, 400)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Express route to logout from a backend
|
|
275
|
+
*/
|
|
276
|
+
export async function routeLogout(req, res) {
|
|
277
|
+
try {
|
|
278
|
+
const query = req.query as ApiBackendLogoutRequestQuery
|
|
279
|
+
// Get the backend
|
|
280
|
+
const backendId = requiredParam(query.backendId, 'Backend id')
|
|
281
|
+
const config = requiredParam(req.app.get('config') as ServerConfig, 'Config object on express js APP')
|
|
282
|
+
const type = toBackendEnum(requiredParam(query.type, 'Backend type'))
|
|
283
|
+
const backend = await getBackend<Backend>(config, req['session'], type, backendId)
|
|
284
|
+
if (!backend) throw new Error(`Backend not found ${backendId} ${type}`)
|
|
285
|
+
try {
|
|
286
|
+
// Logout
|
|
287
|
+
await backend.logout(req['session'])
|
|
288
|
+
// Return success
|
|
289
|
+
res.json({
|
|
290
|
+
error: false,
|
|
291
|
+
message: 'OK',
|
|
292
|
+
} as ApiResponseError)
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Error while logging out', error)
|
|
295
|
+
res.status(error?.code ?? 500).json({
|
|
296
|
+
error: true,
|
|
297
|
+
message: 'Error while logging out: ' + error.message,
|
|
298
|
+
} as ApiResponseError)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('Error in the logout request', error)
|
|
303
|
+
res.status(error?.code ?? 400).json({
|
|
304
|
+
error: true,
|
|
305
|
+
message: 'Error in the logout request: ' + error.message,
|
|
306
|
+
} as ApiResponseError)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
}
|