diff --git a/.deploy/artwork-evolution-release/.env.example b/.deploy/artwork-evolution-release/.env.example index fea5f577..e88813b6 100644 --- a/.deploy/artwork-evolution-release/.env.example +++ b/.deploy/artwork-evolution-release/.env.example @@ -41,6 +41,14 @@ SESSION_ENCRYPT=false SESSION_PATH=/ 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 FILESYSTEM_DISK=local QUEUE_CONNECTION=redis diff --git a/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalShareErrorsFromSession.php b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalShareErrorsFromSession.php new file mode 100644 index 00000000..1666082d --- /dev/null +++ b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalShareErrorsFromSession.php @@ -0,0 +1,25 @@ +attributes->get('skinbase.session_skipped') === true || ! $request->hasSession()) { + return $next($request); + } + + return parent::handle($request, $next); + } +} \ No newline at end of file diff --git a/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalStartSession.php b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalStartSession.php new file mode 100644 index 00000000..d1a9e3c0 --- /dev/null +++ b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalStartSession.php @@ -0,0 +1,122 @@ +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; + } +} \ No newline at end of file diff --git a/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalValidateCsrfToken.php b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalValidateCsrfToken.php new file mode 100644 index 00000000..0112ae5e --- /dev/null +++ b/.deploy/artwork-evolution-release/app/Http/Middleware/ConditionalValidateCsrfToken.php @@ -0,0 +1,21 @@ +attributes->get('skinbase.session_skipped') === true) { + return $next($request); + } + + return parent::handle($request, $next); + } +} \ No newline at end of file diff --git a/.deploy/artwork-evolution-release/app/Http/Middleware/HandleInertiaRequests.php b/.deploy/artwork-evolution-release/app/Http/Middleware/HandleInertiaRequests.php index 86f3f3ea..dd870222 100644 --- a/.deploy/artwork-evolution-release/app/Http/Middleware/HandleInertiaRequests.php +++ b/.deploy/artwork-evolution-release/app/Http/Middleware/HandleInertiaRequests.php @@ -12,6 +12,15 @@ final class HandleInertiaRequests extends Middleware { 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. */ @@ -58,13 +67,16 @@ final class HandleInertiaRequests extends Middleware public function share(Request $request): array { + $canReadSessionAuth = $this->canReadSessionAuth($request); + $user = $canReadSessionAuth ? $request->user() : null; + return array_merge(parent::share($request), [ 'auth' => [ - 'user' => $request->user() ? [ - 'id' => $request->user()->id, - 'name' => $request->user()->name, - 'is_admin' => $request->user()->isAdmin(), - 'is_moderator' => $request->user()->isModerator(), + 'user' => $user ? [ + 'id' => $user->id, + 'name' => $user->name, + 'is_admin' => $user->isAdmin(), + 'is_moderator' => $user->isModerator(), ] : null, ], 'cdn' => [ @@ -84,8 +96,8 @@ final class HandleInertiaRequests extends Middleware 'group_assets' => (bool) config('features.group_assets', true), 'group_activity_feed' => (bool) config('features.group_activity_feed', true), ], - 'studio_groups' => $request->user() - ? app(GroupService::class)->studioOptionsForUser($request->user()) + 'studio_groups' => $user + ? app(GroupService::class)->studioOptionsForUser($user) : [], ]); } diff --git a/.deploy/artwork-evolution-release/app/Providers/AppServiceProvider.php b/.deploy/artwork-evolution-release/app/Providers/AppServiceProvider.php index 173ffa50..7c232247 100644 --- a/.deploy/artwork-evolution-release/app/Providers/AppServiceProvider.php +++ b/.deploy/artwork-evolution-release/app/Providers/AppServiceProvider.php @@ -151,6 +151,11 @@ class AppServiceProvider extends ServiceProvider $displayName = null; $userId = null; $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 { $toolbarContentTypes = $this->app @@ -162,8 +167,9 @@ class AppServiceProvider extends ServiceProvider $toolbarContentTypes = collect(); } - if (Auth::check()) { - $userId = Auth::id(); + if ($authUser) { + $authUser->loadMissing('profile'); + $userId = (int) $authUser->id; try { $uploadCount = DB::table('artworks')->where('user_id', $userId)->count(); } catch (\Throwable $e) { @@ -200,19 +206,18 @@ class AppServiceProvider extends ServiceProvider try { $receivedCommentsCount = $this->app->make(ReceivedCommentsInboxService::class) - ->unreadCountForUser(Auth::user()); + ->unreadCountForUser($authUser); } catch (\Throwable $e) { $receivedCommentsCount = 0; } try { - $profile = DB::table('user_profiles')->where('user_id', $userId)->first(); - $avatarHash = $profile->avatar_hash ?? null; + $avatarHash = $authUser->profile?->avatar_hash; } catch (\Throwable $e) { $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')); diff --git a/.deploy/artwork-evolution-release/bootstrap/app.php b/.deploy/artwork-evolution-release/bootstrap/app.php index 1e203fb6..f108a625 100644 --- a/.deploy/artwork-evolution-release/bootstrap/app.php +++ b/.deploy/artwork-evolution-release/bootstrap/app.php @@ -1,8 +1,14 @@ withRouting( @@ -13,6 +19,12 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->web(replace: [ + StartSession::class => ConditionalStartSession::class, + ShareErrorsFromSession::class => ConditionalShareErrorsFromSession::class, + ValidateCsrfToken::class => ConditionalValidateCsrfToken::class, + ]); + $middleware->validateCsrfTokens(except: [ 'chat_post', 'chat_post/*', diff --git a/.deploy/artwork-evolution-release/config/skinbase-sessions.php b/.deploy/artwork-evolution-release/config/skinbase-sessions.php new file mode 100644 index 00000000..f1c5e9eb --- /dev/null +++ b/.deploy/artwork-evolution-release/config/skinbase-sessions.php @@ -0,0 +1,124 @@ + 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', + ], +]; \ No newline at end of file diff --git a/.deploy/artwork-evolution-release/resources/views/artworks/show.blade.php b/.deploy/artwork-evolution-release/resources/views/artworks/show.blade.php index 2e1f8e04..e1ed3c3c 100644 --- a/.deploy/artwork-evolution-release/resources/views/artworks/show.blade.php +++ b/.deploy/artwork-evolution-release/resources/views/artworks/show.blade.php @@ -21,6 +21,7 @@ $comments = $comments ?? []; $groupSummary = $groupSummary ?? null; $useUnifiedSeo = true; + $canReadSessionAuth = request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped'); @endphp @push('head') @@ -49,7 +50,7 @@ data-canonical='@json($meta["canonical"])' data-comments='@json($comments)' data-group-summary='@json($groupSummary)' - data-is-authenticated='@json(auth()->check())'> + data-is-authenticated='@json($canReadSessionAuth && auth()->check())'> @vite(['resources/js/Pages/ArtworkPage.jsx']) diff --git a/.deploy/artwork-evolution-release/resources/views/collections.blade.php b/.deploy/artwork-evolution-release/resources/views/collections.blade.php index 672e42ec..31de4271 100644 --- a/.deploy/artwork-evolution-release/resources/views/collections.blade.php +++ b/.deploy/artwork-evolution-release/resources/views/collections.blade.php @@ -1,7 +1,9 @@ @extends('layouts.nova') @push('head') + @if(request()->hasSession() && ! request()->attributes->get('skinbase.session_skipped')) + @endif @vite(['resources/js/collections.jsx'])