package handlers import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type MemberHandler struct { db *xorm.Engine } func NewMemberHandler(db *xorm.Engine) *MemberHandler { return &MemberHandler{db: db} } type memberResponse struct { UserID int64 `json:"userId"` Username string `json:"username"` AvatarURL string `json:"avatarUrl"` Permission string `json:"permission"` IsOwner bool `json:"isOwner"` AddedAt string `json:"addedAt"` } // lookupRepoForMembers resolves the repo from URL params and returns the owner User. func (h *MemberHandler) lookupRepoAndOwner(w http.ResponseWriter, r *http.Request) (*models.Repository, *models.User, bool) { ownerName := chi.URLParam(r, "owner") repoName := chi.URLParam(r, "repo") var owner models.User if found, _ := h.db.Where("username = ?", ownerName).Get(&owner); !found { jsonError(w, "repository not found", http.StatusNotFound) return nil, nil, false } var repo models.Repository if found, _ := h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo); !found { jsonError(w, "repository not found", http.StatusNotFound) return nil, nil, false } return &repo, &owner, true } // callerCanManage returns true if callerID is the repo owner or has admin permission. func (h *MemberHandler) callerCanManage(repo *models.Repository, callerID int64) bool { if callerID == repo.OwnerID { return true } var m models.RepoMember found, _ := h.db.Where("repo_id = ? AND user_id = ? AND permission = 'admin'", repo.ID, callerID).Get(&m) return found } // List returns all members (explicit + owner) for a repo. func (h *MemberHandler) List(w http.ResponseWriter, r *http.Request) { repo, owner, ok := h.lookupRepoAndOwner(w, r) if !ok { return } result := []memberResponse{ { UserID: owner.ID, Username: owner.Username, AvatarURL: owner.AvatarURL, Permission: "admin", IsOwner: true, AddedAt: repo.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }, } var members []models.RepoMember h.db.Where("repo_id = ?", repo.ID).OrderBy("created_at").Find(&members) for _, m := range members { var u models.User if found, _ := h.db.ID(m.UserID).Get(&u); !found { continue } result = append(result, memberResponse{ UserID: u.ID, Username: u.Username, AvatarURL: u.AvatarURL, Permission: m.Permission, IsOwner: false, AddedAt: m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }) } jsonOK(w, result) } // Add grants a user access to the repo. func (h *MemberHandler) Add(w http.ResponseWriter, r *http.Request) { repo, _, ok := h.lookupRepoAndOwner(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.callerCanManage(repo, callerID) { jsonError(w, "only the owner or an admin can manage members", http.StatusForbidden) return } var body struct { Username string `json:"username"` Permission string `json:"permission"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Username == "" { jsonError(w, "username is required", http.StatusBadRequest) return } if body.Permission != "read" && body.Permission != "write" && body.Permission != "admin" { jsonError(w, "permission must be read, write, or admin", http.StatusBadRequest) return } var target models.User if found, _ := h.db.Where("username = ?", body.Username).Get(&target); !found { jsonError(w, "user not found", http.StatusNotFound) return } if target.ID == repo.OwnerID { jsonError(w, "the repository owner always has admin access", http.StatusConflict) return } // Upsert: if already a member, update permission. var existing models.RepoMember found, _ := h.db.Where("repo_id = ? AND user_id = ?", repo.ID, target.ID).Get(&existing) if found { existing.Permission = body.Permission h.db.ID(existing.ID).Cols("permission").Update(&existing) } else { m := &models.RepoMember{RepoID: repo.ID, UserID: target.ID, Permission: body.Permission} if _, err := h.db.Insert(m); err != nil { jsonError(w, "could not add member", http.StatusInternalServerError) return } } jsonOK(w, memberResponse{ UserID: target.ID, Username: target.Username, AvatarURL: target.AvatarURL, Permission: body.Permission, IsOwner: false, }) } // UpdatePermission changes an existing member's permission level. func (h *MemberHandler) UpdatePermission(w http.ResponseWriter, r *http.Request) { repo, owner, ok := h.lookupRepoAndOwner(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.callerCanManage(repo, callerID) { jsonError(w, "only the owner or an admin can manage members", http.StatusForbidden) return } targetUsername := chi.URLParam(r, "username") var target models.User if found, _ := h.db.Where("username = ?", targetUsername).Get(&target); !found { jsonError(w, "user not found", http.StatusNotFound) return } if target.ID == owner.ID { jsonError(w, "cannot change the owner's permission", http.StatusConflict) return } var body struct { Permission string `json:"permission"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { jsonError(w, "invalid body", http.StatusBadRequest) return } if body.Permission != "read" && body.Permission != "write" && body.Permission != "admin" { jsonError(w, "permission must be read, write, or admin", http.StatusBadRequest) return } var m models.RepoMember if found, _ := h.db.Where("repo_id = ? AND user_id = ?", repo.ID, target.ID).Get(&m); !found { jsonError(w, "user is not a member", http.StatusNotFound) return } m.Permission = body.Permission h.db.ID(m.ID).Cols("permission").Update(&m) jsonOK(w, memberResponse{ UserID: target.ID, Username: target.Username, Permission: body.Permission, IsOwner: false, }) } // Remove revokes a user's access to the repo. func (h *MemberHandler) Remove(w http.ResponseWriter, r *http.Request) { repo, owner, ok := h.lookupRepoAndOwner(w, r) if !ok { return } callerID, _ := middleware.UserIDFromContext(r.Context()) if !h.callerCanManage(repo, callerID) { jsonError(w, "only the owner or an admin can manage members", http.StatusForbidden) return } targetUsername := chi.URLParam(r, "username") var target models.User if found, _ := h.db.Where("username = ?", targetUsername).Get(&target); !found { jsonError(w, "user not found", http.StatusNotFound) return } if target.ID == owner.ID { jsonError(w, "cannot remove the repository owner", http.StatusConflict) return } h.db.Where("repo_id = ? AND user_id = ?", repo.ID, target.ID).Delete(&models.RepoMember{}) w.WriteHeader(http.StatusNoContent) } // HasPermission checks if a user (by username) has at least the given permission level on a repo. // Permission hierarchy: read < write < admin. Owner always passes. func HasPermission(db *xorm.Engine, repo *models.Repository, username, required string) bool { var u models.User if found, _ := db.Where("username = ?", username).Get(&u); !found { return false } if u.ID == repo.OwnerID { return true } var m models.RepoMember if found, _ := db.Where("repo_id = ? AND user_id = ?", repo.ID, u.ID).Get(&m); !found { return false } rank := map[string]int{"read": 1, "write": 2, "admin": 3} return rank[m.Permission] >= rank[required] }