Premier commit
This commit is contained in:
69
app/Console/Commands/SendContractAlerts.php
Normal file
69
app/Console/Commands/SendContractAlerts.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\User;
|
||||
use App\Mail\ContractExpiringNotification;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendContractAlerts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'contracts:check-expirations';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check for expiring contracts and notify admins';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Checking for expiring contracts...');
|
||||
|
||||
$expiringContracts = Contract::where('status', '!=', 'expired') // Assuming 'active' or 'pending'
|
||||
->where('end_date', '<=', now()->addDays(30))
|
||||
->where('end_date', '>=', now())
|
||||
->get();
|
||||
|
||||
if ($expiringContracts->isEmpty()) {
|
||||
$this->info('No contracts expiring soon.');
|
||||
return;
|
||||
}
|
||||
|
||||
$admins = User::where('role', 'admin')->get();
|
||||
|
||||
if ($admins->isEmpty()) {
|
||||
$this->warn('No admins found to notify.');
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($admins as $admin) {
|
||||
// In a real app, you might group these or send one email per contract/batch
|
||||
// Here we just simulate sending a notification for the batch
|
||||
// Or send individual emails
|
||||
|
||||
// Simplification: Send one email with list
|
||||
try {
|
||||
Mail::to($admin->email)->send(new ContractExpiringNotification($expiringContracts));
|
||||
$this->info("Notification sent to {$admin->email}");
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Failed to send email to {$admin->email}: " . $e->getMessage());
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Done.');
|
||||
}
|
||||
}
|
||||
80
app/Console/Commands/TestCortexConnection.php
Normal file
80
app/Console/Commands/TestCortexConnection.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\CortexXdrService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class TestCortexConnection extends Command
|
||||
{
|
||||
protected $signature = 'cortex:test';
|
||||
protected $description = 'Test connectivity to Cortex XDR API and display results.';
|
||||
|
||||
public function handle(CortexXdrService $cortexService)
|
||||
{
|
||||
$this->info("--- Cortex XDR Connectivity Test ---");
|
||||
|
||||
// 1. Config Check
|
||||
$key = Config::get('services.cortex.key');
|
||||
$id = Config::get('services.cortex.id');
|
||||
$url = Config::get('services.cortex.url');
|
||||
|
||||
$this->line("\n1. Checking Environment Variables:");
|
||||
$this->line(" - API Key: " . ($key ? 'OK (' . substr($key, 0, 5) . '...)' : 'MISSING'));
|
||||
$this->line(" - API Key ID: " . ($id ? 'OK (' . $id . ')' : 'MISSING'));
|
||||
$this->line(" - Base URL: " . ($url ? 'OK (' . $url . ')' : 'MISSING'));
|
||||
|
||||
if (!$key || !$id || !$url) {
|
||||
$this->error("\n[ERROR] Configuration is incomplete.");
|
||||
$this->line("You need to add the following variables to your .env file:");
|
||||
if (!$key) $this->line(" - CORTEX_XDR_API_KEY=your_api_key");
|
||||
if (!$id) $this->line(" - CORTEX_XDR_API_KEY_ID=your_key_id (Integer ID)");
|
||||
if (!$url) $this->line(" - CORTEX_XDR_BASE_URL=https://api-fqdn.xdr.cortex.paloaltonetworks.com");
|
||||
|
||||
$this->warn("\nIMPORTANT: After editing .env, you may need to restart 'php artisan serve' for the web app to see changes.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 2. Test Incidents
|
||||
$this->info("\n2. Testing API Connection (Incidents)...");
|
||||
try {
|
||||
$incidents = $cortexService->getIncidents();
|
||||
if (empty($incidents)) {
|
||||
$this->warn("No incidents returned. (This might be normal if no incidents exist, or check API permissions).");
|
||||
} else {
|
||||
$this->info("SUCCESS: Fetched " . count($incidents) . " incidents.");
|
||||
if (count($incidents) > 0) {
|
||||
$this->line("DEBUG: First Incident ID: " . ($incidents[0]['incident_id'] ?? 'N/A'));
|
||||
$this->line("DEBUG: Incident keys: " . print_r(array_keys($incidents[0]), true));
|
||||
$this->line("DEBUG: Incident values: " . print_r(array_values($incidents[0]), true));
|
||||
}
|
||||
$this->line("Latest Incident ID: " . ($incidents[0]['incident_id'] ?? 'N/A'));
|
||||
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("EXCEPTION: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// 3. Test Endpoints
|
||||
$this->info("\n3. Testing API Connection (Endpoints)...");
|
||||
try {
|
||||
$endpoints = $cortexService->getEndpoints();
|
||||
if (empty($endpoints)) {
|
||||
$this->warn("No endpoints returned.");
|
||||
} else {
|
||||
$this->info("SUCCESS: Fetched " . count($endpoints) . " endpoints.");
|
||||
if (count($endpoints) > 0) {
|
||||
$ep = $endpoints[0];
|
||||
$this->line("DEBUG: First Endpoint Status key: 'endpoint_status' => " . ($ep['endpoint_status'] ?? 'N/A'));
|
||||
$this->line("DEBUG: Keys available: " . print_r(array_keys($ep), true));
|
||||
$this->line("DEBUG: First Endpoint Status key: 'endpoint_status' => " . ($ep['endpoint_status'] ?? 'N/A'));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("EXCEPTION: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
36
app/Http/Controllers/Admin/LicenseLevelController.php
Normal file
36
app/Http/Controllers/Admin/LicenseLevelController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use App\Models\LicenseLevel;
|
||||
|
||||
class LicenseLevelController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$levels = LicenseLevel::all();
|
||||
return view('admin.license_levels.index', compact('levels'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate(['name' => 'required|unique:license_levels,name']);
|
||||
LicenseLevel::create($request->only('name'));
|
||||
return back()->with('success', 'Niveau de licence ajouté.');
|
||||
}
|
||||
|
||||
public function toggle(LicenseLevel $licenseLevel)
|
||||
{
|
||||
$licenseLevel->update(['is_active' => !$licenseLevel->is_active]);
|
||||
return back()->with('success', 'Statut mis à jour.');
|
||||
}
|
||||
|
||||
public function destroy(LicenseLevel $licenseLevel)
|
||||
{
|
||||
$licenseLevel->delete();
|
||||
return back()->with('success', 'Niveau de licence supprimé.');
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/Admin/LinkController.php
Normal file
63
app/Http/Controllers/Admin/LinkController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Link;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LinkController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$links = Link::orderBy('order', 'asc')->get();
|
||||
return view('admin.links.index', compact('links'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'url' => 'required|url',
|
||||
'icon' => 'nullable|string',
|
||||
'color' => 'nullable|string',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
Link::create($validated);
|
||||
|
||||
return back()->with('success', 'Lien ajouté.');
|
||||
}
|
||||
|
||||
public function edit(Link $link)
|
||||
{
|
||||
return view('admin.links.edit', compact('link'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Link $link)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'url' => 'required|url',
|
||||
'icon' => 'nullable|string',
|
||||
'color' => 'nullable|string',
|
||||
'order' => 'integer',
|
||||
]);
|
||||
|
||||
$link->update($validated);
|
||||
|
||||
return redirect()->route('admin.links.index')->with('success', 'Lien mis à jour.');
|
||||
}
|
||||
|
||||
public function toggle(Link $link)
|
||||
{
|
||||
$link->update(['is_active' => !$link->is_active]);
|
||||
return back()->with('success', 'Statut mis à jour.');
|
||||
}
|
||||
|
||||
public function destroy(Link $link)
|
||||
{
|
||||
$link->delete();
|
||||
return back()->with('success', 'Lien supprimé.');
|
||||
}
|
||||
}
|
||||
23
app/Http/Controllers/Admin/MunicipalityController.php
Normal file
23
app/Http/Controllers/Admin/MunicipalityController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use App\Models\Municipality;
|
||||
|
||||
class MunicipalityController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$municipalities = Municipality::all();
|
||||
return view('admin.municipalities.index', compact('municipalities'));
|
||||
}
|
||||
|
||||
public function toggle(Municipality $municipality)
|
||||
{
|
||||
$municipality->update(['is_active' => !$municipality->is_active]);
|
||||
return back()->with('success', 'Statut de la commune mis à jour.');
|
||||
}
|
||||
}
|
||||
76
app/Http/Controllers/AuthController.php
Normal file
76
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Defaults\Password;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showLogin()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required'],
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
$request->session()->regenerate();
|
||||
if (!Auth::user()->is_active) {
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return back()->withErrors(['email' => 'Your account is pending approval by an administrator.']);
|
||||
}
|
||||
return redirect()->intended('/dashboard');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'The provided credentials do not match our records.',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
public function showRegister()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'is_active' => false, // Require approval
|
||||
'role' => 'reader', // Default
|
||||
]);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
// Notify admin in real app
|
||||
|
||||
return redirect('/dashboard')->with('status', 'Account created. Wait for approval.');
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
148
app/Http/Controllers/ContractController.php
Normal file
148
app/Http/Controllers/ContractController.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\ContractMeta;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class ContractController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$contracts = Contract::with('municipality')->latest()->paginate(10);
|
||||
return view('contracts.index', compact('contracts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (!auth()->user()->isManager()) {
|
||||
abort(403);
|
||||
}
|
||||
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
|
||||
$licenseLevels = \App\Models\LicenseLevel::active()->orderBy('name')->get();
|
||||
return view('contracts.create', compact('municipalities', 'licenseLevels'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
if (!auth()->user()->isManager()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'reference' => 'nullable|string|unique:contracts',
|
||||
'provider' => 'required|string',
|
||||
'municipality_id' => 'nullable|exists:municipalities,id',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'amount' => 'nullable|numeric',
|
||||
'type' => 'required|string',
|
||||
'meta' => 'nullable|array', // key-value pairs
|
||||
]);
|
||||
|
||||
$contract = Contract::create($validated);
|
||||
|
||||
if ($request->has('meta')) {
|
||||
foreach ($request->meta as $key => $value) {
|
||||
if ($value) {
|
||||
$contract->meta()->create(['key' => $key, 'value' => $value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('contracts.index')
|
||||
->with('success', 'Contract created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Contract $contract)
|
||||
{
|
||||
$contract->load(['meta', 'documents', 'municipality']);
|
||||
return view('contracts.show', compact('contract'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Contract $contract)
|
||||
{
|
||||
if (!auth()->user()->isManager()) {
|
||||
abort(403);
|
||||
}
|
||||
$contract->load('meta');
|
||||
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
|
||||
$licenseLevels = \App\Models\LicenseLevel::active()->orderBy('name')->get();
|
||||
return view('contracts.edit', compact('contract', 'municipalities', 'licenseLevels'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Contract $contract)
|
||||
{
|
||||
if (!auth()->user()->isManager()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'reference' => 'nullable|string|unique:contracts,reference,' . $contract->id,
|
||||
'provider' => 'required|string',
|
||||
'municipality_id' => 'nullable|exists:municipalities,id',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'amount' => 'nullable|numeric',
|
||||
'status' => 'required|string',
|
||||
]);
|
||||
|
||||
$contract->update($validated);
|
||||
|
||||
// Handle Meta Data Update
|
||||
if ($request->has('meta')) {
|
||||
foreach ($request->meta as $key => $value) {
|
||||
if ($value) {
|
||||
$contract->meta()->updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value]
|
||||
);
|
||||
} else {
|
||||
// If value is empty, maybe delete? Or just leave null.
|
||||
// Let's delete if empty to keep clean
|
||||
$contract->meta()->where('key', $key)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('contracts.index')
|
||||
->with('success', 'Contract updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Contract $contract)
|
||||
{
|
||||
if (!auth()->user()->isAdmin()) { // Only admin can delete? Or manager?
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$contract->delete();
|
||||
|
||||
return redirect()->route('contracts.index')
|
||||
->with('success', 'Contract deleted.');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
32
app/Http/Controllers/CortexXdrController.php
Normal file
32
app/Http/Controllers/CortexXdrController.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\CortexXdrService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CortexXdrController extends Controller
|
||||
{
|
||||
protected $cortex;
|
||||
|
||||
public function __construct(CortexXdrService $cortex)
|
||||
{
|
||||
$this->cortex = $cortex;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// Return view effectively immediately with a loading state
|
||||
return view('cortex.index');
|
||||
}
|
||||
|
||||
public function getData(CortexXdrService $cortexService)
|
||||
{
|
||||
try {
|
||||
$summary = $cortexService->getSummary();
|
||||
return response()->json($summary);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Http/Controllers/DashboardController.php
Normal file
54
app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\GlobalSetting;
|
||||
use App\Models\Link;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the dashboard.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$contractStats = [
|
||||
'total' => Contract::count(),
|
||||
'active' => Contract::active()->count(),
|
||||
'expiring_soon' => Contract::expiringSoon(30)->count(),
|
||||
'expired' => Contract::where('status', 'expired')->count(),
|
||||
'by_type' => Contract::select('type', \DB::raw('count(*) as count'))->groupBy('type')->get()->keyBy('type'),
|
||||
];
|
||||
|
||||
// Recent logs
|
||||
$recentLogs = AuditLog::with('user')->latest()->take(10)->get();
|
||||
|
||||
// Upcoming Contracts (Timeline)
|
||||
$upcomingContracts = Contract::whereNotNull('end_date')
|
||||
->whereDate('end_date', '>=', now())
|
||||
->orderBy('end_date', 'asc')
|
||||
->take(6)
|
||||
->get();
|
||||
|
||||
// Dashboard Note
|
||||
$dashboardNote = GlobalSetting::get('dashboard_note');
|
||||
|
||||
// External Links
|
||||
$links = Link::active()->get();
|
||||
|
||||
return view('dashboard', compact('contractStats', 'recentLogs', 'upcomingContracts', 'dashboardNote', 'links'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the dashboard note.
|
||||
*/
|
||||
public function updateNote(Request $request)
|
||||
{
|
||||
GlobalSetting::set('dashboard_note', $request->input('note'));
|
||||
|
||||
return back()->with('success', 'Pense-bête mis à jour.');
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/DocumentController.php
Normal file
58
app/Http/Controllers/DocumentController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DocumentController extends Controller
|
||||
{
|
||||
/**
|
||||
* Store a new document for a contract.
|
||||
*/
|
||||
public function store(Request $request, Contract $contract)
|
||||
{
|
||||
// Simple validation
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:pdf,docx,jpg,png|max:10240', // 10MB limit
|
||||
'description' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
$path = $request->file('file')->store('contracts/' . $contract->id, 'public');
|
||||
|
||||
$contract->documents()->create([
|
||||
'filename' => $request->file('file')->getClientOriginalName(),
|
||||
'path' => $path,
|
||||
'mime_type' => $request->file('file')->getMimeType(),
|
||||
'size' => $request->file('file')->getSize(),
|
||||
'description' => $request->input('description'),
|
||||
'uploaded_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Document uploaded successfully.');
|
||||
}
|
||||
|
||||
return back()->with('error', 'No file uploaded.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document.
|
||||
*/
|
||||
public function destroy(Document $document)
|
||||
{
|
||||
// Check permission (manager or admin or uploader?)
|
||||
if (!auth()->user()->isManager() && auth()->id() !== $document->uploaded_by) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Delete from storage
|
||||
Storage::disk('public')->delete($document->path);
|
||||
|
||||
$document->delete();
|
||||
|
||||
return back()->with('success', 'Document deleted.');
|
||||
}
|
||||
}
|
||||
46
app/Http/Controllers/MunicipalityController.php
Normal file
46
app/Http/Controllers/MunicipalityController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MunicipalityController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$municipalities = \App\Models\Municipality::active()->orderBy('name')->get();
|
||||
return view('municipalities.index', compact('municipalities'));
|
||||
}
|
||||
|
||||
public function show(\App\Models\Municipality $municipality)
|
||||
{
|
||||
if (!$municipality->is_active) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$municipality->load(['contracts.meta']);
|
||||
|
||||
$contracts = $municipality->contracts;
|
||||
|
||||
// M365 Statistics
|
||||
$m365Contracts = $contracts->where('type', 'microsoft_365');
|
||||
|
||||
// Fetch all active license levels to initialize stats keys
|
||||
$licenseLevels = \App\Models\LicenseLevel::active()->pluck('name')->toArray();
|
||||
$m365Stats = array_fill_keys($licenseLevels, 0);
|
||||
$m365Stats['Autre'] = 0; // Fallback category
|
||||
|
||||
foreach ($m365Contracts as $contract) {
|
||||
$level = $contract->meta->where('key', 'm365_license_level')->first()?->value;
|
||||
$quantity = (int) $contract->meta->where('key', 'm365_quantity')->first()?->value ?? 0;
|
||||
|
||||
if ($level && array_key_exists($level, $m365Stats)) {
|
||||
$m365Stats[$level] += $quantity;
|
||||
} else {
|
||||
$m365Stats['Autre'] += $quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return view('municipalities.show', compact('municipality', 'contracts', 'm365Stats'));
|
||||
}
|
||||
}
|
||||
25
app/Http/Middleware/EnsureUserIsActive.php
Normal file
25
app/Http/Middleware/EnsureUserIsActive.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureUserIsActive
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (auth()->check() && !auth()->user()->is_active) {
|
||||
auth()->logout();
|
||||
return redirect()->route('login')->withErrors(['email' => 'Your account is pending approval by an administrator.']);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
24
app/Http/Middleware/EnsureUserIsAdmin.php
Normal file
24
app/Http/Middleware/EnsureUserIsAdmin.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureUserIsAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!$request->user() || !$request->user()->isAdmin()) {
|
||||
abort(403, 'Unauthorized. Admin access only.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
42
app/Mail/ContractExpiringNotification.php
Normal file
42
app/Mail/ContractExpiringNotification.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ContractExpiringNotification extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Collection $expiringContracts
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Alertes: Contrats arrivant à échéance',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.contracts.expiring',
|
||||
);
|
||||
}
|
||||
}
|
||||
23
app/Models/AuditLog.php
Normal file
23
app/Models/AuditLog.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AuditLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'changes' => 'array',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
60
app/Models/Contract.php
Normal file
60
app/Models/Contract.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Contract extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'reference',
|
||||
'provider',
|
||||
'status',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'amount',
|
||||
'currency',
|
||||
'notes',
|
||||
'type',
|
||||
'municipality_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function meta(): HasMany
|
||||
{
|
||||
return $this->hasMany(ContractMeta::class);
|
||||
}
|
||||
|
||||
public function documents(): HasMany
|
||||
{
|
||||
return $this->hasMany(Document::class);
|
||||
}
|
||||
|
||||
public function municipality(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Municipality::class);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function scopeExpiringSoon($query, $days = 30)
|
||||
{
|
||||
return $query->where('end_date', '<=', now()->addDays($days))
|
||||
->where('end_date', '>=', now())
|
||||
->where('status', '!=', 'expired');
|
||||
}
|
||||
}
|
||||
25
app/Models/ContractMeta.php
Normal file
25
app/Models/ContractMeta.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ContractMeta extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'contract_meta'; // singular/plural convention can be tricky if not explicit
|
||||
|
||||
protected $fillable = [
|
||||
'contract_id',
|
||||
'key',
|
||||
'value',
|
||||
];
|
||||
|
||||
public function contract(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contract::class);
|
||||
}
|
||||
}
|
||||
32
app/Models/Document.php
Normal file
32
app/Models/Document.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Document extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'contract_id',
|
||||
'filename',
|
||||
'path',
|
||||
'mime_type',
|
||||
'size',
|
||||
'description',
|
||||
'uploaded_by',
|
||||
];
|
||||
|
||||
public function contract(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contract::class);
|
||||
}
|
||||
|
||||
public function uploader(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by');
|
||||
}
|
||||
}
|
||||
30
app/Models/GlobalSetting.php
Normal file
30
app/Models/GlobalSetting.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GlobalSetting extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'value'];
|
||||
|
||||
/**
|
||||
* Get a global setting value by key.
|
||||
*/
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
$setting = self::where('key', $key)->first();
|
||||
return $setting ? $setting->value : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a global setting value.
|
||||
*/
|
||||
public static function set(string $key, $value)
|
||||
{
|
||||
return self::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => $value]
|
||||
);
|
||||
}
|
||||
}
|
||||
15
app/Models/LicenseLevel.php
Normal file
15
app/Models/LicenseLevel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class LicenseLevel extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'type', 'is_active'];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
22
app/Models/Link.php
Normal file
22
app/Models/Link.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Link extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['title', 'url', 'icon', 'color', 'order', 'is_active'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('order', 'asc');
|
||||
}
|
||||
}
|
||||
20
app/Models/Municipality.php
Normal file
20
app/Models/Municipality.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Municipality extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'zip_code', 'is_active'];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function contracts()
|
||||
{
|
||||
return $this->hasMany(Contract::class);
|
||||
}
|
||||
}
|
||||
66
app/Models/User.php
Normal file
66
app/Models/User.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
public function isManager(): bool
|
||||
{
|
||||
return $this->role === 'manager' || $this->isAdmin();
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
52
app/Observers/ContractObserver.php
Normal file
52
app/Observers/ContractObserver.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\AuditLog;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ContractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Contract "created" event.
|
||||
*/
|
||||
public function created(Contract $contract): void
|
||||
{
|
||||
AuditLog::create([
|
||||
'user_id' => Auth::id(),
|
||||
'action' => 'contract_created',
|
||||
'description' => "Contract {$contract->name} created.",
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
'changes' => $contract->getAttributes(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Contract "updated" event.
|
||||
*/
|
||||
public function updated(Contract $contract): void
|
||||
{
|
||||
AuditLog::create([
|
||||
'user_id' => Auth::id(),
|
||||
'action' => 'contract_updated',
|
||||
'description' => "Contract {$contract->name} updated.",
|
||||
'ip_address' => request()->ip(),
|
||||
'changes' => $contract->getChanges(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Contract "deleted" event.
|
||||
*/
|
||||
public function deleted(Contract $contract): void
|
||||
{
|
||||
AuditLog::create([
|
||||
'user_id' => Auth::id(),
|
||||
'action' => 'contract_deleted',
|
||||
'description' => "Contract {$contract->name} deleted.",
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
app/Policies/AuditLogPolicy.php
Normal file
66
app/Policies/AuditLogPolicy.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class AuditLogPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, AuditLog $auditLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, AuditLog $auditLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, AuditLog $auditLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, AuditLog $auditLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, AuditLog $auditLog): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
66
app/Policies/ContractPolicy.php
Normal file
66
app/Policies/ContractPolicy.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class ContractPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Contract $contract): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Contract $contract): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Contract $contract): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Contract $contract): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Contract $contract): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
65
app/Policies/UserPolicy.php
Normal file
65
app/Policies/UserPolicy.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
171
app/Services/CortexXdrService.php
Normal file
171
app/Services/CortexXdrService.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CortexXdrService
|
||||
{
|
||||
protected ?string $apiKey;
|
||||
protected ?string $apiKeyId;
|
||||
protected ?string $baseUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.cortex.key');
|
||||
$this->apiKeyId = config('services.cortex.id');
|
||||
$this->baseUrl = config('services.cortex.url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate headers.
|
||||
* Switched to Standard Authentication (Direct Key) based on documentation screenshot.
|
||||
*/
|
||||
protected function getHeaders()
|
||||
{
|
||||
// STANDARD AUTHENTICATION
|
||||
return [
|
||||
'x-xdr-auth-id' => $this->apiKeyId,
|
||||
'Authorization' => $this->apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Incidents
|
||||
* Using pagination to fetch up to $limit incidents.
|
||||
*/
|
||||
public function getIncidents($limit = 1000)
|
||||
{
|
||||
return $this->fetchAll('incidents/get_incidents', 'incidents', $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Endpoints
|
||||
* Using pagination to fetch up to $limit endpoints.
|
||||
*/
|
||||
public function getEndpoints($limit = 1000)
|
||||
{
|
||||
return $this->fetchAll('endpoints/get_endpoint', 'endpoints', $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic fetch method with pagination.
|
||||
*/
|
||||
private function fetchAll($endpoint, $dataKey, $limit)
|
||||
{
|
||||
if (empty($this->apiKey) || empty($this->apiKeyId) || empty($this->baseUrl)) {
|
||||
Log::warning('Cortex XDR credentials missing.');
|
||||
return [];
|
||||
}
|
||||
|
||||
$allResults = [];
|
||||
$batchSize = 100; // API Limit
|
||||
$offset = 0;
|
||||
|
||||
try {
|
||||
while ($offset < $limit) {
|
||||
// Adjust batch size for last page if needed
|
||||
$currentBatchSize = min($batchSize, $limit - $offset);
|
||||
|
||||
// API confusingly uses search_to as count? Or offset + count?
|
||||
// Documentation: "search_to: Integer representing the ending offset" ?
|
||||
// Error message: "0 < search_size <= 100".
|
||||
// Wait, request_data documentation usually says:
|
||||
// search_from: integer
|
||||
// search_to: integer (exclusive end index?) or count?
|
||||
// Let's assume standard Cortex: offset based.
|
||||
// If search_from=0, search_to=100 -> gets 0 to 99 (100 items).
|
||||
|
||||
$response = Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->baseUrl}/public_api/v1/{$endpoint}/", [
|
||||
'request_data' => [
|
||||
'search_from' => $offset,
|
||||
'search_to' => $offset + $currentBatchSize,
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$items = $response->json()['reply'][$dataKey] ?? [];
|
||||
|
||||
if (empty($items)) {
|
||||
break; // No more items
|
||||
}
|
||||
|
||||
$allResults = array_merge($allResults, $items);
|
||||
$offset += count($items);
|
||||
|
||||
// If we got fewer items than requested, we are done
|
||||
if (count($items) < $currentBatchSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
$err = "Cortex API Error ({$endpoint}): " . $response->status() . ' - ' . $response->body();
|
||||
Log::error($err);
|
||||
throw new \Exception($err);
|
||||
}
|
||||
}
|
||||
|
||||
return $allResults;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Dashboard Summary
|
||||
*/
|
||||
public function getSummary()
|
||||
{
|
||||
try {
|
||||
$incidents = $this->getIncidents(1000); // Limit incidents to 1000 recent
|
||||
$endpoints = $this->getEndpoints(1000); // Limit endpoints to 2000
|
||||
} catch (\Exception $e) {
|
||||
// In dashboard context, we verify configuration separately.
|
||||
// Returning empty arrays here avoids crashing the page if just one call fails.
|
||||
$incidents = [];
|
||||
$endpoints = [];
|
||||
}
|
||||
|
||||
// Calculations
|
||||
$endpointsTotal = count($endpoints);
|
||||
$endpointsConnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'connected')->count();
|
||||
$endpointsDisconnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'disconnected')->count();
|
||||
|
||||
// Filter only Active Incidents (New or Under Investigation)
|
||||
$activeIncidents = collect($incidents)->filter(fn($i) => in_array($i['status'] ?? '', ['new', 'under_investigation']));
|
||||
|
||||
//die(var_dump($activeIncidents));
|
||||
|
||||
$incidentsCritical = $activeIncidents->where('severity', 'critical')->count();
|
||||
$incidentsHigh = $activeIncidents->where('severity', 'high')->count();
|
||||
$incidentsMedium = $activeIncidents->where('severity', 'medium')->count();
|
||||
$incidentsLow = $activeIncidents->where('severity', 'low')->count();
|
||||
|
||||
// Endpoint Types (Workstation, Server, etc.)
|
||||
$endpointTypes = collect($endpoints)->groupBy(function ($e) {
|
||||
return ucfirst(strtolower($e['endpoint_type'] ?? 'Unknown'));
|
||||
})->map->count();
|
||||
|
||||
return [
|
||||
'incidents_total' => $activeIncidents->count(),
|
||||
'incidents_critical' => $incidentsCritical,
|
||||
'incidents_high' => $incidentsHigh,
|
||||
'incidents_medium' => $incidentsMedium,
|
||||
'incidents_low' => $incidentsLow,
|
||||
'endpoints_total' => $endpointsTotal,
|
||||
'endpoints_connected' => $endpointsConnected,
|
||||
'endpoints_disconnected' => $endpointsDisconnected,
|
||||
'endpoints_types' => $endpointTypes, // New Data
|
||||
'recent_incidents' => collect($incidents)
|
||||
->filter(fn($i) => !in_array($i['status'] ?? '', ['resolved_true_positive', 'resolved_false_positive']))
|
||||
->slice(0, 10)
|
||||
->values()
|
||||
->all()
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user