Merge pull request #7439 from pixelfed/dev

resync staging
pull/7438/head
Shlee 21 hours ago committed by GitHub
commit 1b89472a50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -112,7 +112,9 @@ class AppServiceProvider extends ServiceProvider
Event::listen(Failed::class, LogFailedLogin::class);
Gate::define('viewPulse', function (User $user) {
return $user->is_admin === 1;
// is_admin is cast to bool on the User model, so a strict `=== 1`
// never matches. Use a boolean check, consistent with viewHorizon.
return (bool) $user->is_admin === true;
});
if (config('pulse.enabled', false)) {
@ -230,6 +232,7 @@ class AppServiceProvider extends ServiceProvider
public function register()
{
Passport::ignoreRoutes();
Pulse::ignoreRoutes();
$this->app->bind(UserOidcService::class, function () {
return UserOidcService::build();

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Services\Captcha\CaptchaManager;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
@ -11,7 +12,12 @@ class CaptchaServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton('captcha.manager', fn ($app): CaptchaManager => new CaptchaManager($app));
// Resolve the current container via Container::getInstance() rather than
// capturing the $app injected at boot time, so the manager never holds a
// stale container instance across requests under Octane.
$this->app->singleton('captcha.manager', function (): CaptchaManager {
return new CaptchaManager(Container::getInstance());
});
$this->app->alias('captcha.manager', CaptchaManager::class);
}

@ -99,9 +99,6 @@ class RestrictedNames
'mix-manifest.json',
'robots.txt',
// Laravel Horizon
'horizon',
// Reserved routes
'a',
'app',

@ -26,7 +26,7 @@ return [
|
*/
'path' => 'horizon',
'path' => env('HORIZON_PATH', 'admin/horizon'),
/*
|--------------------------------------------------------------------------

@ -30,7 +30,7 @@ return [
|
*/
'path' => env('PULSE_PATH', 'pulse'),
'path' => env('PULSE_PATH', 'admin/pulse'),
/*
|--------------------------------------------------------------------------

@ -146,12 +146,21 @@
</li>
<li class="nav-item">
<a class="nav-link" href="/horizon">
<a class="nav-link" href="/admin/horizon">
<i class="ni ni-bold-right text-primary"></i>
<span class="nav-link-text">Horizon</span>
</a>
</li>
@if(config('pulse.enabled'))
<li class="nav-item">
<a class="nav-link" href="/{{ config('pulse.path', 'admin/pulse') }}">
<i class="ni ni-bold-right text-primary"></i>
<span class="nav-link-text">Pulse</span>
</a>
</li>
@endif
<li class="nav-item">
<a class="nav-link {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">

@ -67,7 +67,10 @@
<a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a>
<a class="dropdown-item font-weight-bold {{request()->is('*profiles*')?'active':''}}" href="/i/admin/profiles">Profiles</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a>
<a class="dropdown-item font-weight-bold" href="/admin/horizon">Horizon</a>
@if(config('pulse.enabled'))
<a class="dropdown-item font-weight-bold" href="/{{ config('pulse.path', 'admin/pulse') }}">Pulse</a>
@endif
</div>
</li>
</ul>

@ -5,6 +5,21 @@ use App\Http\Controllers\AdminController;
use App\Http\Controllers\AdminCuratedRegisterController;
use App\Http\Controllers\AdminShadowFilterController;
use App\Http\Controllers\PageController;
use Illuminate\Contracts\View\Factory;
use Laravel\Horizon\Http\Controllers\HomeController as HorizonHomeController;
use Laravel\Pulse\Pulse;
// Laravel Pulse + Horizon dashboards, kept under `admin/*` so their routes can
// never collide with the `{username}` profile catch-all in routes/web.php
Route::domain(config('pixelfed.domain.app'))->middleware(['localization'])->group(function () {
Route::get(config('pulse.path', 'admin/pulse'), function (Pulse $pulse, Factory $view) {
return $view->make('pulse::dashboard');
})->middleware('pulse')->name('pulse');
Route::get(config('horizon.path'), [HorizonHomeController::class, 'index'])
->middleware('horizon')
->name('horizon.base');
});
Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->middleware(['localization'])->group(function () {
Route::redirect('/', '/dashboard');

@ -500,10 +500,6 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['localization'])->grou
Route::get('auth/invite/a/{code}', [AdminInviteController::class, 'index']);
Route::post('api/v1.1/auth/invite/admin/re', [AdminInviteController::class, 'apiRegister'])->middleware('throttle:5,1440');
// Laravel 13's Horizon no longer redirects the base path to its dashboard,
// so /horizon 404s by default. Redirect admins from /horizon to /horizon/dashboard.
Route::redirect('horizon', '/horizon/dashboard')->middleware('admin');
Route::redirect('groups/', '/groups/home');
Route::redirect('groups/home', '/groups/feed');

@ -0,0 +1,53 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| Horizon base-path dashboard
|--------------------------------------------------------------------------
|
| Horizon lives under `admin/horizon` (config('horizon.path')) so its routes can
| never collide with the `{username}` profile catch-all. Horizon's own optional
| catch-all `GET {view?}` serves the base path directly by rendering the dashboard
| SPA, so there is no separate base-path redirect. Access is gated by Horizon's
| own auth middleware + the `viewHorizon` gate (admins only).
|
*/
it('serves the Horizon dashboard at the base path for an admin', function () {
$path = config('horizon.path');
$admin = User::factory()->admin()->create();
$admin->refresh();
// The base path is handled by Horizon (not a redirect) and renders the SPA.
$this->actingAs($admin)
->get('/'.$path)
->assertOk()
->assertSee('Horizon', false);
});
it('serves the Horizon dashboard at the /dashboard subpath for an admin', function () {
$path = config('horizon.path');
$admin = User::factory()->admin()->create();
$admin->refresh();
$this->actingAs($admin)
->get('/'.$path.'/dashboard')
->assertOk();
});
it('does not allow a non-admin to access the Horizon dashboard', function () {
$user = User::factory()->create(['is_admin' => false]);
$user->refresh();
// Horizon's authorization gate (viewHorizon) forbids non-admins.
$this->actingAs($user)
->get('/'.config('horizon.path'))
->assertForbidden();
});

@ -1,57 +0,0 @@
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| /horizon base-path redirect
|--------------------------------------------------------------------------
|
| Laravel 13's Horizon no longer redirects its base path, so routes/web.php
| adds an admin-only redirect from /horizon to the dashboard. The destination
| must be ABSOLUTE (leading slash): a relative "horizon/dashboard" Location is
| resolved by the browser against a trailing-slash request path /horizon/ to
| /horizon/horizon/dashboard (a doubled path served as HTTP 200 by Horizon's
| SPA catch-all), silently missing the dashboard.
|
*/
it('redirects an admin from /horizon to an absolute /horizon/dashboard', function () {
$admin = User::factory()->admin()->create();
$admin->refresh();
$response = $this->actingAs($admin)->get('/horizon');
$response->assertRedirect('/horizon/dashboard');
// The Location header must be absolute so a trailing-slash request cannot
// resolve it into a doubled /horizon/horizon/dashboard path.
expect($response->headers->get('Location'))->toEndWith('/horizon/dashboard');
expect(parse_url($response->headers->get('Location'), PHP_URL_PATH))
->toBe('/horizon/dashboard');
});
it('redirects with an absolute Location for a trailing-slash /horizon/ request', function () {
$admin = User::factory()->admin()->create();
$admin->refresh();
$response = $this->actingAs($admin)->get('/horizon/');
// Regardless of the trailing slash, the redirect path is the absolute
// dashboard path, never a relative reference that would double up.
expect(parse_url($response->headers->get('Location'), PHP_URL_PATH))
->toBe('/horizon/dashboard');
});
it('does not allow a non-admin to use the /horizon redirect', function () {
$user = User::factory()->create(['is_admin' => false]);
$user->refresh();
// The admin middleware bounces non-admins to the app root, not the dashboard.
$this->actingAs($user)
->get('/horizon')
->assertRedirect(config('app.url'));
});
Loading…
Cancel
Save