1
0
forked from baron/baron-sso

테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거

This commit is contained in:
2026-05-13 18:05:51 +09:00
parent a4d707d4d8
commit 5e7b7b878c
85 changed files with 4808 additions and 734 deletions

View File

@@ -2,6 +2,7 @@ package handler
import (
"baron-sso-backend/internal/domain"
"baron-sso-backend/internal/pagination"
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"baron-sso-backend/internal/utils"
@@ -80,6 +81,20 @@ func mergeUserAppointmentMetadata(metadata map[string]any, appointments []map[st
return metadata
}
func sanitizeUserMetadata(metadata map[string]any) map[string]any {
if metadata == nil {
return nil
}
sanitized := make(map[string]any, len(metadata))
for key, value := range metadata {
if key == "hanmacFamily" || key == "userType" {
continue
}
sanitized[key] = value
}
return sanitized
}
func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any, appointments []map[string]any) string {
if value := strings.TrimSpace(primaryTenantID); value != "" {
return value
@@ -206,6 +221,25 @@ func gradeFromTraits(traits map[string]interface{}) string {
return value
}
func tenantSlugFromRequest(tenantSlug string, legacyCompanyCode string) string {
if value := strings.TrimSpace(tenantSlug); value != "" {
return value
}
return strings.TrimSpace(legacyCompanyCode)
}
func tenantSlugPointerFromRequest(tenantSlug *string, legacyCompanyCode *string) *string {
if tenantSlug != nil {
value := strings.TrimSpace(*tenantSlug)
return &value
}
if legacyCompanyCode != nil {
value := strings.TrimSpace(*legacyCompanyCode)
return &value
}
return nil
}
type userSummary struct {
ID string `json:"id"`
Email string `json:"email"`
@@ -215,6 +249,7 @@ type userSummary struct {
Phone string `json:"phone"`
Role string `json:"role"`
Status string `json:"status"`
TenantSlug string `json:"tenantSlug,omitempty"`
CompanyCode string `json:"companyCode"`
Metadata domain.JSONMap `json:"metadata,omitempty"`
Tenant *domain.Tenant `json:"tenant,omitempty"`
@@ -229,10 +264,20 @@ type userSummary struct {
}
type userListResponse struct {
Items []userSummary `json:"items"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int64 `json:"total"`
Items []userSummary `json:"items"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int64 `json:"total"`
Cursor string `json:"cursor,omitempty"`
NextCursor string `json:"nextCursor,omitempty"`
}
func kratosIdentityCursorKey(identity service.KratosIdentity) (time.Time, string) {
timestamp := identity.CreatedAt
if timestamp.IsZero() {
timestamp = time.Unix(0, 0).UTC()
}
return timestamp, identity.ID
}
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
@@ -246,6 +291,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0)
search := strings.TrimSpace(c.Query("search"))
tenantSlug := strings.TrimSpace(c.Query("tenantSlug"))
cursorRaw := strings.TrimSpace(c.Query("cursor"))
if limit <= 0 {
limit = 50
@@ -399,17 +445,33 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
filtered = append(filtered, identity)
}
pagination.SortByKeyDesc(filtered, kratosIdentityCursorKey)
total := int64(len(filtered))
if offset > len(filtered) {
offset = len(filtered)
}
end := offset + limit
if end > len(filtered) {
end = len(filtered)
nextCursor := ""
var pageIdentities []service.KratosIdentity
if cursorRaw != "" {
pageIdentities, nextCursor, err = pagination.PageByCursor(filtered, limit, cursorRaw, kratosIdentityCursorKey)
if err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
}
offset = 0
} else {
if offset > len(filtered) {
offset = len(filtered)
}
end := offset + limit
if end > len(filtered) {
end = len(filtered)
}
pageIdentities = filtered[offset:end]
if total > int64(end) && len(pageIdentities) > 0 {
lastTimestamp, lastID := kratosIdentityCursorKey(pageIdentities[len(pageIdentities)-1])
nextCursor = pagination.Encode(lastTimestamp, lastID)
}
}
items := make([]userSummary, 0, end-offset)
for _, identity := range filtered[offset:end] {
items := make([]userSummary, 0, len(pageIdentities))
for _, identity := range pageIdentities {
summary := h.mapIdentitySummary(c.Context(), identity)
items = append(items, summary)
}
@@ -427,7 +489,14 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
}(filtered)
}
return c.JSON(userListResponse{Items: items, Limit: limit, Offset: offset, Total: total})
return c.JSON(userListResponse{
Items: items,
Limit: limit,
Offset: offset,
Total: total,
Cursor: cursorRaw,
NextCursor: nextCursor,
})
}
slog.Warn("Kratos unavailable for user list", "error", err)
@@ -490,6 +559,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
TenantSlug string `json:"tenantSlug"`
CompanyCode string `json:"companyCode"`
Department string `json:"department"`
Grade string `json:"grade"`
@@ -504,7 +574,8 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
req.Metadata = mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner)
req.CompanyCode = tenantSlugFromRequest(req.TenantSlug, req.CompanyCode)
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
email := strings.TrimSpace(req.Email)
if email == "" {
@@ -724,6 +795,7 @@ type bulkUserItem struct {
Role string `json:"role"`
TenantID string `json:"tenantId"`
TenantSlug string `json:"tenantSlug"`
CompanyCode string `json:"companyCode"`
EmailDomain string `json:"emailDomain"`
Department string `json:"department"`
Grade string `json:"grade"`
@@ -881,7 +953,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
email := strings.TrimSpace(item.Email)
name := strings.TrimSpace(item.Name)
tenantID := strings.TrimSpace(item.TenantID)
tenantSlug := strings.TrimSpace(item.TenantSlug)
tenantSlug := tenantSlugFromRequest(item.TenantSlug, item.CompanyCode)
dept := strings.TrimSpace(item.Department)
if email == "" || name == "" {
@@ -1054,6 +1126,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
item.Metadata["additionalAppointments"] = resolvedAppointments
}
item.Metadata = sanitizeUserMetadata(item.Metadata)
password, _ := utils.GeneratePasswordWithPolicy(policy)
role := item.Role
@@ -1218,10 +1291,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
search := strings.TrimSpace(c.Query("search"))
companyCode := strings.TrimSpace(c.Query("companyCode"))
if companyCode == "" {
companyCode = strings.TrimSpace(c.Query("tenantSlug"))
}
tenantSlug := tenantSlugFromRequest(c.Query("tenantSlug"), c.Query("companyCode"))
var requesterRole string
var manageableSlugs []string
@@ -1269,8 +1339,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
}
// 1. Fetch Users using Repo for efficiency
// repo.List expects (ctx, offset, limit, search, companyCode)
users, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, companyCode)
users, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, tenantSlug)
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users for export")
}
@@ -1386,6 +1455,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
UserIDs []string `json:"userIds"`
Status *string `json:"status"`
Role *string `json:"role"`
TenantSlug *string `json:"tenantSlug"`
CompanyCode *string `json:"companyCode"`
Department *string `json:"department"`
Grade *string `json:"grade"`
@@ -1395,6 +1465,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
req.CompanyCode = tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
if len(req.UserIDs) == 0 {
return errorJSON(c, fiber.StatusBadRequest, "no user IDs provided")
@@ -1404,6 +1475,16 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
if requester == nil {
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
}
if req.Role != nil {
if domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
return errorJSON(c, fiber.StatusForbidden, "forbidden: only super admin can change user role")
}
role, ok := domain.NormalizeRoleAlias(*req.Role)
if !ok {
return errorJSON(c, fiber.StatusBadRequest, "invalid role")
}
*req.Role = role
}
// [New] Pre-fetch tenant cache if companyCode is being changed
type tenantCacheItem struct {
@@ -1683,6 +1764,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
Phone *string `json:"phone"`
Role *string `json:"role"`
Status *string `json:"status"`
TenantSlug *string `json:"tenantSlug"`
CompanyCode *string `json:"companyCode"`
IsAddTenant bool `json:"isAddTenant"`
IsRemoveTenant bool `json:"isRemoveTenant"`
@@ -1699,7 +1781,18 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
req.Metadata = mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner)
req.CompanyCode = tenantSlugPointerFromRequest(req.TenantSlug, req.CompanyCode)
req.Metadata = sanitizeUserMetadata(mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner))
if req.Role != nil {
if requester == nil || domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
return errorJSON(c, fiber.StatusForbidden, "forbidden: only super admin can change user role")
}
role, ok := domain.NormalizeRoleAlias(*req.Role)
if !ok {
return errorJSON(c, fiber.StatusBadRequest, "invalid role")
}
*req.Role = role
}
// [New] Tenant Admin restriction: Cannot change companyCode (except when adding/removing secondary membership)
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
@@ -1754,6 +1847,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if traits == nil {
traits = map[string]interface{}{}
}
delete(traits, "hanmacFamily")
delete(traits, "userType")
// [Preserve & Merge] Multi-Tenant Info
var existingCodes []string
@@ -2191,6 +2286,7 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
Phone: extractTraitString(traits, "phone_number"),
Role: role,
Status: normalizeStatus(identity.State),
TenantSlug: compCode,
CompanyCode: compCode,
Department: extractTraitString(traits, "department"),
Grade: gradeFromTraits(traits),