]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/config.ts
bd100ef9cddf03d46a4ff547071ee0bca3138091
[github/Chocobozzz/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 searchIndex: {
81 enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
82 url: CONFIG.SEARCH.SEARCH_INDEX.URL,
83 disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
84 isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
85 }
86 },
87 plugin: {
88 registered: getRegisteredPlugins(),
89 registeredExternalAuths: getExternalAuthsPlugins(),
90 registeredIdAndPassAuths: getIdAndPassAuthPlugins()
91 },
92 theme: {
93 registered: getRegisteredThemes(),
94 default: defaultTheme
95 },
96 email: {
97 enabled: isEmailEnabled()
98 },
99 contactForm: {
100 enabled: CONFIG.CONTACT_FORM.ENABLED
101 },
102 serverVersion: PEERTUBE_VERSION,
103 serverCommit,
104 signup: {
105 allowed,
106 allowedForCurrentIP,
107 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
108 },
109 transcoding: {
110 hls: {
111 enabled: CONFIG.TRANSCODING.HLS.ENABLED
112 },
113 webtorrent: {
114 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
115 },
116 enabledResolutions: getEnabledResolutions('vod')
117 },
118 live: {
119 enabled: CONFIG.LIVE.ENABLED,
120
121 transcoding: {
122 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
123 enabledResolutions: getEnabledResolutions('live')
124 }
125 },
126 import: {
127 videos: {
128 http: {
129 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
130 },
131 torrent: {
132 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
133 }
134 }
135 },
136 autoBlacklist: {
137 videos: {
138 ofUsers: {
139 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
140 }
141 }
142 },
143 avatar: {
144 file: {
145 size: {
146 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
147 },
148 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
149 }
150 },
151 video: {
152 image: {
153 extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
154 size: {
155 max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
156 }
157 },
158 file: {
159 extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
160 }
161 },
162 videoCaption: {
163 file: {
164 size: {
165 max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
166 },
167 extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
168 }
169 },
170 user: {
171 videoQuota: CONFIG.USER.VIDEO_QUOTA,
172 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
173 },
174 trending: {
175 videos: {
176 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
177 }
178 },
179 tracker: {
180 enabled: CONFIG.TRACKER.ENABLED
181 },
182
183 followings: {
184 instance: {
185 autoFollowIndex: {
186 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
187 }
188 }
189 },
190
191 broadcastMessage: {
192 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
193 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
194 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
195 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
196 }
197 }
198
199 return res.json(json)
200 }
201
202 function getAbout (req: express.Request, res: express.Response) {
203 const about: About = {
204 instance: {
205 name: CONFIG.INSTANCE.NAME,
206 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
207 description: CONFIG.INSTANCE.DESCRIPTION,
208 terms: CONFIG.INSTANCE.TERMS,
209 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
210
211 hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
212
213 creationReason: CONFIG.INSTANCE.CREATION_REASON,
214 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
215 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
216 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
217 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
218
219 languages: CONFIG.INSTANCE.LANGUAGES,
220 categories: CONFIG.INSTANCE.CATEGORIES
221 }
222 }
223
224 return res.json(about).end()
225 }
226
227 function getCustomConfig (req: express.Request, res: express.Response) {
228 const data = customConfig()
229
230 return res.json(data).end()
231 }
232
233 async function deleteCustomConfig (req: express.Request, res: express.Response) {
234 await remove(CONFIG.CUSTOM_FILE)
235
236 auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
237
238 reloadConfig()
239 ClientHtml.invalidCache()
240
241 const data = customConfig()
242
243 return res.json(data)
244 }
245
246 async function updateCustomConfig (req: express.Request, res: express.Response) {
247 const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
248
249 // camelCase to snake_case key + Force number conversion
250 const toUpdateJSON = convertCustomConfigBody(req.body)
251
252 await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
253
254 reloadConfig()
255 ClientHtml.invalidCache()
256
257 const data = customConfig()
258
259 auditLogger.update(
260 getAuditIdFromRes(res),
261 new CustomConfigAuditView(data),
262 oldCustomConfigAuditKeys
263 )
264
265 return res.json(data)
266 }
267
268 function getRegisteredThemes () {
269 return PluginManager.Instance.getRegisteredThemes()
270 .map(t => ({
271 name: t.name,
272 version: t.version,
273 description: t.description,
274 css: t.css,
275 clientScripts: t.clientScripts
276 }))
277 }
278
279 function getEnabledResolutions (type: 'vod' | 'live') {
280 const transcoding = type === 'vod'
281 ? CONFIG.TRANSCODING
282 : CONFIG.LIVE.TRANSCODING
283
284 return Object.keys(transcoding.RESOLUTIONS)
285 .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
286 .map(r => parseInt(r, 10))
287 }
288
289 function getRegisteredPlugins () {
290 return PluginManager.Instance.getRegisteredPlugins()
291 .map(p => ({
292 name: p.name,
293 version: p.version,
294 description: p.description,
295 clientScripts: p.clientScripts
296 }))
297 }
298
299 function getIdAndPassAuthPlugins () {
300 const result: RegisteredIdAndPassAuthConfig[] = []
301
302 for (const p of PluginManager.Instance.getIdAndPassAuths()) {
303 for (const auth of p.idAndPassAuths) {
304 result.push({
305 npmName: p.npmName,
306 name: p.name,
307 version: p.version,
308 authName: auth.authName,
309 weight: auth.getWeight()
310 })
311 }
312 }
313
314 return result
315 }
316
317 function getExternalAuthsPlugins () {
318 const result: RegisteredExternalAuthConfig[] = []
319
320 for (const p of PluginManager.Instance.getExternalAuths()) {
321 for (const auth of p.externalAuths) {
322 result.push({
323 npmName: p.npmName,
324 name: p.name,
325 version: p.version,
326 authName: auth.authName,
327 authDisplayName: auth.authDisplayName()
328 })
329 }
330 }
331
332 return result
333 }
334
335 // ---------------------------------------------------------------------------
336
337 export {
338 configRouter,
339 getEnabledResolutions,
340 getRegisteredPlugins,
341 getRegisteredThemes
342 }
343
344 // ---------------------------------------------------------------------------
345
346 function customConfig (): CustomConfig {
347 return {
348 instance: {
349 name: CONFIG.INSTANCE.NAME,
350 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
351 description: CONFIG.INSTANCE.DESCRIPTION,
352 terms: CONFIG.INSTANCE.TERMS,
353 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
354
355 creationReason: CONFIG.INSTANCE.CREATION_REASON,
356 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
357 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
358 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
359 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
360 hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
361
362 languages: CONFIG.INSTANCE.LANGUAGES,
363 categories: CONFIG.INSTANCE.CATEGORIES,
364
365 isNSFW: CONFIG.INSTANCE.IS_NSFW,
366 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
367 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
368 customizations: {
369 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
370 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
371 }
372 },
373 theme: {
374 default: CONFIG.THEME.DEFAULT
375 },
376 services: {
377 twitter: {
378 username: CONFIG.SERVICES.TWITTER.USERNAME,
379 whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
380 }
381 },
382 cache: {
383 previews: {
384 size: CONFIG.CACHE.PREVIEWS.SIZE
385 },
386 captions: {
387 size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
388 }
389 },
390 signup: {
391 enabled: CONFIG.SIGNUP.ENABLED,
392 limit: CONFIG.SIGNUP.LIMIT,
393 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
394 },
395 admin: {
396 email: CONFIG.ADMIN.EMAIL
397 },
398 contactForm: {
399 enabled: CONFIG.CONTACT_FORM.ENABLED
400 },
401 user: {
402 videoQuota: CONFIG.USER.VIDEO_QUOTA,
403 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
404 },
405 transcoding: {
406 enabled: CONFIG.TRANSCODING.ENABLED,
407 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
408 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
409 threads: CONFIG.TRANSCODING.THREADS,
410 resolutions: {
411 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
412 '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
413 '360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'],
414 '480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'],
415 '720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'],
416 '1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'],
417 '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
418 },
419 webtorrent: {
420 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
421 },
422 hls: {
423 enabled: CONFIG.TRANSCODING.HLS.ENABLED
424 }
425 },
426 live: {
427 enabled: CONFIG.LIVE.ENABLED,
428 transcoding: {
429 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
430 threads: CONFIG.LIVE.TRANSCODING.THREADS,
431 resolutions: {
432 '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'],
433 '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'],
434 '480p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['480p'],
435 '720p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['720p'],
436 '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'],
437 '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
438 }
439 }
440 },
441 import: {
442 videos: {
443 http: {
444 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
445 },
446 torrent: {
447 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
448 }
449 }
450 },
451 autoBlacklist: {
452 videos: {
453 ofUsers: {
454 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
455 }
456 }
457 },
458 followers: {
459 instance: {
460 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
461 manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
462 }
463 },
464 followings: {
465 instance: {
466 autoFollowBack: {
467 enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
468 },
469
470 autoFollowIndex: {
471 enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
472 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
473 }
474 }
475 },
476 broadcastMessage: {
477 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
478 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
479 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
480 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
481 },
482 search: {
483 remoteUri: {
484 users: CONFIG.SEARCH.REMOTE_URI.USERS,
485 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
486 },
487 searchIndex: {
488 enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
489 url: CONFIG.SEARCH.SEARCH_INDEX.URL,
490 disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
491 isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
492 }
493 }
494 }
495 }
496
497 function convertCustomConfigBody (body: CustomConfig) {
498 function keyConverter (k: string) {
499 // Transcoding resolutions exception
500 if (/^\d{3,4}p$/.exec(k)) return k
501 if (k === '0p') return k
502
503 return snakeCase(k)
504 }
505
506 function valueConverter (v: any) {
507 if (validator.isNumeric(v + '')) return parseInt('' + v, 10)
508
509 return v
510 }
511
512 return objectConverter(body, keyConverter, valueConverter)
513 }