108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"rawmark/internal/i18n"
|
|
|
|
"rawmark/internal/auth"
|
|
"rawmark/internal/events"
|
|
)
|
|
|
|
// Application settings are one JSON object shared across devices. The frontend defines contents; the server merges, saves, and validates server-enforced retention.
|
|
const settingsKey = "app.settings"
|
|
|
|
func (s *Server) readSettings() (map[string]json.RawMessage, error) {
|
|
v, ok, err := s.store.Setting(settingsKey)
|
|
if err != nil || !ok {
|
|
return map[string]json.RawMessage{}, err
|
|
}
|
|
out := map[string]json.RawMessage{}
|
|
return out, json.Unmarshal([]byte(v), &out)
|
|
}
|
|
|
|
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
|
settings, err := s.readSettings()
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
writeJSON(w, 200, settings)
|
|
}
|
|
|
|
// patchSettings merges top-level keys: supplied values replace existing values, and null removes a key.
|
|
// Concurrent changes to different settings do not overwrite each other; notify other devices to reload after merging.
|
|
func (s *Server) patchSettings(w http.ResponseWriter, r *http.Request) {
|
|
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxJSONBytes))
|
|
if err != nil {
|
|
writeError(w, 413, message(w, "settings_are_too_large_8a580f"))
|
|
return
|
|
}
|
|
var patch map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &patch); err != nil || patch == nil {
|
|
writeError(w, 400, message(w, "settings_must_be_a_json_45b36b"))
|
|
return
|
|
}
|
|
s.settingsMu.Lock()
|
|
defer s.settingsMu.Unlock()
|
|
settings, err := s.readSettings()
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
for k, v := range patch {
|
|
if bytes.Equal(v, []byte("null")) {
|
|
delete(settings, k)
|
|
} else {
|
|
settings[k] = v
|
|
}
|
|
}
|
|
if err := validateLocale(settings); err != nil {
|
|
writeError(w, 400, i18n.RenderError(err, responseLocale(w)))
|
|
return
|
|
}
|
|
if _, err := trashRetentionDays(settings); err != nil {
|
|
writeError(w, 400, i18n.RenderError(err, responseLocale(w)))
|
|
return
|
|
}
|
|
merged, err := json.Marshal(settings)
|
|
if err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
if len(merged) > maxJSONBytes {
|
|
writeError(w, 413, message(w, "settings_are_too_large_8a580f"))
|
|
return
|
|
}
|
|
if err := s.store.SetSetting(settingsKey, string(merged)); err != nil {
|
|
internalError(w, r, err)
|
|
return
|
|
}
|
|
s.hub.Broadcast(events.Event{Type: "settings"})
|
|
writeJSON(w, 200, settings)
|
|
}
|
|
|
|
// changePassword changes the login password, retaining the current session and requiring other devices to sign in again.
|
|
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
|
|
var body struct{ Current, Next string }
|
|
if !readJSON(w, r, &body) {
|
|
return
|
|
}
|
|
switch err := s.auth.ChangePassword(clientIP(r), identity(r).Token, body.Current, body.Next); {
|
|
case errors.Is(err, auth.ErrRateLimited):
|
|
writeError(w, 429, i18n.RenderError(err, responseLocale(w)))
|
|
case errors.Is(err, auth.ErrBadCredentials):
|
|
writeFailure(w, 400, CodeCurrentPassword, message(w, "current_password_is_incorrect_e0f0aa"), nil)
|
|
case errors.Is(err, auth.ErrWeakPassword):
|
|
writeError(w, 400, i18n.RenderError(err, responseLocale(w)))
|
|
case err != nil:
|
|
internalError(w, r, err)
|
|
default:
|
|
writeJSON(w, 200, map[string]bool{"ok": true})
|
|
}
|
|
}
|