317 lines
9.3 KiB
TypeScript
317 lines
9.3 KiB
TypeScript
/**
|
|
* useMandateRoles Hook
|
|
*
|
|
* Hook for managing roles within a specific mandate.
|
|
* Uses the /api/rbac/roles endpoints.
|
|
*/
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
import api from '../api';
|
|
|
|
// Types
|
|
export interface Role {
|
|
id: string;
|
|
roleLabel: string;
|
|
description?: string | { [key: string]: string };
|
|
mandateId?: string;
|
|
featureInstanceId?: string;
|
|
featureCode?: string;
|
|
isSystemRole?: boolean;
|
|
isTemplate?: boolean;
|
|
createdAt?: number;
|
|
updatedAt?: number;
|
|
}
|
|
|
|
export interface RoleCreate {
|
|
roleLabel: string;
|
|
description?: string | { [key: string]: string };
|
|
mandateId?: string;
|
|
featureInstanceId?: string;
|
|
featureCode?: string;
|
|
}
|
|
|
|
export interface RoleUpdate {
|
|
roleLabel?: string;
|
|
description?: string | { [key: string]: string };
|
|
mandateId?: string | null;
|
|
}
|
|
|
|
export interface PaginationParams {
|
|
page?: number;
|
|
pageSize?: number;
|
|
search?: string;
|
|
filters?: Record<string, any>;
|
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
|
scopeFilter?: 'all' | 'mandate' | 'global'; // Backend filter for role scope
|
|
}
|
|
|
|
export interface PaginationMetadata {
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
/**
|
|
* Hook for managing mandate roles
|
|
*/
|
|
export function useMandateRoles() {
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
|
|
|
|
// Store current mandateId for refetch
|
|
const currentMandateIdRef = useRef<string | undefined>(undefined);
|
|
|
|
/**
|
|
* Fetch all roles with pagination support
|
|
* @param mandateIdOrParams - Either a mandateId string (backward compatible) or pagination params
|
|
* @param additionalParams - Additional parameters like scopeFilter (when first param is mandateId)
|
|
*/
|
|
const fetchRoles = useCallback(async (
|
|
mandateIdOrParams?: string | PaginationParams,
|
|
additionalParams?: PaginationParams
|
|
): Promise<Role[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const headers: Record<string, string> = {};
|
|
let paginationParams: PaginationParams = {};
|
|
let mandateId: string | undefined;
|
|
let scopeFilter: string | undefined;
|
|
|
|
// Handle backward compatibility: first param can be mandateId string or pagination object
|
|
if (typeof mandateIdOrParams === 'string') {
|
|
mandateId = mandateIdOrParams;
|
|
currentMandateIdRef.current = mandateId;
|
|
// If additional params provided, use them
|
|
if (additionalParams) {
|
|
paginationParams = additionalParams;
|
|
scopeFilter = additionalParams.scopeFilter;
|
|
}
|
|
} else if (mandateIdOrParams && typeof mandateIdOrParams === 'object') {
|
|
paginationParams = mandateIdOrParams;
|
|
mandateId = currentMandateIdRef.current;
|
|
scopeFilter = mandateIdOrParams.scopeFilter;
|
|
}
|
|
|
|
if (mandateId) {
|
|
headers['X-Mandate-Id'] = mandateId;
|
|
}
|
|
|
|
// Build query params for pagination (exclude scopeFilter from pagination JSON)
|
|
const { scopeFilter: _, ...paginationWithoutScope } = paginationParams;
|
|
const queryParams: Record<string, string> = {};
|
|
if (Object.keys(paginationWithoutScope).length > 0) {
|
|
queryParams.pagination = JSON.stringify(paginationWithoutScope);
|
|
}
|
|
// Do NOT include feature template roles - they belong to Feature-Rollen page
|
|
// According to admin_ui_concept.md: Filter: featureCode=null AND featureInstanceId=null
|
|
queryParams.includeTemplates = 'false';
|
|
// Include mandate-specific roles for the selected mandate
|
|
if (mandateId) {
|
|
queryParams.mandateId = mandateId;
|
|
}
|
|
// Include scopeFilter as separate query parameter
|
|
if (scopeFilter) {
|
|
queryParams.scopeFilter = scopeFilter;
|
|
}
|
|
|
|
const response = await api.get('/api/rbac/roles', {
|
|
headers,
|
|
params: queryParams
|
|
});
|
|
|
|
let data: Role[] = [];
|
|
let paginationMeta: PaginationMetadata | null = null;
|
|
|
|
if (response.data?.items && Array.isArray(response.data.items)) {
|
|
data = response.data.items;
|
|
if (response.data.pagination) {
|
|
paginationMeta = response.data.pagination;
|
|
}
|
|
} else if (Array.isArray(response.data)) {
|
|
data = response.data;
|
|
}
|
|
|
|
// No client-side filtering needed - backend already filters
|
|
|
|
setRoles(data);
|
|
setPagination(paginationMeta);
|
|
return data;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch roles';
|
|
setError(errorMessage);
|
|
setRoles([]);
|
|
setPagination(null);
|
|
return [];
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get a single role by ID
|
|
*/
|
|
const getRole = useCallback(async (roleId: string): Promise<Role | null> => {
|
|
try {
|
|
const response = await api.get(`/api/rbac/roles/${roleId}`);
|
|
return response.data;
|
|
} catch (err: any) {
|
|
console.error('Error fetching role:', err);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Create a new role
|
|
*/
|
|
const createRole = useCallback(async (
|
|
data: RoleCreate,
|
|
mandateId?: string
|
|
): Promise<{ success: boolean; data?: Role; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const headers: Record<string, string> = {};
|
|
if (mandateId) {
|
|
headers['X-Mandate-Id'] = mandateId;
|
|
}
|
|
|
|
const response = await api.post('/api/rbac/roles', data, { headers });
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create role';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Update an existing role
|
|
*/
|
|
const updateRole = useCallback(async (
|
|
roleId: string,
|
|
data: RoleUpdate
|
|
): Promise<{ success: boolean; data?: Role; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.put(`/api/rbac/roles/${roleId}`, data);
|
|
// Optimistically update local state (convert null to undefined for mandateId)
|
|
const updateData = {
|
|
...data,
|
|
mandateId: data.mandateId === null ? undefined : data.mandateId
|
|
};
|
|
setRoles(prev => prev.map(r => r.id === roleId ? { ...r, ...updateData } : r));
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update role';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Delete a role
|
|
*/
|
|
const deleteRole = useCallback(async (
|
|
roleId: string
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.delete(`/api/rbac/roles/${roleId}`);
|
|
// Optimistically update local state
|
|
setRoles(prev => prev.filter(r => r.id !== roleId));
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete role';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get role options (for dropdowns)
|
|
*/
|
|
const fetchRoleOptions = useCallback(async (): Promise<Array<{ value: string; label: string }>> => {
|
|
try {
|
|
const response = await api.get('/api/rbac/roles/options');
|
|
if (Array.isArray(response.data)) {
|
|
return response.data.map((r: any) => ({
|
|
value: r.id || r.value,
|
|
label: r.roleLabel || r.label || r.id
|
|
}));
|
|
}
|
|
return [];
|
|
} catch (err: any) {
|
|
console.error('Error fetching role options:', err);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get users with a specific role
|
|
*/
|
|
const getUsersWithRole = useCallback(async (
|
|
roleLabel: string
|
|
): Promise<Array<{ userId: string; username: string; email?: string }>> => {
|
|
try {
|
|
const response = await api.get(`/api/rbac/roles/roles/${roleLabel}/users`);
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
} catch (err: any) {
|
|
console.error('Error fetching users with role:', err);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Filter roles by type
|
|
*/
|
|
const getMandateRoles = useCallback((mandateId: string) => {
|
|
return roles.filter(r =>
|
|
r.mandateId === mandateId && !r.featureInstanceId
|
|
);
|
|
}, [roles]);
|
|
|
|
const getFeatureRoles = useCallback((featureInstanceId: string) => {
|
|
return roles.filter(r => r.featureInstanceId === featureInstanceId);
|
|
}, [roles]);
|
|
|
|
const getGlobalRoles = useCallback(() => {
|
|
return roles.filter(r => !r.mandateId && !r.featureInstanceId);
|
|
}, [roles]);
|
|
|
|
const getTemplateRoles = useCallback(() => {
|
|
return roles.filter(r => r.isTemplate === true);
|
|
}, [roles]);
|
|
|
|
return {
|
|
roles,
|
|
loading,
|
|
error,
|
|
pagination,
|
|
fetchRoles,
|
|
getRole,
|
|
createRole,
|
|
updateRole,
|
|
deleteRole,
|
|
fetchRoleOptions,
|
|
getUsersWithRole,
|
|
getMandateRoles,
|
|
getFeatureRoles,
|
|
getGlobalRoles,
|
|
getTemplateRoles,
|
|
};
|
|
}
|
|
|
|
export default useMandateRoles;
|