mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-19 04:08:08 -05:00
241 lines
7.8 KiB
Go
241 lines
7.8 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/deluan/rest"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/server"
|
|
)
|
|
|
|
func (api *Router) addPluginRoute(r chi.Router) {
|
|
constructor := func(ctx context.Context) rest.Repository {
|
|
return api.ds.Plugin(ctx)
|
|
}
|
|
|
|
r.Route("/plugin", func(r chi.Router) {
|
|
r.Use(pluginsEnabledMiddleware)
|
|
r.Get("/", rest.GetAll(constructor))
|
|
r.Route("/{id}", func(r chi.Router) {
|
|
r.Use(server.URLParamsMiddleware)
|
|
r.Get("/", rest.Get(constructor))
|
|
r.Put("/", api.updatePlugin)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Middleware to check if plugins feature is enabled
|
|
func pluginsEnabledMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !conf.Server.Plugins.Enabled {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// PluginUpdateRequest represents the fields that can be updated via the API
|
|
type PluginUpdateRequest struct {
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
Config *string `json:"config,omitempty"`
|
|
Users *string `json:"users,omitempty"`
|
|
AllUsers *bool `json:"allUsers,omitempty"`
|
|
Libraries *string `json:"libraries,omitempty"`
|
|
AllLibraries *bool `json:"allLibraries,omitempty"`
|
|
}
|
|
|
|
func (api *Router) updatePlugin(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
ctx := r.Context()
|
|
repo := api.ds.Plugin(ctx)
|
|
|
|
// Get existing plugin to verify it exists
|
|
if _, err := repo.Get(id); err != nil {
|
|
if errors.Is(err, rest.ErrPermissionDenied) {
|
|
http.Error(w, "Access denied: admin privileges required", http.StatusForbidden)
|
|
return
|
|
}
|
|
if errors.Is(err, model.ErrNotFound) {
|
|
http.Error(w, "Plugin not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
log.Error(ctx, "Error getting plugin", "id", id, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Parse update request
|
|
var req PluginUpdateRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
log.Error(ctx, "Error decoding request", err)
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Handle config update (if provided)
|
|
if req.Config != nil {
|
|
if err := validateAndUpdateConfig(ctx, api.pluginManager, id, *req.Config, w); err != nil {
|
|
log.Error(ctx, "Error updating plugin config", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle users permission update (if provided)
|
|
if req.Users != nil || req.AllUsers != nil {
|
|
if err := validateAndUpdateUsers(ctx, api.pluginManager, repo, id, req, w); err != nil {
|
|
log.Error(ctx, "Error updating plugin users", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle libraries permission update (if provided)
|
|
if req.Libraries != nil || req.AllLibraries != nil {
|
|
if err := validateAndUpdateLibraries(ctx, api.pluginManager, repo, id, req, w); err != nil {
|
|
log.Error(ctx, "Error updating plugin libraries", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle enable/disable
|
|
if req.Enabled != nil {
|
|
if *req.Enabled {
|
|
if enableErr := api.pluginManager.EnablePlugin(ctx, id); enableErr != nil {
|
|
log.Error(ctx, "Error enabling plugin", "id", id, enableErr)
|
|
// Refresh plugin from DB to get the error
|
|
plugin, err := repo.Get(id)
|
|
if err != nil {
|
|
log.Error(ctx, "Error getting updated plugin after enable failure", "id", id, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
// Return error response with message field for React-Admin compatibility
|
|
// and include the plugin data so UI can update its state
|
|
errorResponse := struct {
|
|
Message string `json:"message"`
|
|
Plugin *model.Plugin `json:"plugin"`
|
|
}{
|
|
Message: enableErr.Error(),
|
|
Plugin: plugin,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
_ = json.NewEncoder(w).Encode(errorResponse)
|
|
return
|
|
}
|
|
} else {
|
|
if err := api.pluginManager.DisablePlugin(ctx, id); err != nil {
|
|
log.Error(ctx, "Error disabling plugin", "id", id, err)
|
|
http.Error(w, "Error disabling plugin: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refresh and return updated plugin
|
|
plugin, err := repo.Get(id)
|
|
if err != nil {
|
|
log.Error(ctx, "Error getting updated plugin", "id", id, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(plugin); err != nil {
|
|
log.Error(ctx, "Error encoding plugin response", err)
|
|
}
|
|
}
|
|
|
|
// isValidJSON checks if a string is valid JSON
|
|
func isValidJSON(s string) bool {
|
|
var js json.RawMessage
|
|
return json.Unmarshal([]byte(s), &js) == nil
|
|
}
|
|
|
|
// validateAndUpdateConfig validates the config JSON and updates the plugin.
|
|
// Returns an error if validation or update fails (error response already written).
|
|
func validateAndUpdateConfig(ctx context.Context, pm PluginManager, id, configJSON string, w http.ResponseWriter) error {
|
|
if configJSON != "" && !isValidJSON(configJSON) {
|
|
http.Error(w, "Invalid JSON in config field", http.StatusBadRequest)
|
|
return errors.New("invalid JSON")
|
|
}
|
|
if err := pm.UpdatePluginConfig(ctx, id, configJSON); err != nil {
|
|
log.Error(ctx, "Error updating plugin config", "id", id, err)
|
|
http.Error(w, "Error updating plugin configuration: "+err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateAndUpdateUsers validates the users JSON and updates the plugin.
|
|
// Returns an error if validation or update fails (error response already written).
|
|
func validateAndUpdateUsers(ctx context.Context, pm PluginManager, repo model.PluginRepository, id string, req PluginUpdateRequest, w http.ResponseWriter) error {
|
|
// Get current values if not provided in request
|
|
plugin, err := repo.Get(id)
|
|
if err != nil {
|
|
log.Error(ctx, "Error getting plugin for users update", "id", id, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
|
|
usersJSON := plugin.Users
|
|
allUsers := plugin.AllUsers
|
|
|
|
if req.Users != nil {
|
|
if *req.Users != "" && !isValidJSON(*req.Users) {
|
|
http.Error(w, "Invalid JSON in users field", http.StatusBadRequest)
|
|
return errors.New("invalid JSON")
|
|
}
|
|
usersJSON = *req.Users
|
|
}
|
|
if req.AllUsers != nil {
|
|
allUsers = *req.AllUsers
|
|
}
|
|
|
|
if err := pm.UpdatePluginUsers(ctx, id, usersJSON, allUsers); err != nil {
|
|
log.Error(ctx, "Error updating plugin users", "id", id, err)
|
|
http.Error(w, "Error updating plugin users: "+err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateAndUpdateLibraries validates the libraries JSON and updates the plugin.
|
|
// Returns an error if validation or update fails (error response already written).
|
|
func validateAndUpdateLibraries(ctx context.Context, pm PluginManager, repo model.PluginRepository, id string, req PluginUpdateRequest, w http.ResponseWriter) error {
|
|
// Get current values if not provided in request
|
|
plugin, err := repo.Get(id)
|
|
if err != nil {
|
|
log.Error(ctx, "Error getting plugin for libraries update", "id", id, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
|
|
librariesJSON := plugin.Libraries
|
|
allLibraries := plugin.AllLibraries
|
|
|
|
if req.Libraries != nil {
|
|
if *req.Libraries != "" && !isValidJSON(*req.Libraries) {
|
|
http.Error(w, "Invalid JSON in libraries field", http.StatusBadRequest)
|
|
return errors.New("invalid JSON")
|
|
}
|
|
librariesJSON = *req.Libraries
|
|
}
|
|
if req.AllLibraries != nil {
|
|
allLibraries = *req.AllLibraries
|
|
}
|
|
|
|
if err := pm.UpdatePluginLibraries(ctx, id, librariesJSON, allLibraries); err != nil {
|
|
log.Error(ctx, "Error updating plugin libraries", "id", id, err)
|
|
http.Error(w, "Error updating plugin libraries: "+err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|