feat: implement candidate security honeypots and redesign authenticated layout
This commit is contained in:
55
app/Http/Controllers/Api/CandidateHoneypotController.php
Normal file
55
app/Http/Controllers/Api/CandidateHoneypotController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CandidateHoneypotController extends Controller
|
||||
{
|
||||
public function logDirectoryTraversal(Request $request)
|
||||
{
|
||||
$this->logSecurityAlert('directory_traversal', $request);
|
||||
|
||||
// Fausse réponse pour faire croire que le serveur est vulnérable
|
||||
return response(
|
||||
"<html><body><h1>Index of /documents/private</h1><ul><li><a href='../'>../</a></li><li><a href='reponses_tests_2026.pdf'>reponses_tests_2026.pdf</a></li><li><a href='backup_db.sql'>backup_db.sql</a></li></ul></body></html>",
|
||||
200
|
||||
)->header('Content-Type', 'text/html');
|
||||
}
|
||||
|
||||
public function logMassAssignment(Request $request)
|
||||
{
|
||||
$this->logSecurityAlert('mass_assignment', $request);
|
||||
|
||||
// Faire croire que l'opération a réussi mais renvoyer une erreur 403 discrètement
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Profil mis à jour.',
|
||||
'debug' => 'Attempt logged.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
public function downloadFakeFile(Request $request, $filename)
|
||||
{
|
||||
$this->logSecurityAlert('file_exfiltration', $request, ['filename' => $filename]);
|
||||
|
||||
// Faux contenu
|
||||
$content = "Ceci est un honeypot de sécurité. Votre action a été journalisée.";
|
||||
return response($content, 200)
|
||||
->header('Content-Type', 'text/plain')
|
||||
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
}
|
||||
|
||||
private function logSecurityAlert(string $type, Request $request, array $extraPayload = [])
|
||||
{
|
||||
\App\Models\SecurityAlert::create([
|
||||
'user_id' => auth()->id(),
|
||||
'type' => $type,
|
||||
'endpoint' => $request->path(),
|
||||
'payload' => array_merge($request->all(), $extraPayload),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ class CandidateController extends Controller
|
||||
public function show(Candidate $candidate)
|
||||
{
|
||||
$candidate->load([
|
||||
'user',
|
||||
'user.securityAlerts',
|
||||
'documents',
|
||||
'jobPosition',
|
||||
'tenant'
|
||||
|
||||
91
app/Http/Controllers/PublicJobApplicationController.php
Normal file
91
app/Http/Controllers/PublicJobApplicationController.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\JobPosition;
|
||||
use App\Models\Candidate;
|
||||
use App\Models\User;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PublicJobApplicationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$jobs = JobPosition::with('tenant')->orderBy('created_at', 'desc')->get();
|
||||
return Inertia::render('Public/Jobs/Index', [
|
||||
'jobs' => $jobs
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(JobPosition $jobPosition)
|
||||
{
|
||||
return Inertia::render('Public/Jobs/Show', [
|
||||
'jobPosition' => $jobPosition
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, JobPosition $jobPosition)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'linkedin_url' => 'nullable|url|max:255',
|
||||
'city' => 'nullable|string|max:255',
|
||||
'cv' => 'nullable|mimes:pdf|max:5120',
|
||||
'cover_letter' => 'nullable|mimes:pdf|max:5120',
|
||||
]);
|
||||
|
||||
$password = Str::random(10);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($password),
|
||||
'role' => 'candidate',
|
||||
'tenant_id' => $jobPosition->tenant_id,
|
||||
]);
|
||||
|
||||
$candidate = $user->candidate()->create([
|
||||
'phone' => $request->phone,
|
||||
'linkedin_url' => $request->linkedin_url,
|
||||
'city' => $request->city,
|
||||
'status' => 'en_attente',
|
||||
'tenant_id' => $jobPosition->tenant_id,
|
||||
'job_position_id' => $jobPosition->id,
|
||||
]);
|
||||
|
||||
if ($request->hasFile('cv')) {
|
||||
$this->storeDocument($candidate, $request->file('cv'), 'cv');
|
||||
}
|
||||
if ($request->hasFile('cover_letter')) {
|
||||
$this->storeDocument($candidate, $request->file('cover_letter'), 'cover_letter');
|
||||
}
|
||||
|
||||
// Auto-login the candidate so they can take the quiz immediately if they want
|
||||
Auth::login($user);
|
||||
|
||||
return redirect()->route('dashboard')->with('success', 'Votre candidature a bien été enregistrée. Voici votre mot de passe temporaire pour vous reconnecter : ' . $password);
|
||||
}
|
||||
|
||||
private function storeDocument(Candidate $candidate, $file, string $type)
|
||||
{
|
||||
if (!$file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $file->store('private/documents/' . $candidate->id, 'local');
|
||||
|
||||
Document::create([
|
||||
'candidate_id' => $candidate->id,
|
||||
'type' => $type,
|
||||
'file_path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user