package httpapi import ( "net/http" "spin/internal/models" "github.com/go-chi/chi/v5" ) // ---- members -------------------------------------------------------------- // handleListMembers: admins see everyone; a regular member sees only themselves // (the directory itself is admin-managed, individuals only see their own row). func (s *Server) handleListMembers(w http.ResponseWriter, r *http.Request) { q := s.db.Order("display_name asc") if !s.isAdmin(r) { q = q.Where("lower(email) = ?", s.email(r)) } var out []models.Member if err := q.Find(&out).Error; err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, out) } func (s *Server) handleGetMember(w http.ResponseWriter, r *http.Request) { var m models.Member if err := s.db.First(&m, "id = ?", chi.URLParam(r, "id")).Error; err != nil { writeError(w, http.StatusNotFound, "구성원을 찾을 수 없습니다") return } if !s.isAdmin(r) && !s.owns(r, m.Email) { writeError(w, http.StatusForbidden, "본인 정보만 조회할 수 있습니다") return } writeJSON(w, http.StatusOK, m) } func (s *Server) handleCreateMember(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } var m models.Member if err := decodeJSON(r, &m); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if m.Role == "" { m.Role = models.RoleMember } if m.Status == "" { m.Status = "active" } if err := s.db.Create(&m).Error; err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.audit(r, "create", "member", m.ID, m.Email) writeJSON(w, http.StatusCreated, m) } // memberSelfPatch is the small set of fields a member may edit on their own row. type memberSelfPatch struct { Phone *string `json:"phone"` Position *string `json:"position"` } func (s *Server) handlePatchMember(w http.ResponseWriter, r *http.Request) { var m models.Member if err := s.db.First(&m, "id = ?", chi.URLParam(r, "id")).Error; err != nil { writeError(w, http.StatusNotFound, "구성원을 찾을 수 없습니다") return } if s.isAdmin(r) { var patch map[string]interface{} if err := decodeJSON(r, &patch); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // whitelist admin-editable columns allowed := map[string]bool{"displayName": true, "rank": true, "departmentId": true, "role": true, "isPartner": true, "phone": true, "position": true, "status": true, "joinDate": true, "annualLeave": true, "email": true} cols := map[string]interface{}{} for k, v := range patch { if allowed[k] { cols[colName(k)] = v } } if err := s.db.Model(&m).Updates(cols).Error; err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.audit(r, "update", "member", m.ID, "") } else { if !s.owns(r, m.Email) { writeError(w, http.StatusForbidden, "본인 정보만 수정할 수 있습니다") return } var p memberSelfPatch if err := decodeJSON(r, &p); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if p.Phone != nil { m.Phone = *p.Phone } if p.Position != nil { m.Position = *p.Position } s.db.Save(&m) } s.db.First(&m, "id = ?", m.ID) writeJSON(w, http.StatusOK, m) } func (s *Server) handleDeleteMember(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } id := chi.URLParam(r, "id") if err := s.db.Delete(&models.Member{}, "id = ?", id).Error; err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.audit(r, "delete", "member", id, "") writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } // colName maps a JSON field name to its snake_case DB column. func colName(json string) string { switch json { case "displayName": return "display_name" case "departmentId": return "department_id" case "isPartner": return "is_partner" case "joinDate": return "join_date" case "annualLeave": return "annual_leave" default: return json } } // ---- departments ---------------------------------------------------------- func (s *Server) handleListDepartments(w http.ResponseWriter, r *http.Request) { var out []models.Department s.db.Order("name asc").Find(&out) writeJSON(w, http.StatusOK, out) } func (s *Server) handleCreateDepartment(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } var d models.Department if err := decodeJSON(r, &d); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.db.Create(&d) writeJSON(w, http.StatusCreated, d) } func (s *Server) handlePatchDepartment(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } var d models.Department if err := s.db.First(&d, "id = ?", chi.URLParam(r, "id")).Error; err != nil { writeError(w, http.StatusNotFound, "부서를 찾을 수 없습니다") return } var patch models.Department decodeJSON(r, &patch) if patch.Name != "" { d.Name = patch.Name } d.LeadEmail = patch.LeadEmail s.db.Save(&d) writeJSON(w, http.StatusOK, d) } func (s *Server) handleDeleteDepartment(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } s.db.Delete(&models.Department{}, "id = ?", chi.URLParam(r, "id")) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) } // ---- audit ---------------------------------------------------------------- func (s *Server) handleListAudit(w http.ResponseWriter, r *http.Request) { if !s.requireAdmin(w, r) { return } var out []models.AuditLog s.db.Order("created_at desc").Limit(500).Find(&out) writeJSON(w, http.StatusOK, out) }