- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。 - 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。 - 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
171 lines
4.8 KiB
PHP
171 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use App\Models\AdminUser;
|
|
use App\Services\AuditLogger;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* 对 admin_api_resources.is_audit_required=true 的变更类请求,在成功响应后写入审计日志。
|
|
*
|
|
* 若请求已设置 {@see Request::ATTRIBUTE_AUDIT_RECORDED}(业务层已写更细快照),则跳过。
|
|
*/
|
|
final class RecordAdminApiAudit
|
|
{
|
|
public const ATTRIBUTE_AUDIT_RECORDED = 'admin_audit_recorded';
|
|
|
|
private const MUTATING_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$response = $next($request);
|
|
|
|
if (! $this->shouldRecord($request, $response)) {
|
|
return $response;
|
|
}
|
|
|
|
$admin = $request->user();
|
|
if (! $admin instanceof AdminUser) {
|
|
return $response;
|
|
}
|
|
|
|
$resource = $this->resolveResource($request);
|
|
if ($resource === null || ! (bool) $resource->is_audit_required) {
|
|
return $response;
|
|
}
|
|
|
|
$targetId = $this->resolveTargetId($request);
|
|
$actionCode = $this->resolveActionCode((string) $resource->code);
|
|
|
|
AuditLogger::recordForAdmin(
|
|
$admin,
|
|
$request,
|
|
moduleCode: (string) $resource->module_code,
|
|
actionCode: $actionCode,
|
|
targetType: (string) $resource->code,
|
|
targetId: $targetId,
|
|
beforeJson: null,
|
|
afterJson: [
|
|
'http_method' => $request->method(),
|
|
'route_name' => $this->normalizeRouteName((string) ($request->route()?->getName() ?? '')),
|
|
'status' => $response->getStatusCode(),
|
|
'payload' => $this->sanitizedPayload($request),
|
|
],
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
|
|
private static function isAuditAlreadyRecorded(Request $request): bool
|
|
{
|
|
foreach (self::auditRecordedRequests($request) as $candidate) {
|
|
if ($candidate->attributes->get(self::ATTRIBUTE_AUDIT_RECORDED) === true) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @return list<Request> */
|
|
private static function auditRecordedRequests(Request $request): array
|
|
{
|
|
$requests = [$request];
|
|
|
|
try {
|
|
$resolved = request();
|
|
if ($resolved !== $request) {
|
|
$requests[] = $resolved;
|
|
}
|
|
} catch (\Throwable) {
|
|
}
|
|
|
|
return $requests;
|
|
}
|
|
|
|
private function shouldRecord(Request $request, Response $response): bool
|
|
{
|
|
if (self::isAuditAlreadyRecorded($request)) {
|
|
return false;
|
|
}
|
|
|
|
if (! in_array(strtoupper($request->method()), self::MUTATING_METHODS, true)) {
|
|
return false;
|
|
}
|
|
|
|
$status = $response->getStatusCode();
|
|
|
|
return $status >= 200 && $status < 300;
|
|
}
|
|
|
|
private function resolveResource(Request $request): ?object
|
|
{
|
|
$routeName = $request->route()?->getName();
|
|
if (! is_string($routeName) || $routeName === '') {
|
|
return null;
|
|
}
|
|
|
|
return DB::table('admin_api_resources')
|
|
->where('route_name', $this->normalizeRouteName($routeName))
|
|
->where('status', 1)
|
|
->first(['code', 'module_code', 'is_audit_required']);
|
|
}
|
|
|
|
private function normalizeRouteName(string $routeName): string
|
|
{
|
|
return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName;
|
|
}
|
|
|
|
private function resolveActionCode(string $resourceCode): string
|
|
{
|
|
$pos = strrpos($resourceCode, '.');
|
|
if ($pos === false) {
|
|
return $resourceCode;
|
|
}
|
|
|
|
return substr($resourceCode, $pos + 1);
|
|
}
|
|
|
|
private function resolveTargetId(Request $request): ?string
|
|
{
|
|
$route = $request->route();
|
|
if ($route === null) {
|
|
return null;
|
|
}
|
|
|
|
foreach (['batch', 'draw', 'transfer_no', 'player', 'admin_user', 'admin_role', 'id', 'play_code', 'number_4d', 'key'] as $key) {
|
|
$value = $route->parameter($key);
|
|
if ($value === null) {
|
|
continue;
|
|
}
|
|
|
|
if (is_object($value) && method_exists($value, 'getKey')) {
|
|
return (string) $value->getKey();
|
|
}
|
|
|
|
return (string) $value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function sanitizedPayload(Request $request): ?array
|
|
{
|
|
$data = $request->except([
|
|
'password',
|
|
'password_confirmation',
|
|
'current_password',
|
|
'token',
|
|
]);
|
|
|
|
return $data === [] ? null : $data;
|
|
}
|
|
}
|