All checks were successful
build-and-push / build (push) Successful in 32s
memberSelfPatch에 displayName 추가 — 유저가 계정 설정에서 표시 이름 변경 가능. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
212 lines
5.7 KiB
Go
212 lines
5.7 KiB
Go
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 {
|
|
DisplayName *string `json:"displayName"`
|
|
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.DisplayName != nil {
|
|
m.DisplayName = *p.DisplayName
|
|
}
|
|
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)
|
|
}
|