1
0
forked from baron/baron-sso

fix: resolve tenant user assignment bug (#490)

- Fix frontend payload mapping (tenantSlug -> companyCode) in adminApi.ts.
- Fix backend group member fetching to avoid dummy members in UserGroupService.List.
- Fix backend foreign key violation on group creation by distinguishing between tenant parent and group parent in UserGroupService.Create.
This commit is contained in:
2026-04-01 11:19:09 +09:00
parent 6b30580f36
commit a5fdeabd09
2 changed files with 45 additions and 31 deletions

View File

@@ -445,9 +445,15 @@ export async function fetchUser(userId: string) {
}
export async function createUser(payload: UserCreateRequest) {
// Map tenantSlug to companyCode for backend compatibility
const requestPayload: any = { ...payload };
if (payload.tenantSlug !== undefined) {
requestPayload.companyCode = payload.tenantSlug;
}
const { data } = await apiClient.post<UserCreateResponse>(
"/v1/admin/users",
payload,
requestPayload,
);
return data;
}
@@ -466,9 +472,16 @@ export function exportUsersCSVUrl(search?: string, tenantSlug?: string) {
}
export async function bulkCreateUsers(users: BulkUserItem[]) {
const mappedUsers = users.map((u) => {
const mapped: any = { ...u };
if (u.tenantSlug !== undefined) {
mapped.companyCode = u.tenantSlug;
}
return mapped;
});
const { data } = await apiClient.post<BulkUserResponse>(
"/v1/admin/users/bulk",
{ users },
{ users: mappedUsers },
);
return data;
}
@@ -480,7 +493,11 @@ export async function bulkUpdateUsers(payload: {
tenantSlug?: string;
department?: string;
}) {
const { data } = await apiClient.put("/v1/admin/users/bulk", payload);
const requestPayload: any = { ...payload };
if (payload.tenantSlug !== undefined) {
requestPayload.companyCode = payload.tenantSlug;
}
const { data } = await apiClient.put("/v1/admin/users/bulk", requestPayload);
return data;
}
@@ -492,9 +509,14 @@ export async function bulkDeleteUsers(userIds: string[]) {
}
export async function updateUser(userId: string, payload: UserUpdateRequest) {
const requestPayload: any = { ...payload };
if (payload.tenantSlug !== undefined) {
requestPayload.companyCode = payload.tenantSlug;
}
const { data } = await apiClient.put<UserSummary>(
`/v1/admin/users/${userId}`,
payload,
requestPayload,
);
return data;
}

View File

@@ -55,13 +55,14 @@ func NewUserGroupService(
}
func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID *string, name, description, unitType string) (*domain.UserGroup, error) {
// If no parent user group, the parent is the company tenant
if parentID == nil || *parentID == "" {
parentID = &tenantID
// For Keto and Tenant hierarchy, if no parent group, the company tenant is the parent.
actualParentID := parentID
if actualParentID == nil || *actualParentID == "" {
actualParentID = &tenantID
}
// Validate parent tenant exists
if _, err := s.tenantRepo.FindByID(ctx, *parentID); err != nil {
if _, err := s.tenantRepo.FindByID(ctx, *actualParentID); err != nil {
return nil, fmt.Errorf("parent tenant not found or invalid: %w", err)
}
@@ -71,7 +72,7 @@ func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID
groupTenant := &domain.Tenant{
ID: unitID,
Type: domain.TenantTypeUserGroup,
ParentID: parentID,
ParentID: actualParentID,
Name: name,
Slug: fmt.Sprintf("ug-%s", unitID[:8]),
Description: description,
@@ -84,6 +85,7 @@ func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID
}
// 2. Create UserGroup metadata
// parent_id in user_groups refers to other groups, so use original parentID (which might be nil)
group := &domain.UserGroup{
ID: unitID,
TenantID: tenantID,
@@ -105,7 +107,7 @@ func (s *userGroupService) Create(ctx context.Context, tenantID string, parentID
Namespace: "Tenant",
Object: unitID,
Relation: "parents",
Subject: "Tenant:" + *parentID,
Subject: "Tenant:" + *actualParentID,
Action: domain.KetoOutboxActionCreate,
})
}
@@ -123,17 +125,12 @@ func (s *userGroupService) Delete(ctx context.Context, tenantID, groupID string)
return nil // Placeholder
}
func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
group, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
// Fetch members from Keto (Tenant namespace)
func (s *userGroupService) populateMembers(ctx context.Context, group *domain.UserGroup) {
tuples, err := s.ketoService.ListRelations(ctx, "Tenant", group.ID, "members", "")
if err != nil {
slog.Error("Failed to fetch group members from keto", "error", err, "group_id", group.ID)
return nil, err
group.Members = []domain.User{}
return
}
var userIDs []string
@@ -147,25 +144,21 @@ func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGrou
}
if len(userIDs) > 0 {
// 1. Try to find in local DB
members, err := s.userRepo.FindByIDs(ctx, userIDs)
if err != nil {
slog.Error("Failed to fetch member details from db", "error", err)
}
// 2. Map existing DB members
memberMap := make(map[string]domain.User)
for _, m := range members {
memberMap[m.ID] = m
}
// 3. For IDs not in DB, fetch from Kratos
var finalMembers []domain.User
for _, uid := range userIDs {
if m, ok := memberMap[uid]; ok {
finalMembers = append(finalMembers, m)
} else if s.kratos != nil {
// Fallback to Kratos
identity, err := s.kratos.GetIdentity(ctx, uid)
if err == nil && identity != nil {
name, _ := identity.Traits["name"].(string)
@@ -182,7 +175,14 @@ func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGrou
} else {
group.Members = []domain.User{}
}
}
func (s *userGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
group, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
s.populateMembers(ctx, group)
return group, nil
}
@@ -196,16 +196,8 @@ func (s *userGroupService) List(ctx context.Context, tenantID string) ([]domain.
return groups, nil
}
// For each group, fetch member count from Keto
for i := range groups {
tuples, err := s.ketoService.ListRelations(ctx, "Tenant", groups[i].ID, "members", "")
if err == nil {
// Create dummy members just to carry the count for the JSON response
groups[i].Members = make([]domain.User, len(tuples))
} else {
slog.Warn("Failed to fetch member count from Keto", "groupID", groups[i].ID, "error", err)
groups[i].Members = []domain.User{}
}
s.populateMembers(ctx, &groups[i])
}
return groups, nil