/** * API Client with Streaming Support and Date Conversion * * This demonstrates how to use the generated types with the conversion utilities * for seamless frontend-backend communication, including streaming responses and * automatic date field conversion. */ // Import generated types (from running generate_types.py) import * as Types from 'types/types'; import { formatApiRequest, parseApiResponse, parsePaginatedResponse, handleApiResponse, handlePaginatedApiResponse, createPaginatedRequest, toUrlParams, extractApiData, ApiResponse, PaginatedResponse, PaginatedRequest } from 'types/conversion'; // Import generated date conversion functions import { convertCandidateFromApi, convertEmployerFromApi, convertJobFromApi, convertJobApplicationFromApi, convertChatSessionFromApi, convertChatMessageFromApi, convertFromApi, convertArrayFromApi } from 'types/types'; // ============================ // Streaming Types and Interfaces // ============================ interface StreamingOptions { onStatusChange?: (status: Types.ChatStatusType) => void; onMessage?: (message: Types.ChatMessage) => void; onStreaming?: (chunk: Types.ChatMessageBase) => void; onComplete?: () => void; onError?: (error: string | Types.ChatMessageBase) => void; onWarn?: (warning: string) => void; signal?: AbortSignal; } interface StreamingResponse { messageId: string; cancel: () => void; promise: Promise; } export interface CreateCandidateRequest { email: string; username: string; password: string; firstName: string; lastName: string; phone?: string; } export interface CreateEmployerRequest { email: string; username: string; password: string; companyName: string; industry: string; companySize: string; companyDescription: string; websiteUrl?: string; phone?: string; } export interface PasswordResetRequest { email: string; } export interface PasswordResetConfirm { token: string; newPassword: string; } // ============================ // Chat Types and Interfaces // ============================ export interface CandidateInfo { id: string; name: string; email: string; username: string; skills: string[]; experience: number; location: string; } export interface CreateChatSessionRequest { username?: string; // Optional candidate username to associate with context: Types.ChatContext; title?: string; } export interface UpdateChatSessionRequest { title?: string; context?: Partial; isArchived?: boolean; systemPrompt?: string; } export interface CandidateSessionsResponse { candidate: { id: string; username: string; fullName: string; email: string; }; sessions: PaginatedResponse; } // ============================ // API Client Class // ============================ class ApiClient { private baseUrl: string; private defaultHeaders: Record; constructor(accessToken?: string) { const loc = window.location; if (!loc.host.match(/.*battle-linux.*/)) { this.baseUrl = loc.protocol + "//" + loc.host + "/api/1.0"; } else { this.baseUrl = loc.protocol + "//battle-linux.ketrenos.com:8912/api/1.0"; } this.defaultHeaders = { 'Content-Type': 'application/json', ...(accessToken && { 'Authorization': `Bearer ${accessToken}` }) }; } // ============================ // Response Handlers with Date Conversion // ============================ /** * Handle API response with automatic date conversion for specific model types */ private async handleApiResponseWithConversion( response: Response, modelType?: string ): Promise { if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); const apiResponse = parseApiResponse(data); const extractedData = extractApiData(apiResponse); // Apply model-specific date conversion if modelType is provided if (modelType) { return convertFromApi(extractedData, modelType); } return extractedData; } /** * Handle paginated API response with automatic date conversion */ private async handlePaginatedApiResponseWithConversion( response: Response, modelType?: string ): Promise> { if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); const apiResponse = parsePaginatedResponse(data); const extractedData = extractApiData(apiResponse); // Apply model-specific date conversion to array items if modelType is provided if (modelType && extractedData.data) { return { ...extractedData, data: convertArrayFromApi(extractedData.data, modelType) }; } return extractedData; } /** * Create candidate with email verification */ async createCandidateWithVerification( candidate: CreateCandidateWithVerificationRequest ): Promise { const response = await fetch(`${this.baseUrl}/candidates`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(candidate)) }); return handleApiResponse(response); } /** * Create employer with email verification */ async createEmployerWithVerification( employer: CreateEmployerWithVerificationRequest ): Promise { const response = await fetch(`${this.baseUrl}/employers`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(employer)) }); return handleApiResponse(response); } /** * Verify email address */ async verifyEmail(request: EmailVerificationRequest): Promise { const response = await fetch(`${this.baseUrl}/auth/verify-email`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return handleApiResponse(response); } /** * Resend verification email */ async resendVerificationEmail(request: ResendVerificationRequest): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/auth/resend-verification`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return handleApiResponse<{ message: string }>(response); } /** * Request MFA for new device */ async requestMFA(request: MFARequest): Promise { const response = await fetch(`${this.baseUrl}/auth/mfa/request`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return handleApiResponse(response); } /** * Verify MFA code */ async verifyMFA(request: Types.MFAVerifyRequest): Promise { const formattedRequest = formatApiRequest(request) const response = await fetch(`${this.baseUrl}/auth/mfa/verify`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formattedRequest) }); return handleApiResponse(response); } /** * login with device detection */ async login(auth: Types.LoginRequest): Promise { const response = await fetch(`${this.baseUrl}/auth/login`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(auth)) }); // This could return either a full auth response or MFA request const data = await response.json(); if (!response.ok) { throw new Error(data.error?.message || 'Login failed'); } return data.data; } /** * Logout with token revocation */ async logout(accessToken: string, refreshToken: string): Promise<{ message: string; tokensRevoked: any }> { const response = await fetch(`${this.baseUrl}/auth/logout`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest({ accessToken, refreshToken })) }); return handleApiResponse<{ message: string; tokensRevoked: any }>(response); } /** * Logout from all devices */ async logoutAllDevices(): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/auth/logout-all`, { method: 'POST', headers: this.defaultHeaders }); return handleApiResponse<{ message: string }>(response); } // ============================ // Device Management Methods // ============================ /** * Get trusted devices for current user */ async getTrustedDevices(): Promise { const response = await fetch(`${this.baseUrl}/auth/trusted-devices`, { headers: this.defaultHeaders }); return handleApiResponse(response); } /** * Remove trusted device */ async removeTrustedDevice(deviceId: string): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/auth/trusted-devices/${deviceId}`, { method: 'DELETE', headers: this.defaultHeaders }); return handleApiResponse<{ message: string }>(response); } /** * Get security log for current user */ async getSecurityLog(days: number = 7): Promise { const response = await fetch(`${this.baseUrl}/auth/security-log?days=${days}`, { headers: this.defaultHeaders }); return handleApiResponse(response); } // ============================ // Admin Methods (if user has admin role) // ============================ /** * Get pending user verifications (admin only) */ async getPendingVerifications(): Promise { const response = await fetch(`${this.baseUrl}/admin/pending-verifications`, { headers: this.defaultHeaders }); return handleApiResponse(response); } /** * Manually verify user (admin only) */ async manuallyVerifyUser(userId: string, reason: string): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/admin/verify-user/${userId}`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest({ reason })) }); return handleApiResponse<{ message: string }>(response); } /** * Get user security events (admin only) */ async getUserSecurityEvents(userId: string, days: number = 30): Promise { const response = await fetch(`${this.baseUrl}/admin/users/${userId}/security-events?days=${days}`, { headers: this.defaultHeaders }); return handleApiResponse(response); } // ============================ // Utility Methods // ============================ /** * Generate device fingerprint for MFA */ generateDeviceFingerprint(): string { // Create a basic device fingerprint const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (ctx) { ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillText('Device fingerprint', 2, 2); } const fingerprint = (canvas.toDataURL() || '') + navigator.userAgent + navigator.language + // screen.width + 'x' + screen.height + // (navigator.platform || '') + (navigator.cookieEnabled ? '1' : '0'); // Create a hash-like string from the fingerprint let hash = 0; for (let i = 0; i < fingerprint.length; i++) { const char = fingerprint.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(16).slice(0, 16); } /** * Get device name from user agent */ getDeviceName(): string { const ua = navigator.userAgent; const browser = ua.includes('Chrome') ? 'Chrome' : ua.includes('Firefox') ? 'Firefox' : ua.includes('Safari') ? 'Safari' : ua.includes('Edge') ? 'Edge' : 'Browser'; const os = ua.includes('Windows') ? 'Windows' : ua.includes('Mac') ? 'macOS' : ua.includes('Linux') ? 'Linux' : ua.includes('Android') ? 'Android' : ua.includes('iOS') ? 'iOS' : 'Unknown OS'; return `${browser} on ${os}`; } /** * Check if email verification is pending */ isEmailVerificationPending(): boolean { return localStorage.getItem('pendingEmailVerification') === 'true'; } /** * Set email verification pending status */ setPendingEmailVerification(email: string, pending: boolean = true): void { if (pending) { localStorage.setItem('pendingEmailVerification', 'true'); localStorage.setItem('pendingVerificationEmail', email); } else { localStorage.removeItem('pendingEmailVerification'); localStorage.removeItem('pendingVerificationEmail'); } } /** * Get pending verification email */ getPendingVerificationEmail(): string | null { return localStorage.getItem('pendingVerificationEmail'); } // ============================ // Authentication Methods // ============================ async refreshToken(refreshToken: string): Promise { const response = await fetch(`${this.baseUrl}/auth/refresh`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest({ refreshToken })) }); return handleApiResponse(response); } // ============================ // Candidate Methods with Date Conversion // ============================ async createCandidate(request: CreateCandidateRequest): Promise { const response = await fetch(`${this.baseUrl}/candidates`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return this.handleApiResponseWithConversion(response, 'Candidate'); } async getCandidate(username: string): Promise { const response = await fetch(`${this.baseUrl}/candidates/${username}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'Candidate'); } async updateCandidate(id: string, updates: Partial): Promise { const response = await fetch(`${this.baseUrl}/candidates/${id}`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(updates)) }); return this.handleApiResponseWithConversion(response, 'Candidate'); } async getCandidates(request: Partial = {}): Promise> { const paginatedRequest = createPaginatedRequest(request); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/candidates?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'Candidate'); } async searchCandidates(query: string, filters?: Record): Promise> { const searchRequest = { query, filters, page: 1, limit: 20 }; const params = toUrlParams(formatApiRequest(searchRequest)); const response = await fetch(`${this.baseUrl}/candidates/search?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'Candidate'); } // ============================ // Employer Methods with Date Conversion // ============================ async createEmployer(request: CreateEmployerRequest): Promise { const response = await fetch(`${this.baseUrl}/employers`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return this.handleApiResponseWithConversion(response, 'Employer'); } async getEmployer(id: string): Promise { const response = await fetch(`${this.baseUrl}/employers/${id}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'Employer'); } async updateEmployer(id: string, updates: Partial): Promise { const response = await fetch(`${this.baseUrl}/employers/${id}`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(updates)) }); return this.handleApiResponseWithConversion(response, 'Employer'); } // ============================ // Job Methods with Date Conversion // ============================ async createJob(job: Omit): Promise { const response = await fetch(`${this.baseUrl}/jobs`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(job)) }); return this.handleApiResponseWithConversion(response, 'Job'); } async getJob(id: string): Promise { const response = await fetch(`${this.baseUrl}/jobs/${id}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'Job'); } async getJobs(request: Partial = {}): Promise> { const paginatedRequest = createPaginatedRequest(request); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/jobs?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } async getJobsByEmployer(employerId: string, request: Partial = {}): Promise> { const paginatedRequest = createPaginatedRequest(request); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/employers/${employerId}/jobs?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } async searchJobs(query: string, filters?: Record): Promise> { const searchRequest = { query, filters, page: 1, limit: 20 }; const params = toUrlParams(formatApiRequest(searchRequest)); const response = await fetch(`${this.baseUrl}/jobs/search?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'Job'); } // ============================ // Job Application Methods with Date Conversion // ============================ async applyToJob(application: Omit): Promise { const response = await fetch(`${this.baseUrl}/job-applications`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(application)) }); return this.handleApiResponseWithConversion(response, 'JobApplication'); } async getJobApplication(id: string): Promise { const response = await fetch(`${this.baseUrl}/job-applications/${id}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'JobApplication'); } async getJobApplications(request: Partial = {}): Promise> { const paginatedRequest = createPaginatedRequest(request); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/job-applications?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'JobApplication'); } async updateApplicationStatus(id: string, status: Types.ApplicationStatus): Promise { const response = await fetch(`${this.baseUrl}/job-applications/${id}/status`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest({ status })) }); return this.handleApiResponseWithConversion(response, 'JobApplication'); } // ============================ // Chat Methods with Date Conversion // ============================ /** * Create a chat session with optional candidate association */ async createChatSessionWithCandidate( request: CreateChatSessionRequest ): Promise { const response = await fetch(`${this.baseUrl}/chat/sessions`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return this.handleApiResponseWithConversion(response, 'ChatSession'); } /** * Get all chat sessions related to a specific candidate */ async getCandidateChatSessions( username: string, request: Partial = {} ): Promise { const paginatedRequest = createPaginatedRequest(request); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/candidates/${username}/chat-sessions?${params}`, { headers: this.defaultHeaders }); // Handle the nested sessions with date conversion const result = await this.handleApiResponseWithConversion(response); // Convert the nested sessions array if (result.sessions && result.sessions.data) { result.sessions.data = convertArrayFromApi(result.sessions.data, 'ChatSession'); } return result; } /** * Create a chat session about a specific candidate */ async createCandidateChatSession( username: string, chatType: Types.ChatContextType = 'candidate_chat', title?: string ): Promise { const request: CreateChatSessionRequest = { username, title: title || `Discussion about ${username}`, context: { type: chatType, additionalContext: {} } }; return this.createChatSessionWithCandidate(request); } async createChatSession(context: Types.ChatContext): Promise { const response = await fetch(`${this.baseUrl}/chat/sessions`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest({ context })) }); return this.handleApiResponseWithConversion(response, 'ChatSession'); } async getChatSession(id: string): Promise { const response = await fetch(`${this.baseUrl}/chat/sessions/${id}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'ChatSession'); } /** * Update a chat session's properties */ async updateChatSession( id: string, updates: UpdateChatSessionRequest ): Promise { const response = await fetch(`${this.baseUrl}/chat/sessions/${id}`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(updates)) }); return this.handleApiResponseWithConversion(response, 'ChatSession'); } /** * Delete a chat session */ async deleteChatSession(id: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`${this.baseUrl}/chat/sessions/${id}`, { method: 'DELETE', headers: this.defaultHeaders }); return handleApiResponse<{ success: boolean; message: string }>(response); } /** * Send message with streaming response support and date conversion */ sendMessageStream( chatMessage: Types.ChatMessageUser, options: StreamingOptions = {} ): StreamingResponse { const abortController = new AbortController(); const signal = options.signal || abortController.signal; let messageId = ''; const promise = new Promise(async (resolve, reject) => { try { const response = await fetch(`${this.baseUrl}/chat/sessions/${chatMessage.sessionId}/messages/stream`, { method: 'POST', headers: { ...this.defaultHeaders, 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache' }, body: JSON.stringify(formatApiRequest({ chatMessage })), signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('Response body is not readable'); } const decoder = new TextDecoder(); let buffer = ''; let incomingMessage: Types.ChatMessage | null = null; const incomingMessageList: Types.ChatMessage[] = []; try { while (true) { const { done, value } = await reader.read(); if (done) { // Stream ended naturally - create final message break; } buffer += decoder.decode(value, { stream: true }); // Process complete lines const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim() === '') continue; // Skip blank lines between SSEs try { if (line.startsWith('data: ')) { const data = line.slice(5).trim(); const incoming: Types.ChatMessageBase = JSON.parse(data); // Convert date fields for incoming messages const convertedIncoming = convertChatMessageFromApi(incoming); // Trigger callbacks based on status if (convertedIncoming.status !== incomingMessage?.status) { options.onStatusChange?.(convertedIncoming.status); } // Handle different status types switch (convertedIncoming.status) { case 'streaming': if (incomingMessage === null) { incomingMessage = {...convertedIncoming}; } else { // Can't do a simple += as typescript thinks .content might not be there incomingMessage.content = (incomingMessage?.content || '') + convertedIncoming.content; // Update timestamp to latest incomingMessage.timestamp = convertedIncoming.timestamp; } options.onStreaming?.(convertedIncoming); break; case 'error': options.onError?.(convertedIncoming); break; default: incomingMessageList.push(convertedIncoming); options.onMessage?.(convertedIncoming); break; } } } catch (error) { console.warn('Failed to process SSE:', error); if (error instanceof Error) { options.onWarn?.(error.message); } // Continue processing other lines } } } } finally { reader.releaseLock(); } options.onComplete?.(); resolve(incomingMessageList); } catch (error) { if (signal.aborted) { options.onComplete?.(); reject(new Error('Request was aborted')); } else { options.onError?.((error as Error).message); options.onComplete?.(); reject(error); } } }); return { messageId, cancel: () => abortController.abort(), promise }; } /** * Get persisted chat messages for a session with date conversion */ async getChatMessages( sessionId: string, request: Partial = {} ): Promise> { const paginatedRequest = createPaginatedRequest({ limit: 50, // Higher default for chat messages ...request}); const params = toUrlParams(formatApiRequest(paginatedRequest)); const response = await fetch(`${this.baseUrl}/chat/sessions/${sessionId}/messages?${params}`, { headers: this.defaultHeaders }); return this.handlePaginatedApiResponseWithConversion(response, 'ChatMessage'); } // ============================ // Password Reset Methods // ============================ async requestPasswordReset(request: PasswordResetRequest): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/auth/password-reset/request`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return handleApiResponse<{ message: string }>(response); } async confirmPasswordReset(request: PasswordResetConfirm): Promise<{ message: string }> { const response = await fetch(`${this.baseUrl}/auth/password-reset/confirm`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); return handleApiResponse<{ message: string }>(response); } // ============================ // Password Validation Utilities // ============================ validatePasswordStrength(password: string): { isValid: boolean; issues: string[] } { const issues: string[] = []; if (password.length < 8) { issues.push('Password must be at least 8 characters long'); } if (!/[A-Z]/.test(password)) { issues.push('Password must contain at least one uppercase letter'); } if (!/[a-z]/.test(password)) { issues.push('Password must contain at least one lowercase letter'); } if (!/\d/.test(password)) { issues.push('Password must contain at least one digit'); } const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`"\'\\'; if (!password.split('').some(char => specialChars.includes(char))) { issues.push('Password must contain at least one special character'); } return { isValid: issues.length === 0, issues }; } validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } validateUsername(username: string): { isValid: boolean; issues: string[] } { const issues: string[] = []; if (username.length < 3) { issues.push('Username must be at least 3 characters long'); } if (username.length > 30) { issues.push('Username must be no more than 30 characters long'); } if (!/^[a-zA-Z0-9_-]+$/.test(username)) { issues.push('Username can only contain letters, numbers, underscores, and hyphens'); } return { isValid: issues.length === 0, issues }; } // ============================ // Error Handling Helper // ============================ async handleRequest(requestFn: () => Promise, modelType?: string): Promise { try { const response = await requestFn(); return await this.handleApiResponseWithConversion(response, modelType); } catch (error) { console.error('API request failed:', error); throw error; } } // ============================ // Utility Methods // ============================ setAuthToken(token: string): void { this.defaultHeaders['Authorization'] = `Bearer ${token}`; } clearAuthToken(): void { delete this.defaultHeaders['Authorization']; } getBaseUrl(): string { return this.baseUrl; } } // ============================ // Request/Response Types // ============================ export interface CreateCandidateWithVerificationRequest { email: string; username: string; password: string; firstName: string; lastName: string; phone?: string; } export interface CreateEmployerWithVerificationRequest { email: string; username: string; password: string; companyName: string; industry: string; companySize: string; companyDescription: string; websiteUrl?: string; phone?: string; } export interface EmailVerificationRequest { token: string; } export interface ResendVerificationRequest { email: string; } export interface MFARequest { email: string; password: string; deviceId: string; deviceName: string; } export interface RegistrationResponse { message: string; email: string; verificationRequired: boolean; } export interface EmailVerificationResponse { message: string; accountActivated: boolean; userType: string; } export interface TrustedDevice { deviceId: string; deviceName: string; browser: string; browserVersion: string; os: string; osVersion: string; addedAt: string; lastUsed: string; ipAddress: string; } // ============================ // Additional Types // ============================ export interface SecurityEvent { timestamp: string; userId: string; eventType: 'login' | 'logout' | 'mfa_request' | 'mfa_verify' | 'password_change' | 'email_verify' | 'device_add' | 'device_remove'; details: { ipAddress?: string; deviceName?: string; success?: boolean; failureReason?: string; [key: string]: any; }; } export interface PendingVerification { id: string; email: string; userType: 'candidate' | 'employer'; createdAt: string; expiresAt: string; attempts: number; } // ============================ // Usage Examples // ============================ /* // Registration with email verification const apiClient = new ApiClient(); try { const result = await apiClient.createCandidateWithVerification({ email: 'user@example.com', username: 'johndoe', password: 'SecurePassword123!', firstName: 'John', lastName: 'Doe', phone: '+1234567890' }); console.log(result.message); // "Registration successful! Please check your email..." // Set pending verification status apiClient.setPendingEmailVerification(result.email); // Show success dialog to user showRegistrationSuccessDialog(result); } catch (error) { console.error('Registration failed:', error); } // login with MFA support try { const loginResult = await apiClient.login('user@example.com', 'password'); if ('mfaRequired' in loginResult && loginResult.mfaRequired) { // Show MFA dialog showMFADialog({ email: 'user@example.com', deviceId: loginResult.deviceId!, deviceName: loginResult.message || 'Unknown device' }); } else { // Normal login success const authData = loginResult as Types.AuthResponse; handleLoginSuccess(authData); } } catch (error) { console.error('Login failed:', error); } // Email verification try { const verificationResult = await apiClient.verifyEmail({ token: 'verification-token-from-email' }); console.log(verificationResult.message); // "Email verified successfully!" // Clear pending verification apiClient.setPendingEmailVerification('', false); // Redirect to login window.location.href = '/login'; } catch (error) { console.error('Email verification failed:', error); } // MFA verification try { const mfaResult = await apiClient.verifyMFA({ email: 'user@example.com', code: '123456', deviceId: 'device-fingerprint', rememberDevice: true }); // Handle successful login handleLoginSuccess(mfaResult); } catch (error) { console.error('MFA verification failed:', error); } // Device management try { const devices = await apiClient.getTrustedDevices(); devices.forEach(device => { console.log(`Device: ${device.deviceName}, Last used: ${device.lastUsed}`); }); // Remove a device await apiClient.removeTrustedDevice('device-id-to-remove'); } catch (error) { console.error('Device management failed:', error); } // Security log try { const securityEvents = await apiClient.getSecurityLog(30); // Last 30 days securityEvents.forEach(event => { console.log(`${event.timestamp}: ${event.eventType} from ${event.details.deviceName}`); }); } catch (error) { console.error('Failed to load security log:', error); } */ // ============================ // React Hooks for Streaming with Date Conversion // ============================ /* React Hook Examples for Streaming Chat with proper date handling import { useState, useEffect, useCallback, useRef } from 'react'; export function useStreamingChat(sessionId: string) { const [messages, setMessages] = useState([]); const [currentMessage, setCurrentMessage] = useState(null); const [isStreaming, setIsStreaming] = useState(false); const [error, setError] = useState(null); const apiClient = useApiClient(); const streamingRef = useRef(null); const sendMessage = useCallback(async (query: Types.ChatQuery) => { setError(null); setIsStreaming(true); setCurrentMessage(null); const streamingOptions: StreamingOptions = { onMessage: (message) => { // Message already has proper Date objects from conversion setCurrentMessage(message); }, onStreaming: (chunk) => { // Chunk also has proper Date objects setCurrentMessage(prev => prev ? { ...prev, content: prev.content + chunk.content, timestamp: chunk.timestamp // Update to latest timestamp } : { id: chunk.id || '', sessionId, status: 'streaming', sender: 'ai', content: chunk.content, timestamp: chunk.timestamp // Already a Date object } ); }, onStatusChange: (status) => { setCurrentMessage(prev => prev ? { ...prev, status } : null); }, onComplete: () => { if (currentMessage) { setMessages(prev => [...prev, currentMessage]); } setCurrentMessage(null); setIsStreaming(false); }, onError: (err) => { setError(typeof err === 'string' ? err : err.content); setIsStreaming(false); setCurrentMessage(null); } }; try { streamingRef.current = apiClient.sendMessageStream(sessionId, query, streamingOptions); await streamingRef.current.promise; } catch (err) { setError(err instanceof Error ? err.message : 'Failed to send message'); setIsStreaming(false); } }, [sessionId, apiClient, currentMessage]); const cancelStreaming = useCallback(() => { if (streamingRef.current) { streamingRef.current.cancel(); setIsStreaming(false); setCurrentMessage(null); } }, []); return { messages, currentMessage, isStreaming, error, sendMessage, cancelStreaming }; } // Usage in React component with proper date handling: function ChatInterface({ sessionId }: { sessionId: string }) { const { messages, currentMessage, isStreaming, error, sendMessage, cancelStreaming } = useStreamingChat(sessionId); const handleSendMessage = (text: string) => { sendMessage(text); }; return (
{messages.map(message => (
{message.sender}: {message.timestamp.toLocaleTimeString()}
{message.content}
))} {currentMessage && (
{currentMessage.sender}: {currentMessage.timestamp.toLocaleTimeString()} {isStreaming && ...}
{currentMessage.content}
)}
{error &&
{error}
}
{ if (e.key === 'Enter') { handleSendMessage(e.currentTarget.value); e.currentTarget.value = ''; } }} disabled={isStreaming} /> {isStreaming && ( )}
); } */ // ============================ // Usage Examples with Date Conversion // ============================ /* // Initialize API client const apiClient = new ApiClient(); // All returned objects now have proper Date fields automatically! // Create a candidate - createdAt, updatedAt, lastLogin are Date objects try { const candidate = await apiClient.createCandidate({ email: 'jane@example.com', username: 'jane_doe', password: 'SecurePassword123!', firstName: 'Jane', lastName: 'Doe' }); // These are now Date objects, not strings! console.log('Created at:', candidate.createdAt.toLocaleDateString()); console.log('Profile created on:', candidate.createdAt.toDateString()); if (candidate.lastLogin) { console.log('Last seen:', candidate.lastLogin.toRelativeTimeString()); } } catch (error) { console.error('Failed to create candidate:', error); } // Get jobs with proper date conversion try { const jobs = await apiClient.getJobs({ limit: 10 }); jobs.data.forEach(job => { // datePosted, applicationDeadline, featuredUntil are Date objects console.log(`${job.title} - Posted: ${job.datePosted.toLocaleDateString()}`); if (job.applicationDeadline) { const daysRemaining = Math.ceil( (job.applicationDeadline.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) ); console.log(`Deadline in ${daysRemaining} days`); } }); } catch (error) { console.error('Failed to fetch jobs:', error); } // Update and delete chat sessions with proper date handling try { // Update a session title const updatedSession = await apiClient.updateChatSession('session-id', { title: 'New Session Title', isArchived: false }); console.log('Updated session:', updatedSession.title); console.log('Last activity:', updatedSession.lastActivity.toLocaleString()); // Delete a session const deleteResult = await apiClient.deleteChatSession('session-id'); console.log('Delete result:', deleteResult.message); } catch (error) { console.error('Failed to manage session:', error); } // Streaming with proper date conversion const streamResponse = apiClient.sendMessageStream(sessionId, 'Tell me about job opportunities', { onStreaming: (chunk) => { // chunk.timestamp is a Date object console.log(`Streaming at ${chunk.timestamp.toLocaleTimeString()}:`, chunk.content); }, onMessage: (message) => { // message.timestamp is a Date object console.log(`Final message at ${message.timestamp.toLocaleTimeString()}:`, message.content); }, onComplete: () => { console.log('Streaming completed'); } }); // Chat sessions with date conversion try { const chatSession = await apiClient.createChatSession({ type: 'job_search', additionalContext: {} }); // createdAt and lastActivity are Date objects console.log('Session created:', chatSession.createdAt.toISOString()); console.log('Last activity:', chatSession.lastActivity.toLocaleDateString()); } catch (error) { console.error('Failed to create chat session:', error); } // Get chat messages with date conversion try { const messages = await apiClient.getChatMessages(sessionId); messages.data.forEach(message => { // timestamp is a Date object console.log(`[${message.timestamp.toLocaleString()}] ${message.sender}: ${message.content}`); }); } catch (error) { console.error('Failed to fetch messages:', error); } */ export { ApiClient } export type { StreamingOptions, StreamingResponse }