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>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user