frontend_nyla/src/hooks/useConnections.ts

339 lines
No EOL
10 KiB
TypeScript

import { useState } from 'react';
import { useApiRequest } from './useApi';
import { getApiBaseUrl } from '../../config/config';
// Connection interfaces - exactly matching backend UserConnection model
export interface Connection {
id: string;
userId: string;
authority: 'local' | 'google' | 'msft';
externalId: string;
externalUsername: string;
externalEmail?: string;
status: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt: number; // Backend uses float for UTC timestamp in seconds
lastChecked: number; // Backend uses float for UTC timestamp in seconds
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
}
export interface CreateConnectionData {
id?: string;
userId?: string;
authority?: 'msft' | 'google'; // Keep for compatibility with existing code
type?: 'msft' | 'google'; // Backend expects this field
externalId?: string;
externalUsername?: string;
externalEmail?: string;
status?: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt?: number; // Backend uses float for UTC timestamp in seconds
lastChecked?: number; // Backend uses float for UTC timestamp in seconds
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
}
export interface ConnectResponse {
authUrl: string;
}
// Hook for managing connections
export function useConnections() {
const [connections, setConnections] = useState<Connection[]>([]);
const { request, isLoading, error } = useApiRequest<any, any>();
// Fetch all connections
const fetchConnections = async (): Promise<Connection[]> => {
try {
const data = await request({
url: '/api/connections/',
method: 'get'
});
setConnections(data);
return data;
} catch (error) {
console.error('Error fetching connections:', error);
setConnections([]);
throw error;
}
};
// Create a new connection
const createConnection = async (connectionData: CreateConnectionData): Promise<Connection> => {
try {
const data = await request({
url: '/api/connections/',
method: 'post',
data: connectionData
});
// Update local state
setConnections(prev => {
const existing = prev.find(conn => conn.id === data.id);
if (existing) {
return prev.map(conn => conn.id === data.id ? data : conn);
} else {
return [...prev, data];
}
});
return data;
} catch (error) {
console.error('Error creating connection:', error);
throw error;
}
};
// Connect to a service (initiate OAuth)
const connectService = async (connectionId: string): Promise<ConnectResponse> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/connect`,
method: 'post'
});
return data;
} catch (error) {
console.error('Error connecting service:', error);
throw error;
}
};
// Disconnect from a service
const disconnectService = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/disconnect`,
method: 'post'
});
// Update local state
setConnections(prev =>
prev.map(conn =>
conn.id === connectionId
? { ...conn, status: 'inactive' as any, lastChecked: Math.floor(Date.now() / 1000) }
: conn
)
);
return data;
} catch (error) {
console.error('Error disconnecting service:', error);
throw error;
}
};
// Delete a connection
const deleteConnection = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await request({
url: `/api/connections/${connectionId}`,
method: 'delete'
});
// Update local state
setConnections(prev => prev.filter(conn => conn.id !== connectionId));
return data;
} catch (error) {
console.error('Error deleting connection:', error);
throw error;
}
};
// Update a connection
const updateConnection = async (connectionId: string, updateData: Partial<Connection>): Promise<Connection> => {
try {
// Use PUT endpoint for updating connections
const data = await request({
url: `/api/connections/${connectionId}`,
method: 'put',
data: updateData
});
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error updating connection:', error);
throw error;
}
};
return {
connections,
fetchConnections,
createConnection,
updateConnection,
connectService,
disconnectService,
deleteConnection,
isLoading,
error
};
}
// Hook for OAuth connection popup flow (similar to useMsalAuth)
export function useOAuthConnect() {
const { connectService, fetchConnections } = useConnections();
const [isConnecting, setIsConnecting] = useState(false);
const [connectError, setConnectError] = useState<string | null>(null);
const connectWithPopup = async (connectionId: string): Promise<void> => {
setIsConnecting(true);
setConnectError(null);
try {
// Get the OAuth URL from backend
const response = await connectService(connectionId);
if (!response.authUrl) {
throw new Error('No OAuth URL received from backend');
}
console.log('OAuth URL from backend:', response.authUrl);
return new Promise((resolve, reject) => {
// Convert relative URL to absolute URL if needed
let authUrl = response.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${getApiBaseUrl()}${authUrl}`;
}
// Open popup using the same pattern as useAuthentication.ts
const popup = window.open(
authUrl,
'oauth-connect',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
setConnectError('Popup was blocked. Please allow popups and try again.');
setIsConnecting(false);
reject(new Error('Popup was blocked'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsConnecting(false);
console.log('OAuth popup closed');
// Refresh connections anyway in case it succeeded
fetchConnections();
setConnectError('Authentication was cancelled');
reject(new Error('Authentication was cancelled'));
}
}, 1000);
// Listen for messages from the popup (similar to useMsalAuth)
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(getApiBaseUrl());
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'msft_connection_success' || event.data.type === 'google_connection_success') {
// Clean up - IMPORTANT: clear the checkClosed interval first
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
console.log('OAuth connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (event.data.type === 'msft_connection_error' || event.data.type === 'google_connection_error') {
// Handle error - also clear the checkClosed interval
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
setConnectError(event.data.error || 'OAuth connection failed');
reject(new Error(event.data.error || 'OAuth connection failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
});
} catch (error: any) {
setConnectError(error.message || 'OAuth connection failed');
setIsConnecting(false);
throw error;
}
};
return {
connectWithPopup,
isConnecting,
error: connectError
};
}
// Hook for disconnecting services
export function useDisconnect() {
const { disconnectService, fetchConnections } = useConnections();
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [disconnectError, setDisconnectError] = useState<string | null>(null);
const disconnect = async (connectionId: string): Promise<void> => {
setIsDisconnecting(true);
setDisconnectError(null);
try {
await disconnectService(connectionId);
console.log('Service disconnected successfully');
// Refresh connections to update the status
await fetchConnections();
} catch (error: any) {
setDisconnectError(error.message || 'Disconnect failed');
console.error('Error disconnecting service:', error);
throw error;
} finally {
setIsDisconnecting(false);
}
};
return {
disconnect,
isDisconnecting,
error: disconnectError
};
}
// Hook for individual connection operations
export function useConnection(connectionId?: string) {
const [connection, setConnection] = useState<Connection | null>(null);
const { request, isLoading, error } = useApiRequest<any, Connection[]>();
const fetchConnection = async (id: string = connectionId!): Promise<Connection | null> => {
if (!id) return null;
try {
// Since there's no individual connection endpoint, fetch all and filter
const data: Connection[] = await request({
url: '/api/connections/',
method: 'get'
});
const foundConnection = data.find((conn: Connection) => conn.id === id);
setConnection(foundConnection || null);
return foundConnection || null;
} catch (error) {
console.error('Error fetching connection:', error);
setConnection(null);
throw error;
}
};
return {
connection,
fetchConnection,
isLoading,
error
};
}