diff options
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 | ||
38 | input[type=checkbox] { | 43 | input[type=checkbox] { |
@@ -93,6 +98,11 @@ textarea { | |||
93 | } | 98 | } |
94 | } | 99 | } |
95 | 100 | ||
101 | input[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 | ||
246 | webadmin: | ||
247 | configuration: | ||
248 | edit: | ||
249 | allowed: true | ||
250 | |||
246 | cache: | 251 | cache: |
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 | ||
244 | webadmin: | ||
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' | |||
11 | import { CONFIG, reloadConfig } from '../../initializers/config' | 11 | import { CONFIG, reloadConfig } from '../../initializers/config' |
12 | import { ClientHtml } from '../../lib/client-html' | 12 | import { ClientHtml } from '../../lib/client-html' |
13 | import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' | 13 | import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' |
14 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' | 14 | import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config' |
15 | 15 | ||
16 | const configRouter = express.Router() | 16 | const 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 | ||
413 | function getLocalConfigFilePath () { | 420 | function 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 | |||
430 | function 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 | ||
424 | function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { | 439 | function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { |
@@ -437,19 +452,19 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { | |||
437 | 452 | ||
438 | export function reloadConfig () { | 453 | export 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { isIntOrNull } from '@server/helpers/custom-validators/misc' | 3 | import { isIntOrNull } from '@server/helpers/custom-validators/misc' |
4 | import { isEmailEnabled } from '@server/initializers/config' | 4 | import { CONFIG, isEmailEnabled } from '@server/initializers/config' |
5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
7 | import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | 7 | import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' |
8 | import { logger } from '../../helpers/logger' | 8 | import { logger } from '../../helpers/logger' |
9 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | 9 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' |
10 | import { areValidationErrors } from './shared' | 10 | import { areValidationErrors } from './shared' |
11 | import { HttpStatusCode } from '@shared/models/http/http-error-codes' | ||
11 | 12 | ||
12 | const customConfigUpdateValidator = [ | 13 | const 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 | ||
108 | function 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 | ||
109 | export { | 120 | export { |
110 | customConfigUpdateValidator | 121 | customConfigUpdateValidator, |
122 | ensureConfigIsEditable | ||
111 | } | 123 | } |
112 | 124 | ||
113 | function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) { | 125 | function 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 | ||
204 | const 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 | |||
376 | describe('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 | |||
204 | describe('Test config', function () { | 397 | describe('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 | ||
32 | export interface ServerConfig { | 32 | export 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 | |||
33 | RUN chown -R peertube:peertube /data /config | 33 | RUN chown -R peertube:peertube /data /config |
34 | 34 | ||
35 | ENV NODE_ENV production | 35 | ENV NODE_ENV production |
36 | ENV NODE_CONFIG_DIR /config | 36 | ENV NODE_CONFIG_DIR /app/config:/app/support/docker/production/config:/config |
37 | ENV PEERTUBE_LOCAL_CONFIG /config | ||
37 | 38 | ||
38 | VOLUME /data | 39 | VOLUME /data |
39 | VOLUME /config | 40 | VOLUME /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 | ||
71 | webadmin: | ||
72 | configuration: | ||
73 | edit: | ||
74 | allowed: | ||
75 | __name: "PEERTUBE_ALLOW_WEBADMIN_CONFIG" | ||
76 | __format: "json" | ||
77 | |||
71 | log: | 78 | log: |
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 |
2 | set -e | 2 | set -e |
3 | 3 | ||
4 | # Populate config directory | ||
5 | if [ -z "$(ls -A /config)" ]; then | ||
6 | cp /app/support/docker/production/config/* /config | ||
7 | fi | ||
8 | 4 | ||
9 | # Always copy default and custom env configuration file, in cases where new keys were added | 5 | find /config ! -user peertube -exec chown peertube:peertube {} \; || true |
10 | cp /app/config/default.yaml /config | ||
11 | cp /app/support/docker/production/config/custom-environment-variables.yaml /config | ||
12 | find /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` |