feat: implementation des logs de connexion et correction du chemin de stockage des documents
This commit is contained in:
23
app/Console/Commands/CleanupLoginLogs.php
Normal file
23
app/Console/Commands/CleanupLoginLogs.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\Description;
|
||||||
|
use Illuminate\Console\Attributes\Signature;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
use App\Models\LoginLog;
|
||||||
|
|
||||||
|
#[Signature('app:cleanup-login-logs')]
|
||||||
|
#[Description('Supprime les logs de connexion datant de plus de 1 mois')]
|
||||||
|
class CleanupLoginLogs extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$count = LoginLog::where('login_at', '<', now()->subMonth())->delete();
|
||||||
|
$this->info("{$count} logs de connexion supprimés.");
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Http/Controllers/Admin/LoginLogController.php
Normal file
27
app/Http/Controllers/Admin/LoginLogController.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use App\Models\LoginLog;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class LoginLogController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
if (!auth()->user()->isSuperAdmin()) {
|
||||||
|
abort(403, 'Unauthorized. Super Admin only.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = LoginLog::with('user.tenant')
|
||||||
|
->orderBy('login_at', 'desc')
|
||||||
|
->paginate(50);
|
||||||
|
|
||||||
|
return Inertia::render('Admin/Logs', [
|
||||||
|
'logs' => $logs
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Listeners/LogSuccessfulLogin.php
Normal file
30
app/Listeners/LogSuccessfulLogin.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Login;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
|
||||||
|
use App\Models\LoginLog;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LogSuccessfulLogin
|
||||||
|
{
|
||||||
|
protected $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Login $event): void
|
||||||
|
{
|
||||||
|
LoginLog::create([
|
||||||
|
'user_id' => $event->user->id,
|
||||||
|
'ip_address' => $this->request->ip(),
|
||||||
|
'user_agent' => $this->request->userAgent(),
|
||||||
|
'login_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Models/LoginLog.php
Normal file
16
app/Models/LoginLog.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
|
||||||
|
#[Fillable(['user_id', 'ip_address', 'user_agent', 'login_at'])]
|
||||||
|
class LoginLog extends Model
|
||||||
|
{
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ return [
|
|||||||
|
|
||||||
'local' => [
|
'local' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/private'),
|
'root' => storage_path('app'),
|
||||||
'serve' => true,
|
'serve' => true,
|
||||||
'throw' => false,
|
'throw' => false,
|
||||||
'report' => false,
|
'report' => false,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('login_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->timestamp('login_at')->useCurrent();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('login_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -7,6 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"marked": "^17.0.4"
|
"marked": "^17.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1422,6 +1423,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"marked": "^17.0.4"
|
"marked": "^17.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,18 @@ const isSidebarOpen = ref(true);
|
|||||||
</svg>
|
</svg>
|
||||||
<span v-if="isSidebarOpen" class="truncate">Équipe SaaS</span>
|
<span v-if="isSidebarOpen" class="truncate">Équipe SaaS</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
v-if="$page.props.auth.user.role === 'super_admin'"
|
||||||
|
:href="route('admin.logs.index')"
|
||||||
|
class="flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-300 font-subtitle font-bold text-sm"
|
||||||
|
:class="[route().current('admin.logs.*') ? 'bg-highlight text-[#3a2800] shadow-md shadow-highlight/20' : 'text-white/70 hover:bg-white/10 hover:text-white']"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||||
|
</svg>
|
||||||
|
<span v-if="isSidebarOpen" class="truncate">Logs de connexion</span>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="p-4 border-t border-white/10 bg-primary/80">
|
<div class="p-4 border-t border-white/10 bg-primary/80">
|
||||||
|
|||||||
88
resources/js/Pages/Admin/Logs.vue
Normal file
88
resources/js/Pages/Admin/Logs.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
|
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
logs: Object // Paginated object
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
return format(new Date(dateString), 'PPP à HH:mm', { locale: fr });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Logs de connexion" />
|
||||||
|
<AdminLayout>
|
||||||
|
<template #header>Logs de connexion</template>
|
||||||
|
|
||||||
|
<div class="mb-6 flex justify-between items-center">
|
||||||
|
<h1 class="text-2xl font-serif font-black text-primary capitalize tracking-tight flex items-center gap-3">
|
||||||
|
<div class="w-1.5 h-8 bg-highlight rounded-full"></div>
|
||||||
|
Historique des Connexions
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-3xl shadow-sm border border-anthracite/5 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead class="bg-neutral/50 border-b border-anthracite/5">
|
||||||
|
<tr>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Utilisateur</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Structure</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Adresse IP</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Date & Heure</th>
|
||||||
|
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Appareil / Navigateur</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-anthracite/5">
|
||||||
|
<tr v-for="log in logs.data" :key="log.id" class="hover:bg-sand/30 transition-colors">
|
||||||
|
<td class="px-8 py-5">
|
||||||
|
<div class="font-bold text-primary">{{ log.user.name }}</div>
|
||||||
|
<div class="text-xs text-anthracite/50">{{ log.user.email }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-xs font-bold text-anthracite">
|
||||||
|
{{ log.user.tenant ? log.user.tenant.name : 'Super Admin / Global' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-xs font-mono text-anthracite/70">
|
||||||
|
{{ log.ip_address }}
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-xs font-bold text-anthracite">
|
||||||
|
{{ formatDate(log.login_at) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-8 py-5 text-[10px] text-anthracite/50 max-w-xs truncate" :title="log.user_agent">
|
||||||
|
{{ log.user_agent }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="logs.data.length === 0">
|
||||||
|
<td colspan="5" class="px-8 py-16 text-center text-anthracite/40 italic">
|
||||||
|
Aucun log de connexion trouvé.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple Pagination -->
|
||||||
|
<div v-if="logs.links.length > 3" class="px-8 py-4 bg-neutral/30 border-t border-anthracite/5 flex justify-center gap-2">
|
||||||
|
<Link
|
||||||
|
v-for="link in logs.links"
|
||||||
|
:key="link.label"
|
||||||
|
:href="link.url || '#'"
|
||||||
|
class="px-3 py-1 rounded-lg text-xs font-bold transition-all"
|
||||||
|
:class="[
|
||||||
|
link.active ? 'bg-primary text-white' : 'bg-white text-primary hover:bg-highlight hover:text-white',
|
||||||
|
!link.url ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
]"
|
||||||
|
v-html="link.label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 p-4 bg-highlight/10 border border-highlight/20 rounded-2xl text-xs text-[#3a2800]/60 font-medium">
|
||||||
|
<p><strong>Note :</strong> Les logs sont conservés pendant une période de 1 mois. Un nettoyage automatique est effectué quotidiennement.</p>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Artisan::command('inspire', function () {
|
||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote');
|
})->purpose('Display an inspiring quote');
|
||||||
|
|
||||||
|
Schedule::command('app:cleanup-login-logs')->daily();
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/backup', [\App\Http\Controllers\BackupController::class, 'download'])->name('backup');
|
Route::get('/backup', [\App\Http\Controllers\BackupController::class, 'download'])->name('backup');
|
||||||
Route::delete('/attempts/{attempt}', [\App\Http\Controllers\AttemptController::class, 'destroy'])->name('attempts.destroy');
|
Route::delete('/attempts/{attempt}', [\App\Http\Controllers\AttemptController::class, 'destroy'])->name('attempts.destroy');
|
||||||
Route::patch('/answers/{answer}/score', [\App\Http\Controllers\AttemptController::class, 'updateAnswerScore'])->name('answers.update-score');
|
Route::patch('/answers/{answer}/score', [\App\Http\Controllers\AttemptController::class, 'updateAnswerScore'])->name('answers.update-score');
|
||||||
|
Route::get('/logs', [\App\Http\Controllers\Admin\LoginLogController::class, 'index'])->name('logs.index');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Candidate Routes
|
// Candidate Routes
|
||||||
|
|||||||
Reference in New Issue
Block a user