forked from baron/baron-sso
테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user