fix: prevent sensitive field leaks in /me, add retry logic to Brent price sources
- Made `/api/auth/me` public and return explicit allowlist (name, email, two_factor_confirmed_at, tier, subscription fields) instead of spreading `$user->toArray()` which leaked is_admin, stripe_id, pm_type, pm_last_four, postcode. Returns `null` when unauthenticated rather than 401. - Moved `/auth/logout` to remain behind auth:sanctum gate. - Added 3×200ms retry with exponential backoff to EiaBrentPriceSource and FredBrentPriceSource on ConnectionException or 5xx responses. Timeout raised from 10s to 30s. - Both sources now throw typed BrentPriceFetchException on exhausted retries instead of silently returning null + logging. Updated tests to assert exception message includes HTTP status or "connection failed".
This commit is contained in:
@@ -64,19 +64,24 @@ class AuthController extends Controller
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user === null) {
|
||||
return new JsonResponse('null', json: true);
|
||||
}
|
||||
|
||||
$subscription = $user->subscription();
|
||||
|
||||
$expiresAt = $subscription?->ends_at ?? $subscription?->current_period_end;
|
||||
|
||||
return response()->json(array_merge(
|
||||
$user->toArray(),
|
||||
[
|
||||
'tier' => PlanFeatures::for($user)->tier(),
|
||||
'subscription_cancelled' => $subscription?->canceled() ?? false,
|
||||
'subscription_cadence' => Plan::resolveCadenceForUser($user),
|
||||
'subscribed_at' => $subscription?->created_at?->toIso8601String(),
|
||||
'subscription_expires_at' => $expiresAt?->toIso8601String(),
|
||||
],
|
||||
));
|
||||
return response()->json([
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'two_factor_confirmed_at' => $user->two_factor_confirmed_at?->toIso8601String(),
|
||||
'tier' => PlanFeatures::for($user)->tier(),
|
||||
'subscription_cancelled' => $subscription?->canceled() ?? false,
|
||||
'subscription_cadence' => Plan::resolveCadenceForUser($user),
|
||||
'subscribed_at' => $subscription?->created_at?->toIso8601String(),
|
||||
'subscription_expires_at' => $expiresAt?->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace App\Services\BrentPriceSources;
|
||||
|
||||
use App\Services\ApiLogger;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
final class EiaBrentPriceSource
|
||||
@@ -14,12 +15,16 @@ final class EiaBrentPriceSource
|
||||
public function __construct(private readonly ApiLogger $apiLogger) {}
|
||||
|
||||
/**
|
||||
* @return array{date: string, price_usd: float}[]|null
|
||||
* @return array{date: string, price_usd: float}[]|null null only when the response carried no usable rows
|
||||
*
|
||||
* @throws BrentPriceFetchException on network failure or non-2xx response after retries
|
||||
*/
|
||||
public function fetch(): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->apiLogger->send('eia', 'GET', self::URL, fn () => Http::timeout(10)
|
||||
$response = $this->apiLogger->send('eia', 'GET', self::URL, fn () => Http::timeout(30)
|
||||
->retry(3, 200, fn (Throwable $e) => $this->shouldRetry($e))
|
||||
->throw()
|
||||
->get(self::URL, [
|
||||
'api_key' => config('services.eia.api_key'),
|
||||
'frequency' => 'daily',
|
||||
@@ -29,32 +34,26 @@ final class EiaBrentPriceSource
|
||||
'sort[0][direction]' => 'desc',
|
||||
'length' => 30,
|
||||
]));
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('EiaBrentPriceSource: request failed', ['status' => $response->status()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$rows = collect($response->json('response.data') ?? [])
|
||||
->filter(fn (array $row) => ($row['value'] ?? '.') !== '.')
|
||||
->map(fn (array $row) => [
|
||||
'date' => $row['period'],
|
||||
'price_usd' => (float) $row['value'],
|
||||
])
|
||||
->all();
|
||||
|
||||
if ($rows === []) {
|
||||
Log::warning('EiaBrentPriceSource: no valid observations returned');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
} catch (Throwable $e) {
|
||||
Log::error('EiaBrentPriceSource: fetch failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
} catch (ConnectionException $e) {
|
||||
throw new BrentPriceFetchException("EIA connection failed: {$e->getMessage()}", previous: $e);
|
||||
} catch (RequestException $e) {
|
||||
throw new BrentPriceFetchException("EIA returned HTTP {$e->response->status()}", previous: $e);
|
||||
}
|
||||
|
||||
$rows = collect($response->json('response.data') ?? [])
|
||||
->filter(fn (array $row) => ($row['value'] ?? '.') !== '.')
|
||||
->map(fn (array $row) => [
|
||||
'date' => $row['period'],
|
||||
'price_usd' => (float) $row['value'],
|
||||
])
|
||||
->all();
|
||||
|
||||
return $rows === [] ? null : $rows;
|
||||
}
|
||||
|
||||
private function shouldRetry(Throwable $e): bool
|
||||
{
|
||||
return $e instanceof ConnectionException
|
||||
|| ($e instanceof RequestException && $e->response->serverError());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace App\Services\BrentPriceSources;
|
||||
|
||||
use App\Services\ApiLogger;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
final class FredBrentPriceSource
|
||||
@@ -14,12 +15,16 @@ final class FredBrentPriceSource
|
||||
public function __construct(private readonly ApiLogger $apiLogger) {}
|
||||
|
||||
/**
|
||||
* @return array{date: string, price_usd: float}[]|null
|
||||
* @return array{date: string, price_usd: float}[]|null null only when the response carried no usable rows
|
||||
*
|
||||
* @throws BrentPriceFetchException on network failure or non-2xx response after retries
|
||||
*/
|
||||
public function fetch(): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->apiLogger->send('fred', 'GET', self::URL, fn () => Http::timeout(10)
|
||||
$response = $this->apiLogger->send('fred', 'GET', self::URL, fn () => Http::timeout(30)
|
||||
->retry(3, 200, fn (Throwable $e) => $this->shouldRetry($e))
|
||||
->throw()
|
||||
->get(self::URL, [
|
||||
'series_id' => 'DCOILBRENTEU',
|
||||
'api_key' => config('services.fred.api_key'),
|
||||
@@ -27,32 +32,26 @@ final class FredBrentPriceSource
|
||||
'limit' => 30,
|
||||
'file_type' => 'json',
|
||||
]));
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('FredBrentPriceSource: request failed', ['status' => $response->status()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$rows = collect($response->json('observations') ?? [])
|
||||
->filter(fn (array $obs) => $obs['value'] !== '.')
|
||||
->map(fn (array $obs) => [
|
||||
'date' => $obs['date'],
|
||||
'price_usd' => (float) $obs['value'],
|
||||
])
|
||||
->all();
|
||||
|
||||
if ($rows === []) {
|
||||
Log::warning('FredBrentPriceSource: no valid observations returned');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
} catch (Throwable $e) {
|
||||
Log::error('FredBrentPriceSource: fetch failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
} catch (ConnectionException $e) {
|
||||
throw new BrentPriceFetchException("FRED connection failed: {$e->getMessage()}", previous: $e);
|
||||
} catch (RequestException $e) {
|
||||
throw new BrentPriceFetchException("FRED returned HTTP {$e->response->status()}", previous: $e);
|
||||
}
|
||||
|
||||
$rows = collect($response->json('observations') ?? [])
|
||||
->filter(fn (array $obs) => $obs['value'] !== '.')
|
||||
->map(fn (array $obs) => [
|
||||
'date' => $obs['date'],
|
||||
'price_usd' => (float) $obs['value'],
|
||||
])
|
||||
->all();
|
||||
|
||||
return $rows === [] ? null : $rows;
|
||||
}
|
||||
|
||||
private function shouldRetry(Throwable $e): bool
|
||||
{
|
||||
return $e instanceof ConnectionException
|
||||
|| ($e instanceof RequestException && $e->response->serverError());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user