package server import ( "net/http" "strings" "task/internal/sqlite" "task/internal/syncbackend" ) func (s *Server) handleGetSync(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok { writeError(w, code, "forbidden") return } b, err := s.Projects.GetSyncBackend(id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, b) } func (s *Server) handlePutSync(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, ok, code := s.requireProjectRole(id, sqlite.RoleAdmin, r); !ok { writeError(w, code, "forbidden") return } var body sqlite.SyncBackend if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } body.ProjectID = id if body.Kind == "" { body.Kind = "none" } if err := s.Projects.SetSyncBackend(body); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, body) } func (s *Server) projectSyncConfig(projectID string) (syncbackend.ProjectConfig, error) { b, err := s.Projects.GetSyncBackend(projectID) if err != nil { return syncbackend.ProjectConfig{}, err } cfg := syncbackend.ProjectConfig{ Kind: b.Kind, BaseURL: b.BaseURL, ProjectKey: b.ProjectKey, Query: b.Query, } if b.ConnectionID != "" { conn, err := s.DB.GetConnection(b.ConnectionID) if err != nil { return cfg, err } cfg.Token = conn.Token if cfg.BaseURL == "" { cfg.BaseURL = conn.BaseURL } } return cfg, nil } func (s *Server) handleSyncPull(w http.ResponseWriter, r *http.Request) { s.runSync(w, r, true) } func (s *Server) handleSyncPush(w http.ResponseWriter, r *http.Request) { s.runSync(w, r, false) } func (s *Server) runSync(w http.ResponseWriter, r *http.Request, pull bool) { id := r.PathValue("id") u, ok, code := s.requireProjectRole(id, sqlite.RoleMember, r) if !ok { writeError(w, code, "forbidden") return } cfg, err := s.projectSyncConfig(id) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } backend, err := s.Sync.Get(cfg.Kind) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var res syncbackend.Result if pull { res, err = backend.Pull(r.Context(), id, cfg, s.Tickets, s.Times) } else { res, err = backend.Push(r.Context(), id, cfg, s.Tickets, s.Times) } if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } op := "sync_push" if pull { op = "sync_pull" } tickets, _ := s.Tickets.List(id) for _, t := range tickets { _, _ = s.DB.AppendRevision("ticket", t.ID, id, op, u.ID, t) } writeJSON(w, http.StatusOK, res) } func (s *Server) handleListConnections(w http.ResponseWriter, r *http.Request) { list, err := s.DB.ListConnections() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, list) } func (s *Server) handleCreateConnection(w http.ResponseWriter, r *http.Request) { u, _ := s.currentUser(r) var body struct { Name string `json:"name"` Kind string `json:"kind"` BaseURL string `json:"baseUrl"` Scope string `json:"scope"` Token string `json:"token"` } if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if body.Kind == "" { body.Kind = "youtrack" } c, err := s.DB.CreateConnection(body.Name, body.Kind, body.BaseURL, body.Scope, body.Token, u.ID) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, c) } func (s *Server) handleDeleteConnection(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := s.DB.DeleteConnection(id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleConnectionProjects(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") conn, err := s.DB.GetConnection(id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } backend, err := s.Sync.Get(conn.Kind) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } projects, err := backend.ListProjects(r.Context(), syncbackend.Connection{ Kind: conn.Kind, BaseURL: conn.BaseURL, Token: conn.Token, Scope: conn.Scope, }) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, projects) } func (s *Server) handleImportProject(w http.ResponseWriter, r *http.Request) { u, _ := s.currentUser(r) connID := r.PathValue("id") var body struct { RemoteKey string `json:"remoteKey"` LocalID string `json:"localId"` Name string `json:"name"` Pull bool `json:"pull"` } if err := readJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } body.RemoteKey = strings.TrimSpace(body.RemoteKey) if body.RemoteKey == "" { writeError(w, http.StatusBadRequest, "remoteKey required") return } conn, err := s.DB.GetConnection(connID) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } name := body.Name if name == "" { name = body.RemoteKey } // LocalID optional; empty → auto UUID. Explicit id only for advanced clients. p, err := s.Projects.Create(strings.TrimSpace(body.LocalID), name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } _ = s.DB.SetMember(p.ID, u.ID, sqlite.RoleOwner) _ = s.Projects.SetSyncBackend(sqlite.SyncBackend{ ProjectID: p.ID, Kind: conn.Kind, BaseURL: conn.BaseURL, ProjectKey: body.RemoteKey, ConnectionID: conn.ID, }) result := map[string]any{"project": p} if body.Pull { cfg, err := s.projectSyncConfig(p.ID) if err == nil { if backend, err := s.Sync.Get(cfg.Kind); err == nil { res, err := backend.Pull(r.Context(), p.ID, cfg, s.Tickets, s.Times) if err != nil { result["pullError"] = err.Error() } else { result["pull"] = res } } } } writeJSON(w, http.StatusCreated, result) }