diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5f15b9d..37e374f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,109 @@ { "permissions": { "allow": [ - "Bash(npm run *)" + "Bash(npm run *)", + "Bash(npm --version)", + "Bash(npx --version)", + "Bash(npx --yes create-next-app@latest . --typescript --tailwind --app --src-dir --import-alias \"@/*\" --use-npm --eslint --no-turbopack --yes)", + "Bash(npm install *)", + "Bash(npx prisma *)", + "Bash(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000__TRACKED_VAR__)", + "Bash(curl -s -X POST http://localhost:3000/api/readings -H \"Content-Type: application/json\" -d '{\"moment\":\"FASTING\",\"value\":1.05,\"notes\":\"Test smoke\"}')", + "Bash(curl -s \"http://localhost:3000/api/stats\")", + "Bash(curl -s -X DELETE http://localhost:3000/api/readings/91)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:3000/api/export)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:3000/profil)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:3000/api/patient)", + "Bash(curl -s -X PUT http://localhost:3000/api/patient -H \"Content-Type: application/json\" -d '{\"firstName\":\"Jeremy\",\"lastName\":\"Bayse\",\"email\":\"jeremy.bayse@gmail.com\",\"birthDate\":\"1985-06-15\",\"heightCm\":180,\"weightKg\":78.5}')", + "Bash(curl -s http://localhost:3000/api/patient)", + "Bash(taskkill //PID 40172 //F)", + "Bash(curl -s -o /dev/null -w \"GET /profil %{http_code}\\\\n\" http://localhost:3000/profil)", + "Bash(curl -s -o /dev/null -w \"GET /api/patient %{http_code}\\\\n\" http://localhost:3000/api/patient)", + "Bash(curl -s http://localhost:3000/)", + "Bash(curl -s -X PUT http://localhost:3000/api/patient -H \"Content-Type: application/json\" -d '{\"firstName\":\"Jeremy\",\"lastName\":\"Bayse\",\"email\":\"jeremy.bayse@gmail.com\",\"birthDate\":\"1985-06-15\",\"heightCm\":180,\"weightKg\":78.5,\"sex\":\"M\",\"diabetesType\":\"TYPE_2\",\"treatment\":\"Metformine 1000 mg matin et soir\"}')", + "Bash(curl -s -X PUT http://localhost:3000/api/patient -H \"Content-Type: application/json\" -d '{\"firstName\":\"Jeremy\",\"lastName\":\"Bayse\",\"sex\":\"INVALID\"}')", + "Bash(curl -s http://localhost:3000/profil)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:3001/profil)", + "Bash(curl -s -X PUT http://localhost:3001/api/patient -H \"Content-Type: application/json\" -d '{\"firstName\":\"Jeremy\",\"lastName\":\"Bayse\",\"email\":\"jeremy.bayse@gmail.com\",\"birthDate\":\"1985-06-15\",\"heightCm\":180,\"weightKg\":78.5,\"sex\":\"M\",\"diabetesType\":\"TYPE_2\",\"treatment\":\"Metformine 1000 mg matin et soir\"}')", + "Bash(curl -s http://localhost:3001/api/patient)", + "Bash(curl -s http://localhost:3001/)", + "Bash(grep -oE \"Diab.{1,30}\")", + "Bash(taskkill //PID 37932 //F)", + "Bash(curl -s -X POST http://localhost:3001/api/chat -H \"Content-Type: application/json\" -d '{\"message\":\"Bonjour, comment se passe mon suivi cette semaine ?\",\"history\":[]}' --max-time 30)", + "Bash(curl -s \"https://generativelanguage.googleapis.com/v1beta/models?key=AIzaSyD7ltywmUmEooMOBiMkfyhQygCEU06LbR4\")", + "Bash(curl -s -X POST \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=AIzaSyD7ltywmUmEooMOBiMkfyhQygCEU06LbR4\" -H \"Content-Type: application/json\" -d '{\"contents\":[{\"parts\":[{\"text\":\"Dis bonjour en une phrase.\"}]}]}')", + "Bash(curl -s -X POST \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=AIzaSyD7ltywmUmEooMOBiMkfyhQygCEU06LbR4\" -H \"Content-Type: application/json\" -d '{\"contents\":[{\"parts\":[{\"text\":\"Dis bonjour en une phrase.\"}]}]}')", + "Bash(curl -s -X POST http://localhost:3001/api/chat -H \"Content-Type: application/json\" -d '{\"message\":\"Comment se passe mon suivi cette semaine ?\",\"history\":[]}' --max-time 30)", + "Bash(curl -s http://localhost:3001/api/daily-analysis --max-time 30)", + "Bash(taskkill //PID 42196 //F)", + "Bash(curl -s http://localhost:3001/api/daily-analysis --max-time 35)", + "Bash(curl -s http://localhost:3001/api/daily-analysis --max-time 10)", + "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('fresh:', d.get\\('fresh'\\), '| generatedAt:', d.get\\('generatedAt'\\)\\)\")", + "Bash(taskkill //PID 33804 //F)", + "Bash(curl -s http://localhost:3000/mobile)", + "Bash(curl -s http://localhost:3001/mobile)", + "Bash(npx tsc *)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/)", + "Bash(curl -s http://localhost:3001/dashboard)", + "Bash(curl -sv http://localhost:3001/dashboard)", + "Bash(python3 -c \"import sys; data=sys.stdin.read\\(\\); print\\(data[data.find\\('Error'\\):data.find\\('Error'\\)+500] if 'Error' in data else data[:500]\\)\")", + "Bash(node -e \"console.log\\(require\\('./node_modules/next/package.json'\\).version\\)\")", + "Bash(rm -rf .next)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3001/)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3001/dashboard)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/dashboard)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/auth/login)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/pricing)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/auth/register)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3002/auth/verify-pending)", + "Bash(taskkill //F //IM node.exe)", + "Bash(curl -s -X POST http://localhost:3000/api/auth/register -H 'Content-Type: application/json' -d '{\"name\":\"Test User\",\"email\":\"test@test.com\",\"password\":\"password123\"}')", + "Bash(curl -s -X POST http://localhost:3000/api/auth/register -H 'Content-Type: application/json' -d '{\"name\":\"Jean Dupont\",\"email\":\"jean__CMDSUB_OUTPUT__@example.com\",\"password\":\"motdepasse123\"}')", + "mcp__Claude_in_Chrome__tabs_context_mcp", + "mcp__Claude_in_Chrome__browser_batch", + "mcp__Claude_in_Chrome__switch_browser", + "mcp__Claude_in_Chrome__list_connected_browsers", + "mcp__Claude_in_Chrome__select_browser", + "Bash(taskkill /F /IM node.exe)", + "PowerShell(Stop-Process -Name node -Force -ErrorAction SilentlyContinue)", + "Bash(curl -s http://localhost:3000/pricing)", + "Bash(cat)", + "Bash(chmod +x test-stripe.sh)", + "Bash(./test-stripe.sh)", + "Bash(curl -s http://localhost:3000/pricing -X POST -H \"Content-Type: application/json\")", + "Bash(pkill -9 node)", + "mcp__Claude_in_Chrome__navigate", + "mcp__Claude_in_Chrome__computer", + "mcp__Claude_in_Chrome__form_input", + "Bash(sqlite3 prisma/dev.db \"SELECT id, email, plan FROM User LIMIT 5;\")", + "Bash(node -e ' *)", + "Bash(npm exec *)", + "Bash(node test-cancel-subscription.mjs)", + "Bash(pkill -f \"next dev\")", + "mcp__Claude_in_Chrome__find", + "Bash(curl -s http://localhost:3000)", + "Bash(node add_readings.js)", + "mcp__Claude_in_Chrome__read_network_requests", + "mcp__Claude_in_Chrome__read_console_messages", + "Bash(taskkill /PID 54104 /F)", + "Bash(file ~/Downloads/rapport_glycemie*.pdf)", + "Bash(pdftotext ~/Downloads/rapport_glycemie_2026-04*.pdf -)", + "Bash(node /tmp/check_pdf.js)", + "Bash(tasklist)", + "Bash(curl -s http://localhost:3000/dashboard/rapports -c /tmp/cookies.txt)", + "Bash(curl -s \"http://localhost:3000/api/reports/generate-pdf?month=2026-04-01\" -H \"Cookie: $\\(curl -s http://localhost:3000/dashboard/rapports -c /tmp/cookies.txt)", + "Bash(grep -o '[^ ]*$')", + "Bash(chmod +x /tmp/deploy-setup.sh)", + "Bash(git remote *)", + "Bash(git add *)", + "Bash(git commit -m ' *)", + "Bash(git push *)", + "Bash(tar -czf diabetix-build.tar.gz .next/ node_modules/ package.json package-lock.json public/ prisma/ src/ .env.production next.config.js tsconfig.json)", + "Bash(rm diabetix-build.tar.gz)", + "Bash(tar -czf diabetix-build.tar.gz .next/ node_modules/ package.json package-lock.json public/ prisma/schema.prisma prisma/migrations/ src/ next.config.ts tsconfig.json)", + "Bash(scp diabetix-build.tar.gz root@192.168.20.28:/tmp/)", + "Bash(sshpass -p \"Lucas1978!\" scp -o StrictHostKeyChecking=no diabetix-build.tar.gz root@192.168.20.28:/tmp/)" ] } } diff --git a/app/Http/Controllers/Api/CandidateHoneypotController.php b/app/Http/Controllers/Api/CandidateHoneypotController.php new file mode 100644 index 0000000..acb3298 --- /dev/null +++ b/app/Http/Controllers/Api/CandidateHoneypotController.php @@ -0,0 +1,55 @@ +logSecurityAlert('directory_traversal', $request); + + // Fausse réponse pour faire croire que le serveur est vulnérable + return response( + "

Index of /documents/private

", + 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(), + ]); + } +} diff --git a/app/Http/Controllers/CandidateController.php b/app/Http/Controllers/CandidateController.php index 61e7975..bc9f196 100644 --- a/app/Http/Controllers/CandidateController.php +++ b/app/Http/Controllers/CandidateController.php @@ -132,7 +132,7 @@ class CandidateController extends Controller public function show(Candidate $candidate) { $candidate->load([ - 'user', + 'user.securityAlerts', 'documents', 'jobPosition', 'tenant' diff --git a/app/Http/Controllers/PublicJobApplicationController.php b/app/Http/Controllers/PublicJobApplicationController.php new file mode 100644 index 0000000..46593c2 --- /dev/null +++ b/app/Http/Controllers/PublicJobApplicationController.php @@ -0,0 +1,91 @@ +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(), + ]); + } +} diff --git a/app/Models/SecurityAlert.php b/app/Models/SecurityAlert.php new file mode 100644 index 0000000..fa4ed5c --- /dev/null +++ b/app/Models/SecurityAlert.php @@ -0,0 +1,26 @@ + 'array', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index d07a2d9..33af870 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -42,6 +42,11 @@ class User extends Authenticatable return $this->belongsTo(Tenant::class); } + public function securityAlerts() + { + return $this->hasMany(SecurityAlert::class); + } + /** * Get the attributes that should be cast. * diff --git a/database/migrations/2026_05_08_075358_create_security_alerts_table.php b/database/migrations/2026_05_08_075358_create_security_alerts_table.php new file mode 100644 index 0000000..f56f75e --- /dev/null +++ b/database/migrations/2026_05_08_075358_create_security_alerts_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); + $table->string('type'); // 'mass_assignment', 'directory_traversal', etc. + $table->string('endpoint')->nullable(); + $table->json('payload')->nullable(); + $table->string('ip_address')->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('security_alerts'); + } +}; diff --git a/resources/js/Components/Dropdown.vue b/resources/js/Components/Dropdown.vue index 0f4b441..90b9c38 100644 --- a/resources/js/Components/Dropdown.vue +++ b/resources/js/Components/Dropdown.vue @@ -69,13 +69,13 @@ const open = ref(false); >