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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user