Optimize anonymous public sessions
This commit is contained in:
@@ -41,6 +41,14 @@ SESSION_ENCRYPT=false
|
|||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
# Skinbase Nova conditional public sessions
|
||||||
|
SKINBASE_CONDITIONAL_SESSIONS_ENABLED=true
|
||||||
|
SKINBASE_SKIP_ANONYMOUS_PUBLIC_GET_SESSIONS=true
|
||||||
|
SKINBASE_SKIP_BOT_PUBLIC_GET_SESSIONS=true
|
||||||
|
|
||||||
|
# Debug only; do not enable permanently in production
|
||||||
|
SKINBASE_SESSION_DEBUG_HEADER=false
|
||||||
|
|
||||||
BROADCAST_CONNECTION=reverb
|
BROADCAST_CONNECTION=reverb
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=redis
|
QUEUE_CONNECTION=redis
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
|
class ConditionalShareErrorsFromSession extends ShareErrorsFromSession
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if (! $request instanceof Request) {
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->attributes->get('skinbase.session_skipped') === true || ! $request->hasSession()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class ConditionalStartSession extends StartSession
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if (! $request instanceof Request || ! config('skinbase-sessions.enabled', true)) {
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldSkipSession($request)) {
|
||||||
|
$request->attributes->set('skinbase.session_skipped', true);
|
||||||
|
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
|
||||||
|
$response->headers->set('X-Skinbase-Session', 'skipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->attributes->set('skinbase.session_skipped', false);
|
||||||
|
|
||||||
|
$response = parent::handle($request, $next);
|
||||||
|
|
||||||
|
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
|
||||||
|
$response->headers->set('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function shouldSkipSession(Request $request): bool
|
||||||
|
{
|
||||||
|
if (! $this->isSafeReadMethod($request)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasExistingSessionCookie($request)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->matchesAnyPath($request, config('skinbase-sessions.always_session_paths', []))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->matchesAnyPath($request, config('skinbase-sessions.public_paths', []))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config('skinbase-sessions.skip_anonymous_public_get', true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config('skinbase-sessions.skip_known_crawlers_on_public_get', true)
|
||||||
|
&& $this->isKnownCrawler($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isSafeReadMethod(Request $request): bool
|
||||||
|
{
|
||||||
|
return in_array($request->getMethod(), ['GET', 'HEAD'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hasExistingSessionCookie(Request $request): bool
|
||||||
|
{
|
||||||
|
$cookieName = config('session.cookie');
|
||||||
|
|
||||||
|
return is_string($cookieName)
|
||||||
|
&& $cookieName !== ''
|
||||||
|
&& $request->cookies->has($cookieName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function matchesAnyPath(Request $request, array $patterns): bool
|
||||||
|
{
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (! is_string($pattern) || $pattern === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pattern === '/' && $request->path() === '/') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedPattern = trim($pattern, '/');
|
||||||
|
|
||||||
|
if ($normalizedPattern !== '' && $request->is($normalizedPattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isKnownCrawler(Request $request): bool
|
||||||
|
{
|
||||||
|
$userAgent = strtolower((string) $request->userAgent());
|
||||||
|
|
||||||
|
if ($userAgent === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (config('skinbase-sessions.bot_user_agent_keywords', []) as $keyword) {
|
||||||
|
$normalizedKeyword = strtolower((string) $keyword);
|
||||||
|
|
||||||
|
if ($normalizedKeyword !== '' && str_contains($userAgent, $normalizedKeyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ConditionalValidateCsrfToken extends ValidateCsrfToken
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if ($request instanceof Request && $request->attributes->get('skinbase.session_skipped') === true) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,15 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
{
|
{
|
||||||
protected $rootView = 'upload';
|
protected $rootView = 'upload';
|
||||||
|
|
||||||
|
protected function canReadSessionAuth(Request $request): bool
|
||||||
|
{
|
||||||
|
if ($request->attributes->get('skinbase.session_skipped') === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->hasSession();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the root Blade view based on route prefix.
|
* Select the root Blade view based on route prefix.
|
||||||
*/
|
*/
|
||||||
@@ -58,13 +67,16 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
|
|
||||||
public function share(Request $request): array
|
public function share(Request $request): array
|
||||||
{
|
{
|
||||||
|
$canReadSessionAuth = $this->canReadSessionAuth($request);
|
||||||
|
$user = $canReadSessionAuth ? $request->user() : null;
|
||||||
|
|
||||||
return array_merge(parent::share($request), [
|
return array_merge(parent::share($request), [
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user() ? [
|
'user' => $user ? [
|
||||||
'id' => $request->user()->id,
|
'id' => $user->id,
|
||||||
'name' => $request->user()->name,
|
'name' => $user->name,
|
||||||
'is_admin' => $request->user()->isAdmin(),
|
'is_admin' => $user->isAdmin(),
|
||||||
'is_moderator' => $request->user()->isModerator(),
|
'is_moderator' => $user->isModerator(),
|
||||||
] : null,
|
] : null,
|
||||||
],
|
],
|
||||||
'cdn' => [
|
'cdn' => [
|
||||||
@@ -84,8 +96,8 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
'group_assets' => (bool) config('features.group_assets', true),
|
'group_assets' => (bool) config('features.group_assets', true),
|
||||||
'group_activity_feed' => (bool) config('features.group_activity_feed', true),
|
'group_activity_feed' => (bool) config('features.group_activity_feed', true),
|
||||||
],
|
],
|
||||||
'studio_groups' => $request->user()
|
'studio_groups' => $user
|
||||||
? app(GroupService::class)->studioOptionsForUser($request->user())
|
? app(GroupService::class)->studioOptionsForUser($user)
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$displayName = null;
|
$displayName = null;
|
||||||
$userId = null;
|
$userId = null;
|
||||||
$toolbarContentTypes = collect();
|
$toolbarContentTypes = collect();
|
||||||
|
$request = request();
|
||||||
|
$canReadSessionAuth = $request instanceof \Illuminate\Http\Request
|
||||||
|
&& $request->hasSession()
|
||||||
|
&& $request->attributes->get('skinbase.session_skipped') !== true;
|
||||||
|
$authUser = $canReadSessionAuth ? Auth::user() : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$toolbarContentTypes = $this->app
|
$toolbarContentTypes = $this->app
|
||||||
@@ -162,8 +167,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$toolbarContentTypes = collect();
|
$toolbarContentTypes = collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Auth::check()) {
|
if ($authUser) {
|
||||||
$userId = Auth::id();
|
$authUser->loadMissing('profile');
|
||||||
|
$userId = (int) $authUser->id;
|
||||||
try {
|
try {
|
||||||
$uploadCount = DB::table('artworks')->where('user_id', $userId)->count();
|
$uploadCount = DB::table('artworks')->where('user_id', $userId)->count();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -200,19 +206,18 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
|
$receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
|
||||||
->unreadCountForUser(Auth::user());
|
->unreadCountForUser($authUser);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$receivedCommentsCount = 0;
|
$receivedCommentsCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$profile = DB::table('user_profiles')->where('user_id', $userId)->first();
|
$avatarHash = $authUser->profile?->avatar_hash;
|
||||||
$avatarHash = $profile->avatar_hash ?? null;
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$avatarHash = null;
|
$avatarHash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$displayName = Auth::user()->name ?: (Auth::user()->username ?? '');
|
$displayName = $authUser->name ?: ($authUser->username ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes'));
|
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes'));
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\ConditionalShareErrorsFromSession;
|
||||||
|
use App\Http\Middleware\ConditionalStartSession;
|
||||||
|
use App\Http\Middleware\ConditionalValidateCsrfToken;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@@ -13,6 +19,12 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->web(replace: [
|
||||||
|
StartSession::class => ConditionalStartSession::class,
|
||||||
|
ShareErrorsFromSession::class => ConditionalShareErrorsFromSession::class,
|
||||||
|
ValidateCsrfToken::class => ConditionalValidateCsrfToken::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$middleware->validateCsrfTokens(except: [
|
$middleware->validateCsrfTokens(except: [
|
||||||
'chat_post',
|
'chat_post',
|
||||||
'chat_post/*',
|
'chat_post/*',
|
||||||
|
|||||||
124
.deploy/artwork-evolution-release/config/skinbase-sessions.php
Normal file
124
.deploy/artwork-evolution-release/config/skinbase-sessions.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'enabled' => env('SKINBASE_CONDITIONAL_SESSIONS_ENABLED', true),
|
||||||
|
|
||||||
|
'skip_anonymous_public_get' => env('SKINBASE_SKIP_ANONYMOUS_PUBLIC_GET_SESSIONS', true),
|
||||||
|
|
||||||
|
'skip_known_crawlers_on_public_get' => env('SKINBASE_SKIP_BOT_PUBLIC_GET_SESSIONS', true),
|
||||||
|
|
||||||
|
'debug_header' => env('SKINBASE_SESSION_DEBUG_HEADER', false),
|
||||||
|
|
||||||
|
'public_paths' => [
|
||||||
|
'/',
|
||||||
|
'featured',
|
||||||
|
'uploads/latest',
|
||||||
|
'uploads/daily',
|
||||||
|
'members/photos',
|
||||||
|
'downloads/today',
|
||||||
|
'comments/monthly',
|
||||||
|
'discover',
|
||||||
|
'discover/*',
|
||||||
|
'explore',
|
||||||
|
'explore/*',
|
||||||
|
'blog',
|
||||||
|
'blog/*',
|
||||||
|
'pages/*',
|
||||||
|
'about',
|
||||||
|
'help',
|
||||||
|
'help/*',
|
||||||
|
'contact',
|
||||||
|
'faq',
|
||||||
|
'rules-and-guidelines',
|
||||||
|
'privacy-policy',
|
||||||
|
'terms-of-service',
|
||||||
|
'staff',
|
||||||
|
'bug-report',
|
||||||
|
'rss-feeds',
|
||||||
|
'rss',
|
||||||
|
'rss/*',
|
||||||
|
'news',
|
||||||
|
'news/*',
|
||||||
|
'worlds',
|
||||||
|
'worlds/*',
|
||||||
|
'creators',
|
||||||
|
'creators/*',
|
||||||
|
'stories',
|
||||||
|
'stories/*',
|
||||||
|
'tags',
|
||||||
|
'tags/*',
|
||||||
|
'categories',
|
||||||
|
'leaderboard',
|
||||||
|
'art',
|
||||||
|
'art/*',
|
||||||
|
'sitemap.xml',
|
||||||
|
'sitemaps/*',
|
||||||
|
'robots.txt',
|
||||||
|
],
|
||||||
|
|
||||||
|
'always_session_paths' => [
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'register',
|
||||||
|
'register/*',
|
||||||
|
'auth/*',
|
||||||
|
'forgot-password',
|
||||||
|
'reset-password',
|
||||||
|
'reset-password/*',
|
||||||
|
'confirm-password',
|
||||||
|
'email/verification-notification',
|
||||||
|
'verify-email',
|
||||||
|
'verify-email/*',
|
||||||
|
'setup/*',
|
||||||
|
|
||||||
|
'dashboard',
|
||||||
|
'dashboard/*',
|
||||||
|
'manage',
|
||||||
|
'studio',
|
||||||
|
'studio/*',
|
||||||
|
'upload',
|
||||||
|
'upload/*',
|
||||||
|
'settings',
|
||||||
|
'settings/*',
|
||||||
|
'messages',
|
||||||
|
'messages/*',
|
||||||
|
'worlds/create',
|
||||||
|
|
||||||
|
'cp',
|
||||||
|
'cp/*',
|
||||||
|
'admin',
|
||||||
|
'admin/*',
|
||||||
|
|
||||||
|
'api/me',
|
||||||
|
'api/auth/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
'bot_user_agent_keywords' => [
|
||||||
|
'googlebot',
|
||||||
|
'bingbot',
|
||||||
|
'slurp',
|
||||||
|
'duckduckbot',
|
||||||
|
'baiduspider',
|
||||||
|
'yandexbot',
|
||||||
|
'sogou',
|
||||||
|
'exabot',
|
||||||
|
'facebot',
|
||||||
|
'facebookexternalhit',
|
||||||
|
'ia_archiver',
|
||||||
|
'semrushbot',
|
||||||
|
'ahrefsbot',
|
||||||
|
'mj12bot',
|
||||||
|
'dotbot',
|
||||||
|
'petalbot',
|
||||||
|
'applebot',
|
||||||
|
'twitterbot',
|
||||||
|
'linkedinbot',
|
||||||
|
'discordbot',
|
||||||
|
'telegrambot',
|
||||||
|
'whatsapp',
|
||||||
|
'crawler',
|
||||||
|
'spider',
|
||||||
|
'bot',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
$comments = $comments ?? [];
|
$comments = $comments ?? [];
|
||||||
$groupSummary = $groupSummary ?? null;
|
$groupSummary = $groupSummary ?? null;
|
||||||
$useUnifiedSeo = true;
|
$useUnifiedSeo = true;
|
||||||
|
$canReadSessionAuth = request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped');
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
data-canonical='@json($meta["canonical"])'
|
data-canonical='@json($meta["canonical"])'
|
||||||
data-comments='@json($comments)'
|
data-comments='@json($comments)'
|
||||||
data-group-summary='@json($groupSummary)'
|
data-group-summary='@json($groupSummary)'
|
||||||
data-is-authenticated='@json(auth()->check())'>
|
data-is-authenticated='@json($canReadSessionAuth && auth()->check())'>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@vite(['resources/js/Pages/ArtworkPage.jsx'])
|
@vite(['resources/js/Pages/ArtworkPage.jsx'])
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/collections.jsx'])
|
@vite(['resources/js/collections.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-collections main { padding-top: 4rem; }
|
body.page-collections main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@php
|
@php
|
||||||
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
|
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
|
||||||
|
$skinbaseSessionSkipped = request()->attributes->get('skinbase.session_skipped') === true;
|
||||||
|
$skinbaseCanUseSession = request()->hasSession() && ! $skinbaseSessionSkipped;
|
||||||
$deferToolbarSearch = request()->routeIs('index');
|
$deferToolbarSearch = request()->routeIs('index');
|
||||||
$deferFontAwesome = request()->routeIs('index');
|
$deferFontAwesome = request()->routeIs('index');
|
||||||
$deferWebManifest = request()->routeIs('index');
|
$deferWebManifest = request()->routeIs('index');
|
||||||
@@ -21,7 +23,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
@if($skinbaseCanUseSession)
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
@endif
|
||||||
@if($shouldRenderBladeSeo)
|
@if($shouldRenderBladeSeo)
|
||||||
@include('partials.seo.head', ['seo' => $seo ?? null])
|
@include('partials.seo.head', ['seo' => $seo ?? null])
|
||||||
@endif
|
@endif
|
||||||
@@ -235,13 +239,13 @@
|
|||||||
|
|
||||||
<!-- React Topbar mount point -->
|
<!-- React Topbar mount point -->
|
||||||
<div id="topbar-root"
|
<div id="topbar-root"
|
||||||
@auth
|
@if($skinbaseCanUseSession && Auth::check())
|
||||||
data-user-id="{{ Auth::id() }}"
|
data-user-id="{{ Auth::id() }}"
|
||||||
data-display-name="{{ Auth::user()->name ?? '' }}"
|
data-display-name="{{ Auth::user()->name ?? '' }}"
|
||||||
data-username="{{ Auth::user()->username ?? '' }}"
|
data-username="{{ Auth::user()->username ?? '' }}"
|
||||||
data-avatar-url="{{ \App\Support\AvatarUrl::forUser((int) Auth::id(), optional(Auth::user()->profile)->avatar_hash, 64) }}"
|
data-avatar-url="{{ \App\Support\AvatarUrl::forUser((int) Auth::id(), optional(Auth::user()->profile)->avatar_hash, 64) }}"
|
||||||
data-upload-url="{{ Route::has('upload') ? route('upload') : '/upload' }}"
|
data-upload-url="{{ Route::has('upload') ? route('upload') : '/upload' }}"
|
||||||
@endauth
|
@endif
|
||||||
></div>
|
></div>
|
||||||
@include('layouts.nova.toolbar')
|
@include('layouts.nova.toolbar')
|
||||||
<main class="flex-1 @yield('main-class', 'pt-16')">
|
<main class="flex-1 @yield('main-class', 'pt-16')">
|
||||||
@@ -252,9 +256,9 @@
|
|||||||
|
|
||||||
{{-- Toast notifications (Alpine) --}}
|
{{-- Toast notifications (Alpine) --}}
|
||||||
@php
|
@php
|
||||||
$toastMessage = session('status') ?? session('error') ?? null;
|
$toastMessage = $skinbaseCanUseSession ? (session('status') ?? session('error') ?? null) : null;
|
||||||
$toastType = session('error') ? 'error' : 'success';
|
$toastType = $skinbaseCanUseSession && session('error') ? 'error' : 'success';
|
||||||
$toastBorder = session('error') ? 'border-red-500' : 'border-green-500';
|
$toastBorder = $skinbaseCanUseSession && session('error') ? 'border-red-500' : 'border-green-500';
|
||||||
@endphp
|
@endphp
|
||||||
@if($toastMessage)
|
@if($toastMessage)
|
||||||
<div x-data="{show:true}" x-show="show" x-init="setTimeout(()=>show=false,4000)" x-cloak
|
<div x-data="{show:true}" x-show="show" x-init="setTimeout(()=>show=false,4000)" x-cloak
|
||||||
@@ -262,7 +266,7 @@
|
|||||||
<div class="max-w-sm w-full rounded-lg shadow-lg overflow-hidden bg-nova-600 border {{ $toastBorder }}">
|
<div class="max-w-sm w-full rounded-lg shadow-lg overflow-hidden bg-nova-600 border {{ $toastBorder }}">
|
||||||
<div class="px-4 py-3 flex items-start gap-3">
|
<div class="px-4 py-3 flex items-start gap-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@if(session('error'))
|
@if($skinbaseCanUseSession && session('error'))
|
||||||
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/></svg>
|
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/></svg>
|
||||||
@else
|
@else
|
||||||
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
@php
|
||||||
|
$skinbaseCanUseSession = ($skinbaseCanUseSession ?? false) === true;
|
||||||
|
$skinbaseToolbarUser = $skinbaseCanUseSession ? Auth::user() : null;
|
||||||
|
$skinbaseToolbarCanAuth = $skinbaseToolbarUser !== null;
|
||||||
|
@endphp
|
||||||
|
|
||||||
<header id="nova-toolbar" class="fixed inset-x-0 top-0 z-50 h-16 bg-black/40 backdrop-blur border-b border-white/10">
|
<header id="nova-toolbar" class="fixed inset-x-0 top-0 z-50 h-16 bg-black/40 backdrop-blur border-b border-white/10">
|
||||||
<div class="mx-auto w-full h-full px-3 sm:px-4 flex items-center gap-2 sm:gap-3">
|
<div class="mx-auto w-full h-full px-3 sm:px-4 flex items-center gap-2 sm:gap-3">
|
||||||
|
|
||||||
@@ -80,11 +86,11 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/discover/on-this-day">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/discover/on-this-day">
|
||||||
<i class="fa-solid fa-calendar-day w-4 text-center text-sb-muted"></i>On This Day
|
<i class="fa-solid fa-calendar-day w-4 text-center text-sb-muted"></i>On This Day
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('discover.for-you') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('discover.for-you') }}">
|
||||||
<i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You
|
<i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,11 +171,11 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/stories">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/stories">
|
||||||
<i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories
|
<i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.following') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.following') }}">
|
||||||
<i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following
|
<i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('community.activity') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('community.activity') }}">
|
||||||
<i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed
|
<i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.comments.received') }}">
|
<a class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.comments.received') }}">
|
||||||
<span class="flex items-center gap-3">
|
<span class="flex items-center gap-3">
|
||||||
<i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments
|
<i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments
|
||||||
@@ -194,7 +200,7 @@
|
|||||||
<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>
|
<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/forum">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/forum">
|
||||||
<i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum
|
<i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum
|
||||||
</a>
|
</a>
|
||||||
@@ -241,7 +247,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<!-- Notification icons -->
|
<!-- Notification icons -->
|
||||||
<div class="hidden md:flex items-center gap-0.5 lg:gap-1 text-soft shrink-0">
|
<div class="hidden md:flex items-center gap-0.5 lg:gap-1 text-soft shrink-0">
|
||||||
<a href="{{ route('dashboard.favorites') }}"
|
<a href="{{ route('dashboard.favorites') }}"
|
||||||
@@ -458,7 +464,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -466,9 +472,9 @@
|
|||||||
<div class="hidden fixed inset-x-0 top-16 bottom-0 z-40 overflow-y-auto overscroll-contain bg-nova border-b border-panel p-4 shadow-sb" id="mobileMenu">
|
<div class="hidden fixed inset-x-0 top-16 bottom-0 z-40 overflow-y-auto overscroll-contain bg-nova border-b border-panel p-4 shadow-sb" id="mobileMenu">
|
||||||
<div class="space-y-0.5 text-sm text-soft">
|
<div class="space-y-0.5 text-sm text-soft">
|
||||||
|
|
||||||
@guest
|
@if(! $skinbaseToolbarCanAuth)
|
||||||
<div class="my-2 border-t border-panel"></div>
|
<div class="my-2 border-t border-panel"></div>
|
||||||
@endguest
|
@endif
|
||||||
|
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<button type="button" data-mobile-section-toggle aria-controls="mobileSectionDiscover" aria-expanded="true" class="w-full flex items-center justify-between py-2.5 px-3 rounded-lg text-[11px] font-semibold uppercase tracking-widest text-sb-muted hover:bg-white/5">
|
<button type="button" data-mobile-section-toggle aria-controls="mobileSectionDiscover" aria-expanded="true" class="w-full flex items-center justify-between py-2.5 px-3 rounded-lg text-[11px] font-semibold uppercase tracking-widest text-sb-muted hover:bg-white/5">
|
||||||
@@ -528,10 +534,10 @@
|
|||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/leaderboard"><i class="fa-solid fa-trophy w-4 text-center text-sb-muted"></i>Leaderboard</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/leaderboard"><i class="fa-solid fa-trophy w-4 text-center text-sb-muted"></i>Leaderboard</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/creators/rising"><i class="fa-solid fa-arrow-trend-up w-4 text-center text-sb-muted"></i>Rising Creators</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/creators/rising"><i class="fa-solid fa-arrow-trend-up w-4 text-center text-sb-muted"></i>Rising Creators</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -542,9 +548,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="mobileSectionCommunity" data-mobile-section-panel class="hidden mt-0.5 space-y-0.5">
|
<div id="mobileSectionCommunity" data-mobile-section-panel class="hidden mt-0.5 space-y-0.5">
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('community.activity') }}"><i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('community.activity') }}"><i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center justify-between gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.comments.received') }}"><span class="flex items-center gap-3"><i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments</span>@if(($receivedCommentsCount ?? 0) > 0)<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>@endif</a>
|
<a class="flex items-center justify-between gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.comments.received') }}"><span class="flex items-center gap-3"><i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments</span>@if(($receivedCommentsCount ?? 0) > 0)<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>@endif</a>
|
||||||
@endauth
|
@endif
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/forum"><i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/forum"><i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/news"><i class="fa-solid fa-newspaper w-4 text-center text-sb-muted"></i>News</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/news"><i class="fa-solid fa-newspaper w-4 text-center text-sb-muted"></i>News</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
@section('title', 'Messages')
|
@section('title', 'Messages')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/moderation.jsx'])
|
@vite(['resources/js/moderation.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-moderation main { padding-top: 4rem; }
|
body.page-moderation main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/settings.jsx'])
|
@vite(['resources/js/settings.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-settings main { padding-top: 4rem; }
|
body.page-settings main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/studio.jsx'])
|
@vite(['resources/js/studio.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-studio main { padding-top: 2.3rem; }
|
body.page-studio main { padding-top: 2.3rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.SKINBASE_FLAGS = Object.assign({}, window.SKINBASE_FLAGS || {}, {
|
window.SKINBASE_FLAGS = Object.assign({}, window.SKINBASE_FLAGS || {}, {
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ SESSION_ENCRYPT=false
|
|||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
# Skinbase Nova conditional public sessions
|
||||||
|
SKINBASE_CONDITIONAL_SESSIONS_ENABLED=true
|
||||||
|
SKINBASE_SKIP_ANONYMOUS_PUBLIC_GET_SESSIONS=true
|
||||||
|
SKINBASE_SKIP_BOT_PUBLIC_GET_SESSIONS=true
|
||||||
|
|
||||||
|
# Debug only; do not enable permanently in production
|
||||||
|
SKINBASE_SESSION_DEBUG_HEADER=false
|
||||||
|
|
||||||
BROADCAST_CONNECTION=reverb
|
BROADCAST_CONNECTION=reverb
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=redis
|
QUEUE_CONNECTION=redis
|
||||||
|
|||||||
25
app/Http/Middleware/ConditionalShareErrorsFromSession.php
Normal file
25
app/Http/Middleware/ConditionalShareErrorsFromSession.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
|
class ConditionalShareErrorsFromSession extends ShareErrorsFromSession
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if (! $request instanceof Request) {
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->attributes->get('skinbase.session_skipped') === true || ! $request->hasSession()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/Http/Middleware/ConditionalStartSession.php
Normal file
122
app/Http/Middleware/ConditionalStartSession.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class ConditionalStartSession extends StartSession
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if (! $request instanceof Request || ! config('skinbase-sessions.enabled', true)) {
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldSkipSession($request)) {
|
||||||
|
$request->attributes->set('skinbase.session_skipped', true);
|
||||||
|
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
|
||||||
|
$response->headers->set('X-Skinbase-Session', 'skipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->attributes->set('skinbase.session_skipped', false);
|
||||||
|
|
||||||
|
$response = parent::handle($request, $next);
|
||||||
|
|
||||||
|
if ($response instanceof Response && config('skinbase-sessions.debug_header', false)) {
|
||||||
|
$response->headers->set('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function shouldSkipSession(Request $request): bool
|
||||||
|
{
|
||||||
|
if (! $this->isSafeReadMethod($request)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasExistingSessionCookie($request)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->matchesAnyPath($request, config('skinbase-sessions.always_session_paths', []))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->matchesAnyPath($request, config('skinbase-sessions.public_paths', []))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config('skinbase-sessions.skip_anonymous_public_get', true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config('skinbase-sessions.skip_known_crawlers_on_public_get', true)
|
||||||
|
&& $this->isKnownCrawler($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isSafeReadMethod(Request $request): bool
|
||||||
|
{
|
||||||
|
return in_array($request->getMethod(), ['GET', 'HEAD'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hasExistingSessionCookie(Request $request): bool
|
||||||
|
{
|
||||||
|
$cookieName = config('session.cookie');
|
||||||
|
|
||||||
|
return is_string($cookieName)
|
||||||
|
&& $cookieName !== ''
|
||||||
|
&& $request->cookies->has($cookieName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function matchesAnyPath(Request $request, array $patterns): bool
|
||||||
|
{
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (! is_string($pattern) || $pattern === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pattern === '/' && $request->path() === '/') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedPattern = trim($pattern, '/');
|
||||||
|
|
||||||
|
if ($normalizedPattern !== '' && $request->is($normalizedPattern)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isKnownCrawler(Request $request): bool
|
||||||
|
{
|
||||||
|
$userAgent = strtolower((string) $request->userAgent());
|
||||||
|
|
||||||
|
if ($userAgent === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (config('skinbase-sessions.bot_user_agent_keywords', []) as $keyword) {
|
||||||
|
$normalizedKeyword = strtolower((string) $keyword);
|
||||||
|
|
||||||
|
if ($normalizedKeyword !== '' && str_contains($userAgent, $normalizedKeyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Http/Middleware/ConditionalValidateCsrfToken.php
Normal file
21
app/Http/Middleware/ConditionalValidateCsrfToken.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ConditionalValidateCsrfToken extends ValidateCsrfToken
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if ($request instanceof Request && $request->attributes->get('skinbase.session_skipped') === true) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,15 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
{
|
{
|
||||||
protected $rootView = 'upload';
|
protected $rootView = 'upload';
|
||||||
|
|
||||||
|
protected function canReadSessionAuth(Request $request): bool
|
||||||
|
{
|
||||||
|
if ($request->attributes->get('skinbase.session_skipped') === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->hasSession();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the root Blade view based on route prefix.
|
* Select the root Blade view based on route prefix.
|
||||||
*/
|
*/
|
||||||
@@ -58,13 +67,16 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
|
|
||||||
public function share(Request $request): array
|
public function share(Request $request): array
|
||||||
{
|
{
|
||||||
|
$canReadSessionAuth = $this->canReadSessionAuth($request);
|
||||||
|
$user = $canReadSessionAuth ? $request->user() : null;
|
||||||
|
|
||||||
return array_merge(parent::share($request), [
|
return array_merge(parent::share($request), [
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user() ? [
|
'user' => $user ? [
|
||||||
'id' => $request->user()->id,
|
'id' => $user->id,
|
||||||
'name' => $request->user()->name,
|
'name' => $user->name,
|
||||||
'is_admin' => $request->user()->isAdmin(),
|
'is_admin' => $user->isAdmin(),
|
||||||
'is_moderator' => $request->user()->isModerator(),
|
'is_moderator' => $user->isModerator(),
|
||||||
] : null,
|
] : null,
|
||||||
],
|
],
|
||||||
'cdn' => [
|
'cdn' => [
|
||||||
@@ -84,8 +96,8 @@ final class HandleInertiaRequests extends Middleware
|
|||||||
'group_assets' => (bool) config('features.group_assets', true),
|
'group_assets' => (bool) config('features.group_assets', true),
|
||||||
'group_activity_feed' => (bool) config('features.group_activity_feed', true),
|
'group_activity_feed' => (bool) config('features.group_activity_feed', true),
|
||||||
],
|
],
|
||||||
'studio_groups' => $request->user()
|
'studio_groups' => $user
|
||||||
? app(GroupService::class)->studioOptionsForUser($request->user())
|
? app(GroupService::class)->studioOptionsForUser($user)
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$displayName = null;
|
$displayName = null;
|
||||||
$userId = null;
|
$userId = null;
|
||||||
$toolbarContentTypes = collect();
|
$toolbarContentTypes = collect();
|
||||||
|
$request = request();
|
||||||
|
$canReadSessionAuth = $request instanceof \Illuminate\Http\Request
|
||||||
|
&& $request->hasSession()
|
||||||
|
&& $request->attributes->get('skinbase.session_skipped') !== true;
|
||||||
|
$authUser = $canReadSessionAuth ? Auth::user() : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$toolbarContentTypes = $this->app
|
$toolbarContentTypes = $this->app
|
||||||
@@ -168,8 +173,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$toolbarContentTypes = collect();
|
$toolbarContentTypes = collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Auth::check()) {
|
if ($authUser) {
|
||||||
$userId = Auth::id();
|
$userId = (int) $authUser->id;
|
||||||
try {
|
try {
|
||||||
$uploadCount = DB::table('artworks')->where('user_id', $userId)->count();
|
$uploadCount = DB::table('artworks')->where('user_id', $userId)->count();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -206,7 +211,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
|
$receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class)
|
||||||
->unreadCountForUser(Auth::user());
|
->unreadCountForUser($authUser);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$receivedCommentsCount = 0;
|
$receivedCommentsCount = 0;
|
||||||
}
|
}
|
||||||
@@ -218,7 +223,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$avatarHash = null;
|
$avatarHash = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$displayName = Auth::user()->name ?: (Auth::user()->username ?? '');
|
$displayName = $authUser->name ?: ($authUser->username ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes'));
|
$view->with(compact('userId','uploadCount', 'favCount', 'msgCount', 'noticeCount', 'receivedCommentsCount', 'avatarHash', 'displayName', 'toolbarContentTypes'));
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\ConditionalShareErrorsFromSession;
|
||||||
|
use App\Http\Middleware\ConditionalStartSession;
|
||||||
|
use App\Http\Middleware\ConditionalValidateCsrfToken;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
@@ -13,6 +19,12 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->web(replace: [
|
||||||
|
StartSession::class => ConditionalStartSession::class,
|
||||||
|
ShareErrorsFromSession::class => ConditionalShareErrorsFromSession::class,
|
||||||
|
ValidateCsrfToken::class => ConditionalValidateCsrfToken::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$middleware->validateCsrfTokens(except: [
|
$middleware->validateCsrfTokens(except: [
|
||||||
'chat_post',
|
'chat_post',
|
||||||
'chat_post/*',
|
'chat_post/*',
|
||||||
|
|||||||
124
config/skinbase-sessions.php
Normal file
124
config/skinbase-sessions.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'enabled' => env('SKINBASE_CONDITIONAL_SESSIONS_ENABLED', true),
|
||||||
|
|
||||||
|
'skip_anonymous_public_get' => env('SKINBASE_SKIP_ANONYMOUS_PUBLIC_GET_SESSIONS', true),
|
||||||
|
|
||||||
|
'skip_known_crawlers_on_public_get' => env('SKINBASE_SKIP_BOT_PUBLIC_GET_SESSIONS', true),
|
||||||
|
|
||||||
|
'debug_header' => env('SKINBASE_SESSION_DEBUG_HEADER', false),
|
||||||
|
|
||||||
|
'public_paths' => [
|
||||||
|
'/',
|
||||||
|
'featured',
|
||||||
|
'uploads/latest',
|
||||||
|
'uploads/daily',
|
||||||
|
'members/photos',
|
||||||
|
'downloads/today',
|
||||||
|
'comments/monthly',
|
||||||
|
'discover',
|
||||||
|
'discover/*',
|
||||||
|
'explore',
|
||||||
|
'explore/*',
|
||||||
|
'blog',
|
||||||
|
'blog/*',
|
||||||
|
'pages/*',
|
||||||
|
'about',
|
||||||
|
'help',
|
||||||
|
'help/*',
|
||||||
|
'contact',
|
||||||
|
'faq',
|
||||||
|
'rules-and-guidelines',
|
||||||
|
'privacy-policy',
|
||||||
|
'terms-of-service',
|
||||||
|
'staff',
|
||||||
|
'bug-report',
|
||||||
|
'rss-feeds',
|
||||||
|
'rss',
|
||||||
|
'rss/*',
|
||||||
|
'news',
|
||||||
|
'news/*',
|
||||||
|
'worlds',
|
||||||
|
'worlds/*',
|
||||||
|
'creators',
|
||||||
|
'creators/*',
|
||||||
|
'stories',
|
||||||
|
'stories/*',
|
||||||
|
'tags',
|
||||||
|
'tags/*',
|
||||||
|
'categories',
|
||||||
|
'leaderboard',
|
||||||
|
'art',
|
||||||
|
'art/*',
|
||||||
|
'sitemap.xml',
|
||||||
|
'sitemaps/*',
|
||||||
|
'robots.txt',
|
||||||
|
],
|
||||||
|
|
||||||
|
'always_session_paths' => [
|
||||||
|
'login',
|
||||||
|
'logout',
|
||||||
|
'register',
|
||||||
|
'register/*',
|
||||||
|
'auth/*',
|
||||||
|
'forgot-password',
|
||||||
|
'reset-password',
|
||||||
|
'reset-password/*',
|
||||||
|
'confirm-password',
|
||||||
|
'email/verification-notification',
|
||||||
|
'verify-email',
|
||||||
|
'verify-email/*',
|
||||||
|
'setup/*',
|
||||||
|
|
||||||
|
'dashboard',
|
||||||
|
'dashboard/*',
|
||||||
|
'manage',
|
||||||
|
'studio',
|
||||||
|
'studio/*',
|
||||||
|
'upload',
|
||||||
|
'upload/*',
|
||||||
|
'settings',
|
||||||
|
'settings/*',
|
||||||
|
'messages',
|
||||||
|
'messages/*',
|
||||||
|
'worlds/create',
|
||||||
|
|
||||||
|
'cp',
|
||||||
|
'cp/*',
|
||||||
|
'admin',
|
||||||
|
'admin/*',
|
||||||
|
|
||||||
|
'api/me',
|
||||||
|
'api/auth/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
'bot_user_agent_keywords' => [
|
||||||
|
'googlebot',
|
||||||
|
'bingbot',
|
||||||
|
'slurp',
|
||||||
|
'duckduckbot',
|
||||||
|
'baiduspider',
|
||||||
|
'yandexbot',
|
||||||
|
'sogou',
|
||||||
|
'exabot',
|
||||||
|
'facebot',
|
||||||
|
'facebookexternalhit',
|
||||||
|
'ia_archiver',
|
||||||
|
'semrushbot',
|
||||||
|
'ahrefsbot',
|
||||||
|
'mj12bot',
|
||||||
|
'dotbot',
|
||||||
|
'petalbot',
|
||||||
|
'applebot',
|
||||||
|
'twitterbot',
|
||||||
|
'linkedinbot',
|
||||||
|
'discordbot',
|
||||||
|
'telegrambot',
|
||||||
|
'whatsapp',
|
||||||
|
'crawler',
|
||||||
|
'spider',
|
||||||
|
'bot',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
$comments = $comments ?? [];
|
$comments = $comments ?? [];
|
||||||
$groupSummary = $groupSummary ?? null;
|
$groupSummary = $groupSummary ?? null;
|
||||||
$useUnifiedSeo = true;
|
$useUnifiedSeo = true;
|
||||||
|
$canReadSessionAuth = request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped');
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
data-canonical='@json($meta["canonical"])'
|
data-canonical='@json($meta["canonical"])'
|
||||||
data-comments='@json($comments)'
|
data-comments='@json($comments)'
|
||||||
data-group-summary='@json($groupSummary)'
|
data-group-summary='@json($groupSummary)'
|
||||||
data-is-authenticated='@json(auth()->check())'>
|
data-is-authenticated='@json($canReadSessionAuth && auth()->check())'>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@vite(['resources/js/Pages/ArtworkPage.jsx'])
|
@vite(['resources/js/Pages/ArtworkPage.jsx'])
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/collections.jsx'])
|
@vite(['resources/js/collections.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-collections main { padding-top: 4rem; }
|
body.page-collections main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@php
|
@php
|
||||||
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
|
$gridVersion = request()->query('grid') === 'v2' ? 'v2' : 'v1';
|
||||||
|
$skinbaseSessionSkipped = request()->attributes->get('skinbase.session_skipped') === true;
|
||||||
|
$skinbaseCanUseSession = request()->hasSession() && ! $skinbaseSessionSkipped;
|
||||||
$deferToolbarSearch = request()->routeIs('index');
|
$deferToolbarSearch = request()->routeIs('index');
|
||||||
$deferFontAwesome = request()->routeIs('index');
|
$deferFontAwesome = request()->routeIs('index');
|
||||||
$deferWebManifest = request()->routeIs('index');
|
$deferWebManifest = request()->routeIs('index');
|
||||||
@@ -21,7 +23,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
@if($skinbaseCanUseSession)
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
@endif
|
||||||
@if($shouldRenderBladeSeo)
|
@if($shouldRenderBladeSeo)
|
||||||
@include('partials.seo.head', ['seo' => $seo ?? null])
|
@include('partials.seo.head', ['seo' => $seo ?? null])
|
||||||
@endif
|
@endif
|
||||||
@@ -242,13 +246,13 @@
|
|||||||
|
|
||||||
<!-- React Topbar mount point -->
|
<!-- React Topbar mount point -->
|
||||||
<div id="topbar-root"
|
<div id="topbar-root"
|
||||||
@auth
|
@if($skinbaseCanUseSession && Auth::check())
|
||||||
data-user-id="{{ Auth::id() }}"
|
data-user-id="{{ Auth::id() }}"
|
||||||
data-display-name="{{ Auth::user()->name ?? '' }}"
|
data-display-name="{{ Auth::user()->name ?? '' }}"
|
||||||
data-username="{{ Auth::user()->username ?? '' }}"
|
data-username="{{ Auth::user()->username ?? '' }}"
|
||||||
data-avatar-url="{{ \App\Support\AvatarUrl::forUser((int) Auth::id(), optional(Auth::user()->profile)->avatar_hash, 64) }}"
|
data-avatar-url="{{ \App\Support\AvatarUrl::forUser((int) Auth::id(), optional(Auth::user()->profile)->avatar_hash, 64) }}"
|
||||||
data-upload-url="{{ Route::has('upload') ? route('upload') : '/upload' }}"
|
data-upload-url="{{ Route::has('upload') ? route('upload') : '/upload' }}"
|
||||||
@endauth
|
@endif
|
||||||
></div>
|
></div>
|
||||||
@include('layouts.nova.toolbar')
|
@include('layouts.nova.toolbar')
|
||||||
<main class="flex-1 @yield('main-class', 'pt-16')">
|
<main class="flex-1 @yield('main-class', 'pt-16')">
|
||||||
@@ -259,9 +263,9 @@
|
|||||||
|
|
||||||
{{-- Toast notifications (Alpine) --}}
|
{{-- Toast notifications (Alpine) --}}
|
||||||
@php
|
@php
|
||||||
$toastMessage = session('status') ?? session('error') ?? null;
|
$toastMessage = $skinbaseCanUseSession ? (session('status') ?? session('error') ?? null) : null;
|
||||||
$toastType = session('error') ? 'error' : 'success';
|
$toastType = $skinbaseCanUseSession && session('error') ? 'error' : 'success';
|
||||||
$toastBorder = session('error') ? 'border-red-500' : 'border-green-500';
|
$toastBorder = $skinbaseCanUseSession && session('error') ? 'border-red-500' : 'border-green-500';
|
||||||
@endphp
|
@endphp
|
||||||
@if($toastMessage)
|
@if($toastMessage)
|
||||||
<div x-data="{show:true}" x-show="show" x-init="setTimeout(()=>show=false,4000)" x-cloak
|
<div x-data="{show:true}" x-show="show" x-init="setTimeout(()=>show=false,4000)" x-cloak
|
||||||
@@ -269,7 +273,7 @@
|
|||||||
<div class="max-w-sm w-full rounded-lg shadow-lg overflow-hidden bg-nova-600 border {{ $toastBorder }}">
|
<div class="max-w-sm w-full rounded-lg shadow-lg overflow-hidden bg-nova-600 border {{ $toastBorder }}">
|
||||||
<div class="px-4 py-3 flex items-start gap-3">
|
<div class="px-4 py-3 flex items-start gap-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@if(session('error'))
|
@if($skinbaseCanUseSession && session('error'))
|
||||||
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/></svg>
|
<svg class="w-6 h-6 text-red-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/></svg>
|
||||||
@else
|
@else
|
||||||
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
<svg class="w-6 h-6 text-green-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
@php
|
||||||
|
$skinbaseCanUseSession = ($skinbaseCanUseSession ?? false) === true;
|
||||||
|
$skinbaseToolbarUser = $skinbaseCanUseSession ? Auth::user() : null;
|
||||||
|
$skinbaseToolbarCanAuth = $skinbaseToolbarUser !== null;
|
||||||
|
@endphp
|
||||||
|
|
||||||
<header id="nova-toolbar" class="fixed inset-x-0 top-0 z-50 h-16 bg-black/40 backdrop-blur border-b border-white/10">
|
<header id="nova-toolbar" class="fixed inset-x-0 top-0 z-50 h-16 bg-black/40 backdrop-blur border-b border-white/10">
|
||||||
<div class="mx-auto w-full h-full px-3 sm:px-4 flex items-center gap-2 sm:gap-3">
|
<div class="mx-auto w-full h-full px-3 sm:px-4 flex items-center gap-2 sm:gap-3">
|
||||||
|
|
||||||
@@ -84,11 +90,11 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('worlds.index') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('worlds.index') }}">
|
||||||
<i class="fa-solid fa-globe w-4 text-center text-sb-muted"></i>Worlds
|
<i class="fa-solid fa-globe w-4 text-center text-sb-muted"></i>Worlds
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('discover.for-you') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('discover.for-you') }}">
|
||||||
<i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You
|
<i class="fa-solid fa-wand-magic-sparkles w-4 text-center"></i>For You
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,11 +175,11 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/stories">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/stories">
|
||||||
<i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories
|
<i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.following') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.following') }}">
|
||||||
<i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following
|
<i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,7 +195,7 @@
|
|||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('community.activity') }}">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('community.activity') }}">
|
||||||
<i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed
|
<i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.comments.received') }}">
|
<a class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="{{ route('dashboard.comments.received') }}">
|
||||||
<span class="flex items-center gap-3">
|
<span class="flex items-center gap-3">
|
||||||
<i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments
|
<i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments
|
||||||
@@ -198,7 +204,7 @@
|
|||||||
<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>
|
<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endif
|
||||||
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/forum">
|
<a class="flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5" href="/forum">
|
||||||
<i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum
|
<i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum
|
||||||
</a>
|
</a>
|
||||||
@@ -245,7 +251,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<!-- Notification icons -->
|
<!-- Notification icons -->
|
||||||
<div class="hidden md:flex items-center gap-0.5 lg:gap-1 text-soft shrink-0">
|
<div class="hidden md:flex items-center gap-0.5 lg:gap-1 text-soft shrink-0">
|
||||||
<a href="{{ route('dashboard.favorites') }}"
|
<a href="{{ route('dashboard.favorites') }}"
|
||||||
@@ -462,7 +468,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -470,9 +476,9 @@
|
|||||||
<div class="hidden fixed inset-x-0 top-16 bottom-0 z-40 overflow-y-auto overscroll-contain bg-nova border-b border-panel p-4 shadow-sb" id="mobileMenu">
|
<div class="hidden fixed inset-x-0 top-16 bottom-0 z-40 overflow-y-auto overscroll-contain bg-nova border-b border-panel p-4 shadow-sb" id="mobileMenu">
|
||||||
<div class="space-y-0.5 text-sm text-soft">
|
<div class="space-y-0.5 text-sm text-soft">
|
||||||
|
|
||||||
@guest
|
@if(! $skinbaseToolbarCanAuth)
|
||||||
<div class="my-2 border-t border-panel"></div>
|
<div class="my-2 border-t border-panel"></div>
|
||||||
@endguest
|
@endif
|
||||||
|
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<button type="button" data-mobile-section-toggle aria-controls="mobileSectionDiscover" aria-expanded="true" class="w-full flex items-center justify-between py-2.5 px-3 rounded-lg text-[11px] font-semibold uppercase tracking-widest text-sb-muted hover:bg-white/5">
|
<button type="button" data-mobile-section-toggle aria-controls="mobileSectionDiscover" aria-expanded="true" class="w-full flex items-center justify-between py-2.5 px-3 rounded-lg text-[11px] font-semibold uppercase tracking-widest text-sb-muted hover:bg-white/5">
|
||||||
@@ -533,10 +539,10 @@
|
|||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/leaderboard"><i class="fa-solid fa-trophy w-4 text-center text-sb-muted"></i>Leaderboard</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/leaderboard"><i class="fa-solid fa-trophy w-4 text-center text-sb-muted"></i>Leaderboard</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/creators/rising"><i class="fa-solid fa-arrow-trend-up w-4 text-center text-sb-muted"></i>Rising Creators</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/creators/rising"><i class="fa-solid fa-arrow-trend-up w-4 text-center text-sb-muted"></i>Rising Creators</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/stories"><i class="fa-solid fa-microphone w-4 text-center text-sb-muted"></i>Creator Stories</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('creator.stories.index') }}"><i class="fa-solid fa-rectangle-list w-4 text-center text-sb-muted"></i>My Stories</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.following') }}"><i class="fa-solid fa-user-plus w-4 text-center text-sb-muted"></i>Following</a>
|
||||||
@endauth
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -547,9 +553,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="mobileSectionCommunity" data-mobile-section-panel class="hidden mt-0.5 space-y-0.5">
|
<div id="mobileSectionCommunity" data-mobile-section-panel class="hidden mt-0.5 space-y-0.5">
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('community.activity') }}"><i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('community.activity') }}"><i class="fa-solid fa-wave-square w-4 text-center text-sb-muted"></i>Activity Feed</a>
|
||||||
@auth
|
@if($skinbaseToolbarCanAuth)
|
||||||
<a class="flex items-center justify-between gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.comments.received') }}"><span class="flex items-center gap-3"><i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments</span>@if(($receivedCommentsCount ?? 0) > 0)<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>@endif</a>
|
<a class="flex items-center justify-between gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="{{ route('dashboard.comments.received') }}"><span class="flex items-center gap-3"><i class="fa-solid fa-inbox w-4 text-center text-sb-muted"></i>Received Comments</span>@if(($receivedCommentsCount ?? 0) > 0)<span class="rounded-full border border-cyan-400/25 bg-cyan-500/10 px-2 py-0.5 text-[11px] font-semibold text-cyan-200">{{ $receivedCommentsCount }}</span>@endif</a>
|
||||||
@endauth
|
@endif
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/forum"><i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/forum"><i class="fa-solid fa-comments w-4 text-center text-sb-muted"></i>Forum</a>
|
||||||
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/news"><i class="fa-solid fa-newspaper w-4 text-center text-sb-muted"></i>News</a>
|
<a class="flex items-center gap-3 py-2.5 px-3 rounded-lg hover:bg-white/5" href="/news"><i class="fa-solid fa-newspaper w-4 text-center text-sb-muted"></i>News</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
@section('title', 'Messages')
|
@section('title', 'Messages')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/moderation.jsx'])
|
@vite(['resources/js/moderation.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-moderation main { padding-top: 4rem; }
|
body.page-moderation main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/settings.jsx'])
|
@vite(['resources/js/settings.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-settings main { padding-top: 4rem; }
|
body.page-settings main { padding-top: 4rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
@vite(['resources/js/studio.jsx'])
|
@vite(['resources/js/studio.jsx'])
|
||||||
<style>
|
<style>
|
||||||
body.page-studio main { padding-top: 2.3rem; }
|
body.page-studio main { padding-top: 2.3rem; }
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
@extends('layouts.nova')
|
@extends('layouts.nova')
|
||||||
|
|
||||||
@push('head')
|
@push('head')
|
||||||
|
@if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'))
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||||
|
@endif
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.SKINBASE_FLAGS = Object.assign({}, window.SKINBASE_FLAGS || {}, {
|
window.SKINBASE_FLAGS = Object.assign({}, window.SKINBASE_FLAGS || {}, {
|
||||||
|
|||||||
118
tests/Feature/Http/ConditionalPublicSessionsTest.php
Normal file
118
tests/Feature/Http/ConditionalPublicSessionsTest.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\Http;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ConditionalPublicSessionsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Config::set('skinbase-sessions.enabled', true);
|
||||||
|
Config::set('skinbase-sessions.debug_header', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_anonymous_public_get_does_not_receive_session_cookie(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'skipped');
|
||||||
|
|
||||||
|
$this->assertFalse($this->responseHasSessionCookie($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_bot_public_get_does_not_receive_session_cookie(): void
|
||||||
|
{
|
||||||
|
$response = $this
|
||||||
|
->withHeader('User-Agent', 'Googlebot/2.1')
|
||||||
|
->get('/');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'skipped');
|
||||||
|
|
||||||
|
$this->assertFalse($this->responseHasSessionCookie($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_login_page_still_starts_session(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/login');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_request_still_starts_session(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/dashboard');
|
||||||
|
|
||||||
|
$response->assertRedirect('/login');
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_studio_request_still_starts_session(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/studio');
|
||||||
|
|
||||||
|
$response->assertRedirect('/login');
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_request_still_starts_session(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/settings/profile');
|
||||||
|
|
||||||
|
$response->assertRedirect('/login');
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_messages_request_still_starts_session(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/messages');
|
||||||
|
|
||||||
|
$response->assertRedirect('/login');
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_login_post_keeps_normal_session_behavior(): void
|
||||||
|
{
|
||||||
|
$response = $this->post('/login', [
|
||||||
|
'email' => 'missing@example.test',
|
||||||
|
'password' => 'not-the-right-password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect();
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_authenticated_user_keeps_session_on_public_page(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$sessionCookieName = (string) config('session.cookie');
|
||||||
|
|
||||||
|
$response = $this
|
||||||
|
->withCookie($sessionCookieName, 'existing-session-cookie')
|
||||||
|
->actingAs($user)
|
||||||
|
->get('/');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('X-Skinbase-Session', 'started');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function responseHasSessionCookie($response): bool
|
||||||
|
{
|
||||||
|
$sessionCookieName = (string) config('session.cookie');
|
||||||
|
|
||||||
|
return collect($response->headers->getCookies())
|
||||||
|
->contains(fn ($cookie): bool => $cookie->getName() === $sessionCookieName);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user