Skip to content

Commit f3a707e

Browse files
committed
Make API responses uniform
1 parent 3987655 commit f3a707e

17 files changed

Lines changed: 330 additions & 110 deletions

app/Extensions/helper/helpers.php

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ function countryCode(string $country): string
755755
function unzipGzipFile(string $filePath): false|string
756756
{
757757
$string = '';
758-
$gzFile = @gzopen($filePath, 'rb', false);
758+
$gzFile = @gzopen($filePath, 'rb', 0);
759759
if ($gzFile) {
760760
while (! gzeof($gzFile)) {
761761
$temp = gzread($gzFile, 1024);
@@ -914,42 +914,68 @@ function imdb_trailers(mixed $imdbID): string
914914
}
915915
}
916916

917+
if (! function_exists('apiErrorDetails')) {
918+
/**
919+
* @return array{code:int,message:string,status:int,header:string}
920+
*/
921+
function apiErrorDetails(int $errorCode = 900, string $errorText = ''): array
922+
{
923+
[$defaultText, $status, $errorHeader] = match ($errorCode) {
924+
100 => ['Incorrect user credentials', 401, 'HTTP/1.1 401 Unauthorized'],
925+
101 => ['Account suspended', 403, 'HTTP/1.1 403 Forbidden'],
926+
102 => ['Insufficient privileges/not authorized', 401, 'HTTP/1.1 401 Unauthorized'],
927+
103 => ['Registration denied', 403, 'HTTP/1.1 403 Forbidden'],
928+
104 => ['Registrations are closed', 403, 'HTTP/1.1 403 Forbidden'],
929+
105 => ['Invalid registration (Email Address Taken)', 403, 'HTTP/1.1 403 Forbidden'],
930+
106 => ['Invalid registration (Email Address Bad Format)', 403, 'HTTP/1.1 403 Forbidden'],
931+
107 => ['Registration Failed (Data error)', 400, 'HTTP/1.1 400 Bad Request'],
932+
200 => ['Missing parameter', 400, 'HTTP/1.1 400 Bad Request'],
933+
201 => ['Incorrect parameter', 400, 'HTTP/1.1 400 Bad Request'],
934+
202 => ['No such function', 404, 'HTTP/1.1 404 Not Found'],
935+
203 => ['Function not available', 400, 'HTTP/1.1 400 Bad Request'],
936+
300 => ['No such item', 404, 'HTTP/1.1 404 Not Found'],
937+
310 => ['Item already exists', 409, 'HTTP/1.1 409 Conflict'],
938+
500 => ['Request limit reached', 429, 'HTTP/1.1 429 Too Many Requests'],
939+
501 => ['Download limit reached', 429, 'HTTP/1.1 429 Too Many Requests'],
940+
600 => ['Failed to load NZB', 400, 'HTTP/1.1 400 Bad Request'],
941+
601 => ['NZB is duplicate', 409, 'HTTP/1.1 409 Conflict'],
942+
602 => ['NZB is for a non-existent group', 400, 'HTTP/1.1 400 Bad Request'],
943+
603 => ['NZB failed to write to disk', 500, 'HTTP/1.1 500 Internal Server Error'],
944+
910 => ['API disabled', 401, 'HTTP/1.1 401 Unauthorized'],
945+
default => ['Unknown error', 400, 'HTTP/1.1 400 Bad Request'],
946+
};
947+
948+
return [
949+
'code' => $errorCode,
950+
'message' => $errorText !== '' ? $errorText : $defaultText,
951+
'status' => $status,
952+
'header' => $errorHeader,
953+
];
954+
}
955+
}
956+
957+
if (! function_exists('apiJsonError')) {
958+
function apiJsonError(int $errorCode = 900, string $errorText = ''): mixed
959+
{
960+
$error = apiErrorDetails($errorCode, $errorText);
961+
962+
return response()
963+
->json(['error' => $error['message']], $error['status'])
964+
->header('X-NNTmux', 'API ERROR ['.$error['code'].'] '.$error['message']);
965+
}
966+
}
967+
917968
if (! function_exists('showApiError')) {
918969
function showApiError(int $errorCode = 900, string $errorText = ''): mixed
919970
{
920-
$errorHeader = 'HTTP 1.1 400 Bad Request';
921-
if ($errorText === '') {
922-
[$errorText, $errorHeader] = match ($errorCode) {
923-
100 => ['Incorrect user credentials', 'HTTP 1.1 401 Unauthorized'],
924-
101 => ['Account suspended', 'HTTP 1.1 403 Forbidden'],
925-
102 => ['Insufficient privileges/not authorized', 'HTTP 1.1 401 Unauthorized'],
926-
103 => ['Registration denied', 'HTTP 1.1 403 Forbidden'],
927-
104 => ['Registrations are closed', 'HTTP 1.1 403 Forbidden'],
928-
105 => ['Invalid registration (Email Address Taken)', 'HTTP 1.1 403 Forbidden'],
929-
106 => ['Invalid registration (Email Address Bad Format)', 'HTTP 1.1 403 Forbidden'],
930-
107 => ['Registration Failed (Data error)', 'HTTP 1.1 400 Bad Request'],
931-
200 => ['Missing parameter', 'HTTP 1.1 400 Bad Request'],
932-
201 => ['Incorrect parameter', 'HTTP 1.1 400 Bad Request'],
933-
202 => ['No such function', 'HTTP 1.1 404 Not Found'],
934-
203 => ['Function not available', 'HTTP 1.1 400 Bad Request'],
935-
300 => ['No such item', 'HTTP 1.1 404 Not Found'],
936-
310 => ['Item already exists', 'HTTP 1.1 409 Conflict'],
937-
500 => ['Request limit reached', 'HTTP 1.1 429 Too Many Requests'],
938-
501 => ['Download limit reached', 'HTTP 1.1 429 Too Many Requests'],
939-
600 => ['Failed to load NZB', 'HTTP 1.1 400 Bad Request'],
940-
601 => ['NZB is duplicate', 'HTTP 1.1 409 Conflict'],
941-
602 => ['NZB is for a non-existent group', 'HTTP 1.1 400 Bad Request'],
942-
603 => ['NZB failed to write to disk', 'HTTP 1.1 500 Internal Server Error'],
943-
910 => ['API disabled', 'HTTP 1.1 401 Unauthorized'],
944-
default => ['Unknown error', 'HTTP 1.1 400 Bad Request'],
945-
};
946-
}
971+
$error = apiErrorDetails($errorCode, $errorText);
972+
$errorText = $error['message'];
947973

948974
$response =
949975
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".
950976
'<error code="'.$errorCode.'" description="'.$errorText."\"/>\n";
951977

952-
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);
978+
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']);
953979
}
954980
}
955981

app/Http/Controllers/Api/ApiController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public function api(Request $request)
137137
return showApiError(100, 'Incorrect user credentials (wrong API key)');
138138
}
139139

140-
if ($res->hasRole('Disabled')) {
140+
if ($res->is_disabled || $res->hasRole('Disabled')) {
141141
return showApiError(101);
142142
}
143143

app/Http/Controllers/Api/ApiInformController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ public function release(Request $request): JsonResponse
2727
$apiToken = $request->has('api_token') && ! empty($request->input('api_token')) ? $request->input('api_token') : '';
2828
$user = User::findVerifiedByApiToken((string) $request->input('api_token'));
2929
if (! $user) {
30-
return response()->json(['message' => 'Indexer inform error, wrong api key!'], 404);
30+
return apiJsonError(100);
31+
}
32+
33+
if ($user->is_disabled || $user->hasRole('Disabled')) {
34+
return apiJsonError(101);
3135
}
3236

3337
if (! empty($releaseObName) && ! empty($releasePrName) && ! empty($apiToken)) {

app/Http/Controllers/Api/ApiV2Controller.php

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,35 @@ public function __construct(
5050
}
5151

5252
/**
53-
* Validate API token and return cached user, or null on failure.
53+
* Validate API token and return cached user, or a normalized JSON API error on failure.
5454
* Caches user lookup for 5 minutes to reduce DB hits.
5555
*/
56-
private function resolveUser(Request $request): ?User
56+
private function resolveUser(Request $request): User|JsonResponse
5757
{
5858
if ($request->missing('api_token') || $request->isNotFilled('api_token')) {
59-
return null;
59+
return apiJsonError(200, 'Missing parameter (api_token)');
6060
}
6161

6262
$apiToken = $request->input('api_token');
6363
$userCacheKey = 'api_user:'.md5((string) $apiToken);
6464

65-
return Cache::remember($userCacheKey, 300, function () use ($apiToken) {
66-
return User::verifiedApiTokenQuery((string) $apiToken)
67-
->with('role')
65+
$user = Cache::remember($userCacheKey, 300, function () use ($apiToken) {
66+
return User::query()
67+
->whereApiToken((string) $apiToken)
6868
->first();
6969
});
70+
71+
if (! $user || ! $user->hasVerifiedEmail()) {
72+
return apiJsonError(100);
73+
}
74+
75+
if ($user->is_disabled || $user->hasRole('Disabled')) {
76+
return apiJsonError(101);
77+
}
78+
79+
$user->loadMissing('role');
80+
81+
return $user;
7082
}
7183

7284
/**
@@ -189,8 +201,8 @@ public function capabilities(): JsonResponse
189201
public function movie(Request $request): JsonResponse
190202
{
191203
$user = $this->resolveUser($request);
192-
if (! $user) {
193-
return response()->json(['error' => 'Missing or invalid API key'], 403);
204+
if ($user instanceof JsonResponse) {
205+
return $user;
194206
}
195207

196208
UserRequest::addApiRequest($user->id, $request->getRequestUri());
@@ -256,8 +268,8 @@ public function movie(Request $request): JsonResponse
256268
public function audio(Request $request): JsonResponse|Response
257269
{
258270
$user = $this->resolveUser($request);
259-
if (! $user) {
260-
return response()->json(['error' => 'Missing or invalid API key'], 403);
271+
if ($user instanceof JsonResponse) {
272+
return $user;
261273
}
262274

263275
UserRequest::addApiRequest($user->id, $request->getRequestUri());
@@ -308,8 +320,8 @@ public function audio(Request $request): JsonResponse|Response
308320
public function books(Request $request): JsonResponse|Response
309321
{
310322
$user = $this->resolveUser($request);
311-
if (! $user) {
312-
return response()->json(['error' => 'Missing or invalid API key'], 403);
323+
if ($user instanceof JsonResponse) {
324+
return $user;
313325
}
314326

315327
UserRequest::addApiRequest($user->id, $request->getRequestUri());
@@ -360,8 +372,8 @@ public function books(Request $request): JsonResponse|Response
360372
public function anime(Request $request): JsonResponse|Response
361373
{
362374
$user = $this->resolveUser($request);
363-
if (! $user) {
364-
return response()->json(['error' => 'Missing or invalid API key'], 403);
375+
if ($user instanceof JsonResponse) {
376+
return $user;
365377
}
366378

367379
UserRequest::addApiRequest($user->id, $request->getRequestUri());
@@ -416,8 +428,8 @@ public function anime(Request $request): JsonResponse|Response
416428
public function apiSearch(Request $request): JsonResponse
417429
{
418430
$user = $this->resolveUser($request);
419-
if (! $user) {
420-
return response()->json(['error' => 'Missing or invalid API key'], 403);
431+
if ($user instanceof JsonResponse) {
432+
return $user;
421433
}
422434

423435
UserRequest::addApiRequest($user->id, $request->getRequestUri());
@@ -483,8 +495,8 @@ public function apiSearch(Request $request): JsonResponse
483495
public function tv(Request $request): JsonResponse
484496
{
485497
$user = $this->resolveUser($request);
486-
if (! $user) {
487-
return response()->json(['error' => 'Missing or invalid API key'], 403);
498+
if ($user instanceof JsonResponse) {
499+
return $user;
488500
}
489501

490502
$catExclusions = User::getCategoryExclusionById($user->id);
@@ -559,8 +571,8 @@ public function tv(Request $request): JsonResponse
559571
public function getNzb(Request $request): Application|ResponseFactory|JsonResponse|Redirector|RedirectResponse
560572
{
561573
$user = $this->resolveUser($request);
562-
if (! $user) {
563-
return response()->json(['error' => 'Missing or invalid API key'], 403);
574+
if ($user instanceof JsonResponse) {
575+
return $user;
564576
}
565577

566578
event(new UserAccessedApi($user, $request->ip()));
@@ -576,8 +588,8 @@ public function getNzb(Request $request): Application|ResponseFactory|JsonRespon
576588
public function details(Request $request): JsonResponse
577589
{
578590
$user = $this->resolveUser($request);
579-
if (! $user) {
580-
return response()->json(['error' => 'Missing or invalid API key'], 403);
591+
if ($user instanceof JsonResponse) {
592+
return $user;
581593
}
582594
if ($request->missing('id')) {
583595
return response()->json(['error' => 'Missing parameter (guid is required for single release details)'], 400);

app/Http/Controllers/BasePageController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
class BasePageController extends Controller
1818
{
1919
/**
20-
* @var Collection<int, mixed>
20+
* @var \Illuminate\Support\Collection<int, mixed>
2121
*/
2222
public \Illuminate\Support\Collection $settings; // @phpstan-ignore property.phpDocType, class.notFound, missingType.generics
2323

@@ -81,6 +81,10 @@ public function __construct()
8181
if (Auth::check()) {
8282
$userId = Auth::id();
8383
$this->userdata = User::find($userId);
84+
if (! $this->userdata?->hasVerifiedEmail()) {
85+
return $next($request);
86+
}
87+
8488
// Cache category exclusions per user (5 minutes)
8589
$this->userdata->categoryexclusions = $this->rememberWithCacheFallback(
8690
'user_category_exclusions_'.$userId,

app/Http/Controllers/GetNzbController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ private function authenticateUser(Request $request): array|Response
9898
*/
9999
private function getUserDataFromSession(): array|Response
100100
{
101-
if ($this->userdata->hasRole('Disabled')) {
101+
if (! $this->userdata->hasVerifiedEmail()) {
102+
return showApiError(100);
103+
}
104+
105+
if ($this->userdata->is_disabled || $this->userdata->hasRole('Disabled')) {
102106
return showApiError(101);
103107
}
104108

@@ -126,7 +130,7 @@ private function getUserDataFromRssToken(Request $request): array|Response
126130
return showApiError(100);
127131
}
128132

129-
if ($user->hasRole('Disabled')) {
133+
if ($user->is_disabled || $user->hasRole('Disabled')) {
130134
return showApiError(101);
131135
}
132136

app/Http/Controllers/RssController.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,13 @@ private function parseCommonRssParams(Request $request): array
214214
private function userCheck(Request $request): JsonResponse|array
215215
{
216216
if ($request->missing('api_token')) {
217-
return response()->json(['error' => 'API key is required for viewing the RSS!'], 403);
217+
return apiJsonError(200, 'Missing parameter (api_token)');
218218
}
219219

220220
$res = User::findVerifiedByApiToken((string) $request->input('api_token'));
221221

222222
if ($res === null) {
223-
return response()->json(['error' => 'Invalid RSS token'], 403);
223+
return apiJsonError(100);
224224
}
225225

226226
$uid = $res['id'];
@@ -233,12 +233,12 @@ private function userCheck(Request $request): JsonResponse|array
233233
$grabTime = UserDownload::whereUsersId($uid)->min('timestamp');
234234
$oldestGrabTime = $grabTime !== null ? Carbon::createFromTimeString($grabTime)->toRfc2822String() : '';
235235

236-
if ($res->hasRole('Disabled')) {
237-
return response()->json(['error' => 'Your account is disabled'], 403);
236+
if ($res->is_disabled || $res->hasRole('Disabled')) {
237+
return apiJsonError(101);
238238
}
239239

240240
if ($usedRequests > $maxRequests) {
241-
return response()->json(['error' => 'You have reached your daily limit for API requests!'], 403);
241+
return apiJsonError(500, 'Request limit reached ('.$usedRequests.'/'.$maxRequests.')');
242242
}
243243

244244
UserRequest::addApiRequest($rssToken, $request->getRequestUri());

app/Http/Middleware/ThrottleApiRequestsByToken.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App\Http\Middleware;
66

7+
use App\Enums\UserRole;
78
use App\Models\User;
89
use Closure;
910
use Illuminate\Cache\RateLimiter;
@@ -53,17 +54,23 @@ public function handle(Request $request, Closure $next): Response
5354

5455
private function resolveUser(Request $request): ?User
5556
{
56-
$apiToken = $request->input('api_token');
57+
$apiToken = $request->input('api_token') ?? $request->input('apikey');
5758

5859
if (! is_string($apiToken) || $apiToken === '') {
5960
return null;
6061
}
6162

62-
return Cache::remember('api_rate_limit_user:'.md5($apiToken), 300, static function () use ($apiToken) {
63+
$user = Cache::remember('api_rate_limit_user:'.md5($apiToken), 300, static function () use ($apiToken) {
6364
return User::verifiedApiTokenQuery($apiToken)
64-
->select(['id', 'api_token', 'rate_limit'])
65+
->select(['id', 'roles_id', 'api_token', 'rate_limit'])
6566
->first();
6667
});
68+
69+
if ($user?->roles_id === UserRole::DISABLED->value) {
70+
return null;
71+
}
72+
73+
return $user;
6774
}
6875

6976
private function rateLimitKey(int $userId): string
@@ -74,14 +81,16 @@ private function rateLimitKey(int $userId): string
7481
private function buildTooManyRequestsResponse(string $rateLimitKey, int $maxAttempts): JsonResponse
7582
{
7683
$retryAfter = max(1, $this->limiter->availableIn($rateLimitKey));
84+
$error = apiErrorDetails(500, 'Request limit reached');
7785

7886
return response()->json([
79-
'error' => 'API rate limit exceeded.',
87+
'error' => $error['message'],
8088
'retry_after' => $retryAfter,
81-
], 429, [
89+
], $error['status'], [
8290
'Retry-After' => (string) $retryAfter,
8391
'X-RateLimit-Limit' => (string) $maxAttempts,
8492
'X-RateLimit-Remaining' => '0',
93+
'X-NNTmux' => 'API ERROR ['.$error['code'].'] '.$error['message'],
8594
]);
8695
}
8796
}

0 commit comments

Comments
 (0)