package httpapi import ( "net/http" "strings" "spin/internal/models" ) // spin is a single internal company, so authorization is a 2-tier model rather // than eQMS's per-company membership matrix: // // - admin → super-admin (Keycloak group ∩ ADMIN_GROUPS) OR Member.Role==admin. // Sees and manages everything: approvals, incentive console, // accounting, all member/project data, the bracketed [admin-only] // contract & payment fields. // - member → sees only their OWN data and may only SUBMIT requests. // // Ownership is enforced per-handler by comparing the row's member email to the // caller's email (case-insensitive). // isSuperAdmin reports the Keycloak/dev super-admin flag from the auth middleware. func (s *Server) isSuperAdmin(r *http.Request) bool { return currentUser(r.Context()).IsSuperAdmin } // isAdmin reports whether the caller may manage company-wide data: either a // super-admin or a Member whose Role is admin. func (s *Server) isAdmin(r *http.Request) bool { if s.isSuperAdmin(r) { return true } m := s.lookupMember(currentUser(r.Context()).Email) return m != nil && m.Role == models.RoleAdmin } // requireAdmin writes 403 and returns false when the caller is not an admin. func (s *Server) requireAdmin(w http.ResponseWriter, r *http.Request) bool { if s.isAdmin(r) { return true } writeError(w, http.StatusForbidden, "관리자 권한이 필요합니다") return false } // email returns the caller's (lowercased) email. func (s *Server) email(r *http.Request) string { return strings.ToLower(strings.TrimSpace(currentUser(r.Context()).Email)) } // owns reports whether the given member email belongs to the caller. func (s *Server) owns(r *http.Request, memberEmail string) bool { return strings.EqualFold(strings.TrimSpace(memberEmail), s.email(r)) } // lookupMember loads the Member row matched to an email (nil if none). func (s *Server) lookupMember(email string) *models.Member { email = strings.TrimSpace(email) if email == "" { return nil } var m models.Member if err := s.db.Where("lower(email) = lower(?)", email).First(&m).Error; err != nil { return nil } return &m } // audit writes an AuditLog row (best-effort). func (s *Server) audit(r *http.Request, action, entity, entityID, detail string) { s.db.Create(&models.AuditLog{ Actor: currentUser(r.Context()).Email, Action: action, Entity: entity, EntityID: entityID, Detail: detail, }) }