41e5027b9b8c4bdc69d0cff3b4dc864d19251c03
[oweals/peertube.git] / server / controllers / api / config.ts
1 import { Hooks } from '@server/lib/plugins/hooks'
2 import * as express from 'express'
3 import { remove, writeJSON } from 'fs-extra'
4 import { snakeCase } from 'lodash'
5 import validator from 'validator'
6 import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared'
7 import { About } from '../../../shared/models/server/about.model'
8 import { CustomConfig } from '../../../shared/models/server/custom-config.model'
9 import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
10 import { objectConverter } from '../../helpers/core-utils'
11 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
12 import { getServerCommit } from '../../helpers/utils'
13 import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
14 import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
15 import { ClientHtml } from '../../lib/client-html'
16 import { PluginManager } from '../../lib/plugins/plugin-manager'
17 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
18 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
19 import { customConfigUpdateValidator } from '../../middlewares/validators/config'
20
21 const configRouter = express.Router()
22
23 const auditLogger = auditLoggerFactory('config')
24
25 configRouter.get('/about', getAbout)
26 configRouter.get('/',
27   asyncMiddleware(getConfig)
28 )
29
30 configRouter.get('/custom',
31   authenticate,
32   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
33   getCustomConfig
34 )
35 configRouter.put('/custom',
36   authenticate,
37   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
38   customConfigUpdateValidator,
39   asyncMiddleware(updateCustomConfig)
40 )
41 configRouter.delete('/custom',
42   authenticate,
43   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
44   asyncMiddleware(deleteCustomConfig)
45 )
46
47 let serverCommit: string
48
49 async function getConfig (req: express.Request, res: express.Response) {
50   const { allowed } = await Hooks.wrapPromiseFun(
51     isSignupAllowed,
52     {
53       ip: req.ip
54     },
55     'filter:api.user.signup.allowed.result'
56   )
57
58   const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip)
59   const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
60
61   if (serverCommit === undefined) serverCommit = await getServerCommit()
62
63   const json: ServerConfig = {
64     instance: {
65       name: CONFIG.INSTANCE.NAME,
66       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
67       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
68       isNSFW: CONFIG.INSTANCE.IS_NSFW,
69       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
70       customizations: {
71         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
72         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
73       }
74     },
75     search: {
76       remoteUri: {
77         users: CONFIG.SEARCH.REMOTE_URI.USERS,
78         anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
79       }
80     },
81     plugin: {
82       registered: getRegisteredPlugins(),
83       registeredExternalAuths: getExternalAuthsPlugins(),
84       registeredIdAndPassAuths: getIdAndPassAuthPlugins()
85     },
86     theme: {
87       registered: getRegisteredThemes(),
88       default: defaultTheme
89     },
90     email: {
91       enabled: isEmailEnabled()
92     },
93     contactForm: {
94       enabled: CONFIG.CONTACT_FORM.ENABLED
95     },
96     serverVersion: PEERTUBE_VERSION,
97     serverCommit,
98     signup: {
99       allowed,
100       allowedForCurrentIP,
101       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
102     },
103     transcoding: {
104       hls: {
105         enabled: CONFIG.TRANSCODING.HLS.ENABLED
106       },
107       webtorrent: {
108         enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
109       },
110       enabledResolutions: getEnabledResolutions()
111     },
112     import: {
113       videos: {
114         http: {
115           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
116         },
117         torrent: {
118           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
119         }
120       }
121     },
122     autoBlacklist: {
123       videos: {
124         ofUsers: {
125           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
126         }
127       }
128     },
129     avatar: {
130       file: {
131         size: {
132           max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
133         },
134         extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
135       }
136     },
137     video: {
138       image: {
139         extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
140         size: {
141           max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
142         }
143       },
144       file: {
145         extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
146       }
147     },
148     videoCaption: {
149       file: {
150         size: {
151           max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
152         },
153         extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
154       }
155     },
156     user: {
157       videoQuota: CONFIG.USER.VIDEO_QUOTA,
158       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
159     },
160     trending: {
161       videos: {
162         intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
163       }
164     },
165     tracker: {
166       enabled: CONFIG.TRACKER.ENABLED
167     },
168
169     followings: {
170       instance: {
171         autoFollowIndex: {
172           indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
173         }
174       }
175     },
176
177     broadcastMessage: {
178       enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
179       message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
180       level: CONFIG.BROADCAST_MESSAGE.LEVEL,
181       dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
182     }
183   }
184
185   return res.json(json)
186 }
187
188 function getAbout (req: express.Request, res: express.Response) {
189   const about: About = {
190     instance: {
191       name: CONFIG.INSTANCE.NAME,
192       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
193       description: CONFIG.INSTANCE.DESCRIPTION,
194       terms: CONFIG.INSTANCE.TERMS,
195       codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
196
197       hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
198
199       creationReason: CONFIG.INSTANCE.CREATION_REASON,
200       moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
201       administrator: CONFIG.INSTANCE.ADMINISTRATOR,
202       maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
203       businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
204
205       languages: CONFIG.INSTANCE.LANGUAGES,
206       categories: CONFIG.INSTANCE.CATEGORIES
207     }
208   }
209
210   return res.json(about).end()
211 }
212
213 function getCustomConfig (req: express.Request, res: express.Response) {
214   const data = customConfig()
215
216   return res.json(data).end()
217 }
218
219 async function deleteCustomConfig (req: express.Request, res: express.Response) {
220   await remove(CONFIG.CUSTOM_FILE)
221
222   auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
223
224   reloadConfig()
225   ClientHtml.invalidCache()
226
227   const data = customConfig()
228
229   return res.json(data).end()
230 }
231
232 async function updateCustomConfig (req: express.Request, res: express.Response) {
233   const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
234
235   // camelCase to snake_case key + Force number conversion
236   const toUpdateJSON = convertCustomConfigBody(req.body)
237
238   await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
239
240   reloadConfig()
241   ClientHtml.invalidCache()
242
243   const data = customConfig()
244
245   auditLogger.update(
246     getAuditIdFromRes(res),
247     new CustomConfigAuditView(data),
248     oldCustomConfigAuditKeys
249   )
250
251   return res.json(data).end()
252 }
253
254 function getRegisteredThemes () {
255   return PluginManager.Instance.getRegisteredThemes()
256                       .map(t => ({
257                         name: t.name,
258                         version: t.version,
259                         description: t.description,
260                         css: t.css,
261                         clientScripts: t.clientScripts
262                       }))
263 }
264
265 function getEnabledResolutions () {
266   return Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
267                .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
268                .map(r => parseInt(r, 10))
269 }
270
271 function getRegisteredPlugins () {
272   return PluginManager.Instance.getRegisteredPlugins()
273                       .map(p => ({
274                         name: p.name,
275                         version: p.version,
276                         description: p.description,
277                         clientScripts: p.clientScripts
278                       }))
279 }
280
281 function getIdAndPassAuthPlugins () {
282   const result: RegisteredIdAndPassAuthConfig[] = []
283
284   for (const p of PluginManager.Instance.getIdAndPassAuths()) {
285     for (const auth of p.idAndPassAuths) {
286       result.push({
287         npmName: p.npmName,
288         name: p.name,
289         version: p.version,
290         authName: auth.authName,
291         weight: auth.getWeight()
292       })
293     }
294   }
295
296   return result
297 }
298
299 function getExternalAuthsPlugins () {
300   const result: RegisteredExternalAuthConfig[] = []
301
302   for (const p of PluginManager.Instance.getExternalAuths()) {
303     for (const auth of p.externalAuths) {
304       result.push({
305         npmName: p.npmName,
306         name: p.name,
307         version: p.version,
308         authName: auth.authName,
309         authDisplayName: auth.authDisplayName()
310       })
311     }
312   }
313
314   return result
315 }
316
317 // ---------------------------------------------------------------------------
318
319 export {
320   configRouter,
321   getEnabledResolutions,
322   getRegisteredPlugins,
323   getRegisteredThemes
324 }
325
326 // ---------------------------------------------------------------------------
327
328 function customConfig (): CustomConfig {
329   return {
330     instance: {
331       name: CONFIG.INSTANCE.NAME,
332       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
333       description: CONFIG.INSTANCE.DESCRIPTION,
334       terms: CONFIG.INSTANCE.TERMS,
335       codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
336
337       creationReason: CONFIG.INSTANCE.CREATION_REASON,
338       moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
339       administrator: CONFIG.INSTANCE.ADMINISTRATOR,
340       maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
341       businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
342       hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
343
344       languages: CONFIG.INSTANCE.LANGUAGES,
345       categories: CONFIG.INSTANCE.CATEGORIES,
346
347       isNSFW: CONFIG.INSTANCE.IS_NSFW,
348       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
349       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
350       customizations: {
351         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
352         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
353       }
354     },
355     theme: {
356       default: CONFIG.THEME.DEFAULT
357     },
358     services: {
359       twitter: {
360         username: CONFIG.SERVICES.TWITTER.USERNAME,
361         whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
362       }
363     },
364     cache: {
365       previews: {
366         size: CONFIG.CACHE.PREVIEWS.SIZE
367       },
368       captions: {
369         size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
370       }
371     },
372     signup: {
373       enabled: CONFIG.SIGNUP.ENABLED,
374       limit: CONFIG.SIGNUP.LIMIT,
375       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
376     },
377     admin: {
378       email: CONFIG.ADMIN.EMAIL
379     },
380     contactForm: {
381       enabled: CONFIG.CONTACT_FORM.ENABLED
382     },
383     user: {
384       videoQuota: CONFIG.USER.VIDEO_QUOTA,
385       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
386     },
387     transcoding: {
388       enabled: CONFIG.TRANSCODING.ENABLED,
389       allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
390       allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
391       threads: CONFIG.TRANSCODING.THREADS,
392       resolutions: {
393         '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
394         '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
395         '360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'],
396         '480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'],
397         '720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'],
398         '1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'],
399         '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
400       },
401       webtorrent: {
402         enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
403       },
404       hls: {
405         enabled: CONFIG.TRANSCODING.HLS.ENABLED
406       }
407     },
408     import: {
409       videos: {
410         http: {
411           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
412         },
413         torrent: {
414           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
415         }
416       }
417     },
418     autoBlacklist: {
419       videos: {
420         ofUsers: {
421           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
422         }
423       }
424     },
425     followers: {
426       instance: {
427         enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
428         manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
429       }
430     },
431     followings: {
432       instance: {
433         autoFollowBack: {
434           enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
435         },
436
437         autoFollowIndex: {
438           enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
439           indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
440         }
441       }
442     },
443     broadcastMessage: {
444       enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
445       message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
446       level: CONFIG.BROADCAST_MESSAGE.LEVEL,
447       dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
448     }
449   }
450 }
451
452 function convertCustomConfigBody (body: CustomConfig) {
453   function keyConverter (k: string) {
454     // Transcoding resolutions exception
455     if (/^\d{3,4}p$/.exec(k)) return k
456     if (k === '0p') return k
457
458     return snakeCase(k)
459   }
460
461   function valueConverter (v: any) {
462     if (validator.isNumeric(v + '')) return parseInt('' + v, 10)
463
464     return v
465   }
466
467   return objectConverter(body, keyConverter, valueConverter)
468 }