172 lines
5.7 KiB
PHP
172 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\AuditLog;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class AuditLogMiddleware
|
|
{
|
|
/**
|
|
* Handle an incoming request and log mutating actions to the audit log.
|
|
*
|
|
* Automatically logs all POST, PUT, PATCH, DELETE requests with
|
|
* user_id, tenant_id, model info, action, old/new values, and request metadata.
|
|
*/
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
$method = $request->method();
|
|
$isMutating = in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
|
|
// For PUT/PATCH/DELETE, capture the current state before the request runs
|
|
$oldValues = null;
|
|
$modelType = null;
|
|
$modelId = null;
|
|
|
|
if ($isMutating && in_array($method, ['PUT', 'PATCH', 'DELETE'])) {
|
|
$resolvedModel = $this->resolveRouteModel($request);
|
|
|
|
if ($resolvedModel) {
|
|
$modelType = get_class($resolvedModel);
|
|
$modelId = $resolvedModel->getKey();
|
|
|
|
if ($method !== 'POST') {
|
|
$oldValues = $resolvedModel->toArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Execute the request
|
|
$response = $next($request);
|
|
|
|
// Only log if the request was successful (2xx status)
|
|
if ($isMutating && $response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
|
$action = $this->determineAction($method, $request);
|
|
$newValues = null;
|
|
|
|
// For POST/PUT/PATCH, capture new values from the model
|
|
if (in_array($method, ['POST', 'PUT', 'PATCH'])) {
|
|
$resolvedModel = $this->resolveRouteModel($request);
|
|
|
|
if ($resolvedModel) {
|
|
$modelType = get_class($resolvedModel);
|
|
$modelId = $resolvedModel->getKey();
|
|
$newValues = $resolvedModel->toArray();
|
|
} elseif ($method === 'POST') {
|
|
// For POST with resource creation, try to get the model from the response redirect
|
|
$createdModel = $this->extractCreatedModel($request, $response);
|
|
if ($createdModel) {
|
|
$modelType = get_class($createdModel);
|
|
$modelId = $createdModel->getKey();
|
|
$newValues = $createdModel->toArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter out sensitive fields from values
|
|
$sensitiveFields = ['password', 'password_hash', 'remember_token', 'national_code', 'bank_account', 'base_salary'];
|
|
$oldValues = $this->filterSensitiveFields($oldValues, $sensitiveFields);
|
|
$newValues = $this->filterSensitiveFields($newValues, $sensitiveFields);
|
|
|
|
try {
|
|
AuditLog::create([
|
|
'user_id' => Auth::id(),
|
|
'tenant_id' => session('tenant_id'),
|
|
'model_type' => $modelType,
|
|
'model_id' => $modelId,
|
|
'action' => $action,
|
|
'old_values' => $oldValues,
|
|
'new_values' => $newValues,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'url' => $request->fullUrl(),
|
|
'method' => $method,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
// Silently fail — audit logging should never break the request
|
|
logger()->error('Audit log failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Resolve the Eloquent model from the current route.
|
|
*/
|
|
protected function resolveRouteModel(Request $request): ?\Illuminate\Database\Eloquent\Model
|
|
{
|
|
$route = $request->route();
|
|
|
|
if (!$route) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($route->parameters() as $parameter) {
|
|
if ($parameter instanceof \Illuminate\Database\Eloquent\Model) {
|
|
return $parameter;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Determine the action label based on HTTP method.
|
|
*/
|
|
protected function determineAction(string $method, Request $request): string
|
|
{
|
|
// Check if the route name suggests an approve/reject action
|
|
$routeName = $request->route()?->getName() ?? '';
|
|
|
|
if (str_contains($routeName, 'approve')) {
|
|
return 'approve';
|
|
}
|
|
if (str_contains($routeName, 'reject')) {
|
|
return 'reject';
|
|
}
|
|
if (str_contains($routeName, 'settle')) {
|
|
return 'settle';
|
|
}
|
|
if (str_contains($routeName, 'status')) {
|
|
return 'status_change';
|
|
}
|
|
|
|
return match ($method) {
|
|
'POST' => 'create',
|
|
'PUT', 'PATCH' => 'update',
|
|
'DELETE' => 'delete',
|
|
default => strtolower($method),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Try to extract a newly created model from a redirect response.
|
|
*/
|
|
protected function extractCreatedModel(Request $request, $response): ?\Illuminate\Database\Eloquent\Model
|
|
{
|
|
// If the response redirects to a show route, the model was just created
|
|
// We can't easily extract it from the redirect, so we fall back to null
|
|
// The model type/id will be populated by a subsequent GET if needed
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Filter out sensitive fields from an array of values.
|
|
*/
|
|
protected function filterSensitiveFields(?array $values, array $sensitiveFields): ?array
|
|
{
|
|
if ($values === null) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($sensitiveFields as $field) {
|
|
unset($values[$field]);
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
}
|