Files
NanamiAdmin c7642ecca5
Build / ubuntu-latest (push) Failing after 52s
Build / windows-latest (push) Failing after 41m49s
feat(settings): report which saved keys need a restart
Every settings update answered with a bare success, so the console could not
tell a change that took effect at once from one written to the file that the
running program would keep ignoring until it was restarted — the listener
address, the storage paths, the Komari dashboard URL.

Classify the patch instead. config.RestartRequiredKeys expands a patch into the
dot-separated paths of its leaves and intersects them with the settings main
reads before it starts serving, and the settings endpoint returns that list as
data.restartRequired. It is a pure function of the patch, so UpdateSettings
keeps its signature and nothing else has to change; the write succeeds either
way, and the field only says which edits are not live.

Matching is by overlap rather than equality, so a patch that names a section —
replacing it, or deleting it with a null — reports the startup-only keys inside
it, while a sibling subtree like webhook.endpoints is not mistaken for
webhook.enabled.

The result is never nil, so an update with nothing to report serializes as []
rather than null and a client can iterate it without a guard.

The console reads the field in ConfigSection and warns instead of confirming,
naming the keys that are pending; Settings.vue's hints for the Komari URL, the
listen address and the storage paths now say which of their fields is affected
rather than labelling the whole card.
2026-09-28 23:28:47 +08:00

94 lines
3.1 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"nukumizu-backend/config"
"nukumizu-backend/postLog"
"nukumizu-backend/utils"
)
// SettingsGetHandler handles GET /api/settings/get?type=xxx.
// type selects which config file to return and may be one of
// "global", "bot_user_config" or "bot_node_config"; the returned data.config
// object has the same layout as the source JSON file.
func SettingsGetHandler(w http.ResponseWriter, r *http.Request) {
if !utils.Auth(w, r, "GET", "admin") {
return
}
settingsType := r.URL.Query().Get("type")
if settingsType == "" {
utils.SendErrorResponse(w, http.StatusBadRequest, "missing type parameter")
return
}
if !config.IsValidSettingsType(settingsType) {
utils.SendErrorResponse(w, http.StatusBadRequest, "invalid type parameter: "+settingsType)
return
}
data, err := config.GetSettings(settingsType)
if err != nil {
utils.SendErrorResponse(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
return
}
utils.SendSuccessResponse(w, "", map[string]interface{}{
"config": json.RawMessage(data),
})
}
// SettingsSetHandler handles POST /api/settings/set?type=xxx.
// type selects which config file to update and may be one of "global",
// "bot_user_config" or "bot_node_config". The JSON body is a partial config
// object whose keys map directly to config entries, e.g.
//
// {"system": {"debugMode": true}}
//
// Multiple entries may be given at once; only the provided keys are changed.
//
// The response carries data.restartRequired: the keys the update changed that
// are only read at startup, so the caller can say which edits are not live yet.
// It is empty for an update that took effect in full.
func SettingsSetHandler(w http.ResponseWriter, r *http.Request) {
if !utils.Auth(w, r, "POST", "admin") {
return
}
settingsType := r.URL.Query().Get("type")
if settingsType == "" {
utils.SendErrorResponse(w, http.StatusBadRequest, "missing type parameter")
return
}
if !config.IsValidSettingsType(settingsType) {
utils.SendErrorResponse(w, http.StatusBadRequest, "invalid type parameter: "+settingsType)
return
}
dec := json.NewDecoder(r.Body)
dec.UseNumber() // Keep numeric values verbatim (e.g. QQ IDs) instead of float64.
var patch map[string]interface{}
if err := dec.Decode(&patch); err != nil {
utils.SendErrorResponse(w, http.StatusBadRequest, "invalid request body: expected a JSON object")
return
}
if patch == nil {
utils.SendErrorResponse(w, http.StatusBadRequest, "request body must be a JSON object")
return
}
if err := config.UpdateSettings(settingsType, patch); err != nil {
postLog.Error("Failed to update " + settingsType + " settings: " + err.Error())
utils.SendErrorResponse(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
return
}
// The write landed either way. This only tells the caller which of the keys
// it changed will not be live until the program is restarted.
utils.SendSuccessResponse(w, "settings updated successfully", map[string]interface{}{
"type": settingsType,
"restartRequired": config.RestartRequiredKeys(settingsType, patch),
})
}