Add ability to forbid followers
[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     autoBlacklist: {
98       videos: {
99         ofUsers: {
100           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
101         }
102       }
103     },
104     avatar: {
105       file: {
106         size: {
107           max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
108         },
109         extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
110       }
111     },
112     video: {
113       image: {
114         extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
115         size: {
116           max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
117         }
118       },
119       file: {
120         extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
121       }
122     },
123     videoCaption: {
124       file: {
125         size: {
126           max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
127         },
128         extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
129       }
130     },
131     user: {
132       videoQuota: CONFIG.USER.VIDEO_QUOTA,
133       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
134     },
135     trending: {
136       videos: {
137         intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
138       }
139     }
140   }
141
142   return res.json(json)
143 }
144
145 function getAbout (req: express.Request, res: express.Response) {
146   const about: About = {
147     instance: {
148       name: CONFIG.INSTANCE.NAME,
149       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
150       description: CONFIG.INSTANCE.DESCRIPTION,
151       terms: CONFIG.INSTANCE.TERMS
152     }
153   }
154
155   return res.json(about).end()
156 }
157
158 async function getCustomConfig (req: express.Request, res: express.Response) {
159   const data = customConfig()
160
161   return res.json(data).end()
162 }
163
164 async function deleteCustomConfig (req: express.Request, res: express.Response) {
165   await remove(CONFIG.CUSTOM_FILE)
166
167   auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
168
169   reloadConfig()
170   ClientHtml.invalidCache()
171
172   const data = customConfig()
173
174   return res.json(data).end()
175 }
176
177 async function updateCustomConfig (req: express.Request, res: express.Response) {
178   const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
179
180   // camelCase to snake_case key + Force number conversion
181   const toUpdateJSON = convertCustomConfigBody(req.body)
182
183   await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
184
185   reloadConfig()
186   ClientHtml.invalidCache()
187
188   const data = customConfig()
189
190   auditLogger.update(
191     getAuditIdFromRes(res),
192     new CustomConfigAuditView(data),
193     oldCustomConfigAuditKeys
194   )
195
196   return res.json(data).end()
197 }
198
199 // ---------------------------------------------------------------------------
200
201 export {
202   configRouter
203 }
204
205 // ---------------------------------------------------------------------------
206
207 function customConfig (): CustomConfig {
208   return {
209     instance: {
210       name: CONFIG.INSTANCE.NAME,
211       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
212       description: CONFIG.INSTANCE.DESCRIPTION,
213       terms: CONFIG.INSTANCE.TERMS,
214       isNSFW: CONFIG.INSTANCE.IS_NSFW,
215       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
216       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
217       customizations: {
218         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
219         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
220       }
221     },
222     services: {
223       twitter: {
224         username: CONFIG.SERVICES.TWITTER.USERNAME,
225         whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
226       }
227     },
228     cache: {
229       previews: {
230         size: CONFIG.CACHE.PREVIEWS.SIZE
231       },
232       captions: {
233         size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
234       }
235     },
236     signup: {
237       enabled: CONFIG.SIGNUP.ENABLED,
238       limit: CONFIG.SIGNUP.LIMIT,
239       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
240     },
241     admin: {
242       email: CONFIG.ADMIN.EMAIL
243     },
244     contactForm: {
245       enabled: CONFIG.CONTACT_FORM.ENABLED
246     },
247     user: {
248       videoQuota: CONFIG.USER.VIDEO_QUOTA,
249       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
250     },
251     transcoding: {
252       enabled: CONFIG.TRANSCODING.ENABLED,
253       allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
254       threads: CONFIG.TRANSCODING.THREADS,
255       resolutions: {
256         '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
257         '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ],
258         '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
259         '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
260         '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ]
261       },
262       hls: {
263         enabled: CONFIG.TRANSCODING.HLS.ENABLED
264       }
265     },
266     import: {
267       videos: {
268         http: {
269           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
270         },
271         torrent: {
272           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
273         }
274       }
275     },
276     autoBlacklist: {
277       videos: {
278         ofUsers: {
279           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
280         }
281       }
282     },
283     followers: {
284       instance: {
285         enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED
286       }
287     }
288   }
289 }
290
291 function convertCustomConfigBody (body: CustomConfig) {
292   function keyConverter (k: string) {
293     // Transcoding resolutions exception
294     if (/^\d{3,4}p$/.exec(k)) return k
295
296     return snakeCase(k)
297   }
298
299   function valueConverter (v: any) {
300     if (isNumeric(v + '')) return parseInt('' + v, 10)
301
302     return v
303   }
304
305   return objectConverter(body, keyConverter, valueConverter)
306 }