- GET /plugins (backend.view_plugins, superusers always pass) lists the plugins from party.Installed, or party.Activated IDs for older builds, with the built framework version and whether the registry is configured - Settings -> System -> Plugins entry with the new SettingsEntry.path, added only for principals allowed to open it - PluginsView at /system/plugins with an Installed tab (reka-ui Tabs); settings panel, rail, breadcrumbs and page title know the route - OpenAPI and SPA types regenerated; contract, token-class and conformance inventories list the route; en/pl strings; docs
157 lines
6.0 KiB
Go
157 lines
6.0 KiB
Go
package cabana
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/kiosk"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// The admin's read-only Plugins screen (Settings → System → Plugins): the
|
|
// plugins the application was built with, their versions and, when the
|
|
// plugin registry is configured, available updates and a marketplace
|
|
// search. The SPA calls only these endpoints; cabana calls the registry.
|
|
|
|
// PermissionViewPlugins opens the Plugins screen. No role is granted it in
|
|
// code: until an administrator grants it, only superusers see the screen.
|
|
const PermissionViewPlugins = "backend.view_plugins"
|
|
|
|
// pluginsSettingsCode is the settings code of the Plugins screen entry and
|
|
// pluginsScreenPath the SPA route it opens.
|
|
const (
|
|
pluginsSettingsCode = "plugins"
|
|
pluginsScreenPath = "/system/plugins"
|
|
)
|
|
|
|
// States of the plugin registry on the Plugins screen.
|
|
const (
|
|
// registryStatusOK: the registry answered (or is configured and was
|
|
// not needed for this answer).
|
|
registryStatusOK = "ok"
|
|
// registryStatusDisabled: plugins.registry_url is empty; no request
|
|
// leaves the process.
|
|
registryStatusDisabled = "disabled"
|
|
// registryStatusUnavailable: the registry did not answer in time or
|
|
// answered with an error.
|
|
registryStatusUnavailable = "unavailable"
|
|
)
|
|
|
|
// PluginsOverview is the GET /plugins payload: the framework version the
|
|
// application was built with, the state of the plugin registry and every
|
|
// activated plugin in summer.yaml order.
|
|
type PluginsOverview struct {
|
|
Framework string `json:"framework"`
|
|
Registry RegistryState `json:"registry"`
|
|
Plugins []InstalledPlugin `json:"plugins"`
|
|
}
|
|
|
|
// RegistryState tells the admin whether the plugin registry is configured
|
|
// (Enabled, false when plugins.registry_url is empty) and how it answered:
|
|
// Status is ok, disabled or unavailable.
|
|
type RegistryState struct {
|
|
Enabled bool `json:"enabled"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// InstalledPlugin is one activated plugin with the metadata summer build
|
|
// baked in (party.Info) and, for a plugin the registry lists, the newest
|
|
// release the built framework version allows. Name, Description, Version,
|
|
// Source, Module and Package are empty for an application built before
|
|
// plugin metadata existed and for a plugin without plugin.yaml (Name and
|
|
// Description). Latest is the newest compatible release; UpdateAvailable
|
|
// is true when it is newer than the installed version, and UpdateCommand
|
|
// is then the summer command that installs it. LatestFramework names the
|
|
// framework version a newer, incompatible release needs.
|
|
type InstalledPlugin struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Version string `json:"version"`
|
|
Source string `json:"source"`
|
|
Module string `json:"module"`
|
|
Package string `json:"package"`
|
|
Listed bool `json:"listed"`
|
|
Latest string `json:"latest"`
|
|
LatestFramework string `json:"latestFramework"`
|
|
UpdateAvailable bool `json:"updateAvailable"`
|
|
UpdateCommand string `json:"updateCommand"`
|
|
}
|
|
|
|
// pluginsSettingsEntry is the Settings → System link to the Plugins
|
|
// screen. It opens an SPA route, not an admin controller.
|
|
func pluginsSettingsEntry(label, description, category string) SettingsEntry {
|
|
return SettingsEntry{
|
|
Code: pluginsSettingsCode, Label: label, Description: description,
|
|
Category: category, Icon: "puzzle", Order: 120, Keywords: []string{},
|
|
Path: pluginsScreenPath,
|
|
}
|
|
}
|
|
|
|
// pluginsPrincipal answers 401 without a backend principal and 403 when
|
|
// the principal may not open the Plugins screen; ok is false after either.
|
|
func pluginsPrincipal(w http.ResponseWriter, r *http.Request) (*bouncer.Principal, bool) {
|
|
principal, ok := bouncer.User(r.Context())
|
|
if !ok || principal == nil || !principal.Backend {
|
|
WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated)
|
|
return nil, false
|
|
}
|
|
if !Allows(principal, []string{PermissionViewPlugins}) {
|
|
WriteError(w, http.StatusForbidden, "forbidden", msgForbidden)
|
|
return nil, false
|
|
}
|
|
return principal, true
|
|
}
|
|
|
|
// pluginsInstalled answers GET /plugins.
|
|
func (s *service) pluginsInstalled(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := pluginsPrincipal(w, r); !ok {
|
|
return
|
|
}
|
|
overview := s.installedPlugins()
|
|
overview.Registry = s.registryState()
|
|
WriteData(w, http.StatusOK, overview, nil)
|
|
}
|
|
|
|
// registryState reports whether plugins.registry_url is set.
|
|
func (s *service) registryState() RegistryState {
|
|
if kiosk.RegistryURL(s.config()) == "" {
|
|
return RegistryState{Enabled: false, Status: registryStatusDisabled}
|
|
}
|
|
return RegistryState{Enabled: true, Status: registryStatusOK}
|
|
}
|
|
|
|
// installedPlugins lists the activated plugins from the metadata summer
|
|
// build published (party.Installed). An application built before that
|
|
// metadata existed lists the IDs of party.Activated instead, and its
|
|
// framework version comes from the binary's build information.
|
|
func (s *service) installedPlugins() PluginsOverview {
|
|
overview := PluginsOverview{Plugins: []InstalledPlugin{}}
|
|
if s == nil || s.app == nil {
|
|
overview.Framework = bonfire.BuildVersion().Framework
|
|
return overview
|
|
}
|
|
if installed, ok := s.app.Lookup[party.Installed](); ok {
|
|
overview.Framework = installed.Framework
|
|
for _, info := range installed.Plugins {
|
|
overview.Plugins = append(overview.Plugins, InstalledPlugin{
|
|
ID: info.ID, Name: info.Name, Description: info.Description,
|
|
Version: info.Version, Source: info.Source,
|
|
Module: info.Module, Package: info.Package,
|
|
})
|
|
}
|
|
} else if activated, ok := s.app.Lookup[party.Activated](); ok {
|
|
for _, plugin := range activated {
|
|
if plugin == nil || plugin.ID() == backendScreensPluginID {
|
|
continue
|
|
}
|
|
overview.Plugins = append(overview.Plugins, InstalledPlugin{ID: plugin.ID()})
|
|
}
|
|
}
|
|
if overview.Framework == "" {
|
|
overview.Framework = bonfire.BuildVersion().Framework
|
|
}
|
|
return overview
|
|
}
|