sls/app/Http/Controllers/InterviewController.php
2026-07-20 21:50:30 +05:30

283 lines
9.6 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Interview;
use App\Models\InterviewWarning;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class InterviewController extends Controller
{
/**
* Display the HR & Interviewer Live Assessment Portal.
*/
public function index()
{
$user = Auth::user();
// Must be Admin, HR, or assigned interviewer
$interviews = Interview::with(['creator', 'warnings.sender'])
->orderBy('created_at', 'desc')
->get();
$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',
'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();
$expiresAt = now()->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,
'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' => '3.10.0'],
'c' => ['language' => 'c', 'version' => '10.2.0'],
'cpp' => ['language' => 'c++', 'version' => '10.2.0'],
'java' => ['language' => 'java', 'version' => '15.0.2'],
'php' => ['language' => 'php', 'version' => '8.2.3'],
'laravel' => ['language' => 'php', 'version' => '8.2.3'],
'javascript' => ['language' => 'javascript', 'version' => '18.15.0'],
];
$targetLang = $langMap[strtolower($request->language)] ?? ['language' => 'python', 'version' => '3.10.0'];
try {
$response = Http::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'] ?? 'No output generated.';
return response()->json(['success' => true, 'output' => $output]);
}
return response()->json(['success' => false, 'output' => 'Execution engine error: ' . $response->status()]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'output' => 'Code runner service error: ' . $e->getMessage()]);
}
}
/**
* 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']);
}
}