- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
51 lines
1.4 KiB
PHP
51 lines
1.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use CodeIgniter\Model;
|
|
|
|
final class ApiKeyModel extends Model
|
|
{
|
|
protected $table = 'api_keys';
|
|
protected $primaryKey = 'id';
|
|
protected $returnType = 'array';
|
|
protected $useTimestamps = true;
|
|
protected $allowedFields = [
|
|
'name', 'key_hash', 'key_prefix', 'rate_limit_per_hour',
|
|
'request_count', 'is_active', 'last_used_at',
|
|
];
|
|
|
|
public function findByPlainKey(string $plain): ?array
|
|
{
|
|
$hash = hash('sha256', $plain);
|
|
$key = $this->where('key_hash', $hash)->where('is_active', 1)->first();
|
|
if ($key !== null) {
|
|
$this->update((int) $key['id'], [
|
|
'request_count' => (int) $key['request_count'] + 1,
|
|
'last_used_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
/** Creates a key and returns [row, plaintext] — plaintext shown exactly once. */
|
|
public function issue(string $name, int $ratePerHour = 120): array
|
|
{
|
|
helper('text');
|
|
$plain = 'tv_live_' . bin2hex(random_bytes(20));
|
|
$row = [
|
|
'name' => $name,
|
|
'key_hash' => hash('sha256', $plain),
|
|
'key_prefix' => substr($plain, 0, 12),
|
|
'rate_limit_per_hour' => $ratePerHour,
|
|
'is_active' => 1,
|
|
];
|
|
$this->insert($row);
|
|
|
|
return [$row, $plain];
|
|
}
|
|
}
|