From d0535a07d6bf88479e5dbee0da1200345503a1f0 Mon Sep 17 00:00:00 2001 From: Aljaz Ceru Date: Tue, 16 Sep 2025 11:31:36 +0200 Subject: [PATCH] lib --- frontend/src/lib/api-client.ts | 97 ++++++++++++++++ frontend/src/lib/config.ts | 49 +++++++++ frontend/src/lib/file-download.ts | 119 ++++++++++++++++++++ frontend/src/lib/id-utils.ts | 36 ++++++ frontend/src/lib/proxy-auth.ts | 176 ++++++++++++++++++++++++++++++ frontend/src/lib/token-manager.ts | 99 +++++++++++++++++ frontend/src/lib/utils.ts | 98 +++++++++++++++++ 7 files changed, 674 insertions(+) create mode 100644 frontend/src/lib/api-client.ts create mode 100644 frontend/src/lib/config.ts create mode 100644 frontend/src/lib/file-download.ts create mode 100644 frontend/src/lib/id-utils.ts create mode 100644 frontend/src/lib/proxy-auth.ts create mode 100644 frontend/src/lib/token-manager.ts create mode 100644 frontend/src/lib/utils.ts diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts new file mode 100644 index 0000000..75ae0aa --- /dev/null +++ b/frontend/src/lib/api-client.ts @@ -0,0 +1,97 @@ +import axios from 'axios'; +import Cookies from 'js-cookie'; + +const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || ''; + +const axiosInstance = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor to add auth token +axiosInstance.interceptors.request.use( + (config) => { + const token = Cookies.get('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor to handle token refresh +axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + const refreshToken = Cookies.get('refresh_token'); + if (refreshToken) { + const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, { + refresh_token: refreshToken, + }); + + const { access_token } = response.data; + Cookies.set('access_token', access_token, { expires: 7 }); + + originalRequest.headers.Authorization = `Bearer ${access_token}`; + return axiosInstance(originalRequest); + } + } catch (refreshError) { + // Refresh failed, redirect to login + Cookies.remove('access_token'); + Cookies.remove('refresh_token'); + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + + return Promise.reject(error); + } +); + +export const apiClient = { + get: async (url: string, config?: any): Promise => { + const response = await axiosInstance.get(url, config); + return response.data; + }, + + post: async (url: string, data?: any, config?: any): Promise => { + const response = await axiosInstance.post(url, data, config); + return response.data; + }, + + put: async (url: string, data?: any, config?: any): Promise => { + const response = await axiosInstance.put(url, data, config); + return response.data; + }, + + delete: async (url: string, config?: any): Promise => { + const response = await axiosInstance.delete(url, config); + return response.data; + }, + + patch: async (url: string, data?: any, config?: any): Promise => { + const response = await axiosInstance.patch(url, data, config); + return response.data; + }, +}; + +// Chatbot specific API methods +export const chatbotApi = { + create: async (data: any) => apiClient.post('/api/chatbot/create', data), + list: async () => apiClient.get('/api/chatbot/list'), + update: async (id: string, data: any) => apiClient.put(`/api/chatbot/update/${id}`, data), + delete: async (id: string) => apiClient.delete(`/api/chatbot/delete/${id}`), + chat: async (id: string, message: string, config?: any) => + apiClient.post(`/api/chatbot/chat`, { chatbot_id: id, message, ...config }), +}; \ No newline at end of file diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts new file mode 100644 index 0000000..479c7bc --- /dev/null +++ b/frontend/src/lib/config.ts @@ -0,0 +1,49 @@ +export const config = { + API_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL || '', + APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || 'Enclava', + DEFAULT_LANGUAGE: 'en', + SUPPORTED_LANGUAGES: ['en', 'es', 'fr', 'de', 'it'], + + // Feature flags + FEATURES: { + RAG: true, + PLUGINS: true, + ANALYTICS: true, + AUDIT_LOGS: true, + BUDGET_MANAGEMENT: true, + }, + + // Default values + DEFAULTS: { + TEMPERATURE: 0.7, + MAX_TOKENS: 1000, + TOP_K: 5, + MEMORY_LENGTH: 10, + }, + + // API endpoints + ENDPOINTS: { + AUTH: { + LOGIN: '/api/auth/login', + REGISTER: '/api/auth/register', + REFRESH: '/api/auth/refresh', + ME: '/api/auth/me', + }, + CHATBOT: { + LIST: '/api/chatbot/list', + CREATE: '/api/chatbot/create', + UPDATE: '/api/chatbot/update/:id', + DELETE: '/api/chatbot/delete/:id', + CHAT: '/api/chatbot/chat', + }, + LLM: { + MODELS: '/api/llm/models', + API_KEYS: '/api/llm/api-keys', + BUDGETS: '/api/llm/budgets', + }, + RAG: { + COLLECTIONS: '/api/rag/collections', + DOCUMENTS: '/api/rag/documents', + }, + }, +}; \ No newline at end of file diff --git a/frontend/src/lib/file-download.ts b/frontend/src/lib/file-download.ts new file mode 100644 index 0000000..bb0ea2e --- /dev/null +++ b/frontend/src/lib/file-download.ts @@ -0,0 +1,119 @@ +export const downloadFile = async (url: string, filename?: string): Promise => { + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Get the filename from the response headers if not provided + const contentDisposition = response.headers.get('Content-Disposition'); + let defaultFilename = filename || 'download'; + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); + if (filenameMatch && filenameMatch[1]) { + defaultFilename = filenameMatch[1].replace(/['"]/g, ''); + } + } + + // Get the blob from the response + const blob = await response.blob(); + + // Create a temporary URL for the blob + const blobUrl = window.URL.createObjectURL(blob); + + // Create a temporary link element + const link = document.createElement('a'); + link.href = blobUrl; + link.download = defaultFilename; + + // Append the link to the body + document.body.appendChild(link); + + // Trigger the download + link.click(); + + // Clean up + document.body.removeChild(link); + window.URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Error downloading file:', error); + throw error; + } +}; + +export const downloadFileFromData = ( + data: Blob | string, + filename: string, + mimeType?: string +): void => { + try { + let blob: Blob; + + if (typeof data === 'string') { + blob = new Blob([data], { type: mimeType || 'text/plain' }); + } else { + blob = data; + } + + const blobUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = blobUrl; + link.download = filename; + + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Error downloading file from data:', error); + throw error; + } +}; + +export const uploadFile = async ( + file: File, + url: string, + onProgress?: (progress: number) => void +): Promise => { + try { + const formData = new FormData(); + formData.append('file', file); + + const xhr = new XMLHttpRequest(); + + return new Promise((resolve, reject) => { + xhr.upload.onprogress = (event) => { + if (event.lengthComputable && onProgress) { + const progress = (event.loaded / event.total) * 100; + onProgress(progress); + } + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + const response = JSON.parse(xhr.responseText); + resolve(response); + } catch (error) { + resolve(xhr.responseText); + } + } else { + reject(new Error(`Upload failed with status ${xhr.status}`)); + } + }; + + xhr.onerror = () => { + reject(new Error('Network error during upload')); + }; + + xhr.open('POST', url, true); + xhr.send(formData); + }); + } catch (error) { + console.error('Error uploading file:', error); + throw error; + } +}; \ No newline at end of file diff --git a/frontend/src/lib/id-utils.ts b/frontend/src/lib/id-utils.ts new file mode 100644 index 0000000..16551f8 --- /dev/null +++ b/frontend/src/lib/id-utils.ts @@ -0,0 +1,36 @@ +export function generateId(): string { + return Math.random().toString(36).substr(2, 9); +} + +export function generateUniqueId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); +} + +export function generateMessageId(): string { + return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +export function generateChatId(): string { + return `chat_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; +} + +export function generateSessionId(): string { + return `sess_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; +} + +export function generateShortId(): string { + return Math.random().toString(36).substr(2, 6); +} + +export function generateTimestampId(): string { + return `ts_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; +} + +export function isValidId(id: string): boolean { + return typeof id === 'string' && id.length > 0; +} + +export function extractIdFromUrl(url: string): string | null { + const match = url.match(/\/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/); + return match ? match[1] : null; +} \ No newline at end of file diff --git a/frontend/src/lib/proxy-auth.ts b/frontend/src/lib/proxy-auth.ts new file mode 100644 index 0000000..201a547 --- /dev/null +++ b/frontend/src/lib/proxy-auth.ts @@ -0,0 +1,176 @@ +import { NextRequest, NextResponse } from 'next/server'; + +// This is a proxy auth utility for server-side API routes +// It handles authentication and proxying requests to the backend + +export interface ProxyAuthConfig { + backendUrl: string; + requireAuth?: boolean; + allowedRoles?: string[]; +} + +export class ProxyAuth { + private config: ProxyAuthConfig; + + constructor(config: ProxyAuthConfig) { + this.config = { + requireAuth: true, + ...config, + }; + } + + async authenticate(request: NextRequest): Promise<{ success: boolean; user?: any; error?: string }> { + // For server-side auth, we would typically validate the token + // This is a simplified implementation + const authHeader = request.headers.get('authorization'); + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return { success: false, error: 'Missing or invalid authorization header' }; + } + + const token = authHeader.substring(7); + + // Here you would validate the token with your auth service + // For now, we'll just check if it exists + if (!token) { + return { success: false, error: 'Invalid token' }; + } + + // In a real implementation, you would decode and validate the JWT + // and check user roles if required + return { + success: true, + user: { + id: 'user-id', + email: 'user@example.com', + role: 'user' + } + }; + } + + async proxyRequest( + request: NextRequest, + path: string, + options?: { + method?: string; + headers?: Record; + body?: any; + } + ): Promise { + // Authenticate the request if required + if (this.config.requireAuth) { + const authResult = await this.authenticate(request); + if (!authResult.success) { + return NextResponse.json( + { error: authResult.error || 'Authentication failed' }, + { status: 401 } + ); + } + + // Check roles if specified + if (this.config.allowedRoles && authResult.user) { + if (!this.config.allowedRoles.includes(authResult.user.role)) { + return NextResponse.json( + { error: 'Insufficient permissions' }, + { status: 403 } + ); + } + } + } + + // Build the target URL + const targetUrl = new URL(path, this.config.backendUrl); + + // Copy query parameters + targetUrl.search = request.nextUrl.search; + + // Prepare headers + const headers: Record = { + 'Content-Type': 'application/json', + ...options?.headers, + }; + + // Forward authorization header if present + const authHeader = request.headers.get('authorization'); + if (authHeader) { + headers.authorization = authHeader; + } + + try { + const response = await fetch(targetUrl, { + method: options?.method || request.method, + headers, + body: options?.body ? JSON.stringify(options.body) : request.body, + }); + + // Create a new response with the data + const data = await response.json(); + + return NextResponse.json(data, { + status: response.status, + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + console.error('Proxy request failed:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } + } +} + +// Utility function to create a proxy handler +export function createProxyHandler(config: ProxyAuthConfig) { + const proxyAuth = new ProxyAuth(config); + + return async (request: NextRequest, { params }: { params: { path?: string[] } }) => { + const path = params?.path ? params.path.join('/') : ''; + return proxyAuth.proxyRequest(request, path); + }; +} + +// Simplified proxy request function for direct usage +export async function proxyRequest( + request: NextRequest, + backendUrl: string, + path: string = '', + options?: { + method?: string; + headers?: Record; + body?: any; + requireAuth?: boolean; + } +): Promise { + const proxyAuth = new ProxyAuth({ + backendUrl, + requireAuth: options?.requireAuth ?? true, + }); + + return proxyAuth.proxyRequest(request, path, options); +} + +// Helper function to handle proxy responses with error handling +export async function handleProxyResponse( + request: NextRequest, + backendUrl: string, + path: string = '', + options?: { + method?: string; + headers?: Record; + body?: any; + requireAuth?: boolean; + } +): Promise { + try { + return await proxyRequest(request, backendUrl, path, options); + } catch (error) { + console.error('Proxy request failed:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/frontend/src/lib/token-manager.ts b/frontend/src/lib/token-manager.ts new file mode 100644 index 0000000..657d6a6 --- /dev/null +++ b/frontend/src/lib/token-manager.ts @@ -0,0 +1,99 @@ +import Cookies from 'js-cookie'; +import { EventEmitter } from 'events'; + +interface TokenManagerEvents { + tokensUpdated: []; + tokensCleared: []; +} + +export interface TokenManagerInterface { + getTokens(): { access_token: string | null; refresh_token: string | null }; + setTokens(access_token: string, refresh_token: string): void; + clearTokens(): void; + isAuthenticated(): boolean; + getAccessToken(): string | null; + getRefreshToken(): string | null; + getTokenExpiry(): { access_token_expiry: number | null; refresh_token_expiry: number | null }; + on( + event: E, + listener: (...args: TokenManagerEvents[E]) => void + ): this; + off( + event: E, + listener: (...args: TokenManagerEvents[E]) => void + ): this; +} + +class TokenManager extends EventEmitter implements TokenManagerInterface { + private static instance: TokenManager; + + private constructor() { + super(); + // Set max listeners to avoid memory leak warnings + this.setMaxListeners(100); + } + + static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + getTokens() { + return { + access_token: Cookies.get('access_token'), + refresh_token: Cookies.get('refresh_token'), + }; + } + + setTokens(access_token: string, refresh_token: string) { + // Set cookies with secure attributes + Cookies.set('access_token', access_token, { + expires: 7, // 7 days + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + }); + + Cookies.set('refresh_token', refresh_token, { + expires: 30, // 30 days + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + }); + + // Emit event + this.emit('tokensUpdated'); + } + + clearTokens() { + Cookies.remove('access_token'); + Cookies.remove('refresh_token'); + this.emit('tokensCleared'); + } + + isAuthenticated(): boolean { + return !!this.getAccessToken(); + } + + getAccessToken(): string | null { + return Cookies.get('access_token'); + } + + getRefreshToken(): string | null { + return Cookies.get('refresh_token'); + } + + getTokenExpiry(): { access_token_expiry: number | null; refresh_token_expiry: number | null } { + return { + access_token_expiry: parseInt(Cookies.get('access_token_expiry') || '0') || null, + refresh_token_expiry: parseInt(Cookies.get('refresh_token_expiry') || '0') || null, + }; + } + + getRefreshTokenExpiry(): number | null { + return parseInt(Cookies.get('refresh_token_expiry') || '0') || null; + } +} + +// Export singleton instance +export const tokenManager = TokenManager.getInstance(); \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..b98093e --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,98 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function formatDate(date: Date | string): string { + const d = new Date(date); + return d.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +export function formatDateTime(date: Date | string): string { + const d = new Date(date); + return d.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +export function formatRelativeTime(date: Date | string): string { + const d = new Date(date); + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffSeconds < 60) return 'just now'; + if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; + if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + + return formatDate(d); +} + +export function formatBytes(bytes: number, decimals = 2): string { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +export function formatNumber(num: number): string { + return new Intl.NumberFormat('en-US').format(num); +} + +export function formatCurrency(amount: number, currency = 'USD'): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + }).format(amount); +} + +export function truncate(str: string, length: number): string { + if (str.length <= length) return str; + return str.slice(0, length) + '...'; +} + +export function debounce any>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout; + + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +} + +export function throttle any>( + func: T, + limit: number +): (...args: Parameters) => void { + let inThrottle: boolean; + + return (...args: Parameters) => { + if (!inThrottle) { + func(...args); + inThrottle = true; + setTimeout(() => (inThrottle = false), limit); + } + }; +} \ No newline at end of file