128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
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)
|
||
}
|