Add public API v1 with API key auth, rate limiting, and docs page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mark
2026-05-31 23:29:18 +03:00
co-authored by Claude Sonnet 4.6
parent 68d01448d2
commit a583a13da1
17 changed files with 1602 additions and 1 deletions
@@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Game;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class GameApiController extends Controller
{
private function formatGame(Game $game): array
{
return [
'id' => $game->id,
'title' => $game->title,
'slug' => $game->slug,
'cover_url' => $game->cover_url,
'release_year' => $game->release_year,
'release_date' => $game->release_date?->toDateString(),
'rating_avg' => $game->rating_avg ? (float) $game->rating_avg : null,
'rating_count' => $game->rating_count,
'library_count' => $game->library_count,
'genres' => $game->genres->pluck('name'),
'platforms' => $game->platforms->pluck('name'),
];
}
public function index(Request $request): JsonResponse
{
$query = Game::with(['genres', 'platforms'])->published();
if ($request->filled('genre')) {
$query->whereHas('genres', fn ($q) => $q->where('genres.id', $request->genre));
}
if ($request->filled('platform')) {
$query->whereHas('platforms', fn ($q) => $q->where('platforms.id', $request->platform));
}
if ($request->filled('year')) {
$query->where('release_year', $request->year);
}
match ($request->get('sort', 'popular')) {
'rating' => $query->orderByDesc('rating_avg'),
'release' => $query->orderByDesc('release_date'),
'title' => $query->orderBy('title'),
default => $query->orderByDesc('library_count'),
};
$paginator = $query->paginate(20);
return response()->json([
'data' => $paginator->map(fn ($game) => $this->formatGame($game))->values(),
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
],
]);
}
public function show(Game $game): JsonResponse
{
$game->load(['genres', 'platforms', 'companies', 'screenshots', 'reviews.user']);
return response()->json([
'data' => array_merge($this->formatGame($game), [
'description' => $game->summary,
'companies' => $game->companies->map(fn ($c) => [
'name' => $c->name,
'role' => $c->pivot->role,
]),
'screenshots' => $game->screenshots->pluck('url'),
'reviews' => $game->reviews->take(5)->map(fn ($r) => [
'user' => $r->user->slug,
'body' => $r->body,
'rating' => $r->rating,
'created_at' => $r->created_at->toIso8601String(),
]),
]),
]);
}
public function search(Request $request): JsonResponse
{
$q = trim((string) $request->get('q', ''));
if (mb_strlen($q) < 2) {
return response()->json(['error' => 'Query must be at least 2 characters.'], 422);
}
$games = Game::with(['genres', 'platforms'])
->where('title', 'like', '%' . $q . '%')
->published()
->orderBy('title')
->limit(20)
->get();
return response()->json([
'data' => $games->map(fn ($game) => $this->formatGame($game))->values(),
'meta' => ['total' => $games->count()],
]);
}
public function topRated(Request $request): JsonResponse
{
$paginator = Game::with(['genres', 'platforms'])
->published()
->where('rating_count', '>=', 1)
->orderByDesc('rating_avg')
->orderByDesc('rating_count')
->paginate(20);
return response()->json([
'data' => $paginator->map(fn ($game) => $this->formatGame($game))->values(),
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
],
]);
}
public function newReleases(Request $request): JsonResponse
{
$paginator = Game::with(['genres', 'platforms'])
->published()
->where('release_date', '<=', now())
->orderByDesc('release_date')
->paginate(20);
return response()->json([
'data' => $paginator->map(fn ($game) => $this->formatGame($game))->values(),
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
],
]);
}
}