mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
Enhance API functionality and improve JWT authentication
- Added JWT authentication support in AuthService and JwtService. - Implemented token generation and refresh mechanisms. - Updated ApiAuthMiddleware to handle authentication for protected routes. - Created ApiController and BaseApiController for standardized API responses. - Developed MediaController for managing media items with pagination and search capabilities. - Introduced DocsController for serving API documentation via Swagger UI. - Added routes for API documentation and media management. - Improved error handling and response formatting across API endpoints. - Updated composer.json to include necessary JWT and Swagger UI dependencies.
This commit is contained in:
@@ -4,15 +4,30 @@ namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use PDO;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use DomainException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
private PDO $pdo;
|
||||
private ?array $user = null;
|
||||
private JwtService $jwtService;
|
||||
private string $jwtSecret;
|
||||
private string $jwtAlgo = 'HS256';
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
public function __construct(PDO $pdo, JwtService $jwtService = null)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
$this->jwtService = $jwtService ?? new JwtService([
|
||||
'secret' => getenv('JWT_SECRET') ?: 'your-secret-key-change-this-in-production',
|
||||
'algo' => $this->jwtAlgo,
|
||||
'expiration' => 3600,
|
||||
'leeway' => 60
|
||||
]);
|
||||
|
||||
$this->checkSession();
|
||||
}
|
||||
|
||||
@@ -96,6 +111,92 @@ class AuthService
|
||||
}
|
||||
}
|
||||
|
||||
public function generateToken(array $user): array
|
||||
{
|
||||
$now = time();
|
||||
$payload = [
|
||||
'sub' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'] ?? 'user',
|
||||
'iat' => $now,
|
||||
'exp' => $now + 3600, // 1 hour expiration
|
||||
'jti' => bin2hex(random_bytes(16))
|
||||
];
|
||||
|
||||
$token = $this->jwtService->encode($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user['id']);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => 3600,
|
||||
'token_type' => 'Bearer'
|
||||
];
|
||||
}
|
||||
|
||||
public function refreshToken(string $refreshToken): ?array
|
||||
{
|
||||
// Verify refresh token (you might want to store and validate this in your database)
|
||||
$stmt = $this->pdo->prepare("SELECT user_id FROM refresh_tokens WHERE token = :token AND expires_at > NOW()");
|
||||
$stmt->execute(['token' => $refreshToken]);
|
||||
$tokenData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$tokenData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = (new User($this->pdo))->find($tokenData['user_id']);
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
return $this->generateToken($user);
|
||||
}
|
||||
|
||||
private function generateRefreshToken(int $userId): string
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + (30 * 24 * 3600)); // 30 days
|
||||
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT INTO refresh_tokens (user_id, token, expires_at, created_at)
|
||||
VALUES (:user_id, :token, :expires_at, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
token = VALUES(token),
|
||||
expires_at = VALUES(expires_at),
|
||||
created_at = NOW()
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
'user_id' => $userId,
|
||||
'token' => $token,
|
||||
'expires_at' => $expiresAt
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function validateToken(string $token): ?array
|
||||
{
|
||||
try {
|
||||
$payload = $this->jwtService->decode($token);
|
||||
if (!$payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optionally check if user still exists and is active
|
||||
$user = (new User($this->pdo))->find($payload['sub']);
|
||||
if (!$user || !$user['is_active']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function generateCSRFToken(): string
|
||||
{
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
|
||||
@@ -51,13 +51,15 @@ abstract class BaseSyncService
|
||||
{
|
||||
if ($this->logFileHandle) {
|
||||
$this->logProgress("=== Sync completed at " . date('Y-m-d H:i:s') . " ===");
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'completed', [
|
||||
'processed_items' => $this->getProcessedCount(),
|
||||
'new_items' => $this->getNewCount(),
|
||||
'updated_items' => $this->getUpdatedCount(),
|
||||
'deleted_items' => $this->getDeletedCount(),
|
||||
'message' => $this->getCompletionMessage()
|
||||
]);
|
||||
if ($this->currentSyncLogId !== null) {
|
||||
$this->updateSyncLog($this->currentSyncLogId, 'completed', [
|
||||
'processed_items' => $this->getProcessedCount(),
|
||||
'new_items' => $this->getNewCount(),
|
||||
'updated_items' => $this->getUpdatedCount(),
|
||||
'deleted_items' => $this->getDeletedCount(),
|
||||
'message' => $this->getCompletionMessage()
|
||||
]);
|
||||
}
|
||||
fclose($this->logFileHandle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ class JellyfinSyncService extends BaseSyncService
|
||||
'query' => [
|
||||
'IncludeItemTypes' => $type,
|
||||
'Recursive' => 'true',
|
||||
'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating,OfficialRating,Genres,Studios,RunTimeTicks'
|
||||
'Fields' => 'ProviderIds,Overview,PremiereDate,CommunityRating,OfficialRating,Genres,Studios,RunTimeTicks,People'
|
||||
]
|
||||
]);
|
||||
|
||||
|
||||
63
app/Services/JwtService.php
Normal file
63
app/Services/JwtService.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use DomainException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class JwtService
|
||||
{
|
||||
private $secret;
|
||||
private $algo;
|
||||
private $expiration;
|
||||
private $leeway;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->secret = $config['secret'];
|
||||
$this->algo = $config['algo'];
|
||||
$this->expiration = $config['expiration'] ?? 3600;
|
||||
$this->leeway = $config['leeway'] ?? 60;
|
||||
|
||||
JWT::$leeway = $this->leeway;
|
||||
}
|
||||
|
||||
public function encode(array $payload): string
|
||||
{
|
||||
$now = time();
|
||||
$payload = array_merge([
|
||||
'iat' => $now,
|
||||
'exp' => $now + $this->expiration,
|
||||
], $payload);
|
||||
|
||||
return JWT::encode($payload, $this->secret, $this->algo);
|
||||
}
|
||||
|
||||
public function decode(string $token): ?array
|
||||
{
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key($this->secret, $this->algo));
|
||||
return (array) $decoded;
|
||||
} catch (ExpiredException $e) {
|
||||
// Token expired
|
||||
return null;
|
||||
} catch (DomainException | UnexpectedValueException $e) {
|
||||
// Invalid token
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(string $token): ?string
|
||||
{
|
||||
$payload = $this->decode($token);
|
||||
if (!$payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($payload['iat'], $payload['exp'], $payload['nbf']);
|
||||
return $this->encode($payload);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,30 @@ use SplFileInfo;
|
||||
|
||||
class LocalSyncService extends BaseSyncService implements SyncServiceInterface
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function executeSync(string $syncType): void
|
||||
{
|
||||
try {
|
||||
$path = $this->source['path'] ?? null;
|
||||
if (empty($path) || !is_dir($path)) {
|
||||
throw new Exception("Invalid or inaccessible source path: {$path}");
|
||||
}
|
||||
|
||||
$mediaType = $this->determineMediaType($this->source);
|
||||
|
||||
$this->logProgress("Starting {$syncType} sync for media type: {$mediaType}");
|
||||
|
||||
// Process the directory based on media type
|
||||
$this->processDirectory($path, $mediaType);
|
||||
|
||||
$this->logProgress("Completed {$syncType} sync for media type: {$mediaType}");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error during sync: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
protected string $sourceType = 'local';
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ class XbvrSyncService extends BaseSyncService
|
||||
]
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader(__DIR__ . '/../../storage/images');
|
||||
$this->imageDownloader = new ImageDownloader(__DIR__ . '/../../../storage/images');
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
|
||||
Reference in New Issue
Block a user