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

112 lines
2.8 KiB
Go

package server
import (
"context"
"net/http"
"strings"
"task/internal/sqlite"
)
type ctxKey int
const (
userKey ctxKey = 1
tokenAuthKey ctxKey = 2
)
func (s *Server) currentUser(r *http.Request) (sqlite.User, bool) {
u, ok := r.Context().Value(userKey).(sqlite.User)
return u, ok
}
func (s *Server) currentTokenAuth(r *http.Request) (sqlite.TokenAuth, bool) {
t, ok := r.Context().Value(tokenAuthKey).(sqlite.TokenAuth)
return t, ok
}
func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, ok := s.authenticate(r)
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
next(w, r.WithContext(ctx))
}
}
func (s *Server) authenticate(r *http.Request) (context.Context, bool) {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
raw := strings.TrimSpace(auth[len("Bearer "):])
ta, err := s.DB.ResolveBearerToken(raw)
if err != nil {
return r.Context(), false
}
ctx := context.WithValue(r.Context(), userKey, ta.User)
ctx = context.WithValue(ctx, tokenAuthKey, ta)
return ctx, true
}
c, err := r.Cookie(sessionCookie)
if err != nil || c.Value == "" {
return r.Context(), false
}
u, err := s.DB.UserFromSession(c.Value)
if err != nil {
return r.Context(), false
}
u.PasswordHash = ""
return context.WithValue(r.Context(), userKey, u), true
}
func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc {
return s.requireAuth(func(w http.ResponseWriter, r *http.Request) {
if _, tok := s.currentTokenAuth(r); tok {
writeError(w, http.StatusForbidden, "admin required")
return
}
u, _ := s.currentUser(r)
if !u.IsAdmin {
writeError(w, http.StatusForbidden, "admin required")
return
}
next(w, r)
})
}
func (s *Server) requireProjectRole(projectID string, minRole string, r *http.Request) (sqlite.User, bool, int) {
u, ok := s.currentUser(r)
if !ok {
return sqlite.User{}, false, http.StatusUnauthorized
}
if ta, ok := s.currentTokenAuth(r); ok {
if ta.ProjectID != projectID {
return u, false, http.StatusForbidden
}
need := sqlite.TokenScopeRead
if minRole == sqlite.RoleMember || minRole == sqlite.RoleAdmin || minRole == sqlite.RoleOwner {
need = sqlite.TokenScopeWrite
}
// viewer → read; member+ → write
if minRole == sqlite.RoleViewer {
need = sqlite.TokenScopeRead
}
if !sqlite.TokenAllows(ta.Scope, need) {
return u, false, http.StatusForbidden
}
return u, true, 0
}
if u.IsAdmin {
return u, true, 0
}
role, err := s.DB.MemberRole(projectID, u.ID)
if err != nil || role == "" {
return u, false, http.StatusForbidden
}
if !sqlite.RoleAtLeast(role, minRole) {
return u, false, http.StatusForbidden
}
return u, true, 0
}