forked from baron/baron-sso
refactoring
This commit is contained in:
@@ -583,8 +583,8 @@ function UserDetailPage() {
|
|||||||
name:
|
name:
|
||||||
typeof metadata.primaryTenantName === "string"
|
typeof metadata.primaryTenantName === "string"
|
||||||
? metadata.primaryTenantName
|
? metadata.primaryTenantName
|
||||||
: user.tenant?.name || user.companyCode || "",
|
: user.tenant?.name || user.tenantSlug || "",
|
||||||
slug: user.companyCode,
|
slug: user.tenantSlug,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
const fallbackAppointment =
|
const fallbackAppointment =
|
||||||
@@ -603,7 +603,7 @@ function UserDetailPage() {
|
|||||||
role: user.role,
|
role: user.role,
|
||||||
status: user.status,
|
status: user.status,
|
||||||
tenantSlug:
|
tenantSlug:
|
||||||
user.companyCode ||
|
user.tenantSlug ||
|
||||||
user.joinedTenants?.find(
|
user.joinedTenants?.find(
|
||||||
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
|
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
|
||||||
)?.slug ||
|
)?.slug ||
|
||||||
@@ -624,7 +624,6 @@ function UserDetailPage() {
|
|||||||
hanmacFamilyTenantId,
|
hanmacFamilyTenantId,
|
||||||
);
|
);
|
||||||
const isPersonalUser =
|
const isPersonalUser =
|
||||||
user.companyCode === personalTenant.slug ||
|
|
||||||
user.tenantSlug === personalTenant.slug ||
|
user.tenantSlug === personalTenant.slug ||
|
||||||
user.tenant?.id === personalTenant.id ||
|
user.tenant?.id === personalTenant.id ||
|
||||||
user.tenant?.slug === personalTenant.slug ||
|
user.tenant?.slug === personalTenant.slug ||
|
||||||
@@ -896,7 +895,7 @@ function UserDetailPage() {
|
|||||||
>
|
>
|
||||||
<Building2 size={12} className="mr-1.5" />
|
<Building2 size={12} className="mr-1.5" />
|
||||||
{user.tenant?.name ||
|
{user.tenant?.name ||
|
||||||
user.companyCode ||
|
user.tenantSlug ||
|
||||||
user.joinedTenants?.find(
|
user.joinedTenants?.find(
|
||||||
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
|
(t) => t.type === "COMPANY" || t.type === "COMPANY_GROUP",
|
||||||
)?.name ||
|
)?.name ||
|
||||||
|
|||||||
@@ -778,7 +778,7 @@ function UserListPage() {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{user.tenant?.name ||
|
{user.tenant?.name ||
|
||||||
user.companyCode ||
|
user.tenantSlug ||
|
||||||
t("ui.common.unassigned", "미배정")}
|
t("ui.common.unassigned", "미배정")}
|
||||||
</span>
|
</span>
|
||||||
{user.department && (
|
{user.department && (
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ export function isHanmacFamilyUser<T extends TenantFilterTarget>(
|
|||||||
...metadataAppointments.map((appointment) =>
|
...metadataAppointments.map((appointment) =>
|
||||||
tenantById.get(appointment.id ?? ""),
|
tenantById.get(appointment.id ?? ""),
|
||||||
),
|
),
|
||||||
tenantBySlug.get(user.companyCode?.toLowerCase() ?? ""),
|
|
||||||
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
|
tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -4564,14 +4564,6 @@ func (h *AuthHandler) hydrateResolvedProfile(ctx context.Context, profile *domai
|
|||||||
profile.Tenant = tenant
|
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 {
|
if h.TenantService != nil {
|
||||||
|
|||||||
@@ -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{
|
return tenantAccessDeniedTenant{
|
||||||
ID: strings.TrimSpace(pointerValue(profile.TenantID)),
|
ID: strings.TrimSpace(pointerValue(profile.TenantID)),
|
||||||
Slug: strings.TrimSpace(profile.CompanyCode),
|
Identifier: strings.TrimSpace(pointerValue(profile.TenantID)),
|
||||||
Identifier: firstNonEmptyString(strings.TrimSpace(profile.CompanyCode), strings.TrimSpace(pointerValue(profile.TenantID))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -603,7 +603,6 @@ func manageableTenantKeysFromProfile(profile *domain.UserProfileResponse) map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addKey(profile.CompanyCode)
|
|
||||||
if profile.TenantID != nil {
|
if profile.TenantID != nil {
|
||||||
addKey(*profile.TenantID)
|
addKey(*profile.TenantID)
|
||||||
}
|
}
|
||||||
@@ -631,8 +630,6 @@ func canAccessIdentityByTenant(profile *domain.UserProfileResponse, identity ser
|
|||||||
|
|
||||||
for _, raw := range []string{
|
for _, raw := range []string{
|
||||||
extractTraitString(identity.Traits, "tenant_id"),
|
extractTraitString(identity.Traits, "tenant_id"),
|
||||||
extractTraitString(identity.Traits, "companyCode"),
|
|
||||||
extractTraitString(identity.Traits, "company_code"),
|
|
||||||
} {
|
} {
|
||||||
if _, ok := keys[strings.ToLower(strings.TrimSpace(raw))]; ok {
|
if _, ok := keys[strings.ToLower(strings.TrimSpace(raw))]; ok {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -266,15 +266,6 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
|||||||
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
|
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)
|
parentMap := make(map[string]string)
|
||||||
for _, t := range allTenants {
|
for _, t := range allTenants {
|
||||||
if t.ParentID != nil {
|
if t.ParentID != nil {
|
||||||
|
|||||||
@@ -261,9 +261,6 @@ func identityTenantAccessKeys(traits map[string]interface{}) []string {
|
|||||||
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
|
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
|
||||||
keys = append(keys, tenantID)
|
keys = append(keys, tenantID)
|
||||||
}
|
}
|
||||||
if legacySlug := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "companyCode"))); legacySlug != "" {
|
|
||||||
keys = append(keys, legacySlug)
|
|
||||||
}
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +273,60 @@ func anyTenantKeyAllowed(keys []string, allowed map[string]bool) bool {
|
|||||||
return false
|
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 {
|
type userSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@@ -352,10 +403,6 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
|||||||
manageableSlugs[strings.ToLower(t.ID)] = true
|
manageableSlugs[strings.ToLower(t.ID)] = true
|
||||||
baseTenantIDs = append(baseTenantIDs, t.ID)
|
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 {
|
if profile.TenantID != nil {
|
||||||
manageableSlugs[strings.ToLower(*profile.TenantID)] = true
|
manageableSlugs[strings.ToLower(*profile.TenantID)] = true
|
||||||
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
|
baseTenantIDs = append(baseTenantIDs, *profile.TenantID)
|
||||||
@@ -423,21 +470,11 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
|||||||
for _, identity := range identities {
|
for _, identity := range identities {
|
||||||
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
|
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
|
||||||
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
|
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
|
||||||
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
|
||||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||||
secondaryCodes := extractTraitStringArray(identity.Traits, "companyCodes")
|
|
||||||
|
|
||||||
// Tenant Admin & Member filtering
|
// Tenant Admin & Member filtering
|
||||||
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
|
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
|
||||||
hasAccess := manageableSlugs[compCode] || manageableSlugs[tID]
|
hasAccess := manageableSlugs[tID]
|
||||||
if !hasAccess && len(secondaryCodes) > 0 {
|
|
||||||
for _, code := range secondaryCodes {
|
|
||||||
if manageableSlugs[strings.ToLower(code)] {
|
|
||||||
hasAccess = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasAccess {
|
if !hasAccess {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -445,34 +482,16 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Dedicated tenantSlug filter
|
// Dedicated tenantSlug filter
|
||||||
if tenantSlug != "" {
|
if tenantSlug != "" {
|
||||||
matches := strings.EqualFold(compCode, tenantSlug) || tID == targetTenantID
|
matches := tID == targetTenantID
|
||||||
if !matches && len(secondaryCodes) > 0 {
|
|
||||||
for _, code := range secondaryCodes {
|
|
||||||
if strings.EqualFold(code, tenantSlug) {
|
|
||||||
matches = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches {
|
if !matches {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search filtering (Keyword search in email, name, or companyCode)
|
// Search filtering
|
||||||
if search != "" {
|
if search != "" {
|
||||||
matchesSearch := strings.Contains(email, searchLower) ||
|
matchesSearch := strings.Contains(email, searchLower) ||
|
||||||
strings.Contains(name, 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matchesSearch {
|
if !matchesSearch {
|
||||||
continue
|
continue
|
||||||
@@ -560,22 +579,8 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
|||||||
// [New] Check access scope
|
// [New] Check access scope
|
||||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||||
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
|
allowedKeys := profileTenantAccessKeys(requester)
|
||||||
|
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
|
||||||
// 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 {
|
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
|
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
|
// Role-based access check
|
||||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
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"})
|
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1057,11 +1062,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
if appointmentTenantSlug == "" {
|
if appointmentTenantSlug == "" {
|
||||||
continue
|
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)]
|
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -1072,6 +1072,11 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
break
|
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)
|
appointment := make(map[string]any, len(rawAppointment)+3)
|
||||||
for key, value := range rawAppointment {
|
for key, value := range rawAppointment {
|
||||||
@@ -1244,7 +1249,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
Phone: normalizePhoneNumber(item.Phone),
|
Phone: normalizePhoneNumber(item.Phone),
|
||||||
Role: role,
|
Role: role,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
CompanyCode: tenantSlug,
|
|
||||||
Department: dept,
|
Department: dept,
|
||||||
Grade: strings.TrimSpace(item.Grade),
|
Grade: strings.TrimSpace(item.Grade),
|
||||||
AffiliationType: "internal",
|
AffiliationType: "internal",
|
||||||
@@ -1376,9 +1380,10 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
|||||||
if profile != nil && requesterRole == domain.RoleTenantAdmin {
|
if profile != nil && requesterRole == domain.RoleTenantAdmin {
|
||||||
for _, t := range profile.ManageableTenants {
|
for _, t := range profile.ManageableTenants {
|
||||||
manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug))
|
manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug))
|
||||||
|
manageableSlugs = append(manageableSlugs, strings.ToLower(t.ID))
|
||||||
}
|
}
|
||||||
if profile.CompanyCode != "" {
|
if profile.TenantID != nil {
|
||||||
manageableSlugs = append(manageableSlugs, strings.ToLower(profile.CompanyCode))
|
manageableSlugs = append(manageableSlugs, strings.ToLower(*profile.TenantID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1396,7 +1401,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
|||||||
slugMap[s] = true
|
slugMap[s] = true
|
||||||
}
|
}
|
||||||
for _, u := range users {
|
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)
|
filtered = append(filtered, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1452,7 +1457,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
|||||||
u.Name,
|
u.Name,
|
||||||
u.Phone,
|
u.Phone,
|
||||||
u.Status,
|
u.Status,
|
||||||
u.CompanyCode,
|
userTenantSlug(u),
|
||||||
u.Grade,
|
u.Grade,
|
||||||
u.Position,
|
u.Position,
|
||||||
u.JobTitle,
|
u.JobTitle,
|
||||||
@@ -1466,7 +1471,7 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
|||||||
u.Phone,
|
u.Phone,
|
||||||
u.Status,
|
u.Status,
|
||||||
tenantID,
|
tenantID,
|
||||||
u.CompanyCode,
|
userTenantSlug(u),
|
||||||
u.Grade,
|
u.Grade,
|
||||||
u.Position,
|
u.Position,
|
||||||
u.JobTitle,
|
u.JobTitle,
|
||||||
@@ -1534,7 +1539,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
*req.Role = role
|
*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 {
|
type tenantCacheItem struct {
|
||||||
ID string
|
ID string
|
||||||
Schema []interface{}
|
Schema []interface{}
|
||||||
@@ -1543,12 +1548,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
manageableSlugs := make(map[string]bool)
|
manageableSlugs := make(map[string]bool)
|
||||||
if requester.Role == domain.RoleTenantAdmin {
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
for _, t := range requester.ManageableTenants {
|
manageableSlugs = profileTenantAccessKeys(requester)
|
||||||
manageableSlugs[strings.ToLower(t.Slug)] = true
|
|
||||||
}
|
|
||||||
if requester.CompanyCode != "" {
|
|
||||||
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results := make([]map[string]any, 0, len(req.UserIDs))
|
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"})
|
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// If changing companyCode, must be to a manageable one
|
|
||||||
if req.CompanyCode != nil {
|
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"})
|
results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: target tenant not manageable"})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1646,9 +1651,6 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
localUser.Status = *req.Status
|
localUser.Status = *req.Status
|
||||||
}
|
}
|
||||||
if req.CompanyCode != nil {
|
|
||||||
localUser.CompanyCode = *req.CompanyCode
|
|
||||||
}
|
|
||||||
if req.Department != nil {
|
if req.Department != nil {
|
||||||
localUser.Department = *req.Department
|
localUser.Department = *req.Department
|
||||||
}
|
}
|
||||||
@@ -1659,7 +1661,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
|||||||
localUser.JobTitle = *req.JobTitle
|
localUser.JobTitle = *req.JobTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve TenantID if changing companyCode
|
// Resolve TenantID if changing tenantSlug.
|
||||||
if req.CompanyCode != nil && h.TenantService != nil {
|
if req.CompanyCode != nil && h.TenantService != nil {
|
||||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
||||||
localUser.TenantID = &tenant.ID
|
localUser.TenantID = &tenant.ID
|
||||||
@@ -1705,12 +1707,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
manageableSlugs := make(map[string]bool)
|
manageableSlugs := make(map[string]bool)
|
||||||
if requester.Role == domain.RoleTenantAdmin {
|
if requester.Role == domain.RoleTenantAdmin {
|
||||||
for _, t := range requester.ManageableTenants {
|
manageableSlugs = profileTenantAccessKeys(requester)
|
||||||
manageableSlugs[strings.ToLower(t.Slug)] = true
|
|
||||||
}
|
|
||||||
if requester.CompanyCode != "" {
|
|
||||||
manageableSlugs[strings.ToLower(requester.CompanyCode)] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results := make([]map[string]any, 0, len(req.UserIDs))
|
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 {
|
if requester.TenantID != nil {
|
||||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||||
}
|
}
|
||||||
if requester.CompanyCode != "" {
|
|
||||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
|
||||||
}
|
|
||||||
for _, tenant := range requester.ManageableTenants {
|
for _, tenant := range requester.ManageableTenants {
|
||||||
allowed[strings.ToLower(tenant.ID)] = true
|
allowed[strings.ToLower(tenant.ID)] = true
|
||||||
allowed[strings.ToLower(tenant.Slug)] = true
|
|
||||||
}
|
}
|
||||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant")
|
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
|
*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 requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
|
||||||
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
|
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
|
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 {
|
} else {
|
||||||
// Legacy/Flat metadata - validate using primary tenant schema
|
schemaTenantSlug := ""
|
||||||
schemaCompCode := extractTraitString(identity.Traits, "companyCode")
|
|
||||||
if req.CompanyCode != nil {
|
if req.CompanyCode != nil {
|
||||||
schemaCompCode = *req.CompanyCode
|
schemaTenantSlug = *req.CompanyCode
|
||||||
}
|
}
|
||||||
if schemaCompCode != "" && h.TenantService != nil {
|
var tenant *domain.Tenant
|
||||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), schemaCompCode)
|
if schemaTenantSlug != "" && h.TenantService != nil {
|
||||||
if err == nil && tenant != nil {
|
tenant, _ = h.TenantService.GetTenantBySlug(c.Context(), schemaTenantSlug)
|
||||||
if schema, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
} else if tenantID := extractTraitString(identity.Traits, "tenant_id"); tenantID != "" && h.TenantService != nil {
|
||||||
// For flat metadata, we validate the whole req.Metadata against this schema
|
tenant, _ = h.TenantService.GetTenant(c.Context(), tenantID)
|
||||||
if err := h.validateMetadataWithAuth(req.Metadata, schema, isAdmin, false); err != nil {
|
}
|
||||||
return errorJSON(c, fiber.StatusBadRequest, "metadata validation failed: "+err.Error())
|
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 h.KetoOutboxRepo != nil && h.TenantService != nil {
|
||||||
if codes, ok := updated.Traits["companyCodes"].([]interface{}); ok {
|
if updatedLocalUser.TenantID != nil {
|
||||||
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
|
|
||||||
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
_ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{
|
||||||
Namespace: "Tenant",
|
Namespace: "Tenant",
|
||||||
Object: *updatedLocalUser.TenantID,
|
Object: *updatedLocalUser.TenantID,
|
||||||
@@ -2181,12 +2168,8 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
|||||||
if requester.TenantID != nil {
|
if requester.TenantID != nil {
|
||||||
allowed[strings.ToLower(*requester.TenantID)] = true
|
allowed[strings.ToLower(*requester.TenantID)] = true
|
||||||
}
|
}
|
||||||
if requester.CompanyCode != "" {
|
|
||||||
allowed[strings.ToLower(requester.CompanyCode)] = true
|
|
||||||
}
|
|
||||||
for _, tenant := range requester.ManageableTenants {
|
for _, tenant := range requester.ManageableTenants {
|
||||||
allowed[strings.ToLower(tenant.ID)] = true
|
allowed[strings.ToLower(tenant.ID)] = true
|
||||||
allowed[strings.ToLower(tenant.Slug)] = true
|
|
||||||
}
|
}
|
||||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowed) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
|
||||||
|
|||||||
@@ -217,21 +217,23 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
|
|||||||
app.Get("/users/export", h.ExportUsersCSV)
|
app.Get("/users/export", h.ExportUsersCSV)
|
||||||
|
|
||||||
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
|
createdAt := time.Date(2026, 4, 29, 12, 0, 0, 0, time.UTC)
|
||||||
|
tenantID := "tenant-uuid"
|
||||||
mockRepo.On("List", mock.Anything, 0, 10000, "", "test-tenant").
|
mockRepo.On("List", mock.Anything, 0, 10000, "", "test-tenant").
|
||||||
Return([]domain.User{
|
Return([]domain.User{
|
||||||
{
|
{
|
||||||
ID: "u-1",
|
ID: "u-1",
|
||||||
Email: "user@test.com",
|
Email: "user@test.com",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
Phone: "010-1111-2222",
|
Phone: "010-1111-2222",
|
||||||
Role: domain.RoleSuperAdmin,
|
Role: domain.RoleSuperAdmin,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
CompanyCode: "test-tenant",
|
TenantID: &tenantID,
|
||||||
Department: "Legacy Department",
|
Tenant: &domain.Tenant{ID: tenantID, Slug: "test-tenant"},
|
||||||
Grade: "책임",
|
Department: "Legacy Department",
|
||||||
Position: "팀장",
|
Grade: "책임",
|
||||||
JobTitle: "플랫폼 운영",
|
Position: "팀장",
|
||||||
CreatedAt: createdAt,
|
JobTitle: "플랫폼 운영",
|
||||||
|
CreatedAt: createdAt,
|
||||||
},
|
},
|
||||||
}, int64(1), nil).Once()
|
}, int64(1), nil).Once()
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ func TestUserHandler_ExportUsersCSV_UsesTenantSlugAliasAndOmitsRole(t *testing.T
|
|||||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
body := strings.TrimPrefix(string(bodyBytes), "\ufeff")
|
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, "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, "Role")
|
||||||
assert.NotContains(t, body, "Department")
|
assert.NotContains(t, body, "Department")
|
||||||
mockRepo.AssertExpectations(t)
|
mockRepo.AssertExpectations(t)
|
||||||
@@ -267,17 +269,17 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
|
|||||||
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
|
mockRepo.On("List", mock.Anything, 0, 10000, "", "").
|
||||||
Return([]domain.User{
|
Return([]domain.User{
|
||||||
{
|
{
|
||||||
ID: "user-uuid",
|
ID: "user-uuid",
|
||||||
Email: "user@test.com",
|
Email: "user@test.com",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
Phone: "010-1111-2222",
|
Phone: "010-1111-2222",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
CompanyCode: "test-tenant",
|
TenantID: &tenantID,
|
||||||
TenantID: &tenantID,
|
Tenant: &domain.Tenant{ID: tenantID, Slug: "test-tenant"},
|
||||||
Grade: "책임",
|
Grade: "책임",
|
||||||
Position: "팀장",
|
Position: "팀장",
|
||||||
JobTitle: "플랫폼 운영",
|
JobTitle: "플랫폼 운영",
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
},
|
},
|
||||||
}, int64(1), nil).Once()
|
}, int64(1), nil).Once()
|
||||||
|
|
||||||
@@ -296,6 +298,64 @@ func TestUserHandler_ExportUsersCSV_OmitsIDsAndUsesTenantSlug(t *testing.T) {
|
|||||||
mockRepo.AssertExpectations(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) {
|
func TestUserHandler_BulkCreateUsers(t *testing.T) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
mockKratos := new(MockKratosAdmin)
|
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) {
|
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{
|
mockKratos.On("GetIdentity", mock.Anything, "u-1").Return(&service.KratosIdentity{
|
||||||
ID: "u-1",
|
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)
|
}, 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{
|
Config: domain.JSONMap{
|
||||||
"userSchema": []interface{}{
|
"userSchema": []interface{}{
|
||||||
map[string]interface{}{"key": "salary", "adminOnly": true},
|
map[string]interface{}{"key": "salary", "adminOnly": true},
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { filterTenantsByVisibility } from "./tenantVisibility";
|
|||||||
import { getOrgChartUserDisplayName } from "./userDisplay";
|
import { getOrgChartUserDisplayName } from "./userDisplay";
|
||||||
|
|
||||||
function getUserTenantSlug(user: UserSummary) {
|
function getUserTenantSlug(user: UserSummary) {
|
||||||
return (
|
return user.tenantSlug?.toLowerCase() || "";
|
||||||
user.companyCode?.toLowerCase() || user.tenantSlug?.toLowerCase() || ""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOrgFrontTenantType(tenant: TenantSummary) {
|
function isOrgFrontTenantType(tenant: TenantSummary) {
|
||||||
|
|||||||
@@ -990,8 +990,8 @@ function isSystemGlobalUser(user: UserSummary) {
|
|||||||
normalizedRole === "system-admin" ||
|
normalizedRole === "system-admin" ||
|
||||||
isSystemGlobalTenant(user.tenant) ||
|
isSystemGlobalTenant(user.tenant) ||
|
||||||
isSystemGlobalTenant({
|
isSystemGlobalTenant({
|
||||||
id: user.companyCode || user.tenantSlug || "",
|
id: user.tenantSlug || "",
|
||||||
slug: user.companyCode || user.tenantSlug || "",
|
slug: user.tenantSlug || "",
|
||||||
type: user.role,
|
type: user.role,
|
||||||
name: user.role,
|
name: user.role,
|
||||||
})
|
})
|
||||||
@@ -1145,8 +1145,7 @@ function buildUsersMap(
|
|||||||
if (!isVisibleOrgChartUser(user)) continue;
|
if (!isVisibleOrgChartUser(user)) continue;
|
||||||
|
|
||||||
const slugs = new Set<string>();
|
const slugs = new Set<string>();
|
||||||
const primarySlug =
|
const primarySlug = user.tenantSlug?.toLowerCase() || "";
|
||||||
user.companyCode?.toLowerCase() || user.tenantSlug?.toLowerCase() || "";
|
|
||||||
if (
|
if (
|
||||||
primarySlug &&
|
primarySlug &&
|
||||||
!isSystemGlobalTenant({
|
!isSystemGlobalTenant({
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ function ProfilePage() {
|
|||||||
profile.tenantId ||
|
profile.tenantId ||
|
||||||
auth.user?.profile?.tenant_id?.toString() ||
|
auth.user?.profile?.tenant_id?.toString() ||
|
||||||
"-";
|
"-";
|
||||||
const displayCompanyCode =
|
const displayTenantSlug = profile.tenant?.slug || profile.tenantId || "-";
|
||||||
profile.companyCode || auth.user?.profile?.companyCode?.toString() || "-";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl mx-auto">
|
<div className="space-y-6 max-w-4xl mx-auto">
|
||||||
@@ -160,9 +159,9 @@ function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-muted-foreground">
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
{t("ui.dev.profile.org.company_code", "회사 코드")}
|
{t("ui.dev.profile.org.tenant_slug", "테넌트 Slug")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">{displayCompanyCode}</p>
|
<p className="text-sm">{displayTenantSlug}</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -599,6 +599,7 @@ department = "Department"
|
|||||||
email = "Email"
|
email = "Email"
|
||||||
name = "Name"
|
name = "Name"
|
||||||
tenant = "Tenant"
|
tenant = "Tenant"
|
||||||
|
tenant_slug = "Tenant Slug"
|
||||||
|
|
||||||
[ui.userfront.profile.password]
|
[ui.userfront.profile.password]
|
||||||
change = "Change"
|
change = "Change"
|
||||||
|
|||||||
@@ -821,6 +821,7 @@ department = "소속"
|
|||||||
email = "이메일"
|
email = "이메일"
|
||||||
name = "이름"
|
name = "이름"
|
||||||
tenant = "소속 테넌트"
|
tenant = "소속 테넌트"
|
||||||
|
tenant_slug = "테넌트 Slug"
|
||||||
|
|
||||||
[ui.userfront.profile.password]
|
[ui.userfront.profile.password]
|
||||||
change = "비밀번호 변경"
|
change = "비밀번호 변경"
|
||||||
|
|||||||
@@ -125,11 +125,6 @@ class _ErrorScreenState extends State<ErrorScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final companyCode = profile['companyCode']?.toString().trim() ?? '';
|
|
||||||
if (companyCode.isNotEmpty) {
|
|
||||||
return companyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,11 +254,6 @@ class _ErrorScreenState extends State<ErrorScreen> {
|
|||||||
appendLabel(tenant);
|
appendLabel(tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
final companyCode = profile['companyCode']?.toString().trim() ?? '';
|
|
||||||
if (companyCode.isNotEmpty) {
|
|
||||||
appendLabel(companyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1178,11 +1178,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
|
|||||||
profile.tenant!.name,
|
profile.tenant!.name,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (profile.companyCode.isNotEmpty) ...[
|
if (profile.tenant?.slug.isNotEmpty ?? false) ...[
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildReadOnlyTile(
|
_buildReadOnlyTile(
|
||||||
tr('ui.userfront.profile.field.company_code'),
|
tr('ui.userfront.profile.field.tenant_slug'),
|
||||||
profile.companyCode,
|
profile.tenant!.slug,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1930,6 +1930,7 @@ const Map<String, String> koStrings = {
|
|||||||
"ui.userfront.profile.field.email": "이메일",
|
"ui.userfront.profile.field.email": "이메일",
|
||||||
"ui.userfront.profile.field.name": "이름",
|
"ui.userfront.profile.field.name": "이름",
|
||||||
"ui.userfront.profile.field.tenant": "소속 테넌트",
|
"ui.userfront.profile.field.tenant": "소속 테넌트",
|
||||||
|
"ui.userfront.profile.field.tenant_slug": "테넌트 Slug",
|
||||||
"ui.userfront.profile.manage": "프로필 관리",
|
"ui.userfront.profile.manage": "프로필 관리",
|
||||||
"ui.userfront.profile.password.change": "비밀번호 변경",
|
"ui.userfront.profile.password.change": "비밀번호 변경",
|
||||||
"ui.userfront.profile.password.confirm": "새 비밀번호 확인",
|
"ui.userfront.profile.password.confirm": "새 비밀번호 확인",
|
||||||
@@ -4125,6 +4126,7 @@ const Map<String, String> enStrings = {
|
|||||||
"ui.userfront.profile.field.email": "Email",
|
"ui.userfront.profile.field.email": "Email",
|
||||||
"ui.userfront.profile.field.name": "Name",
|
"ui.userfront.profile.field.name": "Name",
|
||||||
"ui.userfront.profile.field.tenant": "Tenant",
|
"ui.userfront.profile.field.tenant": "Tenant",
|
||||||
|
"ui.userfront.profile.field.tenant_slug": "Tenant Slug",
|
||||||
"ui.userfront.profile.manage": "Manage profile",
|
"ui.userfront.profile.manage": "Manage profile",
|
||||||
"ui.userfront.profile.password.change": "Change",
|
"ui.userfront.profile.password.change": "Change",
|
||||||
"ui.userfront.profile.password.confirm": "Confirm",
|
"ui.userfront.profile.password.confirm": "Confirm",
|
||||||
|
|||||||
Reference in New Issue
Block a user