6497cda3ce3ae8fa6e450ff6e2e075fce6836e75
[oweals/peertube.git] / server / controllers / api / config.ts
1 import * as express from 'express'
2 import { snakeCase } from 'lodash'
3 import { ServerConfig, UserRight } from '../../../shared'
4 import { About } from '../../../shared/models/server/about.model'
5 import { CustomConfig } from '../../../shared/models/server/custom-config.model'
6 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
7 import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
8 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
9 import { customConfigUpdateValidator } from '../../middlewares/validators/config'
10 import { ClientHtml } from '../../lib/client-html'
11 import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12 import { remove, writeJSON } from 'fs-extra'
13 import { getServerCommit } from '../../helpers/utils'
14 import { Emailer } from '../../lib/emailer'
15 import { isNumeric } from 'validator'
16 import { objectConverter } from '../../helpers/core-utils'
17
18 const packageJSON = require('../../../../package.json')
19 const configRouter = express.Router()
20
21 const auditLogger = auditLoggerFactory('config')
22
23 configRouter.get('/about', getAbout)
24 configRouter.get('/',
25   asyncMiddleware(getConfig)
26 )
27
28 configRouter.get('/custom',
29   authenticate,
30   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
31   asyncMiddleware(getCustomConfig)
32 )
33 configRouter.put('/custom',
34   authenticate,
35   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
36   asyncMiddleware(customConfigUpdateValidator),
37   asyncMiddleware(updateCustomConfig)
38 )
39 configRouter.delete('/custom',
40   authenticate,
41   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
42   asyncMiddleware(deleteCustomConfig)
43 )
44
45 let serverCommit: string
46 async function getConfig (req: express.Request, res: express.Response) {
47   const allowed = await isSignupAllowed()
48   const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip)
49
50   if (serverCommit === undefined) serverCommit = await getServerCommit()
51
52   const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
53    .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
54    .map(r => parseInt(r, 10))
55
56   const json: ServerConfig = {
57     instance: {
58       name: CONFIG.INSTANCE.NAME,
59       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
60       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
61       isNSFW: CONFIG.INSTANCE.IS_NSFW,
62       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
63       customizations: {
64         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
65         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
66       }
67     },
68     email: {
69       enabled: Emailer.isEnabled()
70     },
71     contactForm: {
72       enabled: CONFIG.CONTACT_FORM.ENABLED
73     },
74     serverVersion: packageJSON.version,
75     serverCommit,
76     signup: {
77       allowed,
78       allowedForCurrentIP,
79       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
80     },
81     transcoding: {
82       hls: {
83         enabled: CONFIG.TRANSCODING.HLS.ENABLED
84       },
85       enabledResolutions
86     },
87     import: {
88       videos: {
89         http: {
90           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
91         },
92         torrent: {
93           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
94         }
95       }
96     },
97     avatar: {
98       file: {
99         size: {
100           max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
101         },
102         extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
103       }
104     },
105     video: {
106       image: {
107         extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
108         size: {
109           max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
110         }
111       },
112       file: {
113         extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
114       }
115     },
116     videoCaption: {
117       file: {
118         size: {
119           max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
120         },
121         extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
122       }
123     },
124     user: {
125       videoQuota: CONFIG.USER.VIDEO_QUOTA,
126       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
127     },
128     trending: {
129       videos: {
130         intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
131       }
132     }
133   }
134
135   return res.json(json)
136 }
137
138 function getAbout (req: express.Request, res: express.Response) {
139   const about: About = {
140     instance: {
141       name: CONFIG.INSTANCE.NAME,
142       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
143       description: CONFIG.INSTANCE.DESCRIPTION,
144       terms: CONFIG.INSTANCE.TERMS
145     }
146   }
147
148   return res.json(about).end()
149 }
150
151 async function getCustomConfig (req: express.Request, res: express.Response) {
152   const data = customConfig()
153
154   return res.json(data).end()
155 }
156
157 async function deleteCustomConfig (req: express.Request, res: express.Response) {
158   await remove(CONFIG.CUSTOM_FILE)
159
160   auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
161
162   reloadConfig()
163   ClientHtml.invalidCache()
164
165   const data = customConfig()
166
167   return res.json(data).end()
168 }
169
170 async function updateCustomConfig (req: express.Request, res: express.Response) {
171   const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
172
173   // camelCase to snake_case key + Force number conversion
174   const toUpdateJSON = convertCustomConfigBody(req.body)
175
176   await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
177
178   reloadConfig()
179   ClientHtml.invalidCache()
180
181   const data = customConfig()
182
183   auditLogger.update(
184     getAuditIdFromRes(res),
185     new CustomConfigAuditView(data),
186     oldCustomConfigAuditKeys
187   )
188
189   return res.json(data).end()
190 }
191
192 // ---------------------------------------------------------------------------
193
194 export {
195   configRouter
196 }
197
198 // ---------------------------------------------------------------------------
199
200 function customConfig (): CustomConfig {
201   return {
202     instance: {
203       name: CONFIG.INSTANCE.NAME,
204       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
205       description: CONFIG.INSTANCE.DESCRIPTION,
206       terms: CONFIG.INSTANCE.TERMS,
207       isNSFW: CONFIG.INSTANCE.IS_NSFW,
208       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
209       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
210       customizations: {
211         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
212         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
213       }
214     },
215     services: {
216       twitter: {
217         username: CONFIG.SERVICES.TWITTER.USERNAME,
218         whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
219       }
220     },
221     cache: {
222       previews: {
223         size: CONFIG.CACHE.PREVIEWS.SIZE
224       },
225       captions: {
226         size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
227       }
228     },
229     signup: {
230       enabled: CONFIG.SIGNUP.ENABLED,
231       limit: CONFIG.SIGNUP.LIMIT,
232       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
233     },
234     admin: {
235       email: CONFIG.ADMIN.EMAIL
236     },
237     contactForm: {
238       enabled: CONFIG.CONTACT_FORM.ENABLED
239     },
240     user: {
241       videoQuota: CONFIG.USER.VIDEO_QUOTA,
242       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
243     },
244     transcoding: {
245       enabled: CONFIG.TRANSCODING.ENABLED,
246       allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
247       threads: CONFIG.TRANSCODING.THREADS,
248       resolutions: {
249         '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
250         '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ],
251         '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
252         '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
253         '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ]
254       },
255       hls: {
256         enabled: CONFIG.TRANSCODING.HLS.ENABLED
257       }
258     },
259     import: {
260       videos: {
261         http: {
262           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
263         },
264         torrent: {
265           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
266         }
267       }
268     }
269   }
270 }
271
272 function convertCustomConfigBody (body: CustomConfig) {
273   function keyConverter (k: string) {
274     // Transcoding resolutions exception
275     if (/^\d{3,4}p$/.exec(k)) return k
276
277     return snakeCase(k)
278   }
279
280   function valueConverter (v: any) {
281     if (isNumeric(v + '')) return parseInt('' + v, 10)
282
283     return v
284   }
285
286   return objectConverter(body, keyConverter, valueConverter)
287 }