diff --git a/.env.production.example b/.env.production.example index 57c84d36..78b144d2 100644 --- a/.env.production.example +++ b/.env.production.example @@ -14,6 +14,15 @@ APP_LOCALE=en APP_FALLBACK_LOCALE=en APP_FAKER_LOCALE=en_US +# Trusted Proxies +# Comma-separated proxy IPs/CIDRs Laravel trusts for X-Forwarded-* headers (no spaces). +# When unset the app trusts all proxies (*), which keeps small private self-hosted +# setups working behind any proxy out of the box. On a publicly exposed deployment set +# this to the actual proxy range: otherwise clients can spoof X-Forwarded-For to forge +# their IP (defeating per-IP rate limiting and poisoning logs). The private ranges below +# cover the Docker network Coolify/Traefik connects from. +TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 + # Logging LOG_CHANNEL=stack LOG_STACK=single,sentry_logs diff --git a/app/Http/Controllers/Api/TransactionController.php b/app/Http/Controllers/Api/TransactionController.php index 9e598876..0c1723ae 100644 --- a/app/Http/Controllers/Api/TransactionController.php +++ b/app/Http/Controllers/Api/TransactionController.php @@ -112,11 +112,14 @@ class TransactionController extends Controller abort(403, 'Some transactions do not belong to the authenticated user.'); } + $userId = $request->user()->id; + foreach ($validated['transactions'] as $data) { $updateData = collect($data)->except('id')->toArray(); Transaction::query() ->where('id', $data['id']) + ->where('user_id', $userId) ->toBase() ->update($updateData); } diff --git a/app/Http/Controllers/OpenBanking/AuthorizationController.php b/app/Http/Controllers/OpenBanking/AuthorizationController.php index 5d715d8d..be103445 100644 --- a/app/Http/Controllers/OpenBanking/AuthorizationController.php +++ b/app/Http/Controllers/OpenBanking/AuthorizationController.php @@ -210,6 +210,10 @@ class AuthorizationController extends Controller } catch (\Throwable $e) { Log::error('EnableBanking session creation failed', ['error' => $e->getMessage()]); + if ($connection) { + $connection->update(['state_token' => null]); + } + return $this->finishRedirect($errorRedirectRoute, $errorRedirectParams, 'error', 'Failed to connect to your bank. Please try again.'); } diff --git a/bootstrap/app.php b/bootstrap/app.php index c04f75df..9a402ed2 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -30,8 +30,10 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->encryptCookies(except: ['appearance', 'sidebar_state', 'chart-color-scheme']); + $trustedProxies = env('TRUSTED_PROXIES'); + $middleware->trustProxies( - at: '*', + at: is_string($trustedProxies) ? array_map('trim', explode(',', $trustedProxies)) : '*', headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT diff --git a/resources/js/hooks/use-decrypt-transactions.ts b/resources/js/hooks/use-decrypt-transactions.ts index 05fe3b79..c6b5809c 100644 --- a/resources/js/hooks/use-decrypt-transactions.ts +++ b/resources/js/hooks/use-decrypt-transactions.ts @@ -6,6 +6,28 @@ import { usePage } from '@inertiajs/react'; import axios from 'axios'; import { useEffect, useRef } from 'react'; +// on 429 wait out the API throttle and retry the same request, +// instead of aborting the whole migration. Honours Laravel's Retry-After header. +async function withRetry(fn: () => Promise): Promise { + while (true) { + try { + return await fn(); + } catch (e) { + if (axios.isAxiosError(e) && e.response?.status === 429) { + const retryAfter = + Number(e.response.headers['retry-after']) || 1; + await new Promise((resolve) => + setTimeout(resolve, retryAfter * 1000), + ); + + continue; + } + + throw e; + } + } +} + interface EncryptedTransaction { id: string; description: string; @@ -55,8 +77,10 @@ export function useDecryptTransactions() { // when a page yields nothing we can decrypt — otherwise rows // that always fail to decrypt would loop forever. while (true) { - const { data: page } = await axios.get( - '/api/transactions?encrypted=true', + const { data: page } = await withRetry(() => + axios.get( + '/api/transactions?encrypted=true', + ), ); if (page.data.length === 0) { @@ -102,9 +126,11 @@ export function useDecryptTransactions() { // Send in chunks of 50 for (let i = 0; i < batch.length; i += 50) { const chunk = batch.slice(i, i + 50); - await axios.patch('/api/transactions/bulk', { - transactions: chunk, - }); + await withRetry(() => + axios.patch('/api/transactions/bulk', { + transactions: chunk, + }), + ); } } diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 3e0de6ce..025d9edb 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -10,7 +10,7 @@ {{-- Inline script to detect system dark mode preference and apply it immediately --}}