diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2588f30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Build stage +FROM node:20-alpine AS build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install all dependencies (including dev dependencies for build) +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built application +COPY --from=build /app/dist/interview-assistant/browser /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/angular.json b/angular.json index 4096969..33bc325 100644 --- a/angular.json +++ b/angular.json @@ -27,6 +27,11 @@ { "glob": "**/*", "input": "public" + }, + { + "glob": "**/*", + "input": "node_modules/pdfjs-dist/build", + "output": "pdfjs-dist/build" } ], "styles": [ @@ -38,13 +43,13 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" + "maximumWarning": "2MB", + "maximumError": "5MB" }, { "type": "anyComponentStyle", - "maximumWarning": "4kB", - "maximumError": "8kB" + "maximumWarning": "20kB", + "maximumError": "50kB" } ], "outputHashing": "all" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2162a2b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + interviewer: + build: . + container_name: interviewer + restart: unless-stopped + ports: + - "127.0.0.1:3007:80" + networks: + - web + labels: + - "traefik.enable=true" + - "traefik.http.routers.interviewer.rule=Host(`interviewer.gm-tech.org`)" + - "traefik.http.routers.interviewer.tls=true" + - "traefik.http.routers.interviewer.tls.certresolver=letsencrypt" + - "traefik.http.services.interviewer.loadbalancer.server.port=80" + - "traefik.docker.network=web" + +networks: + web: + external: true \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d8d9fc3 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,118 @@ +events { + worker_connections 1024; +} + +http { + # Define MIME types including .mjs + types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js mjs; + application/atom+xml atom; + application/rss+xml rss; + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + application/font-woff woff; + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; + } + + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Handle Angular routing + location / { + try_files $uri $uri/ /index.html; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Enable gzip compression + gzip on; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + } +} \ No newline at end of file diff --git a/src/app/components/cv-upload/cv-upload.component.ts b/src/app/components/cv-upload/cv-upload.component.ts index 5c7e0f1..3c9bfef 100644 --- a/src/app/components/cv-upload/cv-upload.component.ts +++ b/src/app/components/cv-upload/cv-upload.component.ts @@ -239,8 +239,8 @@ export class CVUploadComponent implements OnInit { const cvProfile = await this.cvParserService.parseCV(file); this.cvProfile.set(cvProfile); - // Send to n8n for question generation (with Authelia authentication) - this.sendToN8nWithAuth(cvProfile); + // Send to n8n for question generation (with API key authentication) + this.sendToN8n(cvProfile); console.log('CV processed successfully', { profileId: cvProfile.id }); @@ -332,46 +332,11 @@ export class CVUploadComponent implements OnInit { } /** - * Send CV to n8n with Authelia authentication handling + * Send CV to n8n with API key authentication */ - private async sendToN8nWithAuth(cvProfile: CVProfile): Promise { + private async sendToN8n(cvProfile: CVProfile): Promise { try { - // Check if already authenticated - if (this.autheliaAuth.isAuthenticated) { - console.log('✅ Already authenticated, submitting CV...'); - await this.submitToN8n(cvProfile); - return; - } - - // Check authentication status - console.log('🔍 Checking authentication status...'); - this.autheliaAuth.checkAuthenticationStatus().subscribe({ - next: async (isAuthenticated) => { - if (isAuthenticated) { - console.log('✅ Authentication verified, submitting CV...'); - await this.submitToN8n(cvProfile); - } else { - console.log('🔒 Authentication required, showing login...'); - this.showAuthLogin(cvProfile); - } - }, - error: (error) => { - console.log('❌ Authentication check failed:', error); - this.showAuthLogin(cvProfile); - } - }); - } catch (error) { - console.error('❌ Error in sendToN8nWithAuth:', error); - this.uploadError.set('Failed to connect to n8n services. Please try again.'); - } - } - - /** - * Actually submit CV to n8n (assumes authentication is valid) - */ - private async submitToN8n(cvProfile: CVProfile): Promise { - try { - console.log('🚀 Submitting CV to n8n...'); + console.log('🚀 Submitting CV to n8n with API key authentication...'); const analysisResponse = await this.n8nSyncService.submitCVForAnalysis(cvProfile).toPromise(); if (analysisResponse) { @@ -386,52 +351,19 @@ export class CVUploadComponent implements OnInit { } } catch (error) { console.error('❌ n8n submission failed:', error); - - if (error instanceof Error && error.message === 'Authelia authentication required') { - console.log('🔒 Authentication expired, showing login...'); - this.showAuthLogin(cvProfile); - } else { - console.warn('⚠️ n8n analysis failed, continuing with local processing'); - this.uploadError.set('n8n analysis unavailable. CV processed locally.'); - } + console.warn('⚠️ n8n analysis failed, continuing with local processing'); + this.uploadError.set('n8n analysis unavailable. CV processed locally.'); } } /** - * Show authentication login modal - */ - private showAuthLogin(cvProfile?: CVProfile): void { - console.log('🔐 Showing authentication login...'); - - // Store CV profile for retry after login - if (cvProfile) { - (this as any).pendingCVProfile = cvProfile; - } - - this.authLogin.show(); - } - - /** - * Handle successful authentication + * Handle authentication events (kept for backward compatibility) */ public onAuthSuccess(): void { - console.log('✅ Authentication successful!'); - - // Retry CV submission if we had a pending one - const pendingCV = (this as any).pendingCVProfile; - if (pendingCV) { - console.log('🔄 Retrying CV submission after authentication...'); - (this as any).pendingCVProfile = null; - this.submitToN8n(pendingCV); - } + console.log('✅ Authentication successful (not needed for N8N webhooks)'); } - /** - * Handle authentication cancellation - */ public onAuthCancelled(): void { - console.log('❌ Authentication cancelled'); - (this as any).pendingCVProfile = null; - this.uploadError.set('Authentication required for n8n integration. CV processed locally only.'); + console.log('❌ Authentication cancelled (not needed for N8N webhooks)'); } } \ No newline at end of file diff --git a/src/app/models/n8n-sync.interface.ts b/src/app/models/n8n-sync.interface.ts index 2f199b3..bf8f289 100644 --- a/src/app/models/n8n-sync.interface.ts +++ b/src/app/models/n8n-sync.interface.ts @@ -46,8 +46,18 @@ export interface CVAnalysisRequest { export interface CVAnalysisResponse { analysisId: string; status: 'processing' | 'completed' | 'failed'; - estimatedCompletionTime: number; + estimatedCompletionTime?: number; questionBankId: string; + questionsGenerated?: number; + candidateName?: string; + questions?: any[]; + metadata?: { + skillsAnalyzed?: number; + experienceYears?: number; + processingTime?: string; + fallbackMode?: boolean; + message?: string; + }; } export interface QuestionBankResponse { diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 28157d6..c65adf9 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, of, throwError, timer } from 'rxjs'; import { map, catchError, tap, switchMap, filter } from 'rxjs/operators'; @@ -176,18 +176,46 @@ export class AuthService { const verifyUrl = `${this.authConfig.autheliaBaseUrl}${this.authConfig.verifyEndpoint}`; return this.http.get(verifyUrl, { - withCredentials: true + withCredentials: true, + observe: 'response' }).pipe( - map(response => response.status === 'OK'), + map(response => { + console.log('🔍 Session verify response:', { + status: response.status, + body: response.body + }); + + // HTTP 200 means session is valid + return response.status === 200; + }), tap(isValid => { if (!isValid && this.authStateSubject.value.isAuthenticated) { + console.log('🔒 Session invalid, clearing auth state'); this.clearAuthState(); + } else if (isValid) { + console.log('✅ Session verified successfully'); } }), - catchError(error => { - console.warn('Session verification failed:', error); - this.clearAuthState(); - return of(false); + catchError((error: HttpErrorResponse) => { + console.log('🔍 Session verification error details:', { + status: error.status, + statusText: error.statusText, + message: error.message + }); + + // Handle specific status codes + if (error.status === 200) { + console.log('⚠️ Got 200 status in error handler - treating as valid session'); + return of(true); + } else if (error.status === 401 || error.status === 403) { + console.log('🔒 Session expired or unauthorized (401/403)'); + this.clearAuthState(); + return of(false); + } else { + console.warn('❌ Session verification failed with status:', error.status); + this.clearAuthState(); + return of(false); + } }) ); } diff --git a/src/app/services/authelia-auth.service.ts b/src/app/services/authelia-auth.service.ts index 8f02859..ced1572 100644 --- a/src/app/services/authelia-auth.service.ts +++ b/src/app/services/authelia-auth.service.ts @@ -59,31 +59,73 @@ export class AutheliaAuthService { public checkAuthenticationStatus(): Observable { const verifyUrl = `${this.authBaseUrl}/api/verify`; - return this.http.get(verifyUrl, { + return this.http.get(verifyUrl, { withCredentials: true, - headers: this.getHeaders() + headers: this.getHeaders(), + observe: 'response' // This allows us to access both status and body }).pipe( - map(session => { - if (session.authenticated) { - this.sessionSubject.next(session); + map(response => { + console.log('🔍 Authelia verify response:', { + status: response.status, + body: response.body + }); + + // HTTP 200 means authentication is successful + if (response.status === 200) { + const session = response.body; + + // Create a session object with proper fallbacks + const authSession: AutheliaSession = { + username: session?.username || session?.user || 'authenticated_user', + displayName: session?.displayName || session?.display_name || session?.username || 'User', + email: session?.email || '', + groups: session?.groups || [], + authenticated: true, + expires: session?.expires || new Date(Date.now() + 3600000).toISOString() // 1 hour default + }; + + this.sessionSubject.next(authSession); this.userSubject.next({ - username: session.username, - displayName: session.displayName, - email: session.email, - groups: session.groups + username: authSession.username, + displayName: authSession.displayName, + email: authSession.email, + groups: authSession.groups }); this.isAuthenticatedSubject.next(true); - console.log('✅ User authenticated with Authelia:', session.username); + console.log('✅ User authenticated with Authelia:', authSession.username); return true; } else { + console.log('🔒 Authentication not verified, status:', response.status); this.clearSession(); return false; } }), catchError((error: HttpErrorResponse) => { - console.log('❌ Authentication check failed:', error.status); - this.clearSession(); - return of(false); + console.log('🔍 Authentication check error details:', { + status: error.status, + statusText: error.statusText, + message: error.message + }); + + // Handle specific HTTP status codes properly + if (error.status === 200) { + // This shouldn't happen with observe: 'response', but just in case + console.log('⚠️ Got 200 status in error handler - this is likely a parsing issue'); + this.isAuthenticatedSubject.next(true); + return of(true); + } else if (error.status === 401 || error.status === 403) { + console.log('🔒 Authentication required (401/403)'); + this.clearSession(); + return of(false); + } else if (error.status === 0) { + console.log('❌ Network error or CORS issue'); + this.clearSession(); + return of(false); + } else { + console.log('❌ Authentication check failed with status:', error.status); + this.clearSession(); + return of(false); + } }) ); } @@ -210,12 +252,23 @@ export class AutheliaAuthService { } private handleAuthenticatedRequestError(error: HttpErrorResponse): Observable { + console.log('🔍 Authenticated request error details:', { + status: error.status, + statusText: error.statusText, + url: error.url, + message: error.message + }); + if (error.status === 401 || error.status === 403) { - console.log('🔒 Authentication required for protected resource'); + console.log('🔒 Authentication required for protected resource (401/403)'); this.clearSession(); // Don't automatically redirect for API calls, let the calling component handle it return throwError(() => new Error('Authentication required')); + } else if (error.status === 200) { + console.log('⚠️ Got 200 status in authenticated request error handler - this may be a parsing issue'); + // Don't treat 200 as an error + return throwError(() => new Error('Response parsing error')); } return this.handleError(error); diff --git a/src/app/services/cv-parser.service.ts b/src/app/services/cv-parser.service.ts index be4d68d..2596d79 100644 --- a/src/app/services/cv-parser.service.ts +++ b/src/app/services/cv-parser.service.ts @@ -17,11 +17,8 @@ import { } from '../models/cv-profile.interface'; import { DataSanitizer, validateCVProfile } from '../models/validation'; -// Configure PDF.js worker - use local worker from node_modules -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( - 'pdfjs-dist/build/pdf.worker.min.mjs', - import.meta.url -).toString(); +// Configure PDF.js worker - use absolute path to worker file served by Angular build +pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-dist/build/pdf.worker.min.mjs'; @Injectable({ providedIn: 'root' diff --git a/src/app/services/n8n-sync.service.ts b/src/app/services/n8n-sync.service.ts index 9e38759..8ac6114 100644 --- a/src/app/services/n8n-sync.service.ts +++ b/src/app/services/n8n-sync.service.ts @@ -135,35 +135,41 @@ export class N8nSyncService implements N8nDataMapper { // CV Analysis Methods public submitCVForAnalysis(cvProfile: CVProfile): Observable { - // Check if user is authenticated with Authelia - if (!this.autheliaAuth.isAuthenticated) { - console.log('🔒 Authelia authentication required for CV analysis'); - return throwError(() => new Error('Authelia authentication required')); - } - this.syncStatusSubject.next(SyncStatus.IN_PROGRESS); const request = this.mapCVProfileToRequest(cvProfile); const endpoint = environment.apiEndpoints.cvAnalysis; - console.log('🚀 Submitting CV for analysis with Authelia auth'); + console.log('🚀 Submitting CV for analysis with N8N API key auth'); - // Use AutheliaAuthService for authenticated requests - return this.autheliaAuth.makeAuthenticatedRequest(endpoint, request, 'POST').pipe( + // Use direct HTTP with API key authentication + return this.http.post(endpoint, request, { + headers: this.getN8nApiHeaders() + }).pipe( retry(this.apiConfig.retryAttempts), + map(response => { + // Handle empty response from N8N (when no workflow is configured) + if (!response || typeof response !== 'object') { + console.log('⚠️ N8N returned empty response, generating fallback'); + return this.generateFallbackResponse(cvProfile); + } + + // Handle proper N8N workflow response + if (response.analysisId) { + console.log('✅ CV analysis submitted:', response.analysisId); + return response as CVAnalysisResponse; + } + + // Handle unexpected response format + console.log('⚠️ N8N response missing analysisId, generating fallback'); + return this.generateFallbackResponse(cvProfile); + }), tap(response => { - console.log('✅ CV analysis submitted:', response.analysisId); this.syncStatusSubject.next(SyncStatus.SUCCESS); }), catchError(error => { console.error('❌ CV analysis failed:', error); this.syncStatusSubject.next(SyncStatus.FAILED); - - if (error.message === 'Authentication required') { - console.log('🔐 Redirecting to Authelia login...'); - // Let the component handle the redirect to login - } - return throwError(() => error); }) ); @@ -175,7 +181,7 @@ export class N8nSyncService implements N8nDataMapper { return timer(0, 5000).pipe( // Poll every 5 seconds switchMap(() => this.http.get(statusEndpoint, { - headers: this.getAuthHeaders() + headers: this.getN8nApiHeaders() }).pipe( catchError(error => { console.warn('Status polling error:', error); @@ -194,14 +200,10 @@ export class N8nSyncService implements N8nDataMapper { } public getGeneratedQuestionBank(questionBankId: string): Observable { - if (!this.isAuthenticated()) { - return throwError('Authentication required'); - } - const endpoint = `${this.apiConfig.baseUrl}/webhook/question-bank/${questionBankId}`; return this.http.get(endpoint, { - headers: this.getAuthHeaders() + headers: this.getN8nApiHeaders() }).pipe( map(response => this.mapResponseToQuestionBank(response)), retry(this.apiConfig.retryAttempts), @@ -211,17 +213,13 @@ export class N8nSyncService implements N8nDataMapper { // Session Synchronization Methods public syncInterviewSession(session: InterviewSession): Observable { - if (!this.isAuthenticated()) { - return this.queueSyncForLater(session); - } - this.syncStatusSubject.next(SyncStatus.IN_PROGRESS); const request = this.mapSessionToSyncRequest(session); const endpoint = `${this.apiConfig.baseUrl}/webhook/session-sync`; return this.http.post(endpoint, request, { - headers: this.getAuthHeaders(), + headers: this.getN8nApiHeaders(), timeout: this.apiConfig.timeout }).pipe( retry(this.apiConfig.retryAttempts), @@ -236,18 +234,14 @@ export class N8nSyncService implements N8nDataMapper { } }), catchError(error => { + console.error('❌ Session sync failed:', error); this.syncStatusSubject.next(SyncStatus.FAILED); - this.queueSyncForLater(session); return this.handleHttpError(error); }) ); } public syncSessionAnalytics(sessionId: string, analytics: SessionAnalytics, improvements: string[]): Observable { - if (!this.isAuthenticated()) { - return throwError('Authentication required'); - } - const request: AnalyticsRequest = { sessionId, analytics, @@ -257,7 +251,7 @@ export class N8nSyncService implements N8nDataMapper { const endpoint = `${this.apiConfig.baseUrl}/webhook/analytics`; return this.http.post(endpoint, request, { - headers: this.getAuthHeaders() + headers: this.getN8nApiHeaders() }).pipe( retry(this.apiConfig.retryAttempts), catchError(this.handleHttpError.bind(this)) @@ -473,6 +467,14 @@ export class N8nSyncService implements N8nDataMapper { }); } + private getN8nApiHeaders(): HttpHeaders { + return new HttpHeaders({ + 'Content-Type': 'application/json', + 'X-N8N-API-KEY': environment.n8n.apiKey, + 'Accept': 'application/json' + }); + } + private handleHttpError(error: HttpErrorResponse): Observable { let errorMessage = 'An error occurred'; @@ -560,6 +562,68 @@ export class N8nSyncService implements N8nDataMapper { localStorage.removeItem('n8n_auth_token'); } + private generateFallbackResponse(cvProfile: CVProfile): CVAnalysisResponse { + const analysisId = `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const questionBankId = `qb_fallback_${Date.now()}`; + + console.log('🔄 Generating fallback response for CV analysis'); + + return { + status: 'completed', + analysisId: analysisId, + questionBankId: questionBankId, + questionsGenerated: 3, + candidateName: cvProfile.personalInfo?.fullName || 'Candidate', + questions: this.generateFallbackQuestions(cvProfile), + metadata: { + skillsAnalyzed: cvProfile.skills?.length || 0, + experienceYears: Math.max(...(cvProfile.skills?.map(s => s.yearsOfExperience || 0) || [0])), + processingTime: new Date().toISOString(), + fallbackMode: true, + message: 'Questions generated locally (N8N workflow not configured)' + } + }; + } + + private generateFallbackQuestions(cvProfile: CVProfile): any[] { + const skills = cvProfile.skills || []; + const experience = cvProfile.experience || []; + const technicalSkills = skills.filter(s => s.category === 'technical').map(s => s.name); + const primarySkill = technicalSkills[0] || skills[0]?.name || 'your main technology'; + + return [ + { + id: 1, + question: `Tell me about your experience with ${primarySkill} and how you've applied it in recent projects.`, + category: 'technical', + difficulty: 'medium', + expectedSkills: technicalSkills.slice(0, 2), + reasoning: 'Technical assessment based on CV skills', + generatedAt: new Date().toISOString() + }, + { + id: 2, + question: 'Describe a challenging project you worked on and how you overcame the main obstacles.', + category: 'behavioral', + difficulty: 'medium', + expectedSkills: ['Problem Solving', 'Communication', 'Critical Thinking'], + reasoning: 'Behavioral assessment of problem-solving abilities', + generatedAt: new Date().toISOString() + }, + { + id: 3, + question: experience.length > 0 ? + `I see you worked at ${experience[0].company}. What was the most valuable thing you learned there?` : + 'What motivates you in your professional career and where do you see yourself in 5 years?', + category: experience.length > 0 ? 'experience' : 'career', + difficulty: 'easy', + expectedSkills: ['Self-awareness', 'Communication', 'Growth Mindset'], + reasoning: experience.length > 0 ? 'Experience-specific question' : 'Career development assessment', + generatedAt: new Date().toISOString() + } + ]; + } + // Health Check Methods public checkN8nHealth(): Observable { const healthEndpoint = `${this.apiConfig.baseUrl}/healthz`; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index bbb0190..3ea8d75 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -7,6 +7,10 @@ export const environment = { verifyPath: '/api/verify', logoutPath: '/api/logout' }, + n8n: { + apiKey: 'interviewer-secure-api-key-2024', + baseUrl: 'https://n8n.gm-tech.org' + }, apiEndpoints: { cvAnalysis: 'https://n8n.gm-tech.org/webhook/cv-analysis', questionGeneration: 'https://n8n.gm-tech.org/webhook/cv-analysis', diff --git a/test-webhook-response.json b/test-webhook-response.json new file mode 100644 index 0000000..5b7ebf7 --- /dev/null +++ b/test-webhook-response.json @@ -0,0 +1,38 @@ +{ + "status": "completed", + "analysisId": "test_analysis_123", + "questionBankId": "qb_test_123_456789", + "questionsGenerated": 3, + "candidateName": "Test Candidate", + "questions": [ + { + "id": 1, + "question": "Tell me about your experience with software development and what technologies you've worked with recently.", + "category": "technical", + "difficulty": "medium", + "expectedSkills": ["Programming", "Software Development"], + "reasoning": "General technical assessment" + }, + { + "id": 2, + "question": "Describe a challenging project you worked on and how you overcame obstacles.", + "category": "behavioral", + "difficulty": "medium", + "expectedSkills": ["Problem Solving", "Communication"], + "reasoning": "Behavioral assessment of problem-solving skills" + }, + { + "id": 3, + "question": "Where do you see yourself in your career in the next 5 years?", + "category": "career", + "difficulty": "easy", + "expectedSkills": ["Self-awareness", "Planning"], + "reasoning": "Career goals and motivation assessment" + } + ], + "metadata": { + "skillsAnalyzed": 5, + "experienceYears": 3, + "processingTime": "2025-09-17T16:49:00.000Z" + } +} \ No newline at end of file