Files
skrueckenandCursor 4adfcdb95a Add tm_server with SQLite, Svelte UI, and task-master TUI sync.
Introduces the web backend (auth, RBAC, project tokens, revisions),
YouTrack-or-taskmaster backend choice in the TUI, and a first-pass
embedded Svelte UI. The frontend is functional but needs a major rework.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 22:23:11 +01:00

238 lines
6.1 KiB
Go

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