diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx
index 649b2905..502fe16d 100644
--- a/adminfront/src/features/users/UserDetailPage.tsx
+++ b/adminfront/src/features/users/UserDetailPage.tsx
@@ -583,8 +583,8 @@ function UserDetailPage() {
name:
typeof metadata.primaryTenantName === "string"
? metadata.primaryTenantName
- : user.tenant?.name || user.companyCode || "",
- slug: user.companyCode,
+ : user.tenant?.name || user.tenantSlug || "",
+ slug: user.tenantSlug,
}
: null;
const fallbackAppointment =
@@ -603,7 +603,7 @@ function UserDetailPage() {
role: user.role,
status: user.status,
tenantSlug:
- user.companyCode ||
+ user.tenantSlug ||
user.joinedTenants?.find(
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
)?.slug ||
@@ -624,7 +624,6 @@ function UserDetailPage() {
hanmacFamilyTenantId,
);
const isPersonalUser =
- user.companyCode === personalTenant.slug ||
user.tenantSlug === personalTenant.slug ||
user.tenant?.id === personalTenant.id ||
user.tenant?.slug === personalTenant.slug ||
@@ -896,7 +895,7 @@ function UserDetailPage() {
>
{user.tenant?.name ||
- user.companyCode ||
+ user.tenantSlug ||
user.joinedTenants?.find(
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
)?.name ||
diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx
index 7e175093..923c05e1 100644
--- a/adminfront/src/features/users/UserListPage.tsx
+++ b/adminfront/src/features/users/UserListPage.tsx
@@ -778,7 +778,7 @@ function UserListPage() {
{user.tenant?.name ||
- user.companyCode ||
+ user.tenantSlug ||
t("ui.common.unassigned", "미배정")}
{user.department && (
diff --git a/adminfront/src/features/users/orgChartPicker.ts b/adminfront/src/features/users/orgChartPicker.ts
index 3bc61a76..05b7a511 100644
--- a/adminfront/src/features/users/orgChartPicker.ts
+++ b/adminfront/src/features/users/orgChartPicker.ts
@@ -160,7 +160,6 @@ export function isHanmacFamilyUser
(
...metadataAppointments.map((appointment) =>
tenantById.get(appointment.id ?? ""),
),
- tenantBySlug.get(user.companyCode?.toLowerCase() ?? ""),
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
];
diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go
index 9c842e03..b3e79bae 100644
--- a/backend/internal/handler/auth_handler.go
+++ b/backend/internal/handler/auth_handler.go
@@ -4564,14 +4564,6 @@ func (h *AuthHandler) hydrateResolvedProfile(ctx context.Context, profile *domai
profile.Tenant = tenant
}
}
- if profile.Tenant == nil && profile.CompanyCode != "" {
- if tenant, err := h.TenantService.GetTenantBySlug(ctx, profile.CompanyCode); err == nil && tenant != nil {
- profile.Tenant = tenant
- if profile.TenantID == nil || *profile.TenantID == "" {
- profile.TenantID = &tenant.ID
- }
- }
- }
}
if h.TenantService != nil {
diff --git a/backend/internal/handler/client_tenant_access.go b/backend/internal/handler/client_tenant_access.go
index 097be621..8a4c2313 100644
--- a/backend/internal/handler/client_tenant_access.go
+++ b/backend/internal/handler/client_tenant_access.go
@@ -258,22 +258,11 @@ func resolveCurrentTenantDetails(c *fiber.Ctx, tenantSvc service.TenantService,
}
}
}
- if strings.TrimSpace(profile.CompanyCode) != "" {
- if tenant, err := tenantSvc.GetTenantBySlug(c.Context(), strings.TrimSpace(profile.CompanyCode)); err == nil && tenant != nil {
- return tenantAccessDeniedTenant{
- ID: strings.TrimSpace(tenant.ID),
- Slug: strings.TrimSpace(tenant.Slug),
- Name: strings.TrimSpace(tenant.Name),
- Identifier: firstNonEmptyString(strings.TrimSpace(tenant.Slug), strings.TrimSpace(tenant.ID)),
- }
- }
- }
}
return tenantAccessDeniedTenant{
ID: strings.TrimSpace(pointerValue(profile.TenantID)),
- Slug: strings.TrimSpace(profile.CompanyCode),
- Identifier: firstNonEmptyString(strings.TrimSpace(profile.CompanyCode), strings.TrimSpace(pointerValue(profile.TenantID))),
+ Identifier: strings.TrimSpace(pointerValue(profile.TenantID)),
}
}
diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go
index fae65ce8..8f3ed43d 100644
--- a/backend/internal/handler/dev_handler.go
+++ b/backend/internal/handler/dev_handler.go
@@ -603,7 +603,6 @@ func manageableTenantKeysFromProfile(profile *domain.UserProfileResponse) map[st
}
}
- addKey(profile.CompanyCode)
if profile.TenantID != nil {
addKey(*profile.TenantID)
}
@@ -631,8 +630,6 @@ func canAccessIdentityByTenant(profile *domain.UserProfileResponse, identity ser
for _, raw := range []string{
extractTraitString(identity.Traits, "tenant_id"),
- extractTraitString(identity.Traits, "companyCode"),
- extractTraitString(identity.Traits, "company_code"),
} {
if _, ok := keys[strings.ToLower(strings.TrimSpace(raw))]; ok {
return true
diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go
index cf909220..ba446949 100644
--- a/backend/internal/handler/tenant_handler.go
+++ b/backend/internal/handler/tenant_handler.go
@@ -266,15 +266,6 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
}
- // Try to find by companyCode if needed
- if profile.CompanyCode != "" {
- for _, t := range allTenants {
- if strings.EqualFold(t.Slug, profile.CompanyCode) {
- baseTenantIDs = append(baseTenantIDs, t.ID)
- }
- }
- }
-
parentMap := make(map[string]string)
for _, t := range allTenants {
if t.ParentID != nil {
diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go
index 588485b1..4a6abcbf 100644
--- a/backend/internal/handler/user_handler.go
+++ b/backend/internal/handler/user_handler.go
@@ -261,9 +261,6 @@ func identityTenantAccessKeys(traits map[string]interface{}) []string {
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
keys = append(keys, tenantID)
}
- if legacySlug := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "companyCode"))); legacySlug != "" {
- keys = append(keys, legacySlug)
- }
return keys
}
@@ -276,6 +273,60 @@ func anyTenantKeyAllowed(keys []string, allowed map[string]bool) bool {
return false
}
+func profileTenantAccessKeys(profile *domain.UserProfileResponse) map[string]bool {
+ allowed := make(map[string]bool)
+ if profile == nil {
+ return allowed
+ }
+ if profile.TenantID != nil {
+ if id := strings.ToLower(strings.TrimSpace(*profile.TenantID)); id != "" {
+ allowed[id] = true
+ }
+ }
+ for _, tenant := range profile.ManageableTenants {
+ if id := strings.ToLower(strings.TrimSpace(tenant.ID)); id != "" {
+ allowed[id] = true
+ }
+ if slug := strings.ToLower(strings.TrimSpace(tenant.Slug)); slug != "" {
+ allowed[slug] = true
+ }
+ }
+ for _, tenant := range profile.JoinedTenants {
+ if id := strings.ToLower(strings.TrimSpace(tenant.ID)); id != "" {
+ allowed[id] = true
+ }
+ if slug := strings.ToLower(strings.TrimSpace(tenant.Slug)); slug != "" {
+ allowed[slug] = true
+ }
+ }
+ return allowed
+}
+
+func profileCanAccessTenant(profile *domain.UserProfileResponse, tenantID, tenantSlug string) bool {
+ allowed := profileTenantAccessKeys(profile)
+ if id := strings.ToLower(strings.TrimSpace(tenantID)); id != "" && allowed[id] {
+ return true
+ }
+ if slug := strings.ToLower(strings.TrimSpace(tenantSlug)); slug != "" && allowed[slug] {
+ return true
+ }
+ return false
+}
+
+func userTenantID(user domain.User) string {
+ if user.TenantID == nil {
+ return ""
+ }
+ return strings.TrimSpace(*user.TenantID)
+}
+
+func userTenantSlug(user domain.User) string {
+ if user.Tenant != nil {
+ return strings.TrimSpace(user.Tenant.Slug)
+ }
+ return ""
+}
+
type userSummary struct {
ID string `json:"id"`
Email string `json:"email"`
@@ -352,10 +403,6 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
manageableSlugs[strings.ToLower(t.ID)] = true
baseTenantIDs = append(baseTenantIDs, t.ID)
}
- // Include primary tenant slug if not already there
- if profile.CompanyCode != "" {
- manageableSlugs[strings.ToLower(profile.CompanyCode)] = true
- }
if profile.TenantID != nil {
manageableSlugs[strings.ToLower(*profile.TenantID)] = true
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
@@ -423,21 +470,11 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
for _, identity := range identities {
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
- compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
- secondaryCodes := extractTraitStringArray(identity.Traits, "companyCodes")
// Tenant Admin & Member filtering
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
- hasAccess := manageableSlugs[compCode] || manageableSlugs[tID]
- if !hasAccess && len(secondaryCodes) > 0 {
- for _, code := range secondaryCodes {
- if manageableSlugs[strings.ToLower(code)] {
- hasAccess = true
- break
- }
- }
- }
+ hasAccess := manageableSlugs[tID]
if !hasAccess {
continue
}
@@ -445,34 +482,16 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// Dedicated tenantSlug filter
if tenantSlug != "" {
- matches := strings.EqualFold(compCode, tenantSlug) || tID == targetTenantID
- if !matches && len(secondaryCodes) > 0 {
- for _, code := range secondaryCodes {
- if strings.EqualFold(code, tenantSlug) {
- matches = true
- break
- }
- }
- }
+ matches := tID == targetTenantID
if !matches {
continue
}
}
- // Search filtering (Keyword search in email, name, or companyCode)
+ // Search filtering
if search != "" {
matchesSearch := strings.Contains(email, searchLower) ||
- strings.Contains(name, searchLower) ||
- strings.Contains(strings.ToLower(compCode), searchLower)
-
- if !matchesSearch && len(secondaryCodes) > 0 {
- for _, code := range secondaryCodes {
- if strings.Contains(strings.ToLower(code), searchLower) {
- matchesSearch = true
- break
- }
- }
- }
+ strings.Contains(name, searchLower)
if !matchesSearch {
continue
@@ -560,22 +579,8 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
// [New] Check access scope
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
if requester != nil && requester.Role == domain.RoleTenantAdmin {
- compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
-
- // Check if the target user's companyCode is in requester's manageable tenants
- allowed := false
- for _, t := range requester.ManageableTenants {
- if strings.ToLower(t.Slug) == compCode {
- allowed = true
- break
- }
- }
- // Also check primary company code
- if !allowed && strings.ToLower(requester.CompanyCode) == compCode {
- allowed = true
- }
-
- if !allowed {
+ allowedKeys := profileTenantAccessKeys(requester)
+ if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
}
}
@@ -1043,7 +1048,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
// Role-based access check
if requester != nil && requester.Role == domain.RoleTenantAdmin {
- if tenantSlug != requester.CompanyCode {
+ if !profileCanAccessTenant(requester, tItem.ID, tenantSlug) {
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
continue
}
@@ -1057,11 +1062,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
if appointmentTenantSlug == "" {
continue
}
- if requester != nil && requester.Role == domain.RoleTenantAdmin && appointmentTenantSlug != requester.CompanyCode {
- results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
- appointmentFailed = true
- break
- }
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
if !exists {
@@ -1072,6 +1072,11 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
break
}
}
+ if requester != nil && requester.Role == domain.RoleTenantAdmin && !profileCanAccessTenant(requester, appointmentTenant.ID, appointmentTenant.Slug) {
+ results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
+ appointmentFailed = true
+ break
+ }
appointment := make(map[string]any, len(rawAppointment)+3)
for key, value := range rawAppointment {
@@ -1244,7 +1249,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
Phone: normalizePhoneNumber(item.Phone),
Role: role,
Status: "active",
- CompanyCode: tenantSlug,
Department: dept,
Grade: strings.TrimSpace(item.Grade),
AffiliationType: "internal",
@@ -1376,9 +1380,10 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
if profile != nil && requesterRole == domain.RoleTenantAdmin {
for _, t := range profile.ManageableTenants {
manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug))
+ manageableSlugs = append(manageableSlugs, strings.ToLower(t.ID))
}
- if profile.CompanyCode != "" {
- manageableSlugs = append(manageableSlugs, strings.ToLower(profile.CompanyCode))
+ if profile.TenantID != nil {
+ manageableSlugs = append(manageableSlugs, strings.ToLower(*profile.TenantID))
}
}
@@ -1396,7 +1401,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
slugMap[s] = true
}
for _, u := range users {
- if slugMap[strings.ToLower(u.CompanyCode)] {
+ if slugMap[strings.ToLower(userTenantSlug(u))] || slugMap[strings.ToLower(userTenantID(u))] {
filtered = append(filtered, u)
}
}
@@ -1452,7 +1457,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
u.Name,
u.Phone,
u.Status,
- u.CompanyCode,
+ userTenantSlug(u),
u.Grade,
u.Position,
u.JobTitle,
@@ -1466,7 +1471,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
u.Phone,
u.Status,
tenantID,
- u.CompanyCode,
+ userTenantSlug(u),
u.Grade,
u.Position,
u.JobTitle,
@@ -1534,7 +1539,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
*req.Role = role
}
- // [New] Pre-fetch tenant cache if companyCode is being changed
+ // Pre-fetch tenant cache if tenantSlug is being changed.
type tenantCacheItem struct {
ID string
Schema []interface{}
@@ -1543,12 +1548,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
manageableSlugs := make(map[string]bool)
if requester.Role == domain.RoleTenantAdmin {
- for _, t := range requester.ManageableTenants {
- manageableSlugs[strings.ToLower(t.Slug)] = true
- }
- if requester.CompanyCode != "" {
- manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
- }
+ manageableSlugs = profileTenantAccessKeys(requester)
}
results := make([]map[string]any, 0, len(req.UserIDs))
@@ -1576,9 +1576,14 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
continue
}
- // If changing companyCode, must be to a manageable one
if req.CompanyCode != nil {
- if !manageableSlugs[strings.ToLower(*req.CompanyCode)] {
+ targetAllowed := manageableSlugs[strings.ToLower(*req.CompanyCode)]
+ if !targetAllowed && h.TenantService != nil {
+ if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
+ targetAllowed = manageableSlugs[strings.ToLower(tenant.ID)]
+ }
+ }
+ if !targetAllowed {
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: target tenant not manageable"})
continue
}
@@ -1646,9 +1651,6 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
if req.Status != nil {
localUser.Status = *req.Status
}
- if req.CompanyCode != nil {
- localUser.CompanyCode = *req.CompanyCode
- }
if req.Department != nil {
localUser.Department = *req.Department
}
@@ -1659,7 +1661,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
localUser.JobTitle = *req.JobTitle
}
- // Resolve TenantID if changing companyCode
+ // Resolve TenantID if changing tenantSlug.
if req.CompanyCode != nil && h.TenantService != nil {
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
localUser.TenantID = &tenant.ID
@@ -1705,12 +1707,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
manageableSlugs := make(map[string]bool)
if requester.Role == domain.RoleTenantAdmin {
- for _, t := range requester.ManageableTenants {
- manageableSlugs[strings.ToLower(t.Slug)] = true
- }
- if requester.CompanyCode != "" {
- manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
- }
+ manageableSlugs = profileTenantAccessKeys(requester)
}
results := make([]map[string]any, 0, len(req.UserIDs))
@@ -1802,12 +1799,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if requester.TenantID != nil {
allowed[strings.ToLower(*requester.TenantID)] = true
}
- if requester.CompanyCode != "" {
- allowed[strings.ToLower(requester.CompanyCode)] = true
- }
for _, tenant := range requester.ManageableTenants {
allowed[strings.ToLower(tenant.ID)] = true
- allowed[strings.ToLower(tenant.Slug)] = true
}
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant")
@@ -1855,10 +1848,19 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
*req.Role = role
}
- // [New] Tenant Admin restriction: Cannot change companyCode (except when adding/removing secondary membership)
+ // Tenant admins can only move users within tenants they can manage.
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
- if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
- return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
+ if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil {
+ targetSlug := strings.TrimSpace(*req.CompanyCode)
+ targetAllowed := profileCanAccessTenant(requester, "", targetSlug)
+ if !targetAllowed && h.TenantService != nil && targetSlug != "" {
+ if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), targetSlug); err == nil && tenant != nil {
+ targetAllowed = profileCanAccessTenant(requester, tenant.ID, tenant.Slug)
+ }
+ }
+ if !targetAllowed {
+ return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
+ }
}
}
@@ -1884,19 +1886,20 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
}
} else {
- // Legacy/Flat metadata - validate using primary tenant schema
- schemaCompCode := extractTraitString(identity.Traits, "companyCode")
+ schemaTenantSlug := ""
if req.CompanyCode != nil {
- schemaCompCode = *req.CompanyCode
+ schemaTenantSlug = *req.CompanyCode
}
- if schemaCompCode != "" && h.TenantService != nil {
- tenant, err := h.TenantService.GetTenantBySlug(c.Context(), schemaCompCode)
- if err == nil && tenant != nil {
- if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
- // For flat metadata, we validate the whole req.Metadata against this schema
- if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
- return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
- }
+ var tenant *domain.Tenant
+ if schemaTenantSlug != "" && h.TenantService != nil {
+ tenant, _ = h.TenantService.GetTenantBySlug(c.Context(), schemaTenantSlug)
+ } else if tenantID := extractTraitString(identity.Traits, "tenant_id"); tenantID != "" && h.TenantService != nil {
+ tenant, _ = h.TenantService.GetTenant(c.Context(), tenantID)
+ }
+ if tenant != nil {
+ if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
+ if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
+ return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
}
}
}
@@ -2106,24 +2109,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
}
- // [Self-Healing] Sync all companyCodes to Keto
if h.KetoOutboxRepo != nil && h.TenantService != nil {
- if codes, ok := updated.Traits["companyCodes"].([]interface{}); ok {
- for _, cVal := range codes {
- if cStr, ok := cVal.(string); ok && cStr != "" {
- if tenant, err := h.TenantService.GetTenantBySlug(bgCtx, cStr); err == nil && tenant != nil {
- _ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
- Namespace: "Tenant",
- Object: tenant.ID,
- Relation: "members",
- Subject: "User:" + updatedLocalUser.ID,
- Action: domain.KetoOutboxActionCreate,
- })
- }
- }
- }
- } else if updatedLocalUser.TenantID != nil {
- // Fallback if companyCodes doesn't exist
+ if updatedLocalUser.TenantID != nil {
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
Namespace: "Tenant",
Object: *updatedLocalUser.TenantID,
@@ -2181,12 +2168,8 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
if requester.TenantID != nil {
allowed[strings.ToLower(*requester.TenantID)] = true
}
- if requester.CompanyCode != "" {
- allowed[strings.ToLower(requester.CompanyCode)] = true
- }
for _, tenant := range requester.ManageableTenants {
allowed[strings.ToLower(tenant.ID)] = true
- allowed[strings.ToLower(tenant.Slug)] = true
}
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
diff --git a/backend/internal/handler/user_handler_test.go b/backend/internal/handler/user_handler_test.go
index bd7d9f4d..85a61eb5 100644
--- a/backend/internal/handler/user_handler_test.go
+++ b/backend/internal/handler/user_handler_test.go
@@ -217,21 +217,23 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
app.Get("/users/export", h.ExportUsersCSV)
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
+ tenantID := "tenant-uuid"
mockRepo.On("List", mock.Anything, 0, 10000, "", "test-tenant").
Return([]domain.User{
{
- ID: "u-1",
- Email: "user@test.com",
- Name: "Test User",
- Phone: "010-1111-2222",
- Role: domain.RoleSuperAdmin,
- Status: "active",
- CompanyCode: "test-tenant",
- Department: "Legacy Department",
- Grade: "책임",
- Position: "팀장",
- JobTitle: "플랫폼 운영",
- CreatedAt: createdAt,
+ ID: "u-1",
+ Email: "user@test.com",
+ Name: "Test User",
+ Phone: "010-1111-2222",
+ Role: domain.RoleSuperAdmin,
+ Status: "active",
+ TenantID: &tenantID,
+ Tenant: &domain.Tenant{ID: tenantID, Slug: "test-tenant"},
+ Department: "Legacy Department",
+ Grade: "책임",
+ Position: "팀장",
+ JobTitle: "플랫폼 운영",
+ CreatedAt: createdAt,
},
}, int64(1), nil).Once()
@@ -243,7 +245,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
bodyBytes, _ := io.ReadAll(resp.Body)
body := strings.TrimPrefix(string(bodyBytes), "\ufeff")
assert.Contains(t, body, "user_id,Email,Name,Phone,Status,tenant_id,tenant_slug,Grade,Position,JobTitle,CreatedAt")
- assert.Contains(t, body, "u-1,user@test.com,Test User,010-1111-2222,active,,test-tenant,책임,팀장")
+ assert.Contains(t, body, "u-1,user@test.com,Test User,010-1111-2222,active,tenant-uuid,test-tenant,책임,팀장")
assert.NotContains(t, body, "Role")
assert.NotContains(t, body, "Department")
mockRepo.AssertExpectations(t)
@@ -267,17 +269,17 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
Return([]domain.User{
{
- ID: "user-uuid",
- Email: "user@test.com",
- Name: "Test User",
- Phone: "010-1111-2222",
- Status: "active",
- CompanyCode: "test-tenant",
- TenantID: &tenantID,
- Grade: "책임",
- Position: "팀장",
- JobTitle: "플랫폼 운영",
- CreatedAt: createdAt,
+ ID: "user-uuid",
+ Email: "user@test.com",
+ Name: "Test User",
+ Phone: "010-1111-2222",
+ Status: "active",
+ TenantID: &tenantID,
+ Tenant: &domain.Tenant{ID: tenantID, Slug: "test-tenant"},
+ Grade: "책임",
+ Position: "팀장",
+ JobTitle: "플랫폼 운영",
+ CreatedAt: createdAt,
},
}, int64(1), nil).Once()
@@ -296,6 +298,64 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
mockRepo.AssertExpectations(t)
}
+func TestUserHandler_ExportUsersCSV_TenantAdminFiltersByTenantIDWithoutCompanyCode(t *testing.T) {
+ app := fiber.New()
+ mockRepo := new(MockUserRepoForHandler)
+ h := &UserHandler{UserRepo: mockRepo}
+
+ tenantID := "tenant-uuid"
+ app.Use(func(c *fiber.Ctx) error {
+ c.Locals("user_profile", &domain.UserProfileResponse{
+ Role: domain.RoleTenantAdmin,
+ TenantID: &tenantID,
+ ManageableTenants: []domain.Tenant{
+ {ID: tenantID, Slug: "test-tenant"},
+ },
+ })
+ return c.Next()
+ })
+ app.Get("/users/export", h.ExportUsersCSV)
+
+ createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
+ otherTenantID := "other-tenant-uuid"
+ mockRepo.On("List", mock.Anything, 0, 10000, "", "").
+ Return([]domain.User{
+ {
+ ID: "user-uuid",
+ Email: "user@test.com",
+ Name: "Test User",
+ Phone: "010-1111-2222",
+ Status: "active",
+ TenantID: &tenantID,
+ Tenant: &domain.Tenant{ID: tenantID, Slug: "test-tenant"},
+ Grade: "책임",
+ Position: "팀장",
+ JobTitle: "플랫폼 운영",
+ CreatedAt: createdAt,
+ },
+ {
+ ID: "other-user",
+ Email: "other@test.com",
+ Name: "Other User",
+ Status: "active",
+ TenantID: &otherTenantID,
+ Tenant: &domain.Tenant{ID: otherTenantID, Slug: "other-tenant"},
+ CreatedAt: createdAt,
+ },
+ }, int64(2), nil).Once()
+
+ req := httptest.NewRequest("GET", "/users/export?includeIds=false", nil)
+ resp, err := app.Test(req)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ body := strings.TrimPrefix(string(bodyBytes), "\ufeff")
+ assert.Contains(t, body, "user@test.com,Test User,010-1111-2222,active,test-tenant")
+ assert.NotContains(t, body, "other@test.com")
+ mockRepo.AssertExpectations(t)
+}
+
func TestUserHandler_BulkCreateUsers(t *testing.T) {
app := fiber.New()
mockKratos := new(MockKratosAdmin)
@@ -1049,12 +1109,15 @@ func TestUserHandler_UpdateUser_AdminOnlyField(t *testing.T) {
})
t.Run("Fail - Regular user updating admin_only field", func(t *testing.T) {
+ tenantID := "t-123"
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
ID: "u-1",
- Traits: map[string]interface{}{"email": "user@test.com", "companyCode": "test-tenant"},
+ Traits: map[string]interface{}{"email": "user@test.com", "tenant_id": tenantID},
}, nil)
- mockTenant.On("GetTenantBySlug", mock.Anything, "test-tenant").Return(&domain.Tenant{
+ mockTenant.On("GetTenant", mock.Anything, tenantID).Return(&domain.Tenant{
+ ID: tenantID,
+ Slug: "test-tenant",
Config: domain.JSONMap{
"userSchema": []interface{}{
map[string]interface{}{"key": "salary", "adminOnly": true},
diff --git a/orgfront/src/features/orgchart/pickerTree.ts b/orgfront/src/features/orgchart/pickerTree.ts
index c1acbdf9..7aa12922 100644
--- a/orgfront/src/features/orgchart/pickerTree.ts
+++ b/orgfront/src/features/orgchart/pickerTree.ts
@@ -6,9 +6,7 @@ import { filterTenantsByVisibility } from "./tenantVisibility";
import { getOrgChartUserDisplayName } from "./userDisplay";
function getUserTenantSlug(user: UserSummary) {
- return (
- user.companyCode?.toLowerCase() || user.tenantSlug?.toLowerCase() || ""
- );
+ return user.tenantSlug?.toLowerCase() || "";
}
function isOrgFrontTenantType(tenant: TenantSummary) {
diff --git a/orgfront/src/features/orgchart/routes/OrgChartPage.tsx b/orgfront/src/features/orgchart/routes/OrgChartPage.tsx
index 42a7eea3..68d8c1ce 100644
--- a/orgfront/src/features/orgchart/routes/OrgChartPage.tsx
+++ b/orgfront/src/features/orgchart/routes/OrgChartPage.tsx
@@ -990,8 +990,8 @@ function isSystemGlobalUser(user: UserSummary) {
normalizedRole === "system-admin" ||
isSystemGlobalTenant(user.tenant) ||
isSystemGlobalTenant({
- id: user.companyCode || user.tenantSlug || "",
- slug: user.companyCode || user.tenantSlug || "",
+ id: user.tenantSlug || "",
+ slug: user.tenantSlug || "",
type: user.role,
name: user.role,
})
@@ -1145,8 +1145,7 @@ function buildUsersMap(
if (!isVisibleOrgChartUser(user)) continue;
const slugs = new Set();
- const primarySlug =
- user.companyCode?.toLowerCase() || user.tenantSlug?.toLowerCase() || "";
+ const primarySlug = user.tenantSlug?.toLowerCase() || "";
if (
primarySlug &&
!isSystemGlobalTenant({
diff --git a/orgfront/src/features/profile/ProfilePage.tsx b/orgfront/src/features/profile/ProfilePage.tsx
index e19164bc..bc9c8f9a 100644
--- a/orgfront/src/features/profile/ProfilePage.tsx
+++ b/orgfront/src/features/profile/ProfilePage.tsx
@@ -58,8 +58,7 @@ function ProfilePage() {
profile.tenantId ||
auth.user?.profile?.tenant_id?.toString() ||
"-";
- const displayCompanyCode =
- profile.companyCode || auth.user?.profile?.companyCode?.toString() || "-";
+ const displayTenantSlug = profile.tenant?.slug || profile.tenantId || "-";
return (
@@ -160,9 +159,9 @@ function ProfilePage() {
- {t("ui.dev.profile.org.company_code", "회사 코드")}
+ {t("ui.dev.profile.org.tenant_slug", "테넌트 Slug")}
-
{displayCompanyCode}
+
{displayTenantSlug}
diff --git a/userfront/assets/translations/en.toml b/userfront/assets/translations/en.toml
index 4393a094..af3ad5f9 100644
--- a/userfront/assets/translations/en.toml
+++ b/userfront/assets/translations/en.toml
@@ -599,6 +599,7 @@ department = "Department"
email = "Email"
name = "Name"
tenant = "Tenant"
+tenant_slug = "Tenant Slug"
[ui.userfront.profile.password]
change = "Change"
diff --git a/userfront/assets/translations/ko.toml b/userfront/assets/translations/ko.toml
index 8a84d53f..d953140d 100644
--- a/userfront/assets/translations/ko.toml
+++ b/userfront/assets/translations/ko.toml
@@ -821,6 +821,7 @@ department = "소속"
email = "이메일"
name = "이름"
tenant = "소속 테넌트"
+tenant_slug = "테넌트 Slug"
[ui.userfront.profile.password]
change = "비밀번호 변경"
diff --git a/userfront/lib/features/auth/presentation/error_screen.dart b/userfront/lib/features/auth/presentation/error_screen.dart
index f1dc9d81..761456d6 100644
--- a/userfront/lib/features/auth/presentation/error_screen.dart
+++ b/userfront/lib/features/auth/presentation/error_screen.dart
@@ -125,11 +125,6 @@ class _ErrorScreenState extends State {
}
}
- final companyCode = profile['companyCode']?.toString().trim() ?? '';
- if (companyCode.isNotEmpty) {
- return companyCode;
- }
-
return '';
}
@@ -259,11 +254,6 @@ class _ErrorScreenState extends State {
appendLabel(tenant);
}
- final companyCode = profile['companyCode']?.toString().trim() ?? '';
- if (companyCode.isNotEmpty) {
- appendLabel(companyCode);
- }
-
return labels;
}
diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart
index b1b2a04e..4ed3e63b 100644
--- a/userfront/lib/features/profile/presentation/pages/profile_page.dart
+++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart
@@ -1178,11 +1178,11 @@ class _ProfilePageState extends ConsumerState {
profile.tenant!.name,
),
],
- if (profile.companyCode.isNotEmpty) ...[
+ if (profile.tenant?.slug.isNotEmpty ?? false) ...[
const Divider(height: 24),
_buildReadOnlyTile(
- tr('ui.userfront.profile.field.company_code'),
- profile.companyCode,
+ tr('ui.userfront.profile.field.tenant_slug'),
+ profile.tenant!.slug,
),
],
],
diff --git a/userfront/lib/i18n_data.dart b/userfront/lib/i18n_data.dart
index 816137ed..cee5b67e 100644
--- a/userfront/lib/i18n_data.dart
+++ b/userfront/lib/i18n_data.dart
@@ -1930,6 +1930,7 @@ const Map koStrings = {
"ui.userfront.profile.field.email": "이메일",
"ui.userfront.profile.field.name": "이름",
"ui.userfront.profile.field.tenant": "소속 테넌트",
+ "ui.userfront.profile.field.tenant_slug": "테넌트 Slug",
"ui.userfront.profile.manage": "프로필 관리",
"ui.userfront.profile.password.change": "비밀번호 변경",
"ui.userfront.profile.password.confirm": "새 비밀번호 확인",
@@ -4125,6 +4126,7 @@ const Map enStrings = {
"ui.userfront.profile.field.email": "Email",
"ui.userfront.profile.field.name": "Name",
"ui.userfront.profile.field.tenant": "Tenant",
+ "ui.userfront.profile.field.tenant_slug": "Tenant Slug",
"ui.userfront.profile.manage": "Manage profile",
"ui.userfront.profile.password.change": "Change",
"ui.userfront.profile.password.confirm": "Confirm",