1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -23,6 +23,7 @@ import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// OryProviderAPI defines the subset of Ory Provider used by UserHandler
@@ -99,6 +100,76 @@ func sanitizeUserMetadata(metadata map[string]any) map[string]any {
return sanitized
}
func userAppointmentSliceFromRaw(raw any) []any {
switch values := raw.(type) {
case []any:
return append([]any(nil), values...)
case []map[string]any:
appointments := make([]any, 0, len(values))
for _, value := range values {
appointments = append(appointments, value)
}
return appointments
default:
return nil
}
}
func userAppointmentTenantKey(raw any) string {
appointment, ok := raw.(map[string]any)
if !ok {
return ""
}
if value := normalizeMetadataString(appointment["tenantId"]); value != "" {
return "id:" + strings.ToLower(value)
}
if value := normalizeMetadataString(appointment["tenantSlug"]); value != "" {
return "slug:" + strings.ToLower(value)
}
if value := normalizeMetadataString(appointment["slug"]); value != "" {
return "slug:" + strings.ToLower(value)
}
return ""
}
func mergeUserAddTenantAppointment(traits map[string]any, metadata map[string]any, tenant *domain.Tenant) map[string]any {
if tenant == nil {
return metadata
}
if metadata == nil {
metadata = map[string]any{}
}
appointments := userAppointmentSliceFromRaw(traits["additionalAppointments"])
if len(appointments) == 0 {
if legacyMetadata, ok := traits["metadata"].(map[string]any); ok {
appointments = userAppointmentSliceFromRaw(legacyMetadata["additionalAppointments"])
}
}
if incoming := userAppointmentSliceFromRaw(metadata["additionalAppointments"]); len(incoming) > 0 {
appointments = incoming
}
seen := make(map[string]bool, len(appointments)+1)
for _, appointment := range appointments {
if key := userAppointmentTenantKey(appointment); key != "" {
seen[key] = true
}
}
tenantIDKey := "id:" + strings.ToLower(strings.TrimSpace(tenant.ID))
tenantSlugKey := "slug:" + strings.ToLower(strings.TrimSpace(tenant.Slug))
if !seen[tenantIDKey] && !seen[tenantSlugKey] {
appointments = append(appointments, map[string]any{
"tenantId": tenant.ID,
"tenantSlug": tenant.Slug,
"tenantName": tenant.Name,
"isPrimary": false,
})
}
metadata["additionalAppointments"] = appointments
return metadata
}
func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService service.TenantService, metadata map[string]any, appointments []map[string]any) (bool, error) {
if tenantService == nil || metadata == nil {
return false, nil
@@ -534,6 +605,66 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
}
}
if h.UserRepo != nil {
var tenantIDs []string
if tenantSlug != "" {
if targetTenantID == "" {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
if requesterRole != domain.RoleSuperAdmin && !manageableSlugs[targetTenantID] && !manageableSlugs[strings.ToLower(tenantSlug)] {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
tenantIDs = append(tenantIDs, targetTenantID)
} else if requesterRole != domain.RoleSuperAdmin {
for key := range manageableSlugs {
if _, err := uuid.Parse(key); err == nil {
tenantIDs = append(tenantIDs, key)
}
}
if len(tenantIDs) == 0 {
return c.JSON(userListResponse{
Items: []userSummary{},
Limit: limit,
Offset: offset,
Total: 0,
Cursor: cursorRaw,
})
}
}
users, total, nextCursor, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantIDs, cursorRaw)
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to list users")
}
items := make([]userSummary, 0, len(users))
for _, user := range users {
items = append(items, h.mapLocalUserSummary(c.Context(), user))
}
if cursorRaw != "" {
offset = 0
}
return c.JSON(userListResponse{
Items: items,
Limit: limit,
Offset: offset,
Total: total,
Cursor: cursorRaw,
NextCursor: nextCursor,
})
}
if h.KratosAdmin == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
@@ -1615,11 +1746,18 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
// 1. Fetch Users using Repo for efficiency
var exportTenantIDs []string
if tenantSlug != "" && h.TenantService != nil {
t, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
if err == nil && t != nil {
exportTenantIDs = []string{t.ID}
if tenantSlug != "" {
if h.TenantService == nil {
return errorJSON(c, fiber.StatusServiceUnavailable, "tenant service unavailable for scoped export")
}
t, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, "failed to resolve tenant for export")
}
if t == nil || strings.TrimSpace(t.ID) == "" {
return errorJSON(c, fiber.StatusNotFound, "tenant not found for export")
}
exportTenantIDs = []string{t.ID}
}
users, _, _, err := h.UserRepo.List(c.Context(), 0, 10000, search, exportTenantIDs, "")
if err != nil {
@@ -2087,7 +2225,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
// All non-superadmins can only move users within tenants they can manage.
if requester != nil && domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil {
if !req.IsRemoveTenant && req.CompanyCode != nil {
targetSlug := strings.TrimSpace(*req.CompanyCode)
targetAllowed := profileCanAccessTenant(requester, "", targetSlug)
if !targetAllowed && h.TenantService != nil && targetSlug != "" {
@@ -2096,7 +2234,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
}
if !targetAllowed {
return errorJSON(c, fiber.StatusForbidden, "forbidden: non-superadmins cannot change user's tenant to an unmanageable one")
return errorJSON(c, fiber.StatusForbidden, "forbidden: non-superadmins cannot assign user's tenant to an unmanageable one")
}
}
}
@@ -2221,6 +2359,21 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
traits["tenant_id"] = ""
}
}
} else if h.TenantService != nil && code != "" {
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code)
if err != nil || tenant == nil {
return errorJSON(c, fiber.StatusBadRequest, "invalid tenant assignment")
}
req.Metadata = mergeUserAddTenantAppointment(traits, req.Metadata, tenant)
if h.KetoOutboxRepo != nil {
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
Namespace: "Tenant",
Object: tenant.ID,
Relation: "members",
Subject: "User:" + userID,
Action: domain.KetoOutboxActionCreate,
})
}
}
}
delete(traits, "companyCode")
@@ -2775,6 +2928,45 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
return summary
}
func (h *UserHandler) mapLocalUserSummary(ctx context.Context, user domain.User) userSummary {
tenantSlug := userTenantSlug(user)
customLoginIDs := make([]string, 0, len(user.UserLoginIDs))
for _, loginID := range user.UserLoginIDs {
if strings.TrimSpace(loginID.LoginID) != "" {
customLoginIDs = append(customLoginIDs, strings.TrimSpace(loginID.LoginID))
}
}
summary := userSummary{
ID: user.ID,
Email: user.Email,
LoginID: user.Email,
CustomLoginIDs: customLoginIDs,
Name: user.Name,
Phone: user.Phone,
Role: domain.NormalizeRole(user.Role),
Status: normalizeStatus(user.Status),
TenantSlug: tenantSlug,
CompanyCode: tenantSlug,
Department: user.Department,
Grade: user.Grade,
Position: user.Position,
JobTitle: user.JobTitle,
Metadata: user.Metadata,
Tenant: user.Tenant,
CreatedAt: formatTime(user.CreatedAt),
UpdatedAt: formatTime(user.UpdatedAt),
}
if h.TenantService != nil {
if joined, err := h.TenantService.ListJoinedTenants(ctx, user.ID); err == nil {
summary.JoinedTenants = joined
}
}
return summary
}
func (h *UserHandler) normalizePhoneNumber(phone string) string {
return normalizePhoneNumber(phone)
}
@@ -3302,18 +3494,7 @@ func normalizeKratosState(status *string) string {
}
func normalizePhoneNumber(phone string) string {
normalized := strings.ReplaceAll(phone, "-", "")
normalized = strings.ReplaceAll(normalized, " ", "")
if normalized == "" {
return ""
}
if strings.HasPrefix(normalized, "010") {
return "+82" + normalized[1:]
}
if strings.HasPrefix(normalized, "82") {
return "+" + normalized
}
return normalized
return domain.NormalizePhoneNumber(phone)
}
func (h *UserHandler) validateMetadata(metadata map[string]any, schema []any, checkRequired bool) error {