mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Make API responses uniform
This commit is contained in:
@@ -755,7 +755,7 @@ if (! function_exists('unzipGzipFile')) {
|
||||
function unzipGzipFile(string $filePath): false|string
|
||||
{
|
||||
$string = '';
|
||||
$gzFile = @gzopen($filePath, 'rb', false);
|
||||
$gzFile = @gzopen($filePath, 'rb', 0);
|
||||
if ($gzFile) {
|
||||
while (! gzeof($gzFile)) {
|
||||
$temp = gzread($gzFile, 1024);
|
||||
@@ -914,42 +914,68 @@ if (! function_exists('imdb_trailers')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('apiErrorDetails')) {
|
||||
/**
|
||||
* @return array{code:int,message:string,status:int,header:string}
|
||||
*/
|
||||
function apiErrorDetails(int $errorCode = 900, string $errorText = ''): array
|
||||
{
|
||||
[$defaultText, $status, $errorHeader] = match ($errorCode) {
|
||||
100 => ['Incorrect user credentials', 401, 'HTTP/1.1 401 Unauthorized'],
|
||||
101 => ['Account suspended', 403, 'HTTP/1.1 403 Forbidden'],
|
||||
102 => ['Insufficient privileges/not authorized', 401, 'HTTP/1.1 401 Unauthorized'],
|
||||
103 => ['Registration denied', 403, 'HTTP/1.1 403 Forbidden'],
|
||||
104 => ['Registrations are closed', 403, 'HTTP/1.1 403 Forbidden'],
|
||||
105 => ['Invalid registration (Email Address Taken)', 403, 'HTTP/1.1 403 Forbidden'],
|
||||
106 => ['Invalid registration (Email Address Bad Format)', 403, 'HTTP/1.1 403 Forbidden'],
|
||||
107 => ['Registration Failed (Data error)', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
200 => ['Missing parameter', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
201 => ['Incorrect parameter', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
202 => ['No such function', 404, 'HTTP/1.1 404 Not Found'],
|
||||
203 => ['Function not available', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
300 => ['No such item', 404, 'HTTP/1.1 404 Not Found'],
|
||||
310 => ['Item already exists', 409, 'HTTP/1.1 409 Conflict'],
|
||||
500 => ['Request limit reached', 429, 'HTTP/1.1 429 Too Many Requests'],
|
||||
501 => ['Download limit reached', 429, 'HTTP/1.1 429 Too Many Requests'],
|
||||
600 => ['Failed to load NZB', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
601 => ['NZB is duplicate', 409, 'HTTP/1.1 409 Conflict'],
|
||||
602 => ['NZB is for a non-existent group', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
603 => ['NZB failed to write to disk', 500, 'HTTP/1.1 500 Internal Server Error'],
|
||||
910 => ['API disabled', 401, 'HTTP/1.1 401 Unauthorized'],
|
||||
default => ['Unknown error', 400, 'HTTP/1.1 400 Bad Request'],
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => $errorCode,
|
||||
'message' => $errorText !== '' ? $errorText : $defaultText,
|
||||
'status' => $status,
|
||||
'header' => $errorHeader,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('apiJsonError')) {
|
||||
function apiJsonError(int $errorCode = 900, string $errorText = ''): mixed
|
||||
{
|
||||
$error = apiErrorDetails($errorCode, $errorText);
|
||||
|
||||
return response()
|
||||
->json(['error' => $error['message']], $error['status'])
|
||||
->header('X-NNTmux', 'API ERROR ['.$error['code'].'] '.$error['message']);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('showApiError')) {
|
||||
function showApiError(int $errorCode = 900, string $errorText = ''): mixed
|
||||
{
|
||||
$errorHeader = 'HTTP 1.1 400 Bad Request';
|
||||
if ($errorText === '') {
|
||||
[$errorText, $errorHeader] = match ($errorCode) {
|
||||
100 => ['Incorrect user credentials', 'HTTP 1.1 401 Unauthorized'],
|
||||
101 => ['Account suspended', 'HTTP 1.1 403 Forbidden'],
|
||||
102 => ['Insufficient privileges/not authorized', 'HTTP 1.1 401 Unauthorized'],
|
||||
103 => ['Registration denied', 'HTTP 1.1 403 Forbidden'],
|
||||
104 => ['Registrations are closed', 'HTTP 1.1 403 Forbidden'],
|
||||
105 => ['Invalid registration (Email Address Taken)', 'HTTP 1.1 403 Forbidden'],
|
||||
106 => ['Invalid registration (Email Address Bad Format)', 'HTTP 1.1 403 Forbidden'],
|
||||
107 => ['Registration Failed (Data error)', 'HTTP 1.1 400 Bad Request'],
|
||||
200 => ['Missing parameter', 'HTTP 1.1 400 Bad Request'],
|
||||
201 => ['Incorrect parameter', 'HTTP 1.1 400 Bad Request'],
|
||||
202 => ['No such function', 'HTTP 1.1 404 Not Found'],
|
||||
203 => ['Function not available', 'HTTP 1.1 400 Bad Request'],
|
||||
300 => ['No such item', 'HTTP 1.1 404 Not Found'],
|
||||
310 => ['Item already exists', 'HTTP 1.1 409 Conflict'],
|
||||
500 => ['Request limit reached', 'HTTP 1.1 429 Too Many Requests'],
|
||||
501 => ['Download limit reached', 'HTTP 1.1 429 Too Many Requests'],
|
||||
600 => ['Failed to load NZB', 'HTTP 1.1 400 Bad Request'],
|
||||
601 => ['NZB is duplicate', 'HTTP 1.1 409 Conflict'],
|
||||
602 => ['NZB is for a non-existent group', 'HTTP 1.1 400 Bad Request'],
|
||||
603 => ['NZB failed to write to disk', 'HTTP 1.1 500 Internal Server Error'],
|
||||
910 => ['API disabled', 'HTTP 1.1 401 Unauthorized'],
|
||||
default => ['Unknown error', 'HTTP 1.1 400 Bad Request'],
|
||||
};
|
||||
}
|
||||
$error = apiErrorDetails($errorCode, $errorText);
|
||||
$errorText = $error['message'];
|
||||
|
||||
$response =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".
|
||||
'<error code="'.$errorCode.'" description="'.$errorText."\"/>\n";
|
||||
|
||||
return response($response)->header('Content-type', 'text/xml')->header('Content-Length', (string) strlen($response))->header('X-NNTmux', 'API ERROR ['.$errorCode.'] '.$errorText)->header('HTTP/1.1', $errorHeader);
|
||||
return response($response, $error['status'])->header('Content-type', 'text/xml')->header('Content-Length', (string) strlen($response))->header('X-NNTmux', 'API ERROR ['.$errorCode.'] '.$errorText)->header('HTTP/1.1', $error['header']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class ApiController extends BasePageController
|
||||
return showApiError(100, 'Incorrect user credentials (wrong API key)');
|
||||
}
|
||||
|
||||
if ($res->hasRole('Disabled')) {
|
||||
if ($res->is_disabled || $res->hasRole('Disabled')) {
|
||||
return showApiError(101);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ class ApiInformController extends Controller
|
||||
$apiToken = $request->has('api_token') && ! empty($request->input('api_token')) ? $request->input('api_token') : '';
|
||||
$user = User::findVerifiedByApiToken((string) $request->input('api_token'));
|
||||
if (! $user) {
|
||||
return response()->json(['message' => 'Indexer inform error, wrong api key!'], 404);
|
||||
return apiJsonError(100);
|
||||
}
|
||||
|
||||
if ($user->is_disabled || $user->hasRole('Disabled')) {
|
||||
return apiJsonError(101);
|
||||
}
|
||||
|
||||
if (! empty($releaseObName) && ! empty($releasePrName) && ! empty($apiToken)) {
|
||||
|
||||
@@ -50,23 +50,35 @@ class ApiV2Controller extends BasePageController
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API token and return cached user, or null on failure.
|
||||
* Validate API token and return cached user, or a normalized JSON API error on failure.
|
||||
* Caches user lookup for 5 minutes to reduce DB hits.
|
||||
*/
|
||||
private function resolveUser(Request $request): ?User
|
||||
private function resolveUser(Request $request): User|JsonResponse
|
||||
{
|
||||
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
|
||||
return null;
|
||||
return apiJsonError(200, 'Missing parameter (api_token)');
|
||||
}
|
||||
|
||||
$apiToken = $request->input('api_token');
|
||||
$userCacheKey = 'api_user:'.md5((string) $apiToken);
|
||||
|
||||
return Cache::remember($userCacheKey, 300, function () use ($apiToken) {
|
||||
return User::verifiedApiTokenQuery((string) $apiToken)
|
||||
->with('role')
|
||||
$user = Cache::remember($userCacheKey, 300, function () use ($apiToken) {
|
||||
return User::query()
|
||||
->whereApiToken((string) $apiToken)
|
||||
->first();
|
||||
});
|
||||
|
||||
if (! $user || ! $user->hasVerifiedEmail()) {
|
||||
return apiJsonError(100);
|
||||
}
|
||||
|
||||
if ($user->is_disabled || $user->hasRole('Disabled')) {
|
||||
return apiJsonError(101);
|
||||
}
|
||||
|
||||
$user->loadMissing('role');
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,8 +201,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function movie(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($user->id, $request->getRequestUri());
|
||||
@@ -256,8 +268,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function audio(Request $request): JsonResponse|Response
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($user->id, $request->getRequestUri());
|
||||
@@ -308,8 +320,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function books(Request $request): JsonResponse|Response
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($user->id, $request->getRequestUri());
|
||||
@@ -360,8 +372,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function anime(Request $request): JsonResponse|Response
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($user->id, $request->getRequestUri());
|
||||
@@ -416,8 +428,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function apiSearch(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($user->id, $request->getRequestUri());
|
||||
@@ -483,8 +495,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function tv(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$catExclusions = User::getCategoryExclusionById($user->id);
|
||||
@@ -559,8 +571,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function getNzb(Request $request): Application|ResponseFactory|JsonResponse|Redirector|RedirectResponse
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
event(new UserAccessedApi($user, $request->ip()));
|
||||
@@ -576,8 +588,8 @@ class ApiV2Controller extends BasePageController
|
||||
public function details(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->resolveUser($request);
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'Missing or invalid API key'], 403);
|
||||
if ($user instanceof JsonResponse) {
|
||||
return $user;
|
||||
}
|
||||
if ($request->missing('id')) {
|
||||
return response()->json(['error' => 'Missing parameter (guid is required for single release details)'], 400);
|
||||
|
||||
@@ -17,7 +17,7 @@ use Illuminate\View\View;
|
||||
class BasePageController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var Collection<int, mixed>
|
||||
* @var \Illuminate\Support\Collection<int, mixed>
|
||||
*/
|
||||
public \Illuminate\Support\Collection $settings; // @phpstan-ignore property.phpDocType, class.notFound, missingType.generics
|
||||
|
||||
@@ -81,6 +81,10 @@ class BasePageController extends Controller
|
||||
if (Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
$this->userdata = User::find($userId);
|
||||
if (! $this->userdata?->hasVerifiedEmail()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Cache category exclusions per user (5 minutes)
|
||||
$this->userdata->categoryexclusions = $this->rememberWithCacheFallback(
|
||||
'user_category_exclusions_'.$userId,
|
||||
|
||||
@@ -98,7 +98,11 @@ class GetNzbController extends BasePageController
|
||||
*/
|
||||
private function getUserDataFromSession(): array|Response
|
||||
{
|
||||
if ($this->userdata->hasRole('Disabled')) {
|
||||
if (! $this->userdata->hasVerifiedEmail()) {
|
||||
return showApiError(100);
|
||||
}
|
||||
|
||||
if ($this->userdata->is_disabled || $this->userdata->hasRole('Disabled')) {
|
||||
return showApiError(101);
|
||||
}
|
||||
|
||||
@@ -126,7 +130,7 @@ class GetNzbController extends BasePageController
|
||||
return showApiError(100);
|
||||
}
|
||||
|
||||
if ($user->hasRole('Disabled')) {
|
||||
if ($user->is_disabled || $user->hasRole('Disabled')) {
|
||||
return showApiError(101);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,13 +214,13 @@ class RssController extends BasePageController
|
||||
private function userCheck(Request $request): JsonResponse|array
|
||||
{
|
||||
if ($request->missing('api_token')) {
|
||||
return response()->json(['error' => 'API key is required for viewing the RSS!'], 403);
|
||||
return apiJsonError(200, 'Missing parameter (api_token)');
|
||||
}
|
||||
|
||||
$res = User::findVerifiedByApiToken((string) $request->input('api_token'));
|
||||
|
||||
if ($res === null) {
|
||||
return response()->json(['error' => 'Invalid RSS token'], 403);
|
||||
return apiJsonError(100);
|
||||
}
|
||||
|
||||
$uid = $res['id'];
|
||||
@@ -233,12 +233,12 @@ class RssController extends BasePageController
|
||||
$grabTime = UserDownload::whereUsersId($uid)->min('timestamp');
|
||||
$oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : '';
|
||||
|
||||
if ($res->hasRole('Disabled')) {
|
||||
return response()->json(['error' => 'Your account is disabled'], 403);
|
||||
if ($res->is_disabled || $res->hasRole('Disabled')) {
|
||||
return apiJsonError(101);
|
||||
}
|
||||
|
||||
if ($usedRequests > $maxRequests) {
|
||||
return response()->json(['error' => 'You have reached your daily limit for API requests!'], 403);
|
||||
return apiJsonError(500, 'Request limit reached ('.$usedRequests.'/'.$maxRequests.')');
|
||||
}
|
||||
|
||||
UserRequest::addApiRequest($rssToken, $request->getRequestUri());
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
use Closure;
|
||||
use Illuminate\Cache\RateLimiter;
|
||||
@@ -53,17 +54,23 @@ class ThrottleApiRequestsByToken
|
||||
|
||||
private function resolveUser(Request $request): ?User
|
||||
{
|
||||
$apiToken = $request->input('api_token');
|
||||
$apiToken = $request->input('api_token') ?? $request->input('apikey');
|
||||
|
||||
if (! is_string($apiToken) || $apiToken === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::remember('api_rate_limit_user:'.md5($apiToken), 300, static function () use ($apiToken) {
|
||||
$user = Cache::remember('api_rate_limit_user:'.md5($apiToken), 300, static function () use ($apiToken) {
|
||||
return User::verifiedApiTokenQuery($apiToken)
|
||||
->select(['id', 'api_token', 'rate_limit'])
|
||||
->select(['id', 'roles_id', 'api_token', 'rate_limit'])
|
||||
->first();
|
||||
});
|
||||
|
||||
if ($user?->roles_id === UserRole::DISABLED->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function rateLimitKey(int $userId): string
|
||||
@@ -74,14 +81,16 @@ class ThrottleApiRequestsByToken
|
||||
private function buildTooManyRequestsResponse(string $rateLimitKey, int $maxAttempts): JsonResponse
|
||||
{
|
||||
$retryAfter = max(1, $this->limiter->availableIn($rateLimitKey));
|
||||
$error = apiErrorDetails(500, 'Request limit reached');
|
||||
|
||||
return response()->json([
|
||||
'error' => 'API rate limit exceeded.',
|
||||
'error' => $error['message'],
|
||||
'retry_after' => $retryAfter,
|
||||
], 429, [
|
||||
], $error['status'], [
|
||||
'Retry-After' => (string) $retryAfter,
|
||||
'X-RateLimit-Limit' => (string) $maxAttempts,
|
||||
'X-RateLimit-Remaining' => '0',
|
||||
'X-NNTmux' => 'API ERROR ['.$error['code'].'] '.$error['message'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user