package api import ( "crypto/rand" "encoding/hex" "net/http" "time" "task/internal/project" ) func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) { list, err := s.projects.List() if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } if ClientKind(r.Context()) == ClientMCP { pid := MCPProjectID(r.Context()) filtered := make([]project.Project, 0, 1) for _, p := range list { if p.ID == pid { filtered = append(filtered, p) } } list = filtered } writeJSON(w, http.StatusOK, list) } func (s *Server) handleEnsureProject(w http.ResponseWriter, r *http.Request) { var body struct { ID string `json:"id"` } if err := readJSON(r, &body); err != nil { writeErr(w, http.StatusBadRequest, err.Error()) return } if !requireMCPProject(r, body.ID) { writeErr(w, http.StatusForbidden, "mcp token project mismatch") return } if err := s.projects.Ensure(body.ID); err != nil { writeErr(w, http.StatusBadRequest, err.Error()) return } s.notify(body.ID) writeJSON(w, http.StatusOK, map[string]string{"id": body.ID}) } func (s *Server) handleDeleteProject(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if !requireMCPProject(r, id) { writeErr(w, http.StatusForbidden, "mcp token project mismatch") return } if err := s.timeStore.DeleteByProject(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } if t, err := s.timeStore.LoadActiveTimer(); err == nil && t != nil && t.ProjectID == id { _ = s.timeStore.SaveActiveTimer(nil) } if err := s.projects.Delete(id); err != nil { writeErr(w, http.StatusBadRequest, err.Error()) return } s.notify(id) w.WriteHeader(http.StatusNoContent) } func (s *Server) handleGetMeta(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if !requireMCPProject(r, id) { writeErr(w, http.StatusForbidden, "mcp token project mismatch") return } meta, err := s.projects.LoadMeta(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, meta) } func (s *Server) handleSaveMeta(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if !requireMCPProject(r, id) { writeErr(w, http.StatusForbidden, "mcp token project mismatch") return } var meta project.Meta if err := readJSON(r, &meta); err != nil { writeErr(w, http.StatusBadRequest, err.Error()) return } // Preserve existing mcpAuth if client omitted it (TUI sync edit). prev, _ := s.projects.LoadMeta(id) if meta.McpAuth == nil && prev.McpAuth != nil { meta.McpAuth = prev.McpAuth } if err := s.projects.SaveMeta(id, meta); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } s.notify(id) writeJSON(w, http.StatusOK, meta) } func (s *Server) handleMCPTokenStatus(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if ClientKind(r.Context()) == ClientMCP { writeErr(w, http.StatusForbidden, "mcp cannot inspect token status") return } meta, err := s.projects.LoadMeta(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } configured := meta.McpAuth != nil && meta.McpAuth.TokenHash != "" created := "" if meta.McpAuth != nil { created = meta.McpAuth.CreatedAt } writeJSON(w, http.StatusOK, map[string]any{ "configured": configured, "createdAt": created, }) } func (s *Server) handleMCPTokenCreate(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if ClientKind(r.Context()) == ClientMCP { writeErr(w, http.StatusForbidden, "mcp cannot create tokens") return } if err := s.projects.Ensure(id); err != nil { writeErr(w, http.StatusBadRequest, err.Error()) return } raw := make([]byte, 24) if _, err := rand.Read(raw); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } token := "tm_mcp_" + hex.EncodeToString(raw) meta, err := s.projects.LoadMeta(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } created := time.Now().UTC().Format(time.RFC3339) meta.McpAuth = &project.McpAuth{TokenHash: hashToken(token), CreatedAt: created} if err := s.projects.SaveMeta(id, meta); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{ "token": token, "project": id, "createdAt": created, }) } func (s *Server) handleMCPTokenRevoke(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if ClientKind(r.Context()) == ClientMCP { writeErr(w, http.StatusForbidden, "mcp cannot revoke tokens") return } meta, err := s.projects.LoadMeta(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } meta.McpAuth = nil if err := s.projects.SaveMeta(id, meta); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) }