package httpserver import ( "context" "encoding/json" "net/http" "os" "strings" "github.com/go-chi/chi/v5" "github.com/tim/cairnquire/apps/server/internal/sync" ) func (s *Server) handleSyncInit(w http.ResponseWriter, r *http.Request) { var req sync.InitRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.DeviceID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "deviceId is required"}) return } userID := r.Context().Value("userID").(string) snapshot, err := s.syncService.InitSync(r.Context(), req.DeviceID, userID) if err != nil { s.logger.Error("sync init failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to initialize sync"}) return } writeJSON(w, http.StatusOK, sync.InitResponse{ SnapshotID: snapshot.ID, ServerSnapshot: snapshot.Files, }) } func (s *Server) handleSyncDelta(w http.ResponseWriter, r *http.Request) { var req sync.DeltaRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.SnapshotID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "snapshotId is required"}) return } result, err := s.syncService.ApplyDelta(r.Context(), req.SnapshotID, sync.Delta{Changes: req.ClientDelta}) if err != nil { s.logger.Error("sync delta failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to apply delta"}) return } writeJSON(w, http.StatusOK, sync.DeltaResponse{ ServerDelta: result.ServerDelta, Conflicts: result.Conflicts, }) } func (s *Server) handleSyncResolve(w http.ResponseWriter, r *http.Request) { var req sync.ResolveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) return } if req.SnapshotID == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "snapshotId is required"}) return } snapshot, err := s.syncService.ResolveConflicts(r.Context(), req.SnapshotID, req.Resolutions) if err != nil { s.logger.Error("sync resolve failed", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to resolve conflicts"}) return } writeJSON(w, http.StatusOK, sync.ResolveResponse{ NewSnapshotID: snapshot.ID, }) } func (s *Server) handleContentFetch(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if hash == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hash is required"}) return } content, err := s.syncService.GetContent(hash) if err != nil { if os.IsNotExist(err) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "content not found"}) return } s.logger.Error("content fetch failed", "error", err, "hash", hash) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to fetch content"}) return } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("ETag", "\""+hash+"\"") w.WriteHeader(http.StatusOK) w.Write(content) } func (s *Server) apiKeyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip for non-sync routes if !strings.HasPrefix(r.URL.Path, "/api/sync/") && !strings.HasPrefix(r.URL.Path, "/api/content/") { next.ServeHTTP(w, r) return } apiKey := r.Header.Get("X-API-Key") if apiKey == "" { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing API key"}) return } // Validate API key against database record, err := s.syncRepo.ValidateAPIKey(r.Context(), apiKey) if err != nil { s.logger.Warn("invalid api key", "error", err) writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid API key"}) return } ctx := context.WithValue(r.Context(), "userID", record.UserID) next.ServeHTTP(w, r.WithContext(ctx)) }) }