package server import ( "database/sql" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions" "github.com/go-chi/chi/v5" ) type MemberHandler struct { db *sql.DB checker *permissions.Checker } func NewMemberHandler(db *sql.DB) *MemberHandler { return &MemberHandler{ db: db, checker: permissions.NewChecker(db), } } func (h *MemberHandler) RegisterRoutes(r chi.Router) { r.Get("/members", h.ListMembers) r.Patch("/members/{userID}", h.UpdateMember) } type memberResponse struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Nickname *string `json:"nickname"` Avatar string `json:"avatar"` Status string `json:"status"` StatusText string `json:"status_text"` } func (h *MemberHandler) ListMembers(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") // Verify the caller is a member var exists bool err := h.db.QueryRowContext(r.Context(), `SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)`, userID, serverID).Scan(&exists) if err != nil || !exists { http.Error(w, `{"error":"not a member"}`, http.StatusForbidden) return } rows, err := h.db.QueryContext(r.Context(), ` SELECT u.id, u.username, u.display_name, m.nickname, COALESCE(u.avatar, ''), u.status, COALESCE(u.status_text, '') FROM members m JOIN users u ON u.id = m.user_id WHERE m.server_id = $1 ORDER BY u.username `, serverID) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } defer rows.Close() members := make([]memberResponse, 0) for rows.Next() { var m memberResponse var nick sql.NullString if err := rows.Scan(&m.ID, &m.Username, &m.DisplayName, &nick, &m.Avatar, &m.Status, &m.StatusText); err != nil { continue } if nick.Valid { m.Nickname = &nick.String } members = append(members, m) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(members) } type updateMemberRequest struct { Nickname *string `json:"nickname"` } func (h *MemberHandler) UpdateMember(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } serverID := chi.URLParam(r, "serverID") targetUserID := chi.URLParam(r, "userID") var req updateMemberRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.Nickname == nil { http.Error(w, `{"error":"nothing to update"}`, http.StatusBadRequest) return } // Check permissions: // - If self, need CHANGE_NICKNAME // - If other, need MANAGE_NICKNAMES var requiredPerm int64 if userID == targetUserID { requiredPerm = permissions.CHANGE_NICKNAME } else { requiredPerm = permissions.MANAGE_NICKNAMES } allowed, err := h.checker.CheckPermission(r.Context(), serverID, userID, requiredPerm) if err != nil { http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError) return } if !allowed { http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden) return } // Update nickname in the database var nickVal sql.NullString if *req.Nickname != "" { nickVal = sql.NullString{String: *req.Nickname, Valid: true} } else { nickVal = sql.NullString{Valid: false} // NULL value clears the nickname } _, err = h.db.ExecContext(r.Context(), ` UPDATE members SET nickname = $1 WHERE user_id = $2 AND server_id = $3 `, nickVal, targetUserID, serverID) if err != nil { http.Error(w, `{"error":"failed to update nickname"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }