package api import ( "bytes" "context" "encoding/json" "io" "net/http" ) // User preferences are stored as a JSON field named "preferences" on the // PocketBase `users` auth record. Because every request carries the caller's own // auth token, PocketBase enforces that a user can only read and write their own // record — the API Server never needs admin credentials for this. // pbAuthResp is the subset of PocketBase's auth-refresh response we care about. type pbAuthResp struct { Token string `json:"token"` Record map[string]json.RawMessage `json:"record"` } // pbAuthRefresh resolves the caller's user record (id + fields incl. preferences) // from their token. Returns the parsed record, the upstream status, and any // transport error. func (s *Server) pbAuthRefresh(ctx context.Context, token string) (*pbAuthResp, int, error) { req, _ := http.NewRequestWithContext(ctx, http.MethodPost, s.auth.url()+"/api/collections/users/auth-refresh", nil) req.Header.Set("Authorization", token) resp, err := s.auth.client.Do(req) if err != nil { return nil, 0, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return nil, resp.StatusCode, nil } var out pbAuthResp if err := json.Unmarshal(data, &out); err != nil { return nil, resp.StatusCode, err } return &out, resp.StatusCode, nil } // GET /api/preferences (Authorization: ) // Returns {"preferences": } for the authenticated user. func (s *Server) handleGetPreferences(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token == "" { writeError(w, http.StatusUnauthorized, "missing token") return } rec, status, err := s.pbAuthRefresh(r.Context(), token) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach PocketBase", "detail": err.Error()}) return } if status != http.StatusOK || rec == nil { writeError(w, http.StatusUnauthorized, "invalid or expired token") return } prefs := rec.Record["preferences"] if len(prefs) == 0 { prefs = json.RawMessage("null") } writeJSON(w, http.StatusOK, map[string]json.RawMessage{"preferences": prefs}) } // PUT /api/preferences (Authorization: ) // Body: {"preferences": {...}} — persists the blob onto the user's record. func (s *Server) handlePutPreferences(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token == "" { writeError(w, http.StatusUnauthorized, "missing token") return } var body struct { Preferences json.RawMessage `json:"preferences"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if len(body.Preferences) == 0 { body.Preferences = json.RawMessage("{}") } // Resolve the caller's record id (PocketBase authorises the PATCH against it). rec, status, err := s.pbAuthRefresh(r.Context(), token) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach PocketBase", "detail": err.Error()}) return } if status != http.StatusOK || rec == nil { writeError(w, http.StatusUnauthorized, "invalid or expired token") return } var id string _ = json.Unmarshal(rec.Record["id"], &id) if id == "" { writeError(w, http.StatusBadGateway, "could not resolve user id") return } patch, _ := json.Marshal(map[string]json.RawMessage{"preferences": body.Preferences}) req, _ := http.NewRequestWithContext(r.Context(), http.MethodPatch, s.auth.url()+"/api/collections/users/records/"+id, bytes.NewReader(patch)) req.Header.Set("Authorization", token) req.Header.Set("Content-Type", "application/json") resp, err := s.auth.client.Do(req) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach PocketBase", "detail": err.Error()}) return } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { // Relay PocketBase's error (e.g. missing "preferences" field on schema). w.Header().Set("Content-Type", "application/json") w.WriteHeader(resp.StatusCode) _, _ = w.Write(data) return } // Return just the saved preferences blob. var saved struct { Preferences json.RawMessage `json:"preferences"` } _ = json.Unmarshal(data, &saved) if len(saved.Preferences) == 0 { saved.Preferences = json.RawMessage("null") } writeJSON(w, http.StatusOK, map[string]json.RawMessage{"preferences": saved.Preferences}) }