2026-03-17 17:58:24 +01:00
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import { adminFetch } from './admin-api';
|
|
|
|
|
|
|
|
|
|
// ─── Types ───
|
|
|
|
|
|
|
|
|
|
export interface RoleSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
system: boolean;
|
|
|
|
|
source: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GroupSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UserSummary {
|
|
|
|
|
userId: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UserDetail {
|
|
|
|
|
userId: string;
|
|
|
|
|
provider: string;
|
|
|
|
|
email: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
directRoles: RoleSummary[];
|
|
|
|
|
directGroups: GroupSummary[];
|
|
|
|
|
effectiveRoles: RoleSummary[];
|
|
|
|
|
effectiveGroups: GroupSummary[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GroupDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
parentGroupId: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
directRoles: RoleSummary[];
|
|
|
|
|
effectiveRoles: RoleSummary[];
|
|
|
|
|
members: UserSummary[];
|
|
|
|
|
childGroups: GroupSummary[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RoleDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
scope: string;
|
|
|
|
|
system: boolean;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
assignedGroups: GroupSummary[];
|
|
|
|
|
directUsers: UserSummary[];
|
|
|
|
|
effectivePrincipals: UserSummary[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RbacStats {
|
|
|
|
|
userCount: number;
|
|
|
|
|
activeUserCount: number;
|
|
|
|
|
groupCount: number;
|
|
|
|
|
maxGroupDepth: number;
|
|
|
|
|
roleCount: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Query hooks ───
|
|
|
|
|
|
|
|
|
|
export function useUsers() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'users'],
|
|
|
|
|
queryFn: () => adminFetch<UserDetail[]>('/users'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUser(userId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'users', userId],
|
|
|
|
|
queryFn: () => adminFetch<UserDetail>(`/users/${encodeURIComponent(userId!)}`),
|
|
|
|
|
enabled: !!userId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useGroups() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'groups'],
|
|
|
|
|
queryFn: () => adminFetch<GroupDetail[]>('/groups'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useGroup(groupId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'groups', groupId],
|
|
|
|
|
queryFn: () => adminFetch<GroupDetail>(`/groups/${groupId}`),
|
|
|
|
|
enabled: !!groupId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRoles() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'roles'],
|
|
|
|
|
queryFn: () => adminFetch<RoleDetail[]>('/roles'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRole(roleId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'roles', roleId],
|
|
|
|
|
queryFn: () => adminFetch<RoleDetail>(`/roles/${roleId}`),
|
|
|
|
|
enabled: !!roleId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRbacStats() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: ['admin', 'rbac', 'stats'],
|
|
|
|
|
queryFn: () => adminFetch<RbacStats>('/rbac/stats'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Mutation hooks ───
|
|
|
|
|
|
|
|
|
|
export function useAssignRoleToUser() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}/roles/${roleId}`, { method: 'POST' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRemoveRoleFromUser() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}/roles/${roleId}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useAddUserToGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}/groups/${groupId}`, { method: 'POST' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRemoveUserFromGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}/groups/${groupId}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useCreateGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (data: { name: string; parentGroupId?: string }) =>
|
|
|
|
|
adminFetch<{ id: string }>('/groups', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUpdateGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ id, ...data }: { id: string; name?: string; parentGroupId?: string | null }) =>
|
|
|
|
|
adminFetch(`/groups/${id}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useDeleteGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (id: string) =>
|
|
|
|
|
adminFetch(`/groups/${id}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useAssignRoleToGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
|
|
|
|
adminFetch(`/groups/${groupId}/roles/${roleId}`, { method: 'POST' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useRemoveRoleFromGroup() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
|
|
|
|
adminFetch(`/groups/${groupId}/roles/${roleId}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useCreateRole() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (data: { name: string; description?: string; scope?: string }) =>
|
|
|
|
|
adminFetch<{ id: string }>('/roles', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUpdateRole() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ id, ...data }: { id: string; name?: string; description?: string; scope?: string }) =>
|
|
|
|
|
adminFetch(`/roles/${id}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useDeleteRole() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (id: string) =>
|
|
|
|
|
adminFetch(`/roles/${id}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 18:52:07 +01:00
|
|
|
export function useCreateUser() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
2026-03-17 19:08:19 +01:00
|
|
|
mutationFn: (data: { username: string; displayName?: string; email?: string; password?: string }) =>
|
2026-03-17 18:52:07 +01:00
|
|
|
adminFetch<UserDetail>('/users', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUpdateUser() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ userId, ...data }: { userId: string; displayName?: string; email?: string }) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 17:58:24 +01:00
|
|
|
export function useDeleteUser() {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (userId: string) =>
|
|
|
|
|
adminFetch(`/users/${encodeURIComponent(userId)}`, { method: 'DELETE' }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|