@silexlabs/silex 3.0.0-alpha.46 → 3.0.0-alpha.48

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.
@@ -21,7 +21,6 @@ import { ConnectorFile, ConnectorFileContent, StatusCallback, StorageConnector,
21
21
  import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent } from '../../types'
22
22
  import fetch from 'node-fetch'
23
23
  import crypto, { createHash } from 'crypto'
24
- import { PassThrough, Readable } from 'stream'
25
24
 
26
25
  /**
27
26
  * Gitlab connector
@@ -34,6 +33,8 @@ export interface GitlabOptions {
34
33
  clientSecret: string
35
34
  branch: string
36
35
  assetsFolder: string
36
+ metaRepo: string
37
+ metaRepoFile: string
37
38
  }
38
39
 
39
40
  interface GitlabToken {
@@ -50,6 +51,7 @@ interface GitlabToken {
50
51
  scope: string
51
52
  }
52
53
  userId?: number
54
+ username?: string
53
55
  }
54
56
 
55
57
  interface GitlabSession {
@@ -65,7 +67,7 @@ interface GitlabAction {
65
67
  interface GitlabWriteFile {
66
68
  branch: string
67
69
  commit_message: string
68
- id: string
70
+ id?: string
69
71
  actions?: GitlabAction[]
70
72
  content?: string
71
73
  file_path?: string
@@ -90,6 +92,16 @@ interface GitlabCreateBranch {
90
92
  ref: string
91
93
  }
92
94
 
95
+ interface MetaRepoFileContent {
96
+ websites: {
97
+ [websiteId: string]: {
98
+ meta: WebsiteMetaFileContent,
99
+ createdAt: string,
100
+ updatedAt: string,
101
+ }
102
+ }
103
+ }
104
+
93
105
  async function wait(ms: number) {
94
106
  return new Promise(resolve => setTimeout(resolve, ms))
95
107
  }
@@ -108,6 +120,8 @@ export default class GitlabConnector implements StorageConnector {
108
120
  this.options = {
109
121
  branch: 'main',
110
122
  assetsFolder: '',
123
+ metaRepo: 'silex-meta',
124
+ metaRepoFile: 'websites.json',
111
125
  ...opts,
112
126
  } as GitlabOptions
113
127
  if(!this.options.clientId) throw new Error('Missing Gitlab client ID')
@@ -144,6 +158,36 @@ export default class GitlabConnector implements StorageConnector {
144
158
  })
145
159
  }
146
160
 
161
+ /*
162
+ * Get the meta repo path for the current user
163
+ * The meta repo contains a JSON file which contains the list of websites
164
+ */
165
+ private getMetaRepoPath(session: GitlabSession): string {
166
+ if(!session.gitlab?.username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
167
+ return encodeURIComponent(`${session.gitlab.username}/${this.options.metaRepo}`)
168
+ }
169
+
170
+ /**
171
+ * Initialize the storage with a meta repo
172
+ */
173
+ private async initStorage(session: GitlabSession): Promise<void> {
174
+ // Create the meta repo
175
+ try {
176
+ const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
177
+ name: this.options.metaRepo,
178
+ }) as any
179
+ return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
180
+ websites: {}
181
+ } as MetaRepoFileContent))
182
+ } catch (e) {
183
+ console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
184
+ throw e
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Call the Gitlab API with the user's token and handle errors
190
+ */
147
191
  private async callApi(session: GitlabSession, path: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET', body: GitlabWriteFile | GitlabGetToken | GitlabWebsiteName | GitlabCreateBranch | null = null, params: any = {}): Promise<any> {
148
192
  const token = session?.gitlab?.token
149
193
  const tokenParam = token ? `access_token=${token.access_token}&` : ''
@@ -167,7 +211,7 @@ export default class GitlabConnector implements StorageConnector {
167
211
  throw e
168
212
  } else {
169
213
  // Useless error linked to the fact that the response is not JSON
170
- //console.error('Gitlab API error - could not parse response', response.status, response.statusText, {url, method, body, params, text})
214
+ console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, {url, method, body, params, text})
171
215
  return text
172
216
  }
173
217
  }
@@ -196,12 +240,14 @@ export default class GitlabConnector implements StorageConnector {
196
240
  }
197
241
  return await this.callApi(session, path, method, body as any, params)
198
242
  } else {
199
- //console.error('Gitlab API error - could not refresh token', response.status, response.statusText, { url, method, body, params, refreshJson })
200
- throw new ApiError(`Gitlab API error: ${refreshJson?.message ?? response.statusText}`, response.status)
243
+ const message = typeof refreshJson?.message === 'object' ? Object.entries(refreshJson.message).map(entry => entry.join(' ')).join(' ') : refreshJson?.message ?? refreshJson?.error ?? response.statusText
244
+ console.error('Gitlab API error (2) - could not refresh token', response.status, response.statusText, {message})
245
+ throw new ApiError(`Gitlab API error (2): ${message}`, response.status)
201
246
  }
202
247
  } else {
203
- //console.error('Gitlab API error', response.status, response.statusText, { url, method, body, params, json })
204
- throw new ApiError(`Gitlab API error: ${json?.message ?? json?.error ?? response.statusText}`, response.status)
248
+ const message = typeof json?.message === 'object' ? Object.entries(json.message).map(entry => entry.join(' ')).join(' ') : json?.message ?? json?.error ?? response.statusText
249
+ console.error('Gitlab API error (1)', response.status, response.statusText, {message})
250
+ throw new ApiError(`Gitlab API error (1): ${message}`, response.status)
205
251
  }
206
252
  }
207
253
  return json
@@ -281,7 +327,7 @@ export default class GitlabConnector implements StorageConnector {
281
327
  if(!session.gitlab?.codeVerifier) throw new ApiError('Missing code verifier', 401)
282
328
  if(!session.gitlab?.codeChallenge) throw new ApiError('Missing code challenge', 401)
283
329
 
284
- const token = await fetch('https://gitlab.com/oauth/token', {
330
+ const response = await fetch('https://gitlab.com/oauth/token', {
285
331
  method: 'POST',
286
332
  headers: {
287
333
  'Content-Type': 'application/json',
@@ -295,13 +341,16 @@ export default class GitlabConnector implements StorageConnector {
295
341
  code_verifier: session.gitlab.codeVerifier,
296
342
  }),
297
343
  })
298
- .then((response) => response.json())
299
344
 
345
+ const token = await response.json()
346
+
347
+ // Store the token in the session
300
348
  session.gitlab = { token, state: session.gitlab.state, codeVerifier: session.gitlab.codeVerifier, codeChallenge: session.gitlab.codeChallenge }
301
349
 
302
350
  // We need to get the user ID for listWebsites
303
351
  const user = await this.callApi(session, 'api/v4/user') as any
304
352
  session.gitlab.userId = user.id
353
+ session.gitlab.username = user.username
305
354
  }
306
355
 
307
356
  async logout(session: GitlabSession): Promise<void> {
@@ -313,19 +362,43 @@ export default class GitlabConnector implements StorageConnector {
313
362
  return {
314
363
  name: user.name,
315
364
  email: user.email,
365
+ picture: user.avatar_url,
316
366
  storage: await toConnectorData(session, this as StorageConnector),
317
367
  }
318
368
  }
319
369
 
320
370
  async listWebsites(session: GitlabSession): Promise<WebsiteMeta[]> {
321
- const projects = await this.callApi(session, `api/v4/users/${session.gitlab?.userId}/projects`) as any[]
322
- return projects.map(p => ({
323
- websiteId: p.id,
324
- name: p.name,
325
- createdAt: p.created_at,
326
- updatedAt: p.last_activity_at,
327
- connectorUserSettings: {},
328
- }))
371
+ try {
372
+ const result = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
373
+ ref: this.options.branch,
374
+ })
375
+ const { content } = result
376
+ const contentDecoded = Buffer.from(content, 'base64').toString('utf8')
377
+ const websites = (JSON.parse(contentDecoded) as MetaRepoFileContent).websites
378
+ console.log('listWebsites', {websites, contentDecoded, content, result})
379
+ return Object.entries(websites).map(([websiteId, {meta, createdAt, updatedAt}]) => ({
380
+ websiteId,
381
+ createdAt: new Date(createdAt),
382
+ updatedAt: new Date(updatedAt),
383
+ ...meta,
384
+ }))
385
+ } catch (e) {
386
+ console.error('Could not list websites', e.statusCode, e.httpStatusCode, e.code)
387
+ if (e.statusCode === 404 || e.httpStatusCode === 404) {
388
+ await this.initStorage(session)
389
+ return []
390
+ } else {
391
+ throw e
392
+ }
393
+ }
394
+ //const projects = await this.callApi(session, `api/v4/users/${session.gitlab?.user?.id}/projects`) as any[]
395
+ //return projects.map(p => ({
396
+ // websiteId: p.id,
397
+ // name: p.name,
398
+ // createdAt: p.created_at,
399
+ // updatedAt: p.last_activity_at,
400
+ // connectorUserSettings: {},
401
+ //}))
329
402
  }
330
403
 
331
404
  async readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData> {
@@ -343,9 +416,9 @@ export default class GitlabConnector implements StorageConnector {
343
416
  name: websiteMeta.name,
344
417
  }) as any
345
418
  await this.createFile(session, project.id, WEBSITE_DATA_FILE, JSON.stringify({} as WebsiteData))
346
- await this.createFile(session, project.id, WEBSITE_META_DATA_FILE, JSON.stringify(websiteMeta))
419
+ //await this.createFile(session, project.id, WEBSITE_META_DATA_FILE, JSON.stringify(websiteMeta))
347
420
  //await this.updateWebsite(session, project.id, {} as WebsiteData)
348
- //await this.setWebsiteMeta(session, project.id, websiteMeta)
421
+ await this.setWebsiteMeta(session, project.id, websiteMeta)
349
422
  return project.id
350
423
  }
351
424
 
@@ -360,28 +433,75 @@ export default class GitlabConnector implements StorageConnector {
360
433
  }
361
434
 
362
435
  async deleteWebsite(session: GitlabSession, websiteId: WebsiteId): Promise<void> {
436
+ // Delete repo
363
437
  await this.callApi(session, `api/v4/projects/${websiteId}`, 'DELETE')
438
+ // Load the meta repo data
439
+ const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
440
+ ref: this.options.branch,
441
+ })
442
+ const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
443
+ const data = metaRepo.websites[websiteId]
444
+ if(!data) throw new ApiError(`Website ${websiteId} not found`, 404)
445
+ // Update or create the website meta data
446
+ delete metaRepo.websites[websiteId]
447
+ // Save the meta repo data
448
+ const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
449
+ branch: this.options.branch,
450
+ commit_message: `Delete meta data of ${data.meta.name} (${websiteId}) from Silex`,
451
+ content: JSON.stringify(metaRepo),
452
+ file_path: this.options.metaRepoFile,
453
+ })
364
454
  }
365
455
 
366
456
  async getWebsiteMeta(session: GitlabSession, websiteId: WebsiteId): Promise<WebsiteMeta> {
367
- const project = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`) as any
457
+ const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
458
+ ref: this.options.branch,
459
+ })
460
+ const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
461
+ if(!metaRepo.websites[websiteId]) throw new ApiError(`Website ${websiteId} not found`, 404)
368
462
  return {
369
- websiteId: project.id,
370
- name: project.name,
371
- createdAt: project.created_at,
372
- updatedAt: project.last_activity_at,
373
- connectorUserSettings: {},
463
+ websiteId,
464
+ createdAt: new Date(metaRepo.websites[websiteId].createdAt),
465
+ updatedAt: new Date(metaRepo.websites[websiteId].updatedAt),
466
+ ...metaRepo.websites[websiteId].meta,
374
467
  }
468
+ //const project = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`) as any
469
+ //return {
470
+ // websiteId: project.id,
471
+ // name: project.name,
472
+ // createdAt: project.created_at,
473
+ // updatedAt: project.last_activity_at,
474
+ // connectorUserSettings: {},
475
+ //}
375
476
  }
376
477
 
377
478
  async setWebsiteMeta(session: GitlabSession, websiteId: WebsiteId, websiteMeta: WebsiteMetaFileContent): Promise<void> {
378
- const project = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'PUT', {
479
+ // Load the meta repo data
480
+ const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
481
+ ref: this.options.branch,
482
+ })
483
+ const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
484
+ // Update or create the website meta data
485
+ metaRepo.websites[websiteId] = {
486
+ updatedAt: new Date().toISOString(),
487
+ createdAt: metaRepo.websites[websiteId]?.createdAt ?? new Date().toISOString(),
488
+ meta: websiteMeta,
489
+ }
490
+ // Save the meta repo data
491
+ const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
379
492
  branch: this.options.branch,
380
- commit_message: 'Update website meta data from Silex',
381
- content: JSON.stringify(websiteMeta),
382
- file_path: WEBSITE_META_DATA_FILE,
383
- id: websiteId,
493
+ commit_message: `Update website meta data of ${websiteMeta.name} (${websiteId}) from Silex`,
494
+ content: JSON.stringify(metaRepo),
495
+ file_path: this.options.metaRepoFile,
384
496
  })
497
+
498
+ //const project = await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'PUT', {
499
+ // branch: this.options.branch,
500
+ // commit_message: 'Update website meta data from Silex',
501
+ // content: JSON.stringify(websiteMeta),
502
+ // file_path: WEBSITE_META_DATA_FILE,
503
+ // id: websiteId,
504
+ //})
385
505
  }
386
506
 
387
507
  async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback | undefined): Promise<void> {
@@ -393,7 +513,7 @@ export default class GitlabConnector implements StorageConnector {
393
513
  await this.updateFile(session, websiteId, this.getAssetPath(file.path), content, true)
394
514
  } catch (e) {
395
515
  // If the file does not exist, create it
396
- if (e.statusCode === 404 || e.message.endsWith('A file with this name doesn\'t exist')) {
516
+ if (e.statusCode === 404 || e.httpStatusCode === 404 || e.message.endsWith('A file with this name doesn\'t exist')) {
397
517
  await this.createFile(session, websiteId, file.path, content, true)
398
518
  } else {
399
519
  throw e
@@ -21,6 +21,7 @@ import silex from './index'
21
21
  import { parse } from 'cli'
22
22
 
23
23
  const options = parse({
24
+ 'url': ['', 'URL of your server', 'string'],
24
25
  'host': ['', 'Sets the host param of the express module "cookie-session".', 'string'],
25
26
  'port': [ 'p', 'Port to listen to', 'int'],
26
27
  'protocol': ['', 'Sets the protocol param of the express module "cookie-session".', 'string'],
@@ -41,6 +42,7 @@ const options = parse({
41
42
  'debug': [false, 'Debug mode, with live reload, source maps etc.', 'boolean'],
42
43
  }, {})
43
44
 
45
+ if(options['url']) process.env.SILEX_URL = options['url']
44
46
  if(options['host']) process.env.SILEX_HOST = options['host']
45
47
  if(options.port) process.env.SILEX_PORT = options.port
46
48
  if(options['protocol']) process.env.SILEX_PROTOCOL = options['protocol']
@@ -49,7 +49,7 @@ export class ServerConfig extends Config {
49
49
  }
50
50
  public port = process.env.SILEX_PORT
51
51
  public debug = process.env.SILEX_DEBUG === 'true'
52
- public url = `${process.env.SILEX_PROTOCOL}://${process.env.SILEX_HOST}:${process.env.SILEX_PORT}`
52
+ public url = process.env.SILEX_URL ?? `${process.env.SILEX_PROTOCOL}://${process.env.SILEX_HOST}:${process.env.SILEX_PORT}`
53
53
  public userConfigPath: Plugin | undefined = process.env.SILEX_CONFIG
54
54
  public configFilePath: Plugin = resolve(__dirname, '../../../.silex.js')
55
55
 
package/src/ts/types.ts CHANGED
@@ -61,7 +61,7 @@ export type ApiWebsiteCreateBody = WebsiteMetaFileContent
61
61
  export type ApiWebsiteCreateResponse = { message: string }
62
62
  export type ApiWebsiteDeleteQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
63
63
  export type ApiWebsiteMetaReadQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
64
- export type ApiWebsiteMetaReadResponse = WebsiteMetaFileContent
64
+ export type ApiWebsiteMetaReadResponse = WebsiteMeta
65
65
  export type ApiWebsiteMetaWriteQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
66
66
  export type ApiWebsiteMetaWriteBody = WebsiteMetaFileContent
67
67
  export type ApiWebsiteMetaWriteResponse = { message: string }