/** * 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, toSnakeCase } from 'types/conversion'; // Import generated date conversion functions import { convertFromApi, convertArrayFromApi } from 'types/types'; // ============================ // Streaming Types and Interfaces // ============================ interface StreamingOptions { method?: string, headers?: Record, onStatus?: (status: Types.ChatMessageStatus) => void; onMessage?: (message: T) => void; onStreaming?: (chunk: Types.ChatMessageStreaming) => void; onComplete?: () => void; onError?: (error: Types.ChatMessageError) => void; onWarn?: (warning: string) => void; signal?: AbortSignal; } interface DeleteResponse { success: boolean; message: string; } interface StreamingResponse { messageId: string; cancel: () => void; promise: Promise; } interface CreateCandidateAIResponse { message: string; candidate: Types.CandidateAI; resume: 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); console.log("extracted", extractedData); // 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 createCandidate( candidate: CreateCandidateRequest ): Promise { const response = await fetch(`${this.baseUrl}/candidates`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(candidate)) }); return handleApiResponse(response); } async createCandidateAI( userMessage: Types.ChatMessageUser ): Promise { const response = await fetch(`${this.baseUrl}/candidates/ai`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(userMessage)) }); const result = await handleApiResponse(response); return { message: result.message, candidate: convertFromApi(result.candidate, "CandidateAI"), resume: result.resume }; } /** * Create employer with email verification */ async createEmployerWithVerification( employer: CreateEmployerRequest ): 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)) }); return handleApiResponse(response); } /** * 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 // ============================ // reference can be candidateId, username, or email async getCandidate(reference: string): Promise { const response = await fetch(`${this.baseUrl}/candidates/${reference}`, { headers: this.defaultHeaders }); return this.handleApiResponseWithConversion(response, 'Candidate'); } async updateCandidate(id: string, updates: Partial): Promise { const request = formatApiRequest(updates); const response = await fetch(`${this.baseUrl}/candidates/${id}`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(request) }); return this.handleApiResponseWithConversion(response, 'Candidate'); } async deleteCandidate(id: string): Promise { const response = await fetch(`${this.baseUrl}/candidates/${id}`, { method: 'DELETE', headers: this.defaultHeaders, body: JSON.stringify({ id }) }); return handleApiResponse(response); } async deleteJob(id: string): Promise { const response = await fetch(`${this.baseUrl}/jobs/${id}`, { method: 'DELETE', headers: this.defaultHeaders, body: JSON.stringify({ id }) }); return handleApiResponse(response); } async uploadCandidateProfile(file: File): Promise { const formData = new FormData() formData.append('file', file); formData.append('filename', file.name); const response = await fetch(`${this.baseUrl}/candidates/profile/upload`, { method: 'POST', headers: { // Don't set Content-Type - browser will set it automatically with boundary 'Authorization': this.defaultHeaders['Authorization'] }, body: formData }); const result = await handleApiResponse(response); return result; } 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 // ============================ createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions): StreamingResponse { const body = JSON.stringify(job_description); return this.streamify('/jobs/from-content', body, streamingOptions); } async createJob(job: Omit): Promise { const body = JSON.stringify(formatApiRequest(job)); const response = await fetch(`${this.baseUrl}/jobs`, { method: 'POST', headers: this.defaultHeaders, body: body }); 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, 'JobFull'); } 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; } async getOrCreateChatSession(candidate: Types.Candidate, title: string, context_type: Types.ChatContextType) : Promise { const result = await this.getCandidateChatSessions(candidate.username); /* Find the 'candidate_chat' session if it exists, otherwise create it */ let session = result.sessions.data.find(session => session.title === 'candidate_chat'); if (!session) { session = await this.createCandidateChatSession( candidate.username, context_type, title ); } return session; } async getCandidateSimilarContent(query: string ): Promise { const response = await fetch(`${this.baseUrl}/candidates/rag-search`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(query) }); const result = await handleApiResponse(response); return result; } async getCandidateVectors( dimensions: number, ): Promise { const response = await fetch(`${this.baseUrl}/candidates/rag-vectors`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(dimensions) }); const result = await handleApiResponse(response); return result; } async getCandidateRAGContent( documentId: string, ): Promise { const response = await fetch(`${this.baseUrl}/candidates/rag-content`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify({ id: documentId }) }); const result = await handleApiResponse(response); return result; } /** uploadCandidateDocument usage: const controller : StreamingResponse = uploadCandidateDocument(...); const document : Types.Document = await controller.promise; console.log(`Document id: ${document.id}`) */ uploadCandidateDocument(file: File, options: Types.DocumentOptions, streamingOptions?: StreamingOptions): StreamingResponse { const convertedOptions = toSnakeCase(options); const formData = new FormData() formData.append('file', file); formData.append('filename', file.name); formData.append('options', JSON.stringify(convertedOptions)); streamingOptions = { ...streamingOptions, headers: { // Don't set Content-Type - browser will set it automatically with boundary 'Authorization': this.defaultHeaders['Authorization'] } }; return this.streamify('/candidates/documents/upload', formData, streamingOptions); } createJobFromFile(file: File, streamingOptions?: StreamingOptions): StreamingResponse { const formData = new FormData() formData.append('file', file); formData.append('filename', file.name); streamingOptions = { ...streamingOptions, headers: { // Don't set Content-Type - browser will set it automatically with boundary 'Authorization': this.defaultHeaders['Authorization'] } }; return this.streamify('/jobs/upload', formData, streamingOptions); } getJobRequirements(jobId: string, streamingOptions?: StreamingOptions): StreamingResponse { streamingOptions = { ...streamingOptions, headers: this.defaultHeaders, }; return this.streamify(`/jobs/requirements/${jobId}`, null, streamingOptions); } async candidateMatchForRequirement(candidate_id: string, requirement: string) : Promise { const response = await fetch(`${this.baseUrl}/candidates/${candidate_id}/skill-match`, { method: 'POST', headers: this.defaultHeaders, body: JSON.stringify(requirement) }); const result = await handleApiResponse(response); return result; } async updateCandidateDocument(document: Types.Document) : Promise { const request : Types.DocumentUpdateRequest = { filename: document.filename, options: document.options } const response = await fetch(`${this.baseUrl}/candidates/documents/${document.id}`, { method: 'PATCH', headers: this.defaultHeaders, body: JSON.stringify(formatApiRequest(request)) }); const result = await handleApiResponse(response); return result; }; async deleteCandidateDocument(document: Types.Document): Promise { const response = await fetch(`${this.baseUrl}/candidates/documents/${document.id}`, { method: 'DELETE', headers: this.defaultHeaders }); const result = await handleApiResponse(response); return result; } async getCandidateDocuments(): Promise { const response = await fetch(`${this.baseUrl}/candidates/documents`, { headers: this.defaultHeaders, }); const result = await handleApiResponse(response); return result; } async getCandidateDocumentText( document: Types.Document, ): Promise { const response = await fetch(`${this.baseUrl}/candidates/documents/${document.id}/content`, { headers: this.defaultHeaders, }); const result = await handleApiResponse(response); 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); } async resetChatSession(id: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`${this.baseUrl}/chat/sessions/${id}/reset`, { method: 'PATCH', headers: this.defaultHeaders }); return handleApiResponse<{ success: boolean; message: string }>(response); } /** * streamify * @param api API entrypoint * @param data Data to be attached to request Body * @param options callbacks, headers, and method * @returns */ streamify(api: string, data: BodyInit | null, options: StreamingOptions = {}) : StreamingResponse { const abortController = new AbortController(); const signal = options.signal || abortController.signal; const headers = options.headers || null; const method = options.method || 'POST'; let messageId = ''; let finalMessage : T | null = null; const promise = new Promise(async (resolve, reject) => { try { const response = await fetch(`${this.baseUrl}${api}`, { method, headers: headers || { ...this.defaultHeaders, 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: data, 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 streamingMessage: Types.ChatMessageStreaming | null = null; try { while (true) { const { done, value } = await reader.read(); if (done) { // Stream ended naturally 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: any = JSON.parse(data); // Handle different status types switch (incoming.status) { case 'streaming': const streaming = Types.convertChatMessageStreamingFromApi(incoming); if (streamingMessage === null) { streamingMessage = {...streaming}; } else { // Can't do a simple += as typescript thinks .content might not be there streamingMessage.content = (streamingMessage?.content || '') + streaming.content; // Update timestamp to latest streamingMessage.timestamp = streaming.timestamp; } options.onStreaming?.(streamingMessage); break; case 'status': const status = Types.convertChatMessageStatusFromApi(incoming); options.onStatus?.(status); break; case 'error': const error = Types.convertChatMessageErrorFromApi(incoming); options.onError?.(error); break; case 'done': const message = Types.convertApiMessageFromApi(incoming) as T; finalMessage = message as any; try { options.onMessage?.(message); } catch (error) { console.error('onMessage handler failed: ', error); } 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(finalMessage as T); } catch (error) { if (signal.aborted) { options.onComplete?.(); reject(new Error('Request was aborted')); } else { console.error(error); options.onError?.({ sessionId: '', status: 'error', type: 'text', content: (error as Error).message}); options.onComplete?.(); reject(error); } } }); return { messageId, cancel: () => abortController.abort(), promise }; } /** * Send message with streaming response support and date conversion */ sendMessageStream( chatMessage: Types.ChatMessageUser, options: StreamingOptions = {} ): StreamingResponse { const body = JSON.stringify(formatApiRequest(chatMessage)); return this.streamify(`/chat/sessions/${chatMessage.sessionId}/messages/stream`, body, options) } /** * 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 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 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; } export { ApiClient } export type { StreamingOptions, StreamingResponse }