193fbde6c1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.4 KiB
PHP
74 lines
2.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Http\Middleware;
|
||
|
||
use App\Models\ApiKey;
|
||
use Closure;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
/**
|
||
* Аутентификация публичного API по ключу тенанта (G6).
|
||
*
|
||
* Ключ — `Authorization: Bearer lpkapi_...`. В БД лежит bcrypt key_hash + key_prefix
|
||
* (первые 10 символов). Ищем кандидатов по префиксу через pgsql_supplier (BYPASSRLS —
|
||
* публичный роут не ставит tenant-GUC, под RLS api_keys вернул бы пусто), затем
|
||
* Hash::check. Успех → tenant_id в request->attributes (api_tenant_id) + last_used.
|
||
*/
|
||
class ApiKeyAuth
|
||
{
|
||
public function handle(Request $request, Closure $next): Response
|
||
{
|
||
$key = $this->bearer($request);
|
||
if ($key === null || $key === '') {
|
||
return response()->json(['message' => 'Требуется API-ключ.'], 401);
|
||
}
|
||
|
||
$prefix = substr($key, 0, 10);
|
||
|
||
$candidates = ApiKey::on('pgsql_supplier')
|
||
->where('key_prefix', $prefix)
|
||
->where('is_active', true)
|
||
->where('expires_at', '>', now())
|
||
->get();
|
||
|
||
$matched = null;
|
||
foreach ($candidates as $candidate) {
|
||
if (Hash::check($key, (string) $candidate->key_hash)) {
|
||
$matched = $candidate;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($matched === null) {
|
||
return response()->json(['message' => 'Неверный или неактивный API-ключ.'], 401);
|
||
}
|
||
|
||
if (! in_array('read', (array) $matched->scopes, true)) {
|
||
return response()->json(['message' => 'Недостаточно прав ключа.'], 403);
|
||
}
|
||
|
||
ApiKey::on('pgsql_supplier')->whereKey($matched->getKey())->update([
|
||
'last_used_at' => now(),
|
||
'last_used_ip' => $request->ip(),
|
||
]);
|
||
|
||
$request->attributes->set('api_tenant_id', (int) $matched->tenant_id);
|
||
|
||
return $next($request);
|
||
}
|
||
|
||
private function bearer(Request $request): ?string
|
||
{
|
||
$header = (string) $request->header('Authorization', '');
|
||
if (str_starts_with($header, 'Bearer ')) {
|
||
return trim(substr($header, 7));
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|