509 lines
14 KiB
TypeScript
509 lines
14 KiB
TypeScript
/**
|
|
* useFeatureAccess Hook
|
|
*
|
|
* Hook for managing feature instance access (which users can access which feature instances with which roles).
|
|
* Uses the /api/features endpoints.
|
|
*/
|
|
|
|
import { useState, useCallback, useRef } from 'react';
|
|
import api from '../api';
|
|
|
|
// Types
|
|
export interface PaginationParams {
|
|
page?: number;
|
|
pageSize?: number;
|
|
search?: string;
|
|
filters?: Record<string, any>;
|
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
|
}
|
|
|
|
export interface PaginationMetadata {
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export interface Feature {
|
|
code: string;
|
|
label: string | { [key: string]: string };
|
|
icon?: string;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface FeatureInstance {
|
|
id: string;
|
|
featureCode: string;
|
|
mandateId: string;
|
|
label: string;
|
|
enabled: boolean;
|
|
config?: Record<string, any>; // Instance-specific configuration (JSONB)
|
|
}
|
|
|
|
export interface FeatureAccess {
|
|
id: string;
|
|
userId: string;
|
|
featureInstanceId: string;
|
|
enabled: boolean;
|
|
roleIds?: string[];
|
|
}
|
|
|
|
export interface FeatureAccessUser {
|
|
id: string; // FeatureAccess ID as primary key
|
|
userId: string;
|
|
username: string;
|
|
email?: string;
|
|
fullName?: string;
|
|
roleIds: string[];
|
|
roleLabels: string[];
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface FeatureInstanceRole {
|
|
id: string;
|
|
roleLabel: string;
|
|
description?: { [key: string]: string };
|
|
featureCode?: string;
|
|
isSystemRole?: boolean;
|
|
}
|
|
|
|
export interface AddUserToInstanceRequest {
|
|
userId: string;
|
|
roleIds: string[];
|
|
}
|
|
|
|
export interface FeatureInstanceCreate {
|
|
featureCode: string;
|
|
label: string;
|
|
enabled?: boolean;
|
|
copyTemplateRoles?: boolean;
|
|
config?: Record<string, any>; // Instance-specific configuration (JSONB)
|
|
}
|
|
|
|
/**
|
|
* Hook for managing feature access
|
|
*/
|
|
export function useFeatureAccess() {
|
|
const [features, setFeatures] = useState<Feature[]>([]);
|
|
const [instances, setInstances] = useState<FeatureInstance[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [instancesPagination, setInstancesPagination] = useState<PaginationMetadata | null>(null);
|
|
|
|
// Store current context for refetch
|
|
const currentMandateIdRef = useRef<string>('');
|
|
const currentFeatureCodeRef = useRef<string | undefined>(undefined);
|
|
|
|
/**
|
|
* Fetch all available features
|
|
*/
|
|
const fetchFeatures = useCallback(async (): Promise<Feature[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.get('/api/features/');
|
|
// Handle different API response formats (array, {items}, {data})
|
|
let data: Feature[] = [];
|
|
if (Array.isArray(response.data)) {
|
|
data = response.data;
|
|
} else if (response.data?.items && Array.isArray(response.data.items)) {
|
|
data = response.data.items;
|
|
} else if (response.data?.data && Array.isArray(response.data.data)) {
|
|
data = response.data.data;
|
|
}
|
|
setFeatures(data);
|
|
return data;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch features';
|
|
setError(errorMessage);
|
|
setFeatures([]);
|
|
return [];
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Fetch feature instances for a mandate with optional pagination
|
|
*/
|
|
const fetchInstances = useCallback(async (
|
|
mandateIdOrPagination?: string | PaginationParams,
|
|
featureCode?: string
|
|
): Promise<FeatureInstance[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
let mandateId: string;
|
|
let paginationParams: PaginationParams = {};
|
|
|
|
// Handle backward compatibility
|
|
if (typeof mandateIdOrPagination === 'string') {
|
|
mandateId = mandateIdOrPagination;
|
|
currentMandateIdRef.current = mandateId;
|
|
currentFeatureCodeRef.current = featureCode;
|
|
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
|
|
paginationParams = mandateIdOrPagination;
|
|
mandateId = currentMandateIdRef.current;
|
|
featureCode = currentFeatureCodeRef.current;
|
|
} else {
|
|
mandateId = currentMandateIdRef.current;
|
|
featureCode = currentFeatureCodeRef.current;
|
|
}
|
|
|
|
if (!mandateId) {
|
|
setLoading(false);
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (featureCode) {
|
|
params.append('featureCode', featureCode);
|
|
}
|
|
if (Object.keys(paginationParams).length > 0) {
|
|
params.append('pagination', JSON.stringify(paginationParams));
|
|
}
|
|
|
|
const url = params.toString()
|
|
? `/api/features/instances?${params.toString()}`
|
|
: '/api/features/instances';
|
|
|
|
const response = await api.get(url, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
|
|
let data: FeatureInstance[] = [];
|
|
if (response.data?.items && Array.isArray(response.data.items)) {
|
|
data = response.data.items;
|
|
if (response.data.pagination) {
|
|
setInstancesPagination(response.data.pagination);
|
|
}
|
|
} else {
|
|
data = Array.isArray(response.data) ? response.data : [];
|
|
}
|
|
|
|
setInstances(data);
|
|
return data;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch feature instances';
|
|
setError(errorMessage);
|
|
setInstances([]);
|
|
setInstancesPagination(null);
|
|
return [];
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Create a new feature instance
|
|
*/
|
|
const createInstance = useCallback(async (
|
|
mandateId: string,
|
|
data: FeatureInstanceCreate
|
|
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.post('/api/features/instances', data, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create feature instance';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Update a feature instance (label, enabled, config)
|
|
*/
|
|
const updateInstance = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string,
|
|
data: { label?: string; enabled?: boolean; config?: Record<string, any> }
|
|
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.put(`/api/features/instances/${instanceId}`, data, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
// Update local state
|
|
setInstances(prev => prev.map(i => i.id === instanceId ? { ...i, ...response.data } : i));
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update feature instance';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Delete a feature instance
|
|
*/
|
|
const deleteInstance = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.delete(`/api/features/instances/${instanceId}`, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
// Optimistically update the local state
|
|
setInstances(prev => prev.filter(i => i.id !== instanceId));
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete feature instance';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Sync roles for a feature instance from templates
|
|
*/
|
|
const syncInstanceRoles = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string,
|
|
addOnly: boolean = true
|
|
): Promise<{ success: boolean; data?: { added: number; removed: number; unchanged: number }; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.post(`/api/features/instances/${instanceId}/sync-roles?addOnly=${addOnly}`, {}, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to sync instance roles';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get current user's feature instances (grouped by mandate)
|
|
*/
|
|
const fetchMyFeatureInstances = useCallback(async (): Promise<{
|
|
mandates: Array<{
|
|
id: string;
|
|
name: string;
|
|
features: Array<{
|
|
code: string;
|
|
label: string | { [key: string]: string };
|
|
instances: Array<{
|
|
id: string;
|
|
featureCode: string;
|
|
mandateId: string;
|
|
instanceLabel: string;
|
|
}>;
|
|
}>;
|
|
}>;
|
|
}> => {
|
|
try {
|
|
const response = await api.get('/api/features/my');
|
|
return response.data || { mandates: [] };
|
|
} catch (err: any) {
|
|
console.error('Error fetching my feature instances:', err);
|
|
return { mandates: [] };
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get template roles for features
|
|
*/
|
|
const fetchTemplateRoles = useCallback(async (featureCode?: string): Promise<any[]> => {
|
|
try {
|
|
let url = '/api/features/templates/roles';
|
|
if (featureCode) {
|
|
url += `?featureCode=${encodeURIComponent(featureCode)}`;
|
|
}
|
|
const response = await api.get(url);
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
} catch (err: any) {
|
|
console.error('Error fetching template roles:', err);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
// ============================================
|
|
// Feature Instance Users Management
|
|
// ============================================
|
|
|
|
/**
|
|
* Fetch all users with access to a specific feature instance
|
|
*/
|
|
const fetchInstanceUsers = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string
|
|
): Promise<FeatureAccessUser[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.get(`/api/features/instances/${instanceId}/users`, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch instance users';
|
|
setError(errorMessage);
|
|
return [];
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Add a user to a feature instance with specified roles
|
|
*/
|
|
const addUserToInstance = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string,
|
|
data: AddUserToInstanceRequest
|
|
): Promise<{ success: boolean; data?: any; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.post(`/api/features/instances/${instanceId}/users`, data, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to add user to instance';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Remove a user's access from a feature instance
|
|
*/
|
|
const removeUserFromInstance = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string,
|
|
userId: string
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.delete(`/api/features/instances/${instanceId}/users/${userId}`, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to remove user from instance';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Update a user's roles and active flag in a feature instance
|
|
*/
|
|
const updateInstanceUserRoles = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string,
|
|
userId: string,
|
|
payload: { roleIds: string[]; enabled?: boolean }
|
|
): Promise<{ success: boolean; data?: any; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const body = payload.enabled !== undefined
|
|
? { roleIds: payload.roleIds, enabled: payload.enabled }
|
|
: { roleIds: payload.roleIds };
|
|
const response = await api.put(
|
|
`/api/features/instances/${instanceId}/users/${userId}/roles`,
|
|
body,
|
|
{
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
}
|
|
);
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update user roles';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get available roles for a feature instance
|
|
*/
|
|
const fetchInstanceRoles = useCallback(async (
|
|
mandateId: string,
|
|
instanceId: string
|
|
): Promise<FeatureInstanceRole[]> => {
|
|
try {
|
|
const response = await api.get(`/api/features/instances/${instanceId}/available-roles`, {
|
|
headers: {
|
|
'X-Mandate-Id': mandateId
|
|
}
|
|
});
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
} catch (err: any) {
|
|
console.error('Error fetching instance roles:', err);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
features,
|
|
instances,
|
|
instancesPagination,
|
|
loading,
|
|
error,
|
|
fetchFeatures,
|
|
fetchInstances,
|
|
createInstance,
|
|
updateInstance,
|
|
deleteInstance,
|
|
syncInstanceRoles,
|
|
fetchMyFeatureInstances,
|
|
fetchTemplateRoles,
|
|
// Instance users management
|
|
fetchInstanceUsers,
|
|
addUserToInstance,
|
|
removeUserFromInstance,
|
|
updateInstanceUserRoles,
|
|
fetchInstanceRoles,
|
|
};
|
|
}
|
|
|
|
export default useFeatureAccess;
|