aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJelle Besseling <jelle@pingiun.com>2021-10-12 13:33:44 +0200
committerGitHub <noreply@github.com>2021-10-12 13:33:44 +0200
commit8d8a037e3fe9b1d2ccbc4169ce59b13000b59cb0 (patch)
tree755ba56bc3acbd82ec195974545581c1e49aae5e
parentbadacdbb4a3e4a1aae4d324abc496be8e261b2ef (diff)
downloadPeerTube-8d8a037e3fe9b1d2ccbc4169ce59b13000b59cb0.tar.gz
PeerTube-8d8a037e3fe9b1d2ccbc4169ce59b13000b59cb0.tar.zst
PeerTube-8d8a037e3fe9b1d2ccbc4169ce59b13000b59cb0.zip
Allow configuration to be static/readonly (#4315)
* Allow configuration to be static/readonly * Make all components disableable * Improve disabled component styling * Rename edits allowed field in configuration * Fix CI
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss10
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts3
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.html5
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts5
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.html1
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.ts6
-rw-r--r--client/src/app/shared/shared-forms/select/select-custom-value.component.html1
-rw-r--r--client/src/app/shared/shared-forms/select/select-custom-value.component.ts5
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.html1
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts5
-rw-r--r--client/src/sass/include/_mixins.scss3
-rw-r--r--config/default.yaml5
-rw-r--r--config/production.yaml.example5
-rw-r--r--server/controllers/api/config.ts4
-rw-r--r--server/initializers/config.ts31
-rw-r--r--server/lib/server-config-manager.ts1
-rw-r--r--server/middlewares/validators/config.ts16
-rw-r--r--server/tests/api/server/config.ts364
-rw-r--r--shared/models/server/server-config.model.ts1
-rw-r--r--support/docker/production/Dockerfile.buster3
-rw-r--r--support/docker/production/config/custom-environment-variables.yaml7
-rwxr-xr-xsupport/docker/production/entrypoint.sh9
23 files changed, 304 insertions, 195 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 3ceea02ca..6ae7b1b79 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -63,7 +63,7 @@
63 <div class="col-md-7 col-xl-5"></div> 63 <div class="col-md-7 col-xl-5"></div>
64 <div class="col-md-5 col-xl-5"> 64 <div class="col-md-5 col-xl-5">
65 65
66 <div class="form-error submit-error" i18n *ngIf="!form.valid"> 66 <div class="form-error submit-error" i18n *ngIf="!form.valid && serverConfig.allowEdits">
67 There are errors in the form: 67 There are errors in the form:
68 68
69 <ul> 69 <ul>
@@ -77,7 +77,11 @@
77 You cannot allow live replay if you don't enable transcoding. 77 You cannot allow live replay if you don't enable transcoding.
78 </span> 78 </span>
79 79
80 <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions()"> 80 <span i18n *ngIf="!serverConfig.allowEdits">
81 You cannot change the server configuration because it's managed externally.
82 </span>
83
84 <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions() || !serverConfig.allowEdits">
81 </div> 85 </div>
82 </div> 86 </div>
83</form> 87</form>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 5951d0aaa..0458d257f 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -33,6 +33,11 @@ input[type=number] {
33 top: 5px; 33 top: 5px;
34 right: 2.5rem; 34 right: 2.5rem;
35 } 35 }
36
37 input[disabled] {
38 background-color: #f9f9f9;
39 pointer-events: none;
40 }
36} 41}
37 42
38input[type=checkbox] { 43input[type=checkbox] {
@@ -93,6 +98,11 @@ textarea {
93 } 98 }
94} 99}
95 100
101input[disabled] {
102 opacity: 0.5;
103}
104
105
96.form-group-right { 106.form-group-right {
97 padding-top: 2px; 107 padding-top: 2px;
98} 108}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index f13fe4bf9..04b0175a7 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -258,6 +258,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
258 258
259 this.loadConfigAndUpdateForm() 259 this.loadConfigAndUpdateForm()
260 this.loadCategoriesAndLanguages() 260 this.loadCategoriesAndLanguages()
261 if (!this.serverConfig.allowEdits) {
262 this.form.disable()
263 }
261 } 264 }
262 265
263 formValidated () { 266 formValidated () {
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html
index 6e70e2f37..a460cb9b7 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.html
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html
@@ -2,6 +2,7 @@
2 <textarea #textarea 2 <textarea #textarea
3 [(ngModel)]="content" (ngModelChange)="onModelChange()" 3 [(ngModel)]="content" (ngModelChange)="onModelChange()"
4 class="form-control" [ngClass]="classes" 4 class="form-control" [ngClass]="classes"
5 [attr.disabled]="disabled"
5 [ngStyle]="{ height: textareaHeight }" 6 [ngStyle]="{ height: textareaHeight }"
6 [id]="name" [name]="name"> 7 [id]="name" [name]="name">
7 </textarea> 8 </textarea>
@@ -25,11 +26,11 @@
25 </ng-container> 26 </ng-container>
26 27
27 <my-button 28 <my-button
28 *ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()" 29 *ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
29 ></my-button> 30 ></my-button>
30 31
31 <my-button 32 <my-button
32 *ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()" 33 *ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
33 ></my-button> 34 ></my-button>
34 </div> 35 </div>
35 36
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index 80ca6690f..dcb5d20da 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -45,6 +45,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
45 previewHTML: SafeHtml | string = '' 45 previewHTML: SafeHtml | string = ''
46 46
47 isMaximized = false 47 isMaximized = false
48 disabled = false
48 49
49 maximizeInText = $localize`Maximize editor` 50 maximizeInText = $localize`Maximize editor`
50 maximizeOutText = $localize`Exit maximized editor` 51 maximizeOutText = $localize`Exit maximized editor`
@@ -108,6 +109,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
108 } 109 }
109 } 110 }
110 111
112 setDisabledState (isDisabled: boolean) {
113 this.disabled = isDisabled
114 }
115
111 private lockBodyScroll () { 116 private lockBodyScroll () {
112 this.scrollPosition = this.viewportScroller.getScrollPosition() 117 this.scrollPosition = this.viewportScroller.getScrollPosition()
113 document.getElementById('content').classList.add('lock-scroll') 118 document.getElementById('content').classList.add('lock-scroll')
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.html b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
index 7b49a0c01..03db2875b 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
@@ -7,6 +7,7 @@
7 [multiple]="true" 7 [multiple]="true"
8 [searchable]="true" 8 [searchable]="true"
9 [closeOnSelect]="false" 9 [closeOnSelect]="false"
10 [disabled]="disabled"
10 11
11 bindValue="id" 12 bindValue="id"
12 bindLabel="label" 13 bindLabel="label"
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox.component.ts
index 12f697628..c9a500324 100644
--- a/client/src/app/shared/shared-forms/select/select-checkbox.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.ts
@@ -23,6 +23,8 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
23 @Input() selectableGroupAsModel: boolean 23 @Input() selectableGroupAsModel: boolean
24 @Input() placeholder: string 24 @Input() placeholder: string
25 25
26 disabled = false
27
26 ngOnInit () { 28 ngOnInit () {
27 if (!this.placeholder) this.placeholder = $localize`Add a new option` 29 if (!this.placeholder) this.placeholder = $localize`Add a new option`
28 } 30 }
@@ -59,6 +61,10 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
59 this.propagateChange(this.selectedItems) 61 this.propagateChange(this.selectedItems)
60 } 62 }
61 63
64 setDisabledState (isDisabled: boolean) {
65 this.disabled = isDisabled
66 }
67
62 compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) { 68 compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
63 if (typeof selected === 'string' || typeof selected === 'number') { 69 if (typeof selected === 'string' || typeof selected === 'number') {
64 return item.id === selected 70 return item.id === selected
diff --git a/client/src/app/shared/shared-forms/select/select-custom-value.component.html b/client/src/app/shared/shared-forms/select/select-custom-value.component.html
index 9dc8c2ec2..69fdedc10 100644
--- a/client/src/app/shared/shared-forms/select/select-custom-value.component.html
+++ b/client/src/app/shared/shared-forms/select/select-custom-value.component.html
@@ -5,6 +5,7 @@
5 [searchable]="searchable" 5 [searchable]="searchable"
6 [groupBy]="groupBy" 6 [groupBy]="groupBy"
7 [labelForId]="labelForId" 7 [labelForId]="labelForId"
8 [disabled]="disabled"
8 9
9 [(ngModel)]="selectedId" 10 [(ngModel)]="selectedId"
10 (ngModelChange)="onModelChange()" 11 (ngModelChange)="onModelChange()"
diff --git a/client/src/app/shared/shared-forms/select/select-custom-value.component.ts b/client/src/app/shared/shared-forms/select/select-custom-value.component.ts
index bc6b863c7..636bd6101 100644
--- a/client/src/app/shared/shared-forms/select/select-custom-value.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-custom-value.component.ts
@@ -25,6 +25,7 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
25 25
26 customValue: number | string = '' 26 customValue: number | string = ''
27 selectedId: number | string 27 selectedId: number | string
28 disabled = false
28 29
29 itemsWithCustom: SelectOptionsItem[] = [] 30 itemsWithCustom: SelectOptionsItem[] = []
30 31
@@ -75,4 +76,8 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
75 isCustomValue () { 76 isCustomValue () {
76 return this.selectedId === 'other' 77 return this.selectedId === 'other'
77 } 78 }
79
80 setDisabledState (isDisabled: boolean) {
81 this.disabled = isDisabled
82 }
78} 83}
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.html b/client/src/app/shared/shared-forms/select/select-options.component.html
index 3b1761255..83c7de9f5 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.html
+++ b/client/src/app/shared/shared-forms/select/select-options.component.html
@@ -7,6 +7,7 @@
7 [labelForId]="labelForId" 7 [labelForId]="labelForId"
8 [searchable]="searchable" 8 [searchable]="searchable"
9 [searchFn]="searchFn" 9 [searchFn]="searchFn"
10 [disabled]="disabled"
10 11
11 bindLabel="label" 12 bindLabel="label"
12 bindValue="id" 13 bindValue="id"
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 8482b9dea..820a82c24 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -23,6 +23,7 @@ export class SelectOptionsComponent implements ControlValueAccessor {
23 @Input() searchFn: any 23 @Input() searchFn: any
24 24
25 selectedId: number | string 25 selectedId: number | string
26 disabled = false
26 27
27 propagateChange = (_: any) => { /* empty */ } 28 propagateChange = (_: any) => { /* empty */ }
28 29
@@ -48,4 +49,8 @@ export class SelectOptionsComponent implements ControlValueAccessor {
48 onModelChange () { 49 onModelChange () {
49 this.propagateChange(this.selectedId) 50 this.propagateChange(this.selectedId)
50 } 51 }
52
53 setDisabledState (isDisabled: boolean) {
54 this.disabled = isDisabled
55 }
51} 56}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 9f6d69131..679c235a6 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -364,6 +364,9 @@
364 cursor: default; 364 cursor: default;
365 } 365 }
366 } 366 }
367 select[disabled] {
368 background-color: #f9f9f9;
369 }
367 370
368 @media screen and (max-width: $width) { 371 @media screen and (max-width: $width) {
369 width: 100%; 372 width: 100%;
diff --git a/config/default.yaml b/config/default.yaml
index 3865ab5cf..eb96b6bbb 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -243,6 +243,11 @@ peertube:
243 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json 243 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
244 url: 'https://joinpeertube.org/api/v1/versions.json' 244 url: 'https://joinpeertube.org/api/v1/versions.json'
245 245
246webadmin:
247 configuration:
248 edit:
249 allowed: true
250
246cache: 251cache:
247 previews: 252 previews:
248 size: 500 # Max number of previews you want to cache 253 size: 500 # Max number of previews you want to cache
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 94238fad0..082c75e53 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -241,6 +241,11 @@ peertube:
241 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json 241 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
242 url: 'https://joinpeertube.org/api/v1/versions.json' 242 url: 'https://joinpeertube.org/api/v1/versions.json'
243 243
244webadmin:
245 configuration:
246 # Set to false if you want the config to be readonly
247 allow_edits: true
248
244############################################################################### 249###############################################################################
245# 250#
246# From this point, all the following keys can be overridden by the web interface 251# From this point, all the following keys can be overridden by the web interface
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index d542f62aa..5ea1f67c9 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -11,7 +11,7 @@ import { objectConverter } from '../../helpers/core-utils'
11import { CONFIG, reloadConfig } from '../../initializers/config' 11import { CONFIG, reloadConfig } from '../../initializers/config'
12import { ClientHtml } from '../../lib/client-html' 12import { ClientHtml } from '../../lib/client-html'
13import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' 13import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
14import { customConfigUpdateValidator } from '../../middlewares/validators/config' 14import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config'
15 15
16const configRouter = express.Router() 16const configRouter = express.Router()
17 17
@@ -38,6 +38,7 @@ configRouter.put('/custom',
38 openapiOperationDoc({ operationId: 'putCustomConfig' }), 38 openapiOperationDoc({ operationId: 'putCustomConfig' }),
39 authenticate, 39 authenticate,
40 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 40 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
41 ensureConfigIsEditable,
41 customConfigUpdateValidator, 42 customConfigUpdateValidator,
42 asyncMiddleware(updateCustomConfig) 43 asyncMiddleware(updateCustomConfig)
43) 44)
@@ -46,6 +47,7 @@ configRouter.delete('/custom',
46 openapiOperationDoc({ operationId: 'delCustomConfig' }), 47 openapiOperationDoc({ operationId: 'delCustomConfig' }),
47 authenticate, 48 authenticate,
48 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 49 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
50 ensureConfigIsEditable,
49 asyncMiddleware(deleteCustomConfig) 51 asyncMiddleware(deleteCustomConfig)
50) 52)
51 53
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index be9fc61f0..b2a8e9e19 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -195,6 +195,13 @@ const CONFIG = {
195 URL: config.get<string>('peertube.check_latest_version.url') 195 URL: config.get<string>('peertube.check_latest_version.url')
196 } 196 }
197 }, 197 },
198 WEBADMIN: {
199 CONFIGURATION: {
200 EDITS: {
201 ALLOWED: config.get<boolean>('webadmin.configuration.edit.allowed')
202 }
203 }
204 },
198 ADMIN: { 205 ADMIN: {
199 get EMAIL () { return config.get<string>('admin.email') } 206 get EMAIL () { return config.get<string>('admin.email') }
200 }, 207 },
@@ -411,14 +418,22 @@ export {
411// --------------------------------------------------------------------------- 418// ---------------------------------------------------------------------------
412 419
413function getLocalConfigFilePath () { 420function getLocalConfigFilePath () {
414 const configSources = config.util.getConfigSources() 421 const localConfigDir = getLocalConfigDir()
415 if (configSources.length === 0) throw new Error('Invalid config source.')
416 422
417 let filename = 'local' 423 let filename = 'local'
418 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}` 424 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
419 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}` 425 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
420 426
421 return join(dirname(configSources[0].name), filename + '.json') 427 return join(localConfigDir, filename + '.json')
428}
429
430function getLocalConfigDir () {
431 if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG
432
433 const configSources = config.util.getConfigSources()
434 if (configSources.length === 0) throw new Error('Invalid config source.')
435
436 return dirname(configSources[0].name)
422} 437}
423 438
424function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { 439function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
@@ -437,19 +452,19 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
437 452
438export function reloadConfig () { 453export function reloadConfig () {
439 454
440 function getConfigDirectory () { 455 function getConfigDirectories () {
441 if (process.env.NODE_CONFIG_DIR) { 456 if (process.env.NODE_CONFIG_DIR) {
442 return process.env.NODE_CONFIG_DIR 457 return process.env.NODE_CONFIG_DIR.split(":")
443 } 458 }
444 459
445 return join(root(), 'config') 460 return [ join(root(), 'config') ]
446 } 461 }
447 462
448 function purge () { 463 function purge () {
449 const directory = getConfigDirectory() 464 const directories = getConfigDirectories()
450 465
451 for (const fileName in require.cache) { 466 for (const fileName in require.cache) {
452 if (fileName.includes(directory) === false) { 467 if (directories.some((dir) => fileName.includes(dir)) === false) {
453 continue 468 continue
454 } 469 }
455 470
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index 80d87a9d3..358f47133 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -42,6 +42,7 @@ class ServerConfigManager {
42 const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) 42 const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
43 43
44 return { 44 return {
45 allowEdits: CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED,
45 instance: { 46 instance: {
46 name: CONFIG.INSTANCE.NAME, 47 name: CONFIG.INSTANCE.NAME,
47 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 48 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index 16a840667..5f1ac89bc 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -1,13 +1,14 @@
1import express from 'express' 1import express from 'express'
2import { body } from 'express-validator' 2import { body } from 'express-validator'
3import { isIntOrNull } from '@server/helpers/custom-validators/misc' 3import { isIntOrNull } from '@server/helpers/custom-validators/misc'
4import { isEmailEnabled } from '@server/initializers/config' 4import { CONFIG, isEmailEnabled } from '@server/initializers/config'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
6import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 6import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
7import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 7import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
8import { logger } from '../../helpers/logger' 8import { logger } from '../../helpers/logger'
9import { isThemeRegistered } from '../../lib/plugins/theme-utils' 9import { isThemeRegistered } from '../../lib/plugins/theme-utils'
10import { areValidationErrors } from './shared' 10import { areValidationErrors } from './shared'
11import { HttpStatusCode } from '@shared/models/http/http-error-codes'
11 12
12const customConfigUpdateValidator = [ 13const customConfigUpdateValidator = [
13 body('instance.name').exists().withMessage('Should have a valid instance name'), 14 body('instance.name').exists().withMessage('Should have a valid instance name'),
@@ -104,10 +105,21 @@ const customConfigUpdateValidator = [
104 } 105 }
105] 106]
106 107
108function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
109 if (!CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED) {
110 return res.fail({
111 status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
112 message: 'Server configuration is static and cannot be edited'
113 })
114 }
115 return next()
116}
117
107// --------------------------------------------------------------------------- 118// ---------------------------------------------------------------------------
108 119
109export { 120export {
110 customConfigUpdateValidator 121 customConfigUpdateValidator,
122 ensureConfigIsEditable
111} 123}
112 124
113function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) { 125function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index c4dd882b8..e057ec1a2 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -201,6 +201,199 @@ function checkUpdatedConfig (data: CustomConfig) {
201 expect(data.broadcastMessage.dismissable).to.be.true 201 expect(data.broadcastMessage.dismissable).to.be.true
202} 202}
203 203
204const newCustomConfig: CustomConfig = {
205 instance: {
206 name: 'PeerTube updated',
207 shortDescription: 'my short description',
208 description: 'my super description',
209 terms: 'my super terms',
210 codeOfConduct: 'my super coc',
211
212 creationReason: 'my super creation reason',
213 moderationInformation: 'my super moderation information',
214 administrator: 'Kuja',
215 maintenanceLifetime: 'forever',
216 businessModel: 'my super business model',
217 hardwareInformation: '2vCore 3GB RAM',
218
219 languages: [ 'en', 'es' ],
220 categories: [ 1, 2 ],
221
222 isNSFW: true,
223 defaultNSFWPolicy: 'blur' as 'blur',
224
225 defaultClientRoute: '/videos/recently-added',
226
227 customizations: {
228 javascript: 'alert("coucou")',
229 css: 'body { background-color: red; }'
230 }
231 },
232 theme: {
233 default: 'default'
234 },
235 services: {
236 twitter: {
237 username: '@Kuja',
238 whitelisted: true
239 }
240 },
241 cache: {
242 previews: {
243 size: 2
244 },
245 captions: {
246 size: 3
247 },
248 torrents: {
249 size: 4
250 }
251 },
252 signup: {
253 enabled: false,
254 limit: 5,
255 requiresEmailVerification: false,
256 minimumAge: 10
257 },
258 admin: {
259 email: 'superadmin1@example.com'
260 },
261 contactForm: {
262 enabled: false
263 },
264 user: {
265 videoQuota: 5242881,
266 videoQuotaDaily: 318742
267 },
268 transcoding: {
269 enabled: true,
270 allowAdditionalExtensions: true,
271 allowAudioFiles: true,
272 threads: 1,
273 concurrency: 3,
274 profile: 'vod_profile',
275 resolutions: {
276 '0p': false,
277 '240p': false,
278 '360p': true,
279 '480p': true,
280 '720p': false,
281 '1080p': false,
282 '1440p': false,
283 '2160p': false
284 },
285 webtorrent: {
286 enabled: true
287 },
288 hls: {
289 enabled: false
290 }
291 },
292 live: {
293 enabled: true,
294 allowReplay: true,
295 maxDuration: 5000,
296 maxInstanceLives: -1,
297 maxUserLives: 10,
298 transcoding: {
299 enabled: true,
300 threads: 4,
301 profile: 'live_profile',
302 resolutions: {
303 '240p': true,
304 '360p': true,
305 '480p': true,
306 '720p': true,
307 '1080p': true,
308 '1440p': true,
309 '2160p': true
310 }
311 }
312 },
313 import: {
314 videos: {
315 concurrency: 4,
316 http: {
317 enabled: false
318 },
319 torrent: {
320 enabled: false
321 }
322 }
323 },
324 trending: {
325 videos: {
326 algorithms: {
327 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
328 default: 'hot'
329 }
330 }
331 },
332 autoBlacklist: {
333 videos: {
334 ofUsers: {
335 enabled: true
336 }
337 }
338 },
339 followers: {
340 instance: {
341 enabled: false,
342 manualApproval: true
343 }
344 },
345 followings: {
346 instance: {
347 autoFollowBack: {
348 enabled: true
349 },
350 autoFollowIndex: {
351 enabled: true,
352 indexUrl: 'https://updated.example.com'
353 }
354 }
355 },
356 broadcastMessage: {
357 enabled: true,
358 level: 'error',
359 message: 'super bad message',
360 dismissable: true
361 },
362 search: {
363 remoteUri: {
364 anonymous: true,
365 users: true
366 },
367 searchIndex: {
368 enabled: true,
369 url: 'https://search.joinpeertube.org',
370 disableLocalSearch: true,
371 isDefaultSearch: true
372 }
373 }
374}
375
376describe('Test static config', function () {
377 let server: PeerTubeServer = null
378
379 before(async function () {
380 this.timeout(30000)
381
382 server = await createSingleServer(1, { webadmin: { configuration: { edit: { allowed: false } } } })
383 await setAccessTokensToServers([ server ])
384 })
385
386 it('Should tell the client that edits are not allowed', async function () {
387 const data = await server.config.getConfig()
388
389 expect(data.allowEdits).to.be.false
390 })
391
392 it('Should error when client tries to update', async function () {
393 await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 })
394 })
395})
396
204describe('Test config', function () { 397describe('Test config', function () {
205 let server: PeerTubeServer = null 398 let server: PeerTubeServer = null
206 399
@@ -252,177 +445,6 @@ describe('Test config', function () {
252 }) 445 })
253 446
254 it('Should update the customized configuration', async function () { 447 it('Should update the customized configuration', async function () {
255 const newCustomConfig: CustomConfig = {
256 instance: {
257 name: 'PeerTube updated',
258 shortDescription: 'my short description',
259 description: 'my super description',
260 terms: 'my super terms',
261 codeOfConduct: 'my super coc',
262
263 creationReason: 'my super creation reason',
264 moderationInformation: 'my super moderation information',
265 administrator: 'Kuja',
266 maintenanceLifetime: 'forever',
267 businessModel: 'my super business model',
268 hardwareInformation: '2vCore 3GB RAM',
269
270 languages: [ 'en', 'es' ],
271 categories: [ 1, 2 ],
272
273 isNSFW: true,
274 defaultNSFWPolicy: 'blur' as 'blur',
275
276 defaultClientRoute: '/videos/recently-added',
277
278 customizations: {
279 javascript: 'alert("coucou")',
280 css: 'body { background-color: red; }'
281 }
282 },
283 theme: {
284 default: 'default'
285 },
286 services: {
287 twitter: {
288 username: '@Kuja',
289 whitelisted: true
290 }
291 },
292 cache: {
293 previews: {
294 size: 2
295 },
296 captions: {
297 size: 3
298 },
299 torrents: {
300 size: 4
301 }
302 },
303 signup: {
304 enabled: false,
305 limit: 5,
306 requiresEmailVerification: false,
307 minimumAge: 10
308 },
309 admin: {
310 email: 'superadmin1@example.com'
311 },
312 contactForm: {
313 enabled: false
314 },
315 user: {
316 videoQuota: 5242881,
317 videoQuotaDaily: 318742
318 },
319 transcoding: {
320 enabled: true,
321 allowAdditionalExtensions: true,
322 allowAudioFiles: true,
323 threads: 1,
324 concurrency: 3,
325 profile: 'vod_profile',
326 resolutions: {
327 '0p': false,
328 '240p': false,
329 '360p': true,
330 '480p': true,
331 '720p': false,
332 '1080p': false,
333 '1440p': false,
334 '2160p': false
335 },
336 webtorrent: {
337 enabled: true
338 },
339 hls: {
340 enabled: false
341 }
342 },
343 live: {
344 enabled: true,
345 allowReplay: true,
346 maxDuration: 5000,
347 maxInstanceLives: -1,
348 maxUserLives: 10,
349 transcoding: {
350 enabled: true,
351 threads: 4,
352 profile: 'live_profile',
353 resolutions: {
354 '240p': true,
355 '360p': true,
356 '480p': true,
357 '720p': true,
358 '1080p': true,
359 '1440p': true,
360 '2160p': true
361 }
362 }
363 },
364 import: {
365 videos: {
366 concurrency: 4,
367 http: {
368 enabled: false
369 },
370 torrent: {
371 enabled: false
372 }
373 }
374 },
375 trending: {
376 videos: {
377 algorithms: {
378 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
379 default: 'hot'
380 }
381 }
382 },
383 autoBlacklist: {
384 videos: {
385 ofUsers: {
386 enabled: true
387 }
388 }
389 },
390 followers: {
391 instance: {
392 enabled: false,
393 manualApproval: true
394 }
395 },
396 followings: {
397 instance: {
398 autoFollowBack: {
399 enabled: true
400 },
401 autoFollowIndex: {
402 enabled: true,
403 indexUrl: 'https://updated.example.com'
404 }
405 }
406 },
407 broadcastMessage: {
408 enabled: true,
409 level: 'error',
410 message: 'super bad message',
411 dismissable: true
412 },
413 search: {
414 remoteUri: {
415 anonymous: true,
416 users: true
417 },
418 searchIndex: {
419 enabled: true,
420 url: 'https://search.joinpeertube.org',
421 disableLocalSearch: true,
422 isDefaultSearch: true
423 }
424 }
425 }
426 await server.config.updateCustomConfig({ newCustomConfig }) 448 await server.config.updateCustomConfig({ newCustomConfig })
427 449
428 const data = await server.config.getCustomConfig() 450 const data = await server.config.getCustomConfig()
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 585e99aca..3b026e3a5 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -30,6 +30,7 @@ export interface RegisteredIdAndPassAuthConfig {
30} 30}
31 31
32export interface ServerConfig { 32export interface ServerConfig {
33 allowEdits: boolean
33 serverVersion: string 34 serverVersion: string
34 serverCommit?: string 35 serverCommit?: string
35 36
diff --git a/support/docker/production/Dockerfile.buster b/support/docker/production/Dockerfile.buster
index 2ff0591f9..163c514f5 100644
--- a/support/docker/production/Dockerfile.buster
+++ b/support/docker/production/Dockerfile.buster
@@ -33,7 +33,8 @@ RUN mkdir /data /config
33RUN chown -R peertube:peertube /data /config 33RUN chown -R peertube:peertube /data /config
34 34
35ENV NODE_ENV production 35ENV NODE_ENV production
36ENV NODE_CONFIG_DIR /config 36ENV NODE_CONFIG_DIR /app/config:/app/support/docker/production/config:/config
37ENV PEERTUBE_LOCAL_CONFIG /config
37 38
38VOLUME /data 39VOLUME /data
39VOLUME /config 40VOLUME /config
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml
index 1b474582a..7c430a995 100644
--- a/support/docker/production/config/custom-environment-variables.yaml
+++ b/support/docker/production/config/custom-environment-variables.yaml
@@ -68,6 +68,13 @@ object_storage:
68 prefix: "PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX" 68 prefix: "PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX"
69 base_url: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL" 69 base_url: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL"
70 70
71webadmin:
72 configuration:
73 edit:
74 allowed:
75 __name: "PEERTUBE_ALLOW_WEBADMIN_CONFIG"
76 __format: "json"
77
71log: 78log:
72 level: "PEERTUBE_LOG_LEVEL" 79 level: "PEERTUBE_LOG_LEVEL"
73 log_ping_requests: 80 log_ping_requests:
diff --git a/support/docker/production/entrypoint.sh b/support/docker/production/entrypoint.sh
index 7dd626b9f..261055e84 100755
--- a/support/docker/production/entrypoint.sh
+++ b/support/docker/production/entrypoint.sh
@@ -1,15 +1,8 @@
1#!/bin/sh 1#!/bin/sh
2set -e 2set -e
3 3
4# Populate config directory
5if [ -z "$(ls -A /config)" ]; then
6 cp /app/support/docker/production/config/* /config
7fi
8 4
9# Always copy default and custom env configuration file, in cases where new keys were added 5find /config ! -user peertube -exec chown peertube:peertube {} \; || true
10cp /app/config/default.yaml /config
11cp /app/support/docker/production/config/custom-environment-variables.yaml /config
12find /config ! -user peertube -exec chown peertube:peertube {} \;
13 6
14# first arg is `-f` or `--some-option` 7# first arg is `-f` or `--some-option`
15# or first arg is `something.conf` 8# or first arg is `something.conf`