This commit is contained in:
2025-09-16 11:31:36 +02:00
parent 965002687b
commit d0535a07d6
7 changed files with 674 additions and 0 deletions

View File

@@ -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 <T = any>(url: string, config?: any): Promise<T> => {
const response = await axiosInstance.get(url, config);
return response.data;
},
post: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
const response = await axiosInstance.post(url, data, config);
return response.data;
},
put: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
const response = await axiosInstance.put(url, data, config);
return response.data;
},
delete: async <T = any>(url: string, config?: any): Promise<T> => {
const response = await axiosInstance.delete(url, config);
return response.data;
},
patch: async <T = any>(url: string, data?: any, config?: any): Promise<T> => {
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 }),
};

View File

@@ -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',
},
},
};

View File

@@ -0,0 +1,119 @@
export const downloadFile = async (url: string, filename?: string): Promise<void> => {
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<any> => {
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;
}
};

View File

@@ -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;
}

View File

@@ -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<string, string>;
body?: any;
}
): Promise<NextResponse> {
// 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<string, string> = {
'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<string, string>;
body?: any;
requireAuth?: boolean;
}
): Promise<NextResponse> {
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<string, string>;
body?: any;
requireAuth?: boolean;
}
): Promise<NextResponse> {
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 }
);
}
}

View File

@@ -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<E extends keyof TokenManagerEvents>(
event: E,
listener: (...args: TokenManagerEvents[E]) => void
): this;
off<E extends keyof TokenManagerEvents>(
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();

98
frontend/src/lib/utils.ts Normal file
View File

@@ -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<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}