339 lines
No EOL
10 KiB
TypeScript
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
|
|
};
|
|
}
|