isAdmin() && strtolower($user->role) !== 'hr' && !$user->hasActiveInterviewZone()) { return redirect()->route('dashboard')->with('error', 'Interview Zone is available only during active interview timelines assigned by HR or Admin.'); } $interviews = $user->getAccessibleInterviews(); $interviewers = User::orderBy('name', 'asc')->get(); return view('interview.index', compact('interviews', 'interviewers')); } /** * Store a new Candidate Interview session created by HR. */ public function store(Request $request) { $request->validate([ 'candidate_name' => 'required|string|max:255', 'candidate_email' => 'required|email|max:255', 'candidate_phone' => 'required|string|max:50', 'scheduled_at' => 'nullable|date', 'valid_hours' => 'required|integer|min:1|max:72', 'language' => 'required|string', 'assigned_interviewers' => 'nullable|array', ]); $tempPassword = 'Pass-' . rand(100000, 999999); $cleanPhone = preg_replace('/[^0-9]/', '', $request->candidate_phone); $cleanName = Str::slug($request->candidate_name); $submissionUniqueId = $cleanName . '_' . $cleanPhone . '_' . time(); $scheduledAt = $request->scheduled_at ? \Carbon\Carbon::parse($request->scheduled_at) : now(); $expiresAt = $scheduledAt->copy()->addHours((int)$request->valid_hours); $interview = Interview::create([ 'candidate_name' => $request->candidate_name, 'candidate_email' => strtolower(trim($request->candidate_email)), 'candidate_phone' => $request->candidate_phone, 'temp_password' => $tempPassword, 'scheduled_at' => $scheduledAt, 'expires_at' => $expiresAt, 'language' => $request->language, 'status' => 'scheduled', 'assigned_interviewers' => $request->assigned_interviewers ?? [], 'proctor_logs' => [], 'submission_unique_id' => $submissionUniqueId, 'created_by' => Auth::id(), ]); return redirect()->route('interview.index')->with('success', 'Interview session created for ' . $request->candidate_name . '. Temporary login credentials generated!'); } /** * Show Candidate Login Screen. */ public function showCandidateLogin() { return view('interview.candidate_login'); } /** * Handle Candidate Temporary Credentials Authentication. */ public function loginCandidate(Request $request) { $request->validate([ 'identifier' => 'required|string', // Email or Phone 'temp_password' => 'required|string', ]); $identifier = trim($request->identifier); $interview = Interview::where(function ($q) use ($identifier) { $q->where('candidate_email', strtolower($identifier)) ->orWhere('candidate_phone', $identifier) ->orWhere('submission_unique_id', $identifier); })->where('temp_password', $request->temp_password)->first(); if (!$interview) { return back()->with('error', 'Invalid candidate credentials or temporary password.'); } if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return back()->with('error', 'This candidate interview access window has expired. Please contact HR.'); } // Set candidate session session(['candidate_interview_id' => $interview->id]); if ($interview->status === 'scheduled') { $interview->update(['status' => 'in_progress']); } return redirect()->route('interview.room', $interview->submission_unique_id); } /** * Show Candidate Live IDE Proctored Room. */ public function showCandidateRoom($uniqueId) { $interview = Interview::where('submission_unique_id', $uniqueId)->firstOrFail(); if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return response()->view('interview.expired', compact('interview'), 403); } return view('interview.candidate_room', compact('interview')); } /** * Proxy Live Code Execution to Piston Multi-Language API. */ public function executeCode(Request $request) { $request->validate([ 'language' => 'required|string', 'code' => 'required|string', ]); $langMap = [ 'python' => ['language' => 'python', 'version' => '*'], 'c' => ['language' => 'c', 'version' => '*'], 'cpp' => ['language' => 'c++', 'version' => '*'], 'java' => ['language' => 'java', 'version' => '*'], 'php' => ['language' => 'php', 'version' => '*'], 'laravel' => ['language' => 'php', 'version' => '*'], 'javascript' => ['language' => 'javascript', 'version' => '*'], ]; $langKey = strtolower($request->language); $targetLang = $langMap[$langKey] ?? ['language' => 'python', 'version' => '*']; try { $response = Http::withHeaders([ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept' => 'application/json', ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ 'language' => $targetLang['language'], 'version' => $targetLang['version'], 'files' => [ [ 'name' => 'solution', 'content' => $request->code, ] ], ]); if ($response->successful()) { $data = $response->json(); $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully. No stdout output.'); return response()->json(['success' => true, 'output' => $output]); } // Retry with explicit fallback version if wildcard returned non-200 $fallbackVersions = [ 'python' => '3.10.0', 'c' => '10.2.0', 'c++' => '10.2.0', 'java' => '15.0.2', 'php' => '8.2.3', 'javascript' => '18.15.0', ]; $retryResponse = Http::withHeaders([ 'User-Agent' => 'SingleLogin-ProctoredIDE/1.0', 'Accept' => 'application/json', ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ 'language' => $targetLang['language'], 'version' => $fallbackVersions[$targetLang['language']] ?? '3.10.0', 'files' => [ [ 'name' => 'solution', 'content' => $request->code, ] ], ]); if ($retryResponse->successful()) { $data = $retryResponse->json(); $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully.'); return response()->json(['success' => true, 'output' => $output]); } // Fallback for local PHP execution if PHP/Laravel language requested if (in_array($langKey, ['php', 'laravel'])) { ob_start(); try { $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $request->code); eval($cleanCode); $localOutput = ob_get_clean(); return response()->json(['success' => true, 'output' => $localOutput ?: '[Local PHP Execution Succeeded]']); } catch (\Throwable $e) { ob_end_clean(); return response()->json(['success' => true, 'output' => 'PHP Evaluation Error: ' . $e->getMessage()]); } } return response()->json([ 'success' => true, 'output' => "=== Code Output ===\n" . "Code submitted successfully for evaluation.\n(Remote execution engine status: " . $response->status() . ")\nSolution saved to proctored record." ]); } catch (\Exception $e) { return response()->json([ 'success' => true, 'output' => "=== Code Output ===\nCode session active.\nSolution saved to proctored record." ]); } } /** * Save Candidate Submitted Code & Execution Results. */ public function submitCode(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'code' => 'required|string', 'language' => 'required|string', 'output' => 'nullable|string', ]); $interview->update([ 'submitted_code' => $request->code, 'submitted_language' => $request->language, 'code_output' => $request->output, 'status' => 'completed', ]); return response()->json([ 'success' => true, 'message' => 'Code submitted successfully under unique ID: ' . $interview->submission_unique_id, 'unique_id' => $interview->submission_unique_id ]); } /** * Log Anti-Cheat Proctoring Violations (Tab Switch, Focus Loss, Gaze Anomaly). */ public function logViolation(Request $request, $id) { $interview = Interview::findOrFail($id); $type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly $details = $request->input('details', ''); $logs = $interview->proctor_logs ?? []; $logs[] = [ 'type' => $type, 'details' => $details, 'timestamp' => now()->toIso8601String(), ]; $interview->update(['proctor_logs' => $logs]); return response()->json(['success' => true, 'total_violations' => count($logs)]); } /** * Interviewer / HR sends a Silent Warning to Candidate. */ public function sendWarning(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'message' => 'required|string|max:500', ]); $warning = InterviewWarning::create([ 'interview_id' => $interview->id, 'message' => $request->message, 'sent_by' => Auth::id(), ]); return response()->json(['success' => true, 'warning' => $warning]); } /** * Live Polling Endpoint for Candidate Warnings & HR Monitoring. */ public function getPollData($id) { $interview = Interview::with(['warnings.sender'])->findOrFail($id); return response()->json([ 'status' => $interview->status, 'submitted_code' => $interview->submitted_code, 'submitted_language' => $interview->submitted_language, 'code_output' => $interview->code_output, 'proctor_logs' => $interview->proctor_logs ?? [], 'warnings' => $interview->warnings, 'is_expired' => $interview->isExpired(), ]); } /** * Save Candidate Silent Video Recording Blob / Snapshot Stream. */ public function uploadRecording(Request $request, $id) { $interview = Interview::findOrFail($id); if ($request->hasFile('video')) { $file = $request->file('video'); $filename = 'interview_' . $interview->submission_unique_id . '_' . time() . '.webm'; $path = $file->storeAs('public/recordings', $filename); $interview->update(['recording_path' => Storage::url($path)]); return response()->json(['success' => true, 'path' => Storage::url($path)]); } return response()->json(['success' => false, 'message' => 'No video payload received']); } }