<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>
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>
top: 5px;
right: 2.5rem;
}
+
+ input[disabled] {
+ background-color: #f9f9f9;
+ pointer-events: none;
+ }
}
input[type=checkbox] {
}
}
+input[disabled] {
+ opacity: 0.5;
+}
+
+
.form-group-right {
padding-top: 2px;
}
this.loadConfigAndUpdateForm()
this.loadCategoriesAndLanguages()
+ if (!this.serverConfig.allowEdits) {
+ this.form.disable()
+ }
}
formValidated () {
<textarea #textarea
[(ngModel)]="content" (ngModelChange)="onModelChange()"
class="form-control" [ngClass]="classes"
+ [attr.disabled]="disabled"
[ngStyle]="{ height: textareaHeight }"
[id]="name" [name]="name">
</textarea>
</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>
previewHTML: SafeHtml | string = ''
isMaximized = false
+ disabled = false
maximizeInText = $localize`Maximize editor`
maximizeOutText = $localize`Exit maximized editor`
}
}
+ setDisabledState (isDisabled: boolean) {
+ this.disabled = isDisabled
+ }
+
private lockBodyScroll () {
this.scrollPosition = this.viewportScroller.getScrollPosition()
document.getElementById('content').classList.add('lock-scroll')
[multiple]="true"
[searchable]="true"
[closeOnSelect]="false"
+ [disabled]="disabled"
bindValue="id"
bindLabel="label"
@Input() selectableGroupAsModel: boolean
@Input() placeholder: string
+ disabled = false
+
ngOnInit () {
if (!this.placeholder) this.placeholder = $localize`Add a new option`
}
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
[searchable]="searchable"
[groupBy]="groupBy"
[labelForId]="labelForId"
+ [disabled]="disabled"
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"
customValue: number | string = ''
selectedId: number | string
+ disabled = false
itemsWithCustom: SelectOptionsItem[] = []
isCustomValue () {
return this.selectedId === 'other'
}
+
+ setDisabledState (isDisabled: boolean) {
+ this.disabled = isDisabled
+ }
}
[labelForId]="labelForId"
[searchable]="searchable"
[searchFn]="searchFn"
+ [disabled]="disabled"
bindLabel="label"
bindValue="id"
@Input() searchFn: any
selectedId: number | string
+ disabled = false
propagateChange = (_: any) => { /* empty */ }
onModelChange () {
this.propagateChange(this.selectedId)
}
+
+ setDisabledState (isDisabled: boolean) {
+ this.disabled = isDisabled
+ }
}
cursor: default;
}
}
+ select[disabled] {
+ background-color: #f9f9f9;
+ }
@media screen and (max-width: $width) {
width: 100%;
# 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
# 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
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()
openapiOperationDoc({ operationId: 'putCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
+ ensureConfigIsEditable,
customConfigUpdateValidator,
asyncMiddleware(updateCustomConfig)
)
openapiOperationDoc({ operationId: 'delCustomConfig' }),
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
+ ensureConfigIsEditable,
asyncMiddleware(deleteCustomConfig)
)
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') }
},
// ---------------------------------------------------------------------------
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[] {
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
}
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,
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'),
}
]
+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) {
expect(data.broadcastMessage.dismissable).to.be.true
}
+const newCustomConfig: CustomConfig = {
+ instance: {
+ name: 'PeerTube updated',
+ shortDescription: 'my short description',
+ description: 'my super description',
+ terms: 'my super terms',
+ codeOfConduct: 'my super coc',
+
+ creationReason: 'my super creation reason',
+ moderationInformation: 'my super moderation information',
+ administrator: 'Kuja',
+ maintenanceLifetime: 'forever',
+ businessModel: 'my super business model',
+ hardwareInformation: '2vCore 3GB RAM',
+
+ languages: [ 'en', 'es' ],
+ categories: [ 1, 2 ],
+
+ isNSFW: true,
+ defaultNSFWPolicy: 'blur' as 'blur',
+
+ defaultClientRoute: '/videos/recently-added',
+
+ customizations: {
+ javascript: 'alert("coucou")',
+ css: 'body { background-color: red; }'
+ }
+ },
+ theme: {
+ default: 'default'
+ },
+ services: {
+ twitter: {
+ username: '@Kuja',
+ whitelisted: true
+ }
+ },
+ cache: {
+ previews: {
+ size: 2
+ },
+ captions: {
+ size: 3
+ },
+ torrents: {
+ size: 4
+ }
+ },
+ signup: {
+ enabled: false,
+ limit: 5,
+ requiresEmailVerification: false,
+ minimumAge: 10
+ },
+ admin: {
+ email: 'superadmin1@example.com'
+ },
+ contactForm: {
+ enabled: false
+ },
+ user: {
+ videoQuota: 5242881,
+ videoQuotaDaily: 318742
+ },
+ transcoding: {
+ enabled: true,
+ allowAdditionalExtensions: true,
+ allowAudioFiles: true,
+ threads: 1,
+ concurrency: 3,
+ profile: 'vod_profile',
+ resolutions: {
+ '0p': false,
+ '240p': false,
+ '360p': true,
+ '480p': true,
+ '720p': false,
+ '1080p': false,
+ '1440p': false,
+ '2160p': false
+ },
+ webtorrent: {
+ enabled: true
+ },
+ hls: {
+ enabled: false
+ }
+ },
+ live: {
+ enabled: true,
+ allowReplay: true,
+ maxDuration: 5000,
+ maxInstanceLives: -1,
+ maxUserLives: 10,
+ transcoding: {
+ enabled: true,
+ threads: 4,
+ profile: 'live_profile',
+ resolutions: {
+ '240p': true,
+ '360p': true,
+ '480p': true,
+ '720p': true,
+ '1080p': true,
+ '1440p': true,
+ '2160p': true
+ }
+ }
+ },
+ import: {
+ videos: {
+ concurrency: 4,
+ http: {
+ enabled: false
+ },
+ torrent: {
+ enabled: false
+ }
+ }
+ },
+ trending: {
+ videos: {
+ algorithms: {
+ enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
+ default: 'hot'
+ }
+ }
+ },
+ autoBlacklist: {
+ videos: {
+ ofUsers: {
+ enabled: true
+ }
+ }
+ },
+ followers: {
+ instance: {
+ enabled: false,
+ manualApproval: true
+ }
+ },
+ followings: {
+ instance: {
+ autoFollowBack: {
+ enabled: true
+ },
+ autoFollowIndex: {
+ enabled: true,
+ indexUrl: 'https://updated.example.com'
+ }
+ }
+ },
+ broadcastMessage: {
+ enabled: true,
+ level: 'error',
+ message: 'super bad message',
+ dismissable: true
+ },
+ search: {
+ remoteUri: {
+ anonymous: true,
+ users: true
+ },
+ searchIndex: {
+ enabled: true,
+ url: 'https://search.joinpeertube.org',
+ disableLocalSearch: true,
+ isDefaultSearch: true
+ }
+ }
+}
+
+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
})
it('Should update the customized configuration', async function () {
- const newCustomConfig: CustomConfig = {
- instance: {
- name: 'PeerTube updated',
- shortDescription: 'my short description',
- description: 'my super description',
- terms: 'my super terms',
- codeOfConduct: 'my super coc',
-
- creationReason: 'my super creation reason',
- moderationInformation: 'my super moderation information',
- administrator: 'Kuja',
- maintenanceLifetime: 'forever',
- businessModel: 'my super business model',
- hardwareInformation: '2vCore 3GB RAM',
-
- languages: [ 'en', 'es' ],
- categories: [ 1, 2 ],
-
- isNSFW: true,
- defaultNSFWPolicy: 'blur' as 'blur',
-
- defaultClientRoute: '/videos/recently-added',
-
- customizations: {
- javascript: 'alert("coucou")',
- css: 'body { background-color: red; }'
- }
- },
- theme: {
- default: 'default'
- },
- services: {
- twitter: {
- username: '@Kuja',
- whitelisted: true
- }
- },
- cache: {
- previews: {
- size: 2
- },
- captions: {
- size: 3
- },
- torrents: {
- size: 4
- }
- },
- signup: {
- enabled: false,
- limit: 5,
- requiresEmailVerification: false,
- minimumAge: 10
- },
- admin: {
- email: 'superadmin1@example.com'
- },
- contactForm: {
- enabled: false
- },
- user: {
- videoQuota: 5242881,
- videoQuotaDaily: 318742
- },
- transcoding: {
- enabled: true,
- allowAdditionalExtensions: true,
- allowAudioFiles: true,
- threads: 1,
- concurrency: 3,
- profile: 'vod_profile',
- resolutions: {
- '0p': false,
- '240p': false,
- '360p': true,
- '480p': true,
- '720p': false,
- '1080p': false,
- '1440p': false,
- '2160p': false
- },
- webtorrent: {
- enabled: true
- },
- hls: {
- enabled: false
- }
- },
- live: {
- enabled: true,
- allowReplay: true,
- maxDuration: 5000,
- maxInstanceLives: -1,
- maxUserLives: 10,
- transcoding: {
- enabled: true,
- threads: 4,
- profile: 'live_profile',
- resolutions: {
- '240p': true,
- '360p': true,
- '480p': true,
- '720p': true,
- '1080p': true,
- '1440p': true,
- '2160p': true
- }
- }
- },
- import: {
- videos: {
- concurrency: 4,
- http: {
- enabled: false
- },
- torrent: {
- enabled: false
- }
- }
- },
- trending: {
- videos: {
- algorithms: {
- enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
- default: 'hot'
- }
- }
- },
- autoBlacklist: {
- videos: {
- ofUsers: {
- enabled: true
- }
- }
- },
- followers: {
- instance: {
- enabled: false,
- manualApproval: true
- }
- },
- followings: {
- instance: {
- autoFollowBack: {
- enabled: true
- },
- autoFollowIndex: {
- enabled: true,
- indexUrl: 'https://updated.example.com'
- }
- }
- },
- broadcastMessage: {
- enabled: true,
- level: 'error',
- message: 'super bad message',
- dismissable: true
- },
- search: {
- remoteUri: {
- anonymous: true,
- users: true
- },
- searchIndex: {
- enabled: true,
- url: 'https://search.joinpeertube.org',
- disableLocalSearch: true,
- isDefaultSearch: true
- }
- }
- }
await server.config.updateCustomConfig({ newCustomConfig })
const data = await server.config.getCustomConfig()
}
export interface ServerConfig {
+ allowEdits: boolean
serverVersion: string
serverCommit?: string
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
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:
#!/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`