Files

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})
}
}