WIP plugins: plugin settings on server side
This commit is contained in:
parent
ffb321bedc
commit
ad91e7006e
|
@ -14,6 +14,7 @@ import { searchRouter } from './search'
|
||||||
import { overviewsRouter } from './overviews'
|
import { overviewsRouter } from './overviews'
|
||||||
import { videoPlaylistRouter } from './video-playlist'
|
import { videoPlaylistRouter } from './video-playlist'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { pluginsRouter } from '../plugins'
|
||||||
|
|
||||||
const apiRouter = express.Router()
|
const apiRouter = express.Router()
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ apiRouter.use('/videos', videosRouter)
|
||||||
apiRouter.use('/jobs', jobsRouter)
|
apiRouter.use('/jobs', jobsRouter)
|
||||||
apiRouter.use('/search', searchRouter)
|
apiRouter.use('/search', searchRouter)
|
||||||
apiRouter.use('/overviews', overviewsRouter)
|
apiRouter.use('/overviews', overviewsRouter)
|
||||||
|
apiRouter.use('/plugins', pluginsRouter)
|
||||||
apiRouter.use('/ping', pong)
|
apiRouter.use('/ping', pong)
|
||||||
apiRouter.use('/*', badRequest)
|
apiRouter.use('/*', badRequest)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { getFormattedObjects } from '../../helpers/utils'
|
||||||
|
import {
|
||||||
|
asyncMiddleware,
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight,
|
||||||
|
paginationValidator,
|
||||||
|
setDefaultPagination,
|
||||||
|
setDefaultSort
|
||||||
|
} from '../../middlewares'
|
||||||
|
import { pluginsSortValidator } from '../../middlewares/validators'
|
||||||
|
import { PluginModel } from '../../models/server/plugin'
|
||||||
|
import { UserRight } from '../../../shared/models/users'
|
||||||
|
import {
|
||||||
|
enabledPluginValidator,
|
||||||
|
installPluginValidator,
|
||||||
|
listPluginsValidator,
|
||||||
|
uninstallPluginValidator,
|
||||||
|
updatePluginSettingsValidator
|
||||||
|
} from '../../middlewares/validators/plugins'
|
||||||
|
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
||||||
|
import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model'
|
||||||
|
import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
|
||||||
|
|
||||||
|
const pluginRouter = express.Router()
|
||||||
|
|
||||||
|
pluginRouter.get('/',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
listPluginsValidator,
|
||||||
|
paginationValidator,
|
||||||
|
pluginsSortValidator,
|
||||||
|
setDefaultSort,
|
||||||
|
setDefaultPagination,
|
||||||
|
asyncMiddleware(listPlugins)
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginRouter.get('/:pluginName/settings',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
asyncMiddleware(enabledPluginValidator),
|
||||||
|
asyncMiddleware(listPluginSettings)
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginRouter.put('/:pluginName/settings',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
updatePluginSettingsValidator,
|
||||||
|
asyncMiddleware(enabledPluginValidator),
|
||||||
|
asyncMiddleware(updatePluginSettings)
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginRouter.post('/install',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
installPluginValidator,
|
||||||
|
asyncMiddleware(installPlugin)
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginRouter.post('/uninstall',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
uninstallPluginValidator,
|
||||||
|
asyncMiddleware(uninstallPlugin)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
pluginRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function listPlugins (req: express.Request, res: express.Response) {
|
||||||
|
const type = req.query.type
|
||||||
|
|
||||||
|
const resultList = await PluginModel.listForApi({
|
||||||
|
type,
|
||||||
|
start: req.query.start,
|
||||||
|
count: req.query.count,
|
||||||
|
sort: req.query.sort
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPlugin (req: express.Request, res: express.Response) {
|
||||||
|
const body: InstallPlugin = req.body
|
||||||
|
|
||||||
|
await PluginManager.Instance.install(body.npmName)
|
||||||
|
|
||||||
|
return res.sendStatus(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstallPlugin (req: express.Request, res: express.Response) {
|
||||||
|
const body: ManagePlugin = req.body
|
||||||
|
|
||||||
|
await PluginManager.Instance.uninstall(body.npmName)
|
||||||
|
|
||||||
|
return res.sendStatus(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listPluginSettings (req: express.Request, res: express.Response) {
|
||||||
|
const plugin = res.locals.plugin
|
||||||
|
|
||||||
|
const settings = await PluginManager.Instance.getSettings(plugin.name)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
settings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePluginSettings (req: express.Request, res: express.Response) {
|
||||||
|
const plugin = res.locals.plugin
|
||||||
|
|
||||||
|
plugin.settings = req.body.settings
|
||||||
|
await plugin.save()
|
||||||
|
|
||||||
|
return res.sendStatus(204)
|
||||||
|
}
|
|
@ -51,9 +51,7 @@ export {
|
||||||
|
|
||||||
function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
|
function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
|
||||||
if (!videoChannel) {
|
if (!videoChannel) {
|
||||||
res.status(404)
|
``
|
||||||
.json({ error: 'Video channel not found' })
|
|
||||||
.end()
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,9 @@ const SORTABLE_COLUMNS = {
|
||||||
|
|
||||||
USER_NOTIFICATIONS: [ 'createdAt' ],
|
USER_NOTIFICATIONS: [ 'createdAt' ],
|
||||||
|
|
||||||
VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ]
|
VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ],
|
||||||
|
|
||||||
|
PLUGINS: [ 'name', 'createdAt', 'updatedAt' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
const OAUTH_LIFETIME = {
|
const OAUTH_LIFETIME = {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { PluginModel } from '../../models/server/plugin'
|
import { PluginModel } from '../../models/server/plugin'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { RegisterHookOptions } from '../../../shared/models/plugins/register.model'
|
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
|
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
|
||||||
|
@ -11,7 +10,9 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
|
||||||
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
||||||
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
|
||||||
import { outputFile } from 'fs-extra'
|
import { outputFile } from 'fs-extra'
|
||||||
import { ServerConfigPlugin } from '../../../shared/models/server'
|
import { RegisterSettingOptions } from '../../../shared/models/plugins/register-setting.model'
|
||||||
|
import { RegisterHookOptions } from '../../../shared/models/plugins/register-hook.model'
|
||||||
|
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
|
||||||
|
|
||||||
export interface RegisteredPlugin {
|
export interface RegisteredPlugin {
|
||||||
name: string
|
name: string
|
||||||
|
@ -43,26 +44,13 @@ export class PluginManager {
|
||||||
private static instance: PluginManager
|
private static instance: PluginManager
|
||||||
|
|
||||||
private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
|
private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
|
||||||
|
private settings: { [ name: string ]: RegisterSettingOptions[] } = {}
|
||||||
private hooks: { [ name: string ]: HookInformationValue[] } = {}
|
private hooks: { [ name: string ]: HookInformationValue[] } = {}
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerPluginsAndThemes () {
|
// ###################### Getters ######################
|
||||||
await this.resetCSSGlobalFile()
|
|
||||||
|
|
||||||
const plugins = await PluginModel.listEnabledPluginsAndThemes()
|
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
try {
|
|
||||||
await this.registerPluginOrTheme(plugin)
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Cannot register plugin %s, skipping.', plugin.name, { err })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortHooksByPriority()
|
|
||||||
}
|
|
||||||
|
|
||||||
getRegisteredPluginOrTheme (name: string) {
|
getRegisteredPluginOrTheme (name: string) {
|
||||||
return this.registeredPlugins[name]
|
return this.registeredPlugins[name]
|
||||||
|
@ -92,6 +80,12 @@ export class PluginManager {
|
||||||
return this.getRegisteredPluginsOrThemes(PluginType.THEME)
|
return this.getRegisteredPluginsOrThemes(PluginType.THEME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSettings (name: string) {
|
||||||
|
return this.settings[name] || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// ###################### Hooks ######################
|
||||||
|
|
||||||
async runHook (hookName: string, param?: any) {
|
async runHook (hookName: string, param?: any) {
|
||||||
let result = param
|
let result = param
|
||||||
|
|
||||||
|
@ -99,8 +93,11 @@ export class PluginManager {
|
||||||
|
|
||||||
for (const hook of this.hooks[hookName]) {
|
for (const hook of this.hooks[hookName]) {
|
||||||
try {
|
try {
|
||||||
if (wait) result = await hook.handler(param)
|
if (wait) {
|
||||||
else result = hook.handler()
|
result = await hook.handler(param)
|
||||||
|
} else {
|
||||||
|
result = hook.handler()
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
|
logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
|
||||||
}
|
}
|
||||||
|
@ -109,6 +106,24 @@ export class PluginManager {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ###################### Registration ######################
|
||||||
|
|
||||||
|
async registerPluginsAndThemes () {
|
||||||
|
await this.resetCSSGlobalFile()
|
||||||
|
|
||||||
|
const plugins = await PluginModel.listEnabledPluginsAndThemes()
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
try {
|
||||||
|
await this.registerPluginOrTheme(plugin)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Cannot register plugin %s, skipping.', plugin.name, { err })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sortHooksByPriority()
|
||||||
|
}
|
||||||
|
|
||||||
async unregister (name: string) {
|
async unregister (name: string) {
|
||||||
const plugin = this.getRegisteredPlugin(name)
|
const plugin = this.getRegisteredPlugin(name)
|
||||||
|
|
||||||
|
@ -133,7 +148,9 @@ export class PluginManager {
|
||||||
await this.regeneratePluginGlobalCSS()
|
await this.regeneratePluginGlobalCSS()
|
||||||
}
|
}
|
||||||
|
|
||||||
async install (toInstall: string, version: string, fromDisk = false) {
|
// ###################### Installation ######################
|
||||||
|
|
||||||
|
async install (toInstall: string, version?: string, fromDisk = false) {
|
||||||
let plugin: PluginModel
|
let plugin: PluginModel
|
||||||
let name: string
|
let name: string
|
||||||
|
|
||||||
|
@ -206,6 +223,8 @@ export class PluginManager {
|
||||||
logger.info('Plugin %s uninstalled.', packageName)
|
logger.info('Plugin %s uninstalled.', packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ###################### Private register ######################
|
||||||
|
|
||||||
private async registerPluginOrTheme (plugin: PluginModel) {
|
private async registerPluginOrTheme (plugin: PluginModel) {
|
||||||
logger.info('Registering plugin or theme %s.', plugin.name)
|
logger.info('Registering plugin or theme %s.', plugin.name)
|
||||||
|
|
||||||
|
@ -251,13 +270,25 @@ export class PluginManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerSetting = (options: RegisterSettingOptions) => {
|
||||||
|
if (!this.settings[plugin.name]) this.settings[plugin.name] = []
|
||||||
|
|
||||||
|
this.settings[plugin.name].push(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsManager: PluginSettingsManager = {
|
||||||
|
getSetting: (name: string) => PluginModel.getSetting(plugin.name, name),
|
||||||
|
|
||||||
|
setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, name, value)
|
||||||
|
}
|
||||||
|
|
||||||
const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
|
const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
|
||||||
|
|
||||||
if (!isLibraryCodeValid(library)) {
|
if (!isLibraryCodeValid(library)) {
|
||||||
throw new Error('Library code is not valid (miss register or unregister function)')
|
throw new Error('Library code is not valid (miss register or unregister function)')
|
||||||
}
|
}
|
||||||
|
|
||||||
library.register({ registerHook })
|
library.register({ registerHook, registerSetting, settingsManager })
|
||||||
|
|
||||||
logger.info('Add plugin %s CSS to global file.', plugin.name)
|
logger.info('Add plugin %s CSS to global file.', plugin.name)
|
||||||
|
|
||||||
|
@ -266,13 +297,7 @@ export class PluginManager {
|
||||||
return library
|
return library
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortHooksByPriority () {
|
// ###################### CSS ######################
|
||||||
for (const hookName of Object.keys(this.hooks)) {
|
|
||||||
this.hooks[hookName].sort((a, b) => {
|
|
||||||
return b.priority - a.priority
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetCSSGlobalFile () {
|
private resetCSSGlobalFile () {
|
||||||
return outputFile(PLUGIN_GLOBAL_CSS_PATH, '')
|
return outputFile(PLUGIN_GLOBAL_CSS_PATH, '')
|
||||||
|
@ -296,6 +321,26 @@ export class PluginManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async regeneratePluginGlobalCSS () {
|
||||||
|
await this.resetCSSGlobalFile()
|
||||||
|
|
||||||
|
for (const key of Object.keys(this.registeredPlugins)) {
|
||||||
|
const plugin = this.registeredPlugins[key]
|
||||||
|
|
||||||
|
await this.addCSSToGlobalFile(plugin.path, plugin.css)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ###################### Utils ######################
|
||||||
|
|
||||||
|
private sortHooksByPriority () {
|
||||||
|
for (const hookName of Object.keys(this.hooks)) {
|
||||||
|
this.hooks[hookName].sort((a, b) => {
|
||||||
|
return b.priority - a.priority
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getPackageJSON (pluginName: string, pluginType: PluginType) {
|
private getPackageJSON (pluginName: string, pluginType: PluginType) {
|
||||||
const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json')
|
const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json')
|
||||||
|
|
||||||
|
@ -312,15 +357,7 @@ export class PluginManager {
|
||||||
return name.replace(/^peertube-((theme)|(plugin))-/, '')
|
return name.replace(/^peertube-((theme)|(plugin))-/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
private async regeneratePluginGlobalCSS () {
|
// ###################### Private getters ######################
|
||||||
await this.resetCSSGlobalFile()
|
|
||||||
|
|
||||||
for (const key of Object.keys(this.registeredPlugins)) {
|
|
||||||
const plugin = this.registeredPlugins[key]
|
|
||||||
|
|
||||||
await this.addCSSToGlobalFile(plugin.path, plugin.css)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRegisteredPluginsOrThemes (type: PluginType) {
|
private getRegisteredPluginsOrThemes (type: PluginType) {
|
||||||
const plugins: RegisteredPlugin[] = []
|
const plugins: RegisteredPlugin[] = []
|
||||||
|
|
|
@ -5,12 +5,14 @@ import { CONFIG } from '../../initializers/config'
|
||||||
import { outputJSON, pathExists } from 'fs-extra'
|
import { outputJSON, pathExists } from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
async function installNpmPlugin (name: string, version: string) {
|
async function installNpmPlugin (name: string, version?: string) {
|
||||||
// Security check
|
// Security check
|
||||||
checkNpmPluginNameOrThrow(name)
|
checkNpmPluginNameOrThrow(name)
|
||||||
checkPluginVersionOrThrow(version)
|
if (version) checkPluginVersionOrThrow(version)
|
||||||
|
|
||||||
|
let toInstall = name
|
||||||
|
if (version) toInstall += `@${version}`
|
||||||
|
|
||||||
const toInstall = `${name}@${version}`
|
|
||||||
await execYarn('add ' + toInstall)
|
await execYarn('add ' + toInstall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { param } from 'express-validator/check'
|
import { param, query, body } from 'express-validator/check'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
|
import { isPluginNameValid, isPluginTypeValid, isPluginVersionValid, isNpmPluginNameValid } from '../../helpers/custom-validators/plugins'
|
||||||
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
||||||
import { isSafePath } from '../../helpers/custom-validators/misc'
|
import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc'
|
||||||
|
import { PluginModel } from '../../models/server/plugin'
|
||||||
|
|
||||||
const servePluginStaticDirectoryValidator = [
|
const servePluginStaticDirectoryValidator = [
|
||||||
param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
|
param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
|
||||||
|
@ -28,8 +29,88 @@ const servePluginStaticDirectoryValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const listPluginsValidator = [
|
||||||
|
query('type')
|
||||||
|
.optional()
|
||||||
|
.custom(isPluginTypeValid).withMessage('Should have a valid plugin type'),
|
||||||
|
query('uninstalled')
|
||||||
|
.optional()
|
||||||
|
.toBoolean()
|
||||||
|
.custom(isBooleanValid).withMessage('Should have a valid uninstalled attribute'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking listPluginsValidator parameters', { parameters: req.query })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const installPluginValidator = [
|
||||||
|
body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking installPluginValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const uninstallPluginValidator = [
|
||||||
|
body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking managePluginValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const enabledPluginValidator = [
|
||||||
|
body('name').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking enabledPluginValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const plugin = await PluginModel.load(req.body.name)
|
||||||
|
if (!plugin) {
|
||||||
|
return res.status(404)
|
||||||
|
.json({ error: 'Plugin not found' })
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.plugin = plugin
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const updatePluginSettingsValidator = [
|
||||||
|
body('settings').exists().withMessage('Should have settings'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking enabledPluginValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
servePluginStaticDirectoryValidator
|
servePluginStaticDirectoryValidator,
|
||||||
|
updatePluginSettingsValidator,
|
||||||
|
uninstallPluginValidator,
|
||||||
|
enabledPluginValidator,
|
||||||
|
installPluginValidator,
|
||||||
|
listPluginsValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUM
|
||||||
const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
|
const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
|
||||||
const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
|
const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
|
||||||
const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
|
const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
|
||||||
|
const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS)
|
||||||
|
|
||||||
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
||||||
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
||||||
|
@ -41,6 +42,7 @@ const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COL
|
||||||
const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
|
const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
|
||||||
const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS)
|
const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS)
|
||||||
const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS)
|
const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS)
|
||||||
|
const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -63,5 +65,6 @@ export {
|
||||||
accountsBlocklistSortValidator,
|
accountsBlocklistSortValidator,
|
||||||
serversBlocklistSortValidator,
|
serversBlocklistSortValidator,
|
||||||
userNotificationsSortValidator,
|
userNotificationsSortValidator,
|
||||||
videoPlaylistsSortValidator
|
videoPlaylistsSortValidator,
|
||||||
|
pluginsSortValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import { AllowNull, Column, CreatedAt, DataType, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import {
|
import {
|
||||||
isPluginDescriptionValid,
|
isPluginDescriptionValid,
|
||||||
isPluginNameValid,
|
isPluginNameValid,
|
||||||
isPluginTypeValid,
|
isPluginTypeValid,
|
||||||
isPluginVersionValid
|
isPluginVersionValid
|
||||||
} from '../../helpers/custom-validators/plugins'
|
} from '../../helpers/custom-validators/plugins'
|
||||||
|
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
||||||
|
import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
|
||||||
|
import { FindAndCountOptions } from 'sequelize'
|
||||||
|
|
||||||
|
@DefaultScope(() => ({
|
||||||
|
attributes: {
|
||||||
|
exclude: [ 'storage' ]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'plugin',
|
tableName: 'plugin',
|
||||||
|
@ -85,14 +94,75 @@ export class PluginModel extends Model<PluginModel> {
|
||||||
return PluginModel.findOne(query)
|
return PluginModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static uninstall (pluginName: string) {
|
static getSetting (pluginName: string, settingName: string) {
|
||||||
|
const query = {
|
||||||
|
attributes: [ 'settings' ],
|
||||||
|
where: {
|
||||||
|
name: pluginName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PluginModel.findOne(query)
|
||||||
|
.then(p => p.settings)
|
||||||
|
.then(settings => {
|
||||||
|
if (!settings) return undefined
|
||||||
|
|
||||||
|
return settings[settingName]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static setSetting (pluginName: string, settingName: string, settingValue: string) {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
name: pluginName
|
name: pluginName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PluginModel.update({ enabled: false, uninstalled: true }, query)
|
const toSave = {
|
||||||
|
[`settings.${settingName}`]: settingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return PluginModel.update(toSave, query)
|
||||||
|
.then(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
static listForApi (options: {
|
||||||
|
type?: PluginType,
|
||||||
|
uninstalled?: boolean,
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
sort: string
|
||||||
|
}) {
|
||||||
|
const query: FindAndCountOptions = {
|
||||||
|
offset: options.start,
|
||||||
|
limit: options.count,
|
||||||
|
order: getSort(options.sort),
|
||||||
|
where: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.type) query.where['type'] = options.type
|
||||||
|
if (options.uninstalled) query.where['uninstalled'] = options.uninstalled
|
||||||
|
|
||||||
|
return PluginModel
|
||||||
|
.findAndCountAll(query)
|
||||||
|
.then(({ rows, count }) => {
|
||||||
|
return { total: count, data: rows }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON (): PeerTubePlugin {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
type: this.type,
|
||||||
|
version: this.version,
|
||||||
|
enabled: this.enabled,
|
||||||
|
uninstalled: this.uninstalled,
|
||||||
|
peertubeEngine: this.peertubeEngine,
|
||||||
|
description: this.description,
|
||||||
|
settings: this.settings,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
updatedAt: this.updatedAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,10 @@ import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
||||||
import { VideoCaptionModel } from '../models/video/video-caption'
|
import { VideoCaptionModel } from '../models/video/video-caption'
|
||||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||||
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
||||||
|
import { PluginModel } from '../models/server/plugin'
|
||||||
|
|
||||||
declare module 'express' {
|
declare module 'express' {
|
||||||
|
|
||||||
|
|
||||||
interface Response {
|
interface Response {
|
||||||
locals: {
|
locals: {
|
||||||
video?: VideoModel
|
video?: VideoModel
|
||||||
|
@ -81,6 +81,8 @@ declare module 'express' {
|
||||||
authenticated?: boolean
|
authenticated?: boolean
|
||||||
|
|
||||||
registeredPlugin?: RegisteredPlugin
|
registeredPlugin?: RegisteredPlugin
|
||||||
|
|
||||||
|
plugin?: PluginModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface InstallPlugin {
|
||||||
|
npmName: string
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ManagePlugin {
|
||||||
|
npmName: string
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
export interface PeerTubePlugin {
|
||||||
|
name: string
|
||||||
|
type: number
|
||||||
|
version: string
|
||||||
|
enabled: boolean
|
||||||
|
uninstalled: boolean
|
||||||
|
peertubeEngine: string
|
||||||
|
description: string
|
||||||
|
settings: any
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { RegisterOptions } from './register-options.type'
|
import { RegisterOptions } from './register-options.model'
|
||||||
|
|
||||||
export interface PluginLibrary {
|
export interface PluginLibrary {
|
||||||
register: (options: RegisterOptions) => void
|
register: (options: RegisterOptions) => void
|
||||||
|
|
||||||
unregister: () => Promise<any>
|
unregister: () => Promise<any>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as Bluebird from 'bluebird'
|
||||||
|
|
||||||
|
export interface PluginSettingsManager {
|
||||||
|
getSetting: (name: string) => Bluebird<string>
|
||||||
|
|
||||||
|
setSetting: (name: string, value: string) => Bluebird<any>
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { RegisterHookOptions } from './register-hook.model'
|
||||||
|
import { RegisterSettingOptions } from './register-setting.model'
|
||||||
|
import { PluginSettingsManager } from './plugin-settings-manager.model'
|
||||||
|
|
||||||
|
export type RegisterOptions = {
|
||||||
|
registerHook: (options: RegisterHookOptions) => void
|
||||||
|
|
||||||
|
registerSetting: (options: RegisterSettingOptions) => void
|
||||||
|
|
||||||
|
settingsManager: PluginSettingsManager
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
import { RegisterHookOptions } from './register.model'
|
|
||||||
|
|
||||||
export type RegisterOptions = {
|
|
||||||
registerHook: (options: RegisterHookOptions) => void
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface RegisterSettingOptions {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: 'input'
|
||||||
|
default?: string
|
||||||
|
}
|
|
@ -31,5 +31,7 @@ export enum UserRight {
|
||||||
UPDATE_ANY_VIDEO_PLAYLIST,
|
UPDATE_ANY_VIDEO_PLAYLIST,
|
||||||
|
|
||||||
SEE_ALL_VIDEOS,
|
SEE_ALL_VIDEOS,
|
||||||
CHANGE_VIDEO_OWNERSHIP
|
CHANGE_VIDEO_OWNERSHIP,
|
||||||
|
|
||||||
|
MANAGE_PLUGINS
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue