258 lines
7.1 KiB
TypeScript
258 lines
7.1 KiB
TypeScript
/**
|
|
* useInvitations Hook
|
|
*
|
|
* Hook for managing invitations (creating, listing, validating, accepting).
|
|
* Uses the /api/invitations 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 Invitation {
|
|
id: string;
|
|
token: string;
|
|
mandateId: string;
|
|
featureInstanceId?: string;
|
|
roleIds: string[];
|
|
targetUsername: string;
|
|
email?: string;
|
|
createdBy: string;
|
|
createdAt: number;
|
|
expiresAt: number;
|
|
usedBy?: string;
|
|
usedAt?: number;
|
|
revokedAt?: number;
|
|
maxUses: number;
|
|
currentUses: number;
|
|
inviteUrl: string;
|
|
emailSent?: boolean;
|
|
isExpired?: boolean;
|
|
isUsedUp?: boolean;
|
|
}
|
|
|
|
export interface InvitationCreate {
|
|
targetUsername: string;
|
|
email?: string;
|
|
roleIds: string[];
|
|
featureInstanceId?: string;
|
|
expiresInHours?: number;
|
|
maxUses?: number;
|
|
}
|
|
|
|
export interface InvitationValidation {
|
|
valid: boolean;
|
|
reason?: string;
|
|
mandateId?: string;
|
|
mandateName?: string;
|
|
featureInstanceId?: string;
|
|
roleIds: string[];
|
|
roleLabels?: string[];
|
|
targetUsername?: string;
|
|
}
|
|
|
|
|
|
/**
|
|
* Hook for managing invitations
|
|
*/
|
|
export function useInvitations() {
|
|
const [invitations, setInvitations] = useState<Invitation[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
|
|
|
|
// Store current context for refetch
|
|
const currentMandateIdRef = useRef<string>('');
|
|
const currentOptionsRef = useRef<{ includeUsed?: boolean; includeExpired?: boolean }>({});
|
|
|
|
/**
|
|
* Fetch all invitations for a mandate with optional pagination
|
|
*/
|
|
const fetchInvitations = useCallback(async (
|
|
mandateIdOrPagination?: string | PaginationParams,
|
|
options?: { includeUsed?: boolean; includeExpired?: boolean }
|
|
): Promise<Invitation[]> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
let mandateId: string;
|
|
let paginationParams: PaginationParams = {};
|
|
|
|
// Handle backward compatibility
|
|
if (typeof mandateIdOrPagination === 'string') {
|
|
mandateId = mandateIdOrPagination;
|
|
currentMandateIdRef.current = mandateId;
|
|
if (options) {
|
|
currentOptionsRef.current = options;
|
|
}
|
|
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
|
|
// Called with pagination params only (refetch from FormGeneratorTable)
|
|
paginationParams = mandateIdOrPagination;
|
|
mandateId = currentMandateIdRef.current;
|
|
} else {
|
|
mandateId = currentMandateIdRef.current;
|
|
}
|
|
|
|
if (!mandateId) {
|
|
setLoading(false);
|
|
return [];
|
|
}
|
|
|
|
const fetchOptions = options || currentOptionsRef.current;
|
|
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (fetchOptions?.includeUsed) params.append('includeUsed', 'true');
|
|
if (fetchOptions?.includeExpired) params.append('includeExpired', 'true');
|
|
if (Object.keys(paginationParams).length > 0) {
|
|
params.append('pagination', JSON.stringify(paginationParams));
|
|
}
|
|
|
|
const response = await api.get(`/api/invitations/?${params.toString()}`, {
|
|
headers: { 'X-Mandate-Id': mandateId }
|
|
});
|
|
|
|
let data: Invitation[] = [];
|
|
if (response.data?.items && Array.isArray(response.data.items)) {
|
|
data = response.data.items;
|
|
if (response.data.pagination) {
|
|
setPagination(response.data.pagination);
|
|
}
|
|
} else {
|
|
data = Array.isArray(response.data) ? response.data : [];
|
|
}
|
|
|
|
setInvitations(data);
|
|
return data;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch invitations';
|
|
setError(errorMessage);
|
|
setInvitations([]);
|
|
setPagination(null);
|
|
return [];
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Create a new invitation
|
|
*/
|
|
const createInvitation = useCallback(async (
|
|
mandateId: string,
|
|
data: InvitationCreate
|
|
): Promise<{ success: boolean; data?: Invitation; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.post('/api/invitations/', 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 invitation';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Revoke an invitation
|
|
*/
|
|
const revokeInvitation = useCallback(async (
|
|
mandateId: string,
|
|
invitationId: string
|
|
): Promise<{ success: boolean; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await api.delete(`/api/invitations/${invitationId}`, {
|
|
headers: { 'X-Mandate-Id': mandateId }
|
|
});
|
|
// Optimistically update local state
|
|
setInvitations(prev => prev.filter(inv => inv.id !== invitationId));
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to revoke invitation';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Validate an invitation token (public - no auth required)
|
|
*/
|
|
const validateInvitation = useCallback(async (
|
|
token: string
|
|
): Promise<InvitationValidation> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.get(`/api/invitations/validate/${token}`);
|
|
return response.data;
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to validate invitation';
|
|
setError(errorMessage);
|
|
return {
|
|
valid: false,
|
|
reason: errorMessage,
|
|
roleIds: []
|
|
};
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Accept an invitation (requires authentication)
|
|
*/
|
|
const acceptInvitation = useCallback(async (
|
|
token: string
|
|
): Promise<{ success: boolean; data?: any; error?: string }> => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await api.post(`/api/invitations/accept/${token}`);
|
|
return { success: true, data: response.data };
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.detail || err.message || 'Failed to accept invitation';
|
|
setError(errorMessage);
|
|
return { success: false, error: errorMessage };
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
invitations,
|
|
loading,
|
|
error,
|
|
pagination,
|
|
fetchInvitations,
|
|
createInvitation,
|
|
revokeInvitation,
|
|
validateInvitation,
|
|
acceptInvitation,
|
|
};
|
|
}
|
|
|
|
export default useInvitations;
|