Files
paliad/internal/handlers/teams.go

128 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"github.com/google/uuid"
)
// GET /api/projects/{id}/team — returns direct + inherited team members.
// inherited=true rows include inherited_from_id / inherited_from_title.
func handleListProjectTeam(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
rows, err := dbSvc.team.ListEffectiveMembers(r.Context(), uid, id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, rows)
}
// POST /api/projects/{id}/team — add a direct member.
// Body: {"user_id": "<uuid>", "responsibility": "<lead|member|observer|external>"}
//
// Legacy clients that submit `role` are still accepted as a synonym
// during the deprecation window; the field is treated as a
// responsibility value when the new field is absent.
func handleAddProjectTeamMember(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
projectID, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
var body struct {
UserID uuid.UUID `json:"user_id"`
Responsibility string `json:"responsibility"`
// Legacy field, accepted for one release while frontend migrates.
Role string `json:"role"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
resp := body.Responsibility
if resp == "" {
resp = body.Role
}
m, err := dbSvc.team.AddMember(r.Context(), uid, projectID, body.UserID, resp)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, m)
}
// GET /api/team/memberships — bulk index of project_teams membership for
// every (visible) user × project pair. Powers the /team page project-
// multi-select filter (t-paliad-147 / issue #7). Cheap to call: one
// scan per call; client-side filter handles everything from there.
func handleListMembershipsIndex(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
rows, err := dbSvc.team.ListMembershipsIndex(r.Context(), uid)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, rows)
}
// DELETE /api/projects/{id}/team/{user_id} — remove a direct member.
// Inherited memberships can't be removed at the child level.
func handleRemoveProjectTeamMember(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
projectID, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
return
}
userID, err := uuid.Parse(r.PathValue("user_id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user id"})
return
}
if err := dbSvc.team.RemoveMember(r.Context(), uid, projectID, userID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "no direct membership — inherited memberships must be removed at the ancestor",
})
return
}
writeServiceError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}