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
This commit is contained in:
parent
badacdbb4a
commit
8d8a037e3f
|
@ -63,7 +63,7 @@
|
|||
<div class="col-md-7 col-xl-5"></div>
|
||||
<div class="col-md-5 col-xl-5">
|
||||
|
||||
<div class="form-error submit-error" i18n *ngIf="!form.valid">
|
||||
<div class="form-error submit-error" i18n *ngIf="!form.valid && serverConfig.allowEdits">
|
||||
There are errors in the form:
|
||||
|
||||
<ul>
|
||||
|
@ -77,7 +77,11 @@
|
|||
You cannot allow live replay if you don't enable transcoding.
|
||||
</span>
|
||||
|
||||
<input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions()">
|
||||
<span i18n *ngIf="!serverConfig.allowEdits">
|
||||
You cannot change the server configuration because it's managed externally.
|
||||
</span>
|
||||
|
||||
<input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid || !hasConsistentOptions() || !serverConfig.allowEdits">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -33,6 +33,11 @@ input[type=number] {
|
|||
top: 5px;
|
||||
right: 2.5rem;
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
background-color: #f9f9f9;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
|
@ -93,6 +98,11 @@ textarea {
|
|||
}
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
.form-group-right {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
|
|
@ -258,6 +258,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
|
||||
this.loadConfigAndUpdateForm()
|
||||
this.loadCategoriesAndLanguages()
|
||||
if (!this.serverConfig.allowEdits) {
|
||||
this.form.disable()
|
||||
}
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<textarea #textarea
|
||||
[(ngModel)]="content" (ngModelChange)="onModelChange()"
|
||||
class="form-control" [ngClass]="classes"
|
||||
[attr.disabled]="disabled"
|
||||
[ngStyle]="{ height: textareaHeight }"
|
||||
[id]="name" [name]="name">
|
||||
</textarea>
|
||||
|
@ -25,11 +26,11 @@
|
|||
</ng-container>
|
||||
|
||||
<my-button
|
||||
*ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()"
|
||||
*ngIf="!isMaximized" [title]="maximizeInText" className="maximize-button" icon="fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
|
||||
></my-button>
|
||||
|
||||
<my-button
|
||||
*ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()"
|
||||
*ngIf="isMaximized" [title]="maximizeOutText" className="maximize-button" icon="exit-fullscreen" (click)="onMaximizeClick()" [disabled]="disabled"
|
||||
></my-button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
|||
previewHTML: SafeHtml | string = ''
|
||||
|
||||
isMaximized = false
|
||||
disabled = false
|
||||
|
||||
maximizeInText = $localize`Maximize editor`
|
||||
maximizeOutText = $localize`Exit maximized editor`
|
||||
|
@ -108,6 +109,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
setDisabledState (isDisabled: boolean) {
|
||||
this.disabled = isDisabled
|
||||
}
|
||||
|
||||
private lockBodyScroll () {
|
||||
this.scrollPosition = this.viewportScroller.getScrollPosition()
|
||||
document.getElementById('content').classList.add('lock-scroll')
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
[multiple]="true"
|
||||
[searchable]="true"
|
||||
[closeOnSelect]="false"
|
||||
[disabled]="disabled"
|
||||
|
||||
bindValue="id"
|
||||
bindLabel="label"
|
||||
|
|
|
@ -23,6 +23,8 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
|
|||
@Input() selectableGroupAsModel: boolean
|
||||
@Input() placeholder: string
|
||||
|
||||
disabled = false
|
||||
|
||||
ngOnInit () {
|
||||
if (!this.placeholder) this.placeholder = $localize`Add a new option`
|
||||
}
|
||||
|
@ -59,6 +61,10 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
|
|||
this.propagateChange(this.selectedItems)
|
||||
}
|
||||
|
||||
setDisabledState (isDisabled: boolean) {
|
||||
this.disabled = isDisabled
|
||||
}
|
||||
|
||||
compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
|
||||
if (typeof selected === 'string' || typeof selected === 'number') {
|
||||
return item.id === selected
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[searchable]="searchable"
|
||||
[groupBy]="groupBy"
|
||||
[labelForId]="labelForId"
|
||||
[disabled]="disabled"
|
||||
|
||||
[(ngModel)]="selectedId"
|
||||
(ngModelChange)="onModelChange()"
|
||||
|
|
|
@ -25,6 +25,7 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
|
|||
|
||||
customValue: number | string = ''
|
||||
selectedId: number | string
|
||||
disabled = false
|
||||
|
||||
itemsWithCustom: SelectOptionsItem[] = []
|
||||
|
||||
|
@ -75,4 +76,8 @@ export class SelectCustomValueComponent implements ControlValueAccessor, OnChang
|
|||
isCustomValue () {
|
||||
return this.selectedId === 'other'
|
||||
}
|
||||
|
||||
setDisabledState (isDisabled: boolean) {
|
||||
this.disabled = isDisabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
[labelForId]="labelForId"
|
||||
[searchable]="searchable"
|
||||
[searchFn]="searchFn"
|
||||
[disabled]="disabled"
|
||||
|
||||
bindLabel="label"
|
||||
bindValue="id"
|
||||
|
|
|
@ -23,6 +23,7 @@ export class SelectOptionsComponent implements ControlValueAccessor {
|
|||
@Input() searchFn: any
|
||||
|
||||
selectedId: number | string
|
||||
disabled = false
|
||||
|
||||
propagateChange = (_: any) => { /* empty */ }
|
||||
|
||||
|
@ -48,4 +49,8 @@ export class SelectOptionsComponent implements ControlValueAccessor {
|
|||
onModelChange () {
|
||||
this.propagateChange(this.selectedId)
|
||||
}
|
||||
|
||||
setDisabledState (isDisabled: boolean) {
|
||||
this.disabled = isDisabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -364,6 +364,9 @@
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
select[disabled] {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $width) {
|
||||
width: 100%;
|
||||
|
|
|
@ -243,6 +243,11 @@ peertube:
|
|||
# You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
|
||||
url: 'https://joinpeertube.org/api/v1/versions.json'
|
||||
|
||||
webadmin:
|
||||
configuration:
|
||||
edit:
|
||||
allowed: true
|
||||
|
||||
cache:
|
||||
previews:
|
||||
size: 500 # Max number of previews you want to cache
|
||||
|
|
|
@ -241,6 +241,11 @@ peertube:
|
|||
# You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
|
||||
url: 'https://joinpeertube.org/api/v1/versions.json'
|
||||
|
||||
webadmin:
|
||||
configuration:
|
||||
# Set to false if you want the config to be readonly
|
||||
allow_edits: true
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# From this point, all the following keys can be overridden by the web interface
|
||||
|
|
|
@ -11,7 +11,7 @@ import { objectConverter } from '../../helpers/core-utils'
|
|||
import { CONFIG, reloadConfig } from '../../initializers/config'
|
||||
import { ClientHtml } from '../../lib/client-html'
|
||||
import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
|
||||
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
|
||||
import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config'
|
||||
|
||||
const configRouter = express.Router()
|
||||
|
||||
|
@ -38,6 +38,7 @@ configRouter.put('/custom',
|
|||
openapiOperationDoc({ operationId: 'putCustomConfig' }),
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||
ensureConfigIsEditable,
|
||||
customConfigUpdateValidator,
|
||||
asyncMiddleware(updateCustomConfig)
|
||||
)
|
||||
|
@ -46,6 +47,7 @@ configRouter.delete('/custom',
|
|||
openapiOperationDoc({ operationId: 'delCustomConfig' }),
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
|
||||
ensureConfigIsEditable,
|
||||
asyncMiddleware(deleteCustomConfig)
|
||||
)
|
||||
|
||||
|
|
|
@ -195,6 +195,13 @@ const CONFIG = {
|
|||
URL: config.get<string>('peertube.check_latest_version.url')
|
||||
}
|
||||
},
|
||||
WEBADMIN: {
|
||||
CONFIGURATION: {
|
||||
EDITS: {
|
||||
ALLOWED: config.get<boolean>('webadmin.configuration.edit.allowed')
|
||||
}
|
||||
}
|
||||
},
|
||||
ADMIN: {
|
||||
get EMAIL () { return config.get<string>('admin.email') }
|
||||
},
|
||||
|
@ -411,14 +418,22 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getLocalConfigFilePath () {
|
||||
const configSources = config.util.getConfigSources()
|
||||
if (configSources.length === 0) throw new Error('Invalid config source.')
|
||||
const localConfigDir = getLocalConfigDir()
|
||||
|
||||
let filename = 'local'
|
||||
if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
|
||||
if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
|
||||
|
||||
return join(dirname(configSources[0].name), filename + '.json')
|
||||
return join(localConfigDir, filename + '.json')
|
||||
}
|
||||
|
||||
function getLocalConfigDir () {
|
||||
if (process.env.PEERTUBE_LOCAL_CONFIG) return process.env.PEERTUBE_LOCAL_CONFIG
|
||||
|
||||
const configSources = config.util.getConfigSources()
|
||||
if (configSources.length === 0) throw new Error('Invalid config source.')
|
||||
|
||||
return dirname(configSources[0].name)
|
||||
}
|
||||
|
||||
function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
|
||||
|
@ -437,19 +452,19 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
|
|||
|
||||
export function reloadConfig () {
|
||||
|
||||
function getConfigDirectory () {
|
||||
function getConfigDirectories () {
|
||||
if (process.env.NODE_CONFIG_DIR) {
|
||||
return process.env.NODE_CONFIG_DIR
|
||||
return process.env.NODE_CONFIG_DIR.split(":")
|
||||
}
|
||||
|
||||
return join(root(), 'config')
|
||||
return [ join(root(), 'config') ]
|
||||
}
|
||||
|
||||
function purge () {
|
||||
const directory = getConfigDirectory()
|
||||
const directories = getConfigDirectories()
|
||||
|
||||
for (const fileName in require.cache) {
|
||||
if (fileName.includes(directory) === false) {
|
||||
if (directories.some((dir) => fileName.includes(dir)) === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ class ServerConfigManager {
|
|||
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
|
||||
|
||||
return {
|
||||
allowEdits: CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED,
|
||||
instance: {
|
||||
name: CONFIG.INSTANCE.NAME,
|
||||
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isIntOrNull } from '@server/helpers/custom-validators/misc'
|
||||
import { isEmailEnabled } from '@server/initializers/config'
|
||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
|
||||
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
|
||||
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
|
||||
import { areValidationErrors } from './shared'
|
||||
import { HttpStatusCode } from '@shared/models/http/http-error-codes'
|
||||
|
||||
const customConfigUpdateValidator = [
|
||||
body('instance.name').exists().withMessage('Should have a valid instance name'),
|
||||
|
@ -104,10 +105,21 @@ const customConfigUpdateValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (!CONFIG.WEBADMIN.CONFIGURATION.EDITS.ALLOWED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
|
||||
message: 'Server configuration is static and cannot be edited'
|
||||
})
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
customConfigUpdateValidator
|
||||
customConfigUpdateValidator,
|
||||
ensureConfigIsEditable
|
||||
}
|
||||
|
||||
function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
|
||||
|
|
|
@ -201,57 +201,6 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
expect(data.broadcastMessage.dismissable).to.be.true
|
||||
}
|
||||
|
||||
describe('Test config', function () {
|
||||
let server: PeerTubeServer = null
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
it('Should have a correct config on a server with registration enabled', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.signup.allowed).to.be.true
|
||||
})
|
||||
|
||||
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
await Promise.all([
|
||||
server.users.register({ username: 'user1' }),
|
||||
server.users.register({ username: 'user2' }),
|
||||
server.users.register({ username: 'user3' })
|
||||
])
|
||||
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.signup.allowed).to.be.false
|
||||
})
|
||||
|
||||
it('Should have the correct video allowed extensions', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.video.file.extensions).to.have.lengthOf(3)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
})
|
||||
|
||||
it('Should get the customized configuration', async function () {
|
||||
const data = await server.config.getCustomConfig()
|
||||
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
|
||||
it('Should update the customized configuration', async function () {
|
||||
const newCustomConfig: CustomConfig = {
|
||||
instance: {
|
||||
name: 'PeerTube updated',
|
||||
|
@ -423,6 +372,79 @@ describe('Test config', function () {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Test static config', function () {
|
||||
let server: PeerTubeServer = null
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1, { webadmin: { configuration: { edit: { allowed: false } } } })
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
it('Should tell the client that edits are not allowed', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.allowEdits).to.be.false
|
||||
})
|
||||
|
||||
it('Should error when client tries to update', async function () {
|
||||
await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test config', function () {
|
||||
let server: PeerTubeServer = null
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
})
|
||||
|
||||
it('Should have a correct config on a server with registration enabled', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.signup.allowed).to.be.true
|
||||
})
|
||||
|
||||
it('Should have a correct config on a server with registration enabled and a users limit', async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
await Promise.all([
|
||||
server.users.register({ username: 'user1' }),
|
||||
server.users.register({ username: 'user2' }),
|
||||
server.users.register({ username: 'user3' })
|
||||
])
|
||||
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.signup.allowed).to.be.false
|
||||
})
|
||||
|
||||
it('Should have the correct video allowed extensions', async function () {
|
||||
const data = await server.config.getConfig()
|
||||
|
||||
expect(data.video.file.extensions).to.have.lengthOf(3)
|
||||
expect(data.video.file.extensions).to.contain('.mp4')
|
||||
expect(data.video.file.extensions).to.contain('.webm')
|
||||
expect(data.video.file.extensions).to.contain('.ogv')
|
||||
|
||||
await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
|
||||
|
||||
expect(data.contactForm.enabled).to.be.true
|
||||
})
|
||||
|
||||
it('Should get the customized configuration', async function () {
|
||||
const data = await server.config.getCustomConfig()
|
||||
|
||||
checkInitialConfig(server, data)
|
||||
})
|
||||
|
||||
it('Should update the customized configuration', async function () {
|
||||
await server.config.updateCustomConfig({ newCustomConfig })
|
||||
|
||||
const data = await server.config.getCustomConfig()
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface RegisteredIdAndPassAuthConfig {
|
|||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
allowEdits: boolean
|
||||
serverVersion: string
|
||||
serverCommit?: string
|
||||
|
||||
|
|
|
@ -33,7 +33,8 @@ RUN mkdir /data /config
|
|||
RUN chown -R peertube:peertube /data /config
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NODE_CONFIG_DIR /config
|
||||
ENV NODE_CONFIG_DIR /app/config:/app/support/docker/production/config:/config
|
||||
ENV PEERTUBE_LOCAL_CONFIG /config
|
||||
|
||||
VOLUME /data
|
||||
VOLUME /config
|
||||
|
|
|
@ -68,6 +68,13 @@ object_storage:
|
|||
prefix: "PEERTUBE_OBJECT_STORAGE_VIDEOS_PREFIX"
|
||||
base_url: "PEERTUBE_OBJECT_STORAGE_VIDEOS_BASE_URL"
|
||||
|
||||
webadmin:
|
||||
configuration:
|
||||
edit:
|
||||
allowed:
|
||||
__name: "PEERTUBE_ALLOW_WEBADMIN_CONFIG"
|
||||
__format: "json"
|
||||
|
||||
log:
|
||||
level: "PEERTUBE_LOG_LEVEL"
|
||||
log_ping_requests:
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Populate config directory
|
||||
if [ -z "$(ls -A /config)" ]; then
|
||||
cp /app/support/docker/production/config/* /config
|
||||
fi
|
||||
|
||||
# Always copy default and custom env configuration file, in cases where new keys were added
|
||||
cp /app/config/default.yaml /config
|
||||
cp /app/support/docker/production/config/custom-environment-variables.yaml /config
|
||||
find /config ! -user peertube -exec chown peertube:peertube {} \;
|
||||
find /config ! -user peertube -exec chown peertube:peertube {} \; || true
|
||||
|
||||
# first arg is `-f` or `--some-option`
|
||||
# or first arg is `something.conf`
|
||||
|
|
Loading…
Reference in New Issue