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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Middleware/ApiAuthMiddleware.php
Normal file
73
app/Middleware/ApiAuthMiddleware.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
use App\Services\AuthService;
|
||||||
|
|
||||||
|
class ApiAuthMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
private AuthService $authService;
|
||||||
|
private array $publicRoutes = [
|
||||||
|
'/api/auth/check',
|
||||||
|
'/api/auth/login',
|
||||||
|
'/api/auth/register',
|
||||||
|
'/api/status'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(AuthService $authService)
|
||||||
|
{
|
||||||
|
$this->authService = $authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$path = $request->getUri()->getPath();
|
||||||
|
|
||||||
|
// Skip authentication for public routes
|
||||||
|
if (in_array($path, $this->publicRoutes)) {
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from Authorization header
|
||||||
|
$authHeader = $request->getHeaderLine('Authorization');
|
||||||
|
if (empty($authHeader) || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
||||||
|
return $this->createErrorResponse(401, 'Missing or invalid authorization token');
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $matches[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify token and get user
|
||||||
|
$user = $this->authService->verifyToken($token);
|
||||||
|
if (!$user) {
|
||||||
|
return $this->createErrorResponse(401, 'Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user to request attributes for use in controllers
|
||||||
|
$request = $request->withAttribute('user', $user);
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->createErrorResponse(500, 'Authentication error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createErrorResponse(int $status, string $message): Response
|
||||||
|
{
|
||||||
|
$response = new \Slim\Psr7\Response($status);
|
||||||
|
$response->getBody()->write(json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => [
|
||||||
|
'code' => $status,
|
||||||
|
'message' => $message
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -338,6 +338,7 @@ class Game extends Model
|
|||||||
|
|
||||||
$sql = "
|
$sql = "
|
||||||
SELECT
|
SELECT
|
||||||
|
id,
|
||||||
game_key,
|
game_key,
|
||||||
title,
|
title,
|
||||||
COUNT(*) as platform_count,
|
COUNT(*) as platform_count,
|
||||||
|
|||||||
@@ -4,15 +4,30 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
use Firebase\JWT\ExpiredException;
|
||||||
|
use DomainException;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
class AuthService
|
class AuthService
|
||||||
{
|
{
|
||||||
private PDO $pdo;
|
private PDO $pdo;
|
||||||
private ?array $user = null;
|
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->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();
|
$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
|
public function generateCSRFToken(): string
|
||||||
{
|
{
|
||||||
if (!isset($_SESSION['csrf_token'])) {
|
if (!isset($_SESSION['csrf_token'])) {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ abstract class BaseSyncService
|
|||||||
{
|
{
|
||||||
if ($this->logFileHandle) {
|
if ($this->logFileHandle) {
|
||||||
$this->logProgress("=== Sync completed at " . date('Y-m-d H:i:s') . " ===");
|
$this->logProgress("=== Sync completed at " . date('Y-m-d H:i:s') . " ===");
|
||||||
|
if ($this->currentSyncLogId !== null) {
|
||||||
$this->updateSyncLog($this->currentSyncLogId, 'completed', [
|
$this->updateSyncLog($this->currentSyncLogId, 'completed', [
|
||||||
'processed_items' => $this->getProcessedCount(),
|
'processed_items' => $this->getProcessedCount(),
|
||||||
'new_items' => $this->getNewCount(),
|
'new_items' => $this->getNewCount(),
|
||||||
@@ -58,6 +59,7 @@ abstract class BaseSyncService
|
|||||||
'deleted_items' => $this->getDeletedCount(),
|
'deleted_items' => $this->getDeletedCount(),
|
||||||
'message' => $this->getCompletionMessage()
|
'message' => $this->getCompletionMessage()
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
fclose($this->logFileHandle);
|
fclose($this->logFileHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ class JellyfinSyncService extends BaseSyncService
|
|||||||
'query' => [
|
'query' => [
|
||||||
'IncludeItemTypes' => $type,
|
'IncludeItemTypes' => $type,
|
||||||
'Recursive' => 'true',
|
'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
|
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';
|
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
|
protected function executeSync(string $syncType): void
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"vlucas/phpdotenv": "^5.5",
|
"vlucas/phpdotenv": "^5.6",
|
||||||
"guzzlehttp/guzzle": "^7.5",
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
"slim/slim": "^4.10",
|
"slim/slim": "^4.10",
|
||||||
"slim/psr7": "^1.6",
|
"slim/psr7": "^1.6",
|
||||||
"slim/twig-view": "^3.3",
|
"slim/twig-view": "^3.3",
|
||||||
"php-di/php-di": "^7.0",
|
"php-di/php-di": "^7.0",
|
||||||
"illuminate/database": "^10.0",
|
"illuminate/database": "^10.0",
|
||||||
"zircote/swagger-php": "^5.5",
|
"zircote/swagger-php": "^5.7",
|
||||||
"php-middleware/php-debug-bar": "^1.0"
|
"php-middleware/php-debug-bar": "^1.0",
|
||||||
|
"firebase/php-jwt": "^7.0",
|
||||||
|
"swagger-api/swagger-ui": "^5.31"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ $container->set('view', function () use ($container) {
|
|||||||
case 'admin.playnite.upload':
|
case 'admin.playnite.upload':
|
||||||
$basePath = '/admin/playnite/import';
|
$basePath = '/admin/playnite/import';
|
||||||
break;
|
break;
|
||||||
|
case 'admin.games.edit':
|
||||||
|
$basePath = '/admin/games/' . $data['id'] . '/edit';
|
||||||
|
break;
|
||||||
case 'admin.sync':
|
case 'admin.sync':
|
||||||
$basePath = '/admin/sync/' . ($data['id'] ?? '');
|
$basePath = '/admin/sync/' . ($data['id'] ?? '');
|
||||||
break;
|
break;
|
||||||
@@ -483,6 +486,9 @@ $container->set(\App\Http\Middleware\MediaVisibilityMiddleware::class, function
|
|||||||
AppFactory::setContainer($container);
|
AppFactory::setContainer($container);
|
||||||
$app = AppFactory::create();
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Add Method Override Middleware for handling _METHOD field in forms
|
||||||
|
$app->add(new \Slim\Middleware\MethodOverrideMiddleware());
|
||||||
|
|
||||||
// Add Twig-View Middleware
|
// Add Twig-View Middleware
|
||||||
$twig = $container->get('view');
|
$twig = $container->get('view');
|
||||||
$app->add(TwigMiddleware::create($app, $twig));
|
$app->add(TwigMiddleware::create($app, $twig));
|
||||||
@@ -560,5 +566,6 @@ $errorMiddleware = $app->addErrorMiddleware(
|
|||||||
// Register routes
|
// Register routes
|
||||||
require __DIR__ . '/../routes/web.php';
|
require __DIR__ . '/../routes/web.php';
|
||||||
require __DIR__ . '/../routes/api.php';
|
require __DIR__ . '/../routes/api.php';
|
||||||
|
require __DIR__ . '/../routes/api2.php';
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
|
|||||||
@@ -212,16 +212,20 @@
|
|||||||
{% if actors %}
|
{% if actors %}
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Cast</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Cast</h3>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||||
{% for actor in actors %}
|
{% for actor in actors %}
|
||||||
<div class="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
<a href="{{ path_for('actors.show', {'id': actor.id}) }}" class="flex flex-col items-center text-center group hover:scale-105 transition-transform">
|
||||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-400 to-purple-500 rounded-full flex items-center justify-center mr-3">
|
<div class="w-16 h-16 md:w-20 md:h-20 bg-gray-200 rounded-full overflow-hidden mb-2 group-hover:ring-2 group-hover:ring-blue-300 transition-all">
|
||||||
<span class="text-white font-medium text-sm">{{ actor.name|first|upper }}</span>
|
{% if actor.thumbnail_path %}
|
||||||
</div>
|
<img src="/images/{{ actor.thumbnail_path }}" alt="{{ actor.name }}" class="w-full h-full object-cover">
|
||||||
<div>
|
{% else %}
|
||||||
<p class="font-medium text-gray-900 text-sm">{{ actor.name }}</p>
|
<div class="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
|
||||||
|
<span class="text-white font-bold text-lg">{{ actor.name|first|upper }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="font-medium text-gray-900 text-sm leading-tight group-hover:text-blue-600 transition-colors">{{ actor.name }}</p>
|
||||||
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
42
routes/api-docs.php
Normal file
42
routes/api-docs.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
use App\Controllers\Api\DocsController;
|
||||||
|
|
||||||
|
// Documentation routes
|
||||||
|
$app->group('/docs', function (RouteCollectorProxy $group) {
|
||||||
|
$docsController = $this->get(DocsController::class);
|
||||||
|
|
||||||
|
// Documentation UI
|
||||||
|
$group->get('/api', [$docsController, 'showDocs']);
|
||||||
|
|
||||||
|
// OpenAPI JSON specification
|
||||||
|
$group->get('/api-docs.json', [$docsController, 'getOpenApiSpec']);
|
||||||
|
|
||||||
|
// Serve Swagger UI assets
|
||||||
|
$group->get('/swagger-ui/{file:.+}', function (Request $request, Response $response, array $args) {
|
||||||
|
$file = $args['file'];
|
||||||
|
$swaggerUiPath = __DIR__ . '/../../vendor/swagger-api/swagger-ui/dist';
|
||||||
|
$filePath = $swaggerUiPath . '/' . $file;
|
||||||
|
|
||||||
|
if (!file_exists($filePath)) {
|
||||||
|
return $response->withStatus(404, 'File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = pathinfo($file, PATHINFO_EXTENSION);
|
||||||
|
$contentTypes = [
|
||||||
|
'css' => 'text/css',
|
||||||
|
'js' => 'application/javascript',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'json' => 'application/json',
|
||||||
|
'html' => 'text/html',
|
||||||
|
];
|
||||||
|
|
||||||
|
$contentType = $contentTypes[$extension] ?? 'text/plain';
|
||||||
|
|
||||||
|
$response->getBody()->write(file_get_contents($filePath));
|
||||||
|
return $response->withHeader('Content-Type', $contentType);
|
||||||
|
});
|
||||||
|
});
|
||||||
89
routes/api2.php
Normal file
89
routes/api2.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
use App\Middleware\ApiAuthMiddleware;
|
||||||
|
use App\Controllers\Api\MediaController;
|
||||||
|
use App\Controllers\Api\AuthController;
|
||||||
|
use App\Services\AuthService;
|
||||||
|
use App\Services\JwtService;
|
||||||
|
use App\Controllers\Api\DocsController;
|
||||||
|
|
||||||
|
// Get container
|
||||||
|
$container = $app->getContainer();
|
||||||
|
|
||||||
|
// API routes group
|
||||||
|
$app->group('/api', function (RouteCollectorProxy $group) use ($container) {
|
||||||
|
|
||||||
|
$docsController = $this->get(DocsController::class);
|
||||||
|
|
||||||
|
// Public endpoints
|
||||||
|
$group->get('/status', function (Request $request, Response $response) {
|
||||||
|
$response->getBody()->write(json_encode([
|
||||||
|
'status' => 'ok',
|
||||||
|
'timestamp' => time(),
|
||||||
|
'version' => '1.0.0'
|
||||||
|
]));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
$group->get('/doku', [$docsController, 'getOpenApiSpec']);
|
||||||
|
|
||||||
|
$group->get('/docu', [$docsController, 'showDocs']);
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
$group->group('/auth', function (RouteCollectorProxy $group) use ($container) {
|
||||||
|
$authController = $container->get(AuthController::class);
|
||||||
|
|
||||||
|
$group->post('/login', [$authController, 'login']);
|
||||||
|
$group->post('/register', [$authController, 'register']);
|
||||||
|
$group->post('/refresh', [$authController, 'refreshToken']);
|
||||||
|
$group->get('/me', [$authController, 'getCurrentUser'])
|
||||||
|
->add(new ApiAuthMiddleware($container->get(AuthService::class)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Protected routes (require authentication)
|
||||||
|
$group->group('', function (RouteCollectorProxy $group) use ($container) {
|
||||||
|
$mediaController = $container->get(MediaController::class);
|
||||||
|
|
||||||
|
// Games
|
||||||
|
$group->get('/games', [$mediaController, 'listGames']);
|
||||||
|
$group->get('/games/{id:[0-9]+}', [$mediaController, 'getGame']);
|
||||||
|
|
||||||
|
// Movies
|
||||||
|
$group->get('/movies', [$mediaController, 'listMovies']);
|
||||||
|
$group->get('/movies/{id:[0-9]+}', [$mediaController, 'getMovie']);
|
||||||
|
|
||||||
|
// TV Shows
|
||||||
|
$group->get('/tvshows', [$mediaController, 'listTvShows']);
|
||||||
|
$group->get('/tvshows/{id:[0-9]+}', [$mediaController, 'getTvShow']);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
$group->get('/search', [$mediaController, 'search']);
|
||||||
|
|
||||||
|
})->add(new ApiAuthMiddleware($container->get(AuthService::class)));
|
||||||
|
|
||||||
|
// Admin routes (require admin role)
|
||||||
|
$group->group('/admin', function (RouteCollectorProxy $group) use ($container) {
|
||||||
|
// Add admin-specific routes here
|
||||||
|
$group->get('/users', function (Request $request, Response $response) {
|
||||||
|
// Admin-only user listing
|
||||||
|
$response->getBody()->write(json_encode(['message' => 'Admin access granted']));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
});
|
||||||
|
})->add(new ApiAuthMiddleware($container->get(AuthService::class)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CORS middleware
|
||||||
|
$app->add(function (Request $request, $handler) {
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
return $response
|
||||||
|
->withHeader('Access-Control-Allow-Origin', '*')
|
||||||
|
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
|
||||||
|
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
$app->options('/{routes:.+}', function (Request $request, Response $response, $args) {
|
||||||
|
return $response;
|
||||||
|
});
|
||||||
@@ -74,14 +74,14 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
$group->get('', AdminController::class . ':movies')->setName('admin.movies.index');
|
$group->get('', AdminController::class . ':movies')->setName('admin.movies.index');
|
||||||
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editMovie')->setName('admin.movies.create');
|
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editMovie')->setName('admin.movies.create');
|
||||||
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editMovie')->setName('admin.movies.edit');
|
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editMovie')->setName('admin.movies.edit');
|
||||||
$group->delete('/{id}', AdminController::class . ':deleteMovie')->setName('admin.movies.delete');
|
$group->map(['POST', 'DELETE'], '/{id}', AdminController::class . ':deleteMovie')->setName('admin.movies.delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
$adminGroup->group('/games', function (RouteCollectorProxy $group) {
|
$adminGroup->group('/games', function (RouteCollectorProxy $group) {
|
||||||
$group->get('', AdminController::class . ':games')->setName('admin.games.index');
|
$group->get('', AdminController::class . ':games')->setName('admin.games.index');
|
||||||
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editGame')->setName('admin.games.create');
|
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editGame')->setName('admin.games.create');
|
||||||
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editGame')->setName('admin.games.edit');
|
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editGame')->setName('admin.games.edit');
|
||||||
$group->delete('/{id}', AdminController::class . ':deleteGame')->setName('admin.games.delete');
|
$group->map(['POST', 'DELETE'], '/{id}', AdminController::class . ':deleteGame')->setName('admin.games.delete');
|
||||||
|
|
||||||
// SteamGridDB API routes
|
// SteamGridDB API routes
|
||||||
$group->group('/sgdb', function (RouteCollectorProxy $sgdb) {
|
$group->group('/sgdb', function (RouteCollectorProxy $sgdb) {
|
||||||
@@ -95,7 +95,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
$group->get('', AdminController::class . ':shows')->setName('admin.shows.index');
|
$group->get('', AdminController::class . ':shows')->setName('admin.shows.index');
|
||||||
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editShow')->setName('admin.shows.create');
|
$group->map(['GET', 'POST'], '/create', AdminController::class . ':editShow')->setName('admin.shows.create');
|
||||||
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editShow')->setName('admin.shows.edit');
|
$group->map(['GET', 'POST'], '/{id}/edit', AdminController::class . ':editShow')->setName('admin.shows.edit');
|
||||||
$group->delete('/{id}', AdminController::class . ':deleteShow')->setName('admin.shows.delete');
|
$group->map(['POST', 'DELETE'], '/{id}', AdminController::class . ':deleteShow')->setName('admin.shows.delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
$adminGroup->group('/adult', function (RouteCollectorProxy $group) {
|
$adminGroup->group('/adult', function (RouteCollectorProxy $group) {
|
||||||
@@ -128,7 +128,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
$sourcesGroup->post('', 'App\Controllers\MediaSourceController:store')->setName('admin.sources.store');
|
$sourcesGroup->post('', 'App\Controllers\MediaSourceController:store')->setName('admin.sources.store');
|
||||||
$sourcesGroup->get('/{id:\d+}/edit', 'App\Controllers\MediaSourceController:edit')->setName('admin.sources.edit');
|
$sourcesGroup->get('/{id:\d+}/edit', 'App\Controllers\MediaSourceController:edit')->setName('admin.sources.edit');
|
||||||
$sourcesGroup->post('/{id:\d+}', 'App\Controllers\MediaSourceController:update')->setName('admin.sources.update');
|
$sourcesGroup->post('/{id:\d+}', 'App\Controllers\MediaSourceController:update')->setName('admin.sources.update');
|
||||||
$sourcesGroup->delete('/{id:\d+}', 'App\Controllers\MediaSourceController:destroy')->setName('admin.sources.destroy');
|
$sourcesGroup->post('/{id:\d+}/delete', 'App\Controllers\MediaSourceController:destroy')->setName('admin.sources.destroy');
|
||||||
|
|
||||||
// Source sync operations
|
// Source sync operations
|
||||||
$sourcesGroup->post('/{id:\d+}/sync', 'App\Controllers\MediaSourceController:startSync')->setName('admin.sources.sync');
|
$sourcesGroup->post('/{id:\d+}/sync', 'App\Controllers\MediaSourceController:startSync')->setName('admin.sources.sync');
|
||||||
|
|||||||
Reference in New Issue
Block a user