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:
58
app/Controllers/Api/ApiController.php
Normal file
58
app/Controllers/Api/ApiController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
abstract class ApiController
|
||||
{
|
||||
protected function success(Response $response, $data = null, int $status = 200): Response
|
||||
{
|
||||
$responseData = ['success' => true];
|
||||
|
||||
if ($data !== null) {
|
||||
$responseData['data'] = $data;
|
||||
}
|
||||
|
||||
return $this->json($response, $responseData, $status);
|
||||
}
|
||||
|
||||
protected function error(Response $response, string $message, int $status = 400, array $errors = []): Response
|
||||
{
|
||||
$responseData = [
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'message' => $message,
|
||||
'code' => $status
|
||||
]
|
||||
];
|
||||
|
||||
if (!empty($errors)) {
|
||||
$responseData['error']['details'] = $errors;
|
||||
}
|
||||
|
||||
return $this->json($response, $responseData, $status);
|
||||
}
|
||||
|
||||
protected function json(Response $response, $data, int $status = 200): Response
|
||||
{
|
||||
$response->getBody()->write(json_encode($data));
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($status);
|
||||
}
|
||||
|
||||
protected function getPaginationParams(Request $request): array
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$page = max(1, (int)($params['page'] ?? 1));
|
||||
$perPage = min(100, max(1, (int)($params['per_page'] ?? 20)));
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'offset' => ($page - 1) * $perPage
|
||||
];
|
||||
}
|
||||
}
|
||||
62
app/Controllers/Api/BaseApiController.php
Normal file
62
app/Controllers/Api/BaseApiController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Controllers\Controller;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
protected function success(Response $response, $data = null, int $status = 200): Response
|
||||
{
|
||||
$responseData = ['success' => true];
|
||||
|
||||
if ($data !== null) {
|
||||
$responseData['data'] = $data;
|
||||
}
|
||||
|
||||
return $this->json($response, $responseData, $status);
|
||||
}
|
||||
|
||||
protected function error(Response $response, string $message, int $status = 400, array $errors = []): Response
|
||||
{
|
||||
$responseData = [
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'message' => $message,
|
||||
'code' => $status
|
||||
]
|
||||
];
|
||||
|
||||
if (!empty($errors)) {
|
||||
$responseData['error']['details'] = $errors;
|
||||
}
|
||||
|
||||
return $this->json($response, $responseData, $status);
|
||||
}
|
||||
|
||||
protected function getPaginationParams(Request $request): array
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
$page = max(1, (int)($params['page'] ?? 1));
|
||||
$perPage = min(50, max(1, (int)($params['per_page'] ?? 20)));
|
||||
|
||||
return [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'offset' => ($page - 1) * $perPage
|
||||
];
|
||||
}
|
||||
|
||||
protected function getAuthUser(Request $request): ?array
|
||||
{
|
||||
return $request->getAttribute('user');
|
||||
}
|
||||
|
||||
protected function isAdmin(Request $request): bool
|
||||
{
|
||||
$user = $this->getAuthUser($request);
|
||||
return $user && ($user['is_admin'] ?? false);
|
||||
}
|
||||
}
|
||||
107
app/Controllers/Api/DocsController.php
Normal file
107
app/Controllers/Api/DocsController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @OA\Info(
|
||||
* title="Media Library API",
|
||||
* version="1.0.0",
|
||||
* description="API documentation for the Media Library application"
|
||||
* )
|
||||
* @OA\Server(
|
||||
* url="/api",
|
||||
* description="API Server"
|
||||
* )
|
||||
* @OA\SecurityScheme(
|
||||
* securityScheme="bearerAuth",
|
||||
* type="http",
|
||||
* scheme="bearer",
|
||||
* bearerFormat="JWT"
|
||||
* )
|
||||
*/
|
||||
use App\Controllers\Api\ApiController;
|
||||
|
||||
class DocsController extends ApiController
|
||||
{
|
||||
private $basePath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->basePath = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/docs",
|
||||
* summary="API Documentation",
|
||||
* tags={"Documentation"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the Swagger UI interface"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function showDocs(Request $request, Response $response): Response
|
||||
{
|
||||
$swaggerUiPath = __DIR__ . '/../../../vendor/swagger-api/swagger-ui/dist';
|
||||
|
||||
if (!file_exists($swaggerUiPath)) {
|
||||
return $this->error($response, 'Swagger UI not found. Please run: composer require swagger-api/swagger-ui', 404);
|
||||
}
|
||||
|
||||
// Serve the Swagger UI
|
||||
$html = file_get_contents($swaggerUiPath . '/index.html');
|
||||
|
||||
// Update the URL to point to our OpenAPI JSON endpoint
|
||||
$html = str_replace(
|
||||
'url: "https://petstore.swagger.io/v2/swagger.json"',
|
||||
'urls: [
|
||||
{url: "/api-docs.json", name: "Media Library API"}
|
||||
],
|
||||
"dom_id": "#swagger-ui",
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "StandaloneLayout",',
|
||||
$html
|
||||
);
|
||||
|
||||
$response->getBody()->write($html);
|
||||
return $response->withHeader('Content-Type', 'text/html');
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api-docs.json",
|
||||
* summary="OpenAPI Specification",
|
||||
* tags={"Documentation"},
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the OpenAPI specification"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function getOpenApiSpec(Request $request, Response $response): Response
|
||||
{
|
||||
$openapi = \OpenApi\Generator::scan([
|
||||
__DIR__ . '/../../../app',
|
||||
__DIR__ . '/../../../routes',
|
||||
__DIR__ . '/../../../src'
|
||||
], [
|
||||
'exclude' => [
|
||||
__DIR__ . '/../../../database/migrations',
|
||||
__DIR__ . '/../../../vendor',
|
||||
__DIR__ . '/../../../tests'
|
||||
]
|
||||
]);
|
||||
|
||||
$response->getBody()->write(json_encode($openapi));
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
196
app/Controllers/Api/MediaController.php
Normal file
196
app/Controllers/Api/MediaController.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use App\Models\Game;
|
||||
use App\Models\Movie;
|
||||
use App\Models\TvShow;
|
||||
use App\Models\MusicArtist;
|
||||
use App\Controllers\Api\ApiController;
|
||||
|
||||
class MediaController extends ApiController
|
||||
{
|
||||
private $gameModel;
|
||||
private $movieModel;
|
||||
private $tvShowModel;
|
||||
private $musicArtistModel;
|
||||
|
||||
public function __construct(\PDO $pdo)
|
||||
{
|
||||
$this->gameModel = new Game($pdo);
|
||||
$this->movieModel = new Movie($pdo);
|
||||
$this->tvShowModel = new TvShow($pdo);
|
||||
$this->musicArtistModel = new MusicArtist($pdo);
|
||||
}
|
||||
|
||||
// List all games with pagination
|
||||
public function listGames(Request $request, Response $response): Response
|
||||
{
|
||||
try {
|
||||
$pagination = $this->getPaginationParams($request);
|
||||
$filters = $this->getFiltersFromRequest($request);
|
||||
|
||||
$games = $this->gameModel->findAll(
|
||||
$filters,
|
||||
$pagination['per_page'],
|
||||
$pagination['offset']
|
||||
);
|
||||
|
||||
$total = $this->gameModel->count($filters);
|
||||
|
||||
return $this->success($response, [
|
||||
'items' => $games,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $pagination['per_page'],
|
||||
'current_page' => $pagination['page'],
|
||||
'last_page' => ceil($total / $pagination['per_page'])
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($response, 'Failed to fetch games', 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Get single game by ID
|
||||
public function getGame(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
try {
|
||||
$id = (int)($args['id'] ?? 0);
|
||||
if (!$id) {
|
||||
return $this->error($response, 'Invalid game ID', 400);
|
||||
}
|
||||
|
||||
$game = $this->gameModel->find($id);
|
||||
if (!$game) {
|
||||
return $this->error($response, 'Game not found', 404);
|
||||
}
|
||||
|
||||
return $this->success($response, $game);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($response, 'Failed to fetch game', 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Search across all media
|
||||
public function search(Request $request, Response $response): Response
|
||||
{
|
||||
try {
|
||||
$query = $request->getQueryParams()['q'] ?? '';
|
||||
$type = $request->getQueryParams()['type'] ?? 'all';
|
||||
$pagination = $this->getPaginationParams($request);
|
||||
|
||||
$results = [];
|
||||
|
||||
if ($type === 'all' || $type === 'game') {
|
||||
$results['games'] = $this->searchGames($query, $pagination);
|
||||
}
|
||||
|
||||
if ($type === 'all' || $type === 'movie') {
|
||||
$results['movies'] = $this->searchMovies($query, $pagination);
|
||||
}
|
||||
|
||||
if ($type === 'all' || $type === 'tvshow') {
|
||||
$results['tvshows'] = $this->searchTvShows($query, $pagination);
|
||||
}
|
||||
|
||||
if ($type === 'all' || $type === 'music') {
|
||||
$results['artists'] = $this->searchArtists($query, $pagination);
|
||||
}
|
||||
|
||||
return $this->success($response, $results);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($response, 'Search failed: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods for searching different media types
|
||||
private function searchGames(string $query, array $pagination): array
|
||||
{
|
||||
try {
|
||||
// First try to use the model's search method if it exists
|
||||
if (method_exists($this->gameModel, 'search')) {
|
||||
$games = $this->gameModel->search($query, $pagination['per_page'], $pagination['offset']);
|
||||
$total = method_exists($this->gameModel, 'countSearchResults')
|
||||
? $this->gameModel->countSearchResults($query)
|
||||
: count($games);
|
||||
}
|
||||
// Fallback to basic filtering if search method doesn't exist
|
||||
else {
|
||||
$allGames = $this->gameModel->findAll();
|
||||
$filtered = array_filter($allGames, function($game) use ($query) {
|
||||
return stripos($game['title'] ?? '', $query) !== false;
|
||||
});
|
||||
|
||||
// Apply pagination
|
||||
$games = array_slice($filtered, $pagination['offset'], $pagination['per_page']);
|
||||
$total = count($filtered);
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $games,
|
||||
'total' => $total,
|
||||
'page' => $pagination['page'],
|
||||
'per_page' => $pagination['per_page']
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
// If anything goes wrong, return empty results
|
||||
return [
|
||||
'items' => [],
|
||||
'total' => 0,
|
||||
'page' => $pagination['page'],
|
||||
'per_page' => $pagination['per_page'],
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function searchMovies(string $query, array $pagination): array
|
||||
{
|
||||
// Implement movie search logic
|
||||
return [
|
||||
'items' => [],
|
||||
'total' => 0
|
||||
];
|
||||
}
|
||||
|
||||
private function searchTvShows(string $query, array $pagination): array
|
||||
{
|
||||
// Implement TV show search logic
|
||||
return [
|
||||
'items' => [],
|
||||
'total' => 0
|
||||
];
|
||||
}
|
||||
|
||||
private function searchArtists(string $query, array $pagination): array
|
||||
{
|
||||
// Implement artist search logic
|
||||
return [
|
||||
'items' => [],
|
||||
'total' => 0
|
||||
];
|
||||
}
|
||||
|
||||
// Extract filters from request
|
||||
private function getFiltersFromRequest(Request $request): array
|
||||
{
|
||||
$filters = [];
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Add common filters
|
||||
if (!empty($queryParams['genre'])) {
|
||||
$filters['genre'] = $queryParams['genre'];
|
||||
}
|
||||
|
||||
if (!empty($queryParams['year'])) {
|
||||
$filters['year'] = (int)$queryParams['year'];
|
||||
}
|
||||
|
||||
// Add more filters as needed
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user