diff --git a/app/Http/Middleware/CheckForcePasswordReset.php b/app/Http/Middleware/CheckForcePasswordReset.php index 78b1f896c..c857cb836 100644 --- a/app/Http/Middleware/CheckForcePasswordReset.php +++ b/app/Http/Middleware/CheckForcePasswordReset.php @@ -25,7 +25,7 @@ public function handle(Request $request, Closure $next): Response } $force_password_reset = auth()->user()->force_password_reset; if ($force_password_reset) { - if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') { + if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'two-factor-challenge' || $request->path() === 'livewire/update' || $request->path() === 'logout') { return $next($request); } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 4b84fb7f6..709af854a 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -77,6 +77,7 @@ function allowedPathsForUnsubscribedAccounts() 'login', 'logout', 'force-password-reset', + 'two-factor-challenge', 'livewire/update', 'admin', ]; @@ -95,6 +96,7 @@ function allowedPathsForInvalidAccounts() 'logout', 'verify', 'force-password-reset', + 'two-factor-challenge', 'livewire/update', ]; } diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index a329664a2..cf72dfbe9 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -25,7 +25,7 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov {{-- Eye-off icon (shown when password is visible) --}} - diff --git a/resources/views/errors/419.blade.php b/resources/views/errors/419.blade.php index e7cd3fc45..8569f4e22 100644 --- a/resources/views/errors/419.blade.php +++ b/resources/views/errors/419.blade.php @@ -3,15 +3,11 @@

419

This page is definitely old, not like you!

-

Sorry, we couldn't find the page you're looking - for. +

Your session has expired. Please log in again to continue.

- - Go back - - - Dashboard + + Back to Login Contact support diff --git a/templates/compose/beszel-agent.yaml b/templates/compose/beszel-agent.yaml index a318f4702..5d0b4fecc 100644 --- a/templates/compose/beszel-agent.yaml +++ b/templates/compose/beszel-agent.yaml @@ -6,13 +6,26 @@ services: beszel-agent: - image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025 + image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026 + network_mode: host # Network stats graphs won't work if agent cannot access host system network stack environment: + # Required - LISTEN=/beszel_socket/beszel.sock - - HUB_URL=${HUB_URL?} - - 'TOKEN=${TOKEN?}' - - 'KEY=${KEY?}' + - HUB_URL=$SERVICE_URL_BESZEL + - TOKEN=${TOKEN} # From hub token settings + - KEY=${KEY} # SSH public key(s) from hub + # Optional + - DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH + - LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level + - SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring + - SYSTEM_NAME=${SYSTEM_NAME} # Custom system name volumes: - beszel_agent_data:/var/lib/beszel-agent - beszel_socket:/beszel_socket - '/var/run/docker.sock:/var/run/docker.sock:ro' + healthcheck: + test: ['CMD', '/agent', 'health'] + interval: 60s + timeout: 20s + retries: 10 + start_period: 5s \ No newline at end of file diff --git a/templates/compose/beszel.yaml b/templates/compose/beszel.yaml index cba11e4bb..bc68c1825 100644 --- a/templates/compose/beszel.yaml +++ b/templates/compose/beszel.yaml @@ -9,21 +9,41 @@ # Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI) services: beszel: - image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025 + image: 'henrygd/beszel:0.18.4' # Released on 21 Feb 2026 environment: - SERVICE_URL_BESZEL_8090 + - CONTAINER_DETAILS=${CONTAINER_DETAILS:-true} + - SHARE_ALL_SYSTEMS=${SHARE_ALL_SYSTEMS:-false} volumes: - 'beszel_data:/beszel_data' - 'beszel_socket:/beszel_socket' + healthcheck: + test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090'] + interval: 30s + timeout: 20s + retries: 10 + start_period: 5s beszel-agent: - image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025 + image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026 + network_mode: host # Network stats graphs won't work if agent cannot access host system network stack environment: + # Required - LISTEN=/beszel_socket/beszel.sock - - HUB_URL=http://beszel:8090 - - 'TOKEN=${TOKEN}' - - 'KEY=${KEY}' + - HUB_URL=$SERVICE_URL_BESZEL + - TOKEN=${TOKEN} # From hub token settings + - KEY=${KEY} # SSH public key(s) from hub + # Optional + - DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH + - LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level + - SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring + - SYSTEM_NAME=${SYSTEM_NAME} # Custom system name volumes: - beszel_agent_data:/var/lib/beszel-agent - beszel_socket:/beszel_socket - '/var/run/docker.sock:/var/run/docker.sock:ro' - + healthcheck: + test: ['CMD', '/agent', 'health'] + interval: 60s + timeout: 20s + retries: 10 + start_period: 5s \ No newline at end of file diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml index bc2fbd637..346b0c664 100644 --- a/templates/compose/plane.yaml +++ b/templates/compose/plane.yaml @@ -1,3 +1,4 @@ +# ignore: true # documentation: https://docs.plane.so/self-hosting/methods/docker-compose # slogan: The open source project management tool # category: productivity diff --git a/templates/compose/pterodactyl-panel.yaml b/templates/compose/pterodactyl-panel.yaml index 9a3f6c779..c86d9d468 100644 --- a/templates/compose/pterodactyl-panel.yaml +++ b/templates/compose/pterodactyl-panel.yaml @@ -1,3 +1,4 @@ +# ignore: true # documentation: https://pterodactyl.io/ # slogan: Pterodactyl is a free, open-source game server management panel # category: media @@ -102,4 +103,4 @@ services: - MAIL_PORT=$MAIL_PORT - MAIL_USERNAME=$MAIL_USERNAME - MAIL_PASSWORD=$MAIL_PASSWORD - - MAIL_ENCRYPTION=$MAIL_ENCRYPTION + - MAIL_ENCRYPTION=$MAIL_ENCRYPTION \ No newline at end of file diff --git a/templates/compose/pterodactyl-with-wings.yaml b/templates/compose/pterodactyl-with-wings.yaml index 6e1e3614c..20465a139 100644 --- a/templates/compose/pterodactyl-with-wings.yaml +++ b/templates/compose/pterodactyl-with-wings.yaml @@ -1,3 +1,4 @@ +# ignore: true # documentation: https://pterodactyl.io/ # slogan: Pterodactyl is a free, open-source game server management panel # category: media diff --git a/tests/Feature/TwoFactorChallengeAccessTest.php b/tests/Feature/TwoFactorChallengeAccessTest.php new file mode 100644 index 000000000..2bd58d197 --- /dev/null +++ b/tests/Feature/TwoFactorChallengeAccessTest.php @@ -0,0 +1,65 @@ +user = User::factory()->create(); + $this->team = Team::factory()->personal()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); +}); + +it('allows unauthenticated access to two-factor-challenge page', function () { + $response = $this->get('/two-factor-challenge'); + + // Fortify returns a redirect to /login if there's no login.id in session, + // but the important thing is it does NOT return a 419 or 500 + expect($response->status())->toBeIn([200, 302]); +}); + +it('includes two-factor-challenge in allowed paths for unsubscribed accounts', function () { + $paths = allowedPathsForUnsubscribedAccounts(); + + expect($paths)->toContain('two-factor-challenge'); +}); + +it('includes two-factor-challenge in allowed paths for invalid accounts', function () { + $paths = allowedPathsForInvalidAccounts(); + + expect($paths)->toContain('two-factor-challenge'); +}); + +it('includes two-factor-challenge in allowed paths for boarding accounts', function () { + $paths = allowedPathsForBoardingAccounts(); + + expect($paths)->toContain('two-factor-challenge'); +}); + +it('does not redirect authenticated user with force_password_reset from two-factor-challenge', function () { + $this->user->update(['force_password_reset' => true]); + + $response = $this->actingAs($this->user)->get('/two-factor-challenge'); + + // Should NOT redirect to force-password-reset page + if ($response->isRedirect()) { + expect($response->headers->get('Location'))->not->toContain('force-password-reset'); + } +}); + +it('renders 419 error page with login link instead of previous url', function () { + $response = $this->get('/two-factor-challenge', [ + 'X-CSRF-TOKEN' => 'invalid-token', + ]); + + // The 419 page should exist and contain a link to /login + $view = view('errors.419')->render(); + + expect($view)->toContain('/login'); + expect($view)->toContain('Back to Login'); + expect($view)->toContain('This page is definitely old, not like you!'); + expect($view)->not->toContain('url()->previous()'); +});