From a4cf7026bc17e2e1ba7b995f05110e416b9159d9d085d3e6d59bf08fe0903c5e Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 9 Feb 2026 16:52:15 -0500 Subject: [PATCH] Add screenshot auth middleware, remove SupportPal references Screenshot auth: dev-only middleware that authenticates headless Chrome via ?_screenshot_token= query param. Auto-selects admin/customer user by subdomain. Only active when APP_ENV=local or explicitly enabled. SupportPal cleanup: dropped supportpal_ticket_id column, removed env vars and Phase 7 task tracking. 7 new tests (151 total, 782 assertions). Co-Authored-By: Claude Opus 4.6 --- TASKS.md | 32 +----- website/.env.example | 6 +- .../Middleware/ScreenshotAuthMiddleware.php | 78 +++++++++++++ website/app/Models/SupportTicket.php | 2 - website/bootstrap/app.php | 6 + website/config/app.php | 15 +++ ...portpal_ticket_id_from_support_tickets.php | 26 +++++ website/tests/Feature/ScreenshotAuthTest.php | 104 ++++++++++++++++++ 8 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 website/app/Http/Middleware/ScreenshotAuthMiddleware.php create mode 100644 website/database/migrations/2026_02_09_213126_remove_supportpal_ticket_id_from_support_tickets.php create mode 100644 website/tests/Feature/ScreenshotAuthTest.php diff --git a/TASKS.md b/TASKS.md index fb07620..d6fc4c6 100644 --- a/TASKS.md +++ b/TASKS.md @@ -142,11 +142,6 @@ - [x] Tax ID - [x] Password change - [x] 2FA setup (TOTP, passkeys) -- [ ] SupportPal integration: - - [ ] SSO to SupportPal - - [ ] View recent tickets widget - - [ ] Create ticket button (opens SupportPal or API) - ## Phase 5: Admin Panel (admin.ezscale.cloud) - [x] Analytics dashboard: - [x] MRR (Monthly Recurring Revenue) graph @@ -224,31 +219,6 @@ - [ ] Total bandwidth by service type - [ ] Overage revenue report -## Phase 7: SupportPal Integration -- [ ] Implement SSO for SupportPal: - - [ ] Laravel Passport OAuth2 integration - - [ ] SupportPal SAML or custom SSO config - - [ ] Test seamless login flow -- [ ] Build SupportPal API integration: - - [ ] Fetch user's recent tickets - - [ ] Create ticket via API - - [ ] Fetch ticket details and replies -- [ ] Build webhook handlers for SupportPal: - - [ ] New ticket created - - [ ] Ticket reply added - - [ ] Ticket status changed - - [ ] Ticket closed -- [ ] Display tickets in customer dashboard: - - [ ] Recent tickets widget - - [ ] Link to full ticket in SupportPal -- [ ] Admin ticket overview: - - [ ] Open tickets count - - [ ] Tickets by priority - - [ ] Link to SupportPal admin -- [ ] Discord notifications for tickets: - - [ ] New ticket opened - - [ ] Ticket escalated (high priority) - ## Phase 8: Marketing Frontend (ezscale.cloud) - [x] Homepage: - [x] Hero section with value proposition @@ -321,7 +291,7 @@ - [ ] SynergyCP API integration - [ ] Enhance API integration - [ ] ElastiFlow API integration - - [ ] SupportPal API integration + - [ ] Security testing: - [ ] Penetration testing (OWASP Top 10) - [ ] Dependency vulnerability scanning diff --git a/website/.env.example b/website/.env.example index c63804d..77164dd 100644 --- a/website/.env.example +++ b/website/.env.example @@ -82,8 +82,8 @@ SYNERGYCP_API_KEY= ENHANCE_API_URL= ENHANCE_API_KEY= -# SupportPal -SUPPORTPAL_API_URL= -SUPPORTPAL_API_KEY= +# Screenshot Authentication (Dev/Local Only) +SCREENSHOT_AUTH_ENABLED=false +SCREENSHOT_TOKEN= VITE_APP_NAME="${APP_NAME}" diff --git a/website/app/Http/Middleware/ScreenshotAuthMiddleware.php b/website/app/Http/Middleware/ScreenshotAuthMiddleware.php new file mode 100644 index 0000000..9b2667f --- /dev/null +++ b/website/app/Http/Middleware/ScreenshotAuthMiddleware.php @@ -0,0 +1,78 @@ +isEnabled()) { + return $next($request); + } + + $token = $request->query('_screenshot_token'); + + if (! $token || ! is_string($token) || ! $this->isValidToken($token)) { + return $next($request); + } + + if (Auth::check()) { + return $next($request); + } + + $user = $this->getUserForDomain($request); + + if (! $user) { + return $next($request); + } + + Log::info('Screenshot authentication bypass used', [ + 'user_id' => $user->id, + 'user_email' => $user->email, + 'domain' => $request->getHost(), + 'path' => $request->path(), + 'ip' => $request->ip(), + ]); + + Auth::login($user); + + return $next($request); + } + + private function isEnabled(): bool + { + return config('app.env') === 'local' + || config('app.screenshot_auth_enabled', false); + } + + private function isValidToken(string $token): bool + { + $validToken = config('app.screenshot_token'); + + return $validToken && hash_equals($validToken, $token); + } + + private function getUserForDomain(Request $request): ?User + { + $host = $request->getHost(); + + if ($host === config('app.domains.admin')) { + return User::role('admin')->first(); + } + + if ($host === config('app.domains.account')) { + return User::role('customer')->first(); + } + + return User::role('admin')->first(); + } +} diff --git a/website/app/Models/SupportTicket.php b/website/app/Models/SupportTicket.php index e4992d8..0eff385 100644 --- a/website/app/Models/SupportTicket.php +++ b/website/app/Models/SupportTicket.php @@ -15,7 +15,6 @@ class SupportTicket extends Model protected $fillable = [ 'user_id', - 'supportpal_ticket_id', 'subject', 'status', 'priority', @@ -27,7 +26,6 @@ class SupportTicket extends Model protected function casts(): array { return [ - 'supportpal_ticket_id' => 'integer', 'last_reply_at' => 'datetime', ]; } diff --git a/website/bootstrap/app.php b/website/bootstrap/app.php index 5a4decd..d1459c9 100644 --- a/website/bootstrap/app.php +++ b/website/bootstrap/app.php @@ -38,8 +38,14 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, + \App\Http\Middleware\ScreenshotAuthMiddleware::class, ]); + $middleware->prependToPriorityList( + \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class, + \App\Http\Middleware\ScreenshotAuthMiddleware::class, + ); + $middleware->redirectGuestsTo(fn () => 'https://'.config('app.domains.account').'/login'); $middleware->redirectUsersTo('/dashboard'); diff --git a/website/config/app.php b/website/config/app.php index 2ce14a3..e4af86a 100644 --- a/website/config/app.php +++ b/website/config/app.php @@ -139,4 +139,19 @@ return [ 'admin' => env('DOMAIN_ADMIN', 'admin.ezscale.dev'), ], + /* + |-------------------------------------------------------------------------- + | Screenshot Authentication + |-------------------------------------------------------------------------- + | + | These values control the screenshot authentication bypass, which allows + | headless Chrome to access authenticated pages via a URL token. This + | should only be enabled in local/development environments. + | + */ + + 'screenshot_auth_enabled' => env('SCREENSHOT_AUTH_ENABLED', false), + + 'screenshot_token' => env('SCREENSHOT_TOKEN'), + ]; diff --git a/website/database/migrations/2026_02_09_213126_remove_supportpal_ticket_id_from_support_tickets.php b/website/database/migrations/2026_02_09_213126_remove_supportpal_ticket_id_from_support_tickets.php new file mode 100644 index 0000000..0e1d3bf --- /dev/null +++ b/website/database/migrations/2026_02_09_213126_remove_supportpal_ticket_id_from_support_tickets.php @@ -0,0 +1,26 @@ +dropUnique(['supportpal_ticket_id']); + $table->dropColumn('supportpal_ticket_id'); + }); + } + + public function down(): void + { + Schema::table('support_tickets', function (Blueprint $table): void { + $table->unsignedBigInteger('supportpal_ticket_id')->nullable()->unique()->after('user_id'); + }); + } +}; diff --git a/website/tests/Feature/ScreenshotAuthTest.php b/website/tests/Feature/ScreenshotAuthTest.php new file mode 100644 index 0000000..436b587 --- /dev/null +++ b/website/tests/Feature/ScreenshotAuthTest.php @@ -0,0 +1,104 @@ +seed(RoleAndPermissionSeeder::class); + $this->accountUrl = 'http://'.config('app.domains.account'); + $this->adminUrl = 'http://'.config('app.domains.admin'); + $this->validToken = 'test-screenshot-token-abc123'; +}); + +describe('Screenshot Auth Middleware', function (): void { + it('authenticates as admin on admin subdomain with valid token in local env', function (): void { + config([ + 'app.env' => 'local', + 'app.screenshot_token' => $this->validToken, + ]); + + $admin = User::factory()->admin()->create(); + + $this->get($this->adminUrl.'/dashboard?_screenshot_token='.$this->validToken) + ->assertOk(); + }); + + it('authenticates as customer on account subdomain with valid token in local env', function (): void { + config([ + 'app.env' => 'local', + 'app.screenshot_token' => $this->validToken, + ]); + + $customer = User::factory()->customer()->create(); + + $this->get($this->accountUrl.'/dashboard?_screenshot_token='.$this->validToken) + ->assertOk(); + }); + + it('does not authenticate with invalid token', function (): void { + config([ + 'app.env' => 'local', + 'app.screenshot_token' => $this->validToken, + ]); + + User::factory()->admin()->create(); + + $this->get($this->adminUrl.'/dashboard?_screenshot_token=wrong-token') + ->assertRedirect(); + }); + + it('does not authenticate in production env with screenshot auth disabled', function (): void { + config([ + 'app.env' => 'production', + 'app.screenshot_auth_enabled' => false, + 'app.screenshot_token' => $this->validToken, + ]); + + User::factory()->admin()->create(); + + $this->get($this->adminUrl.'/dashboard?_screenshot_token='.$this->validToken) + ->assertRedirect(); + }); + + it('does not authenticate when no token is in URL', function (): void { + config([ + 'app.env' => 'local', + 'app.screenshot_token' => $this->validToken, + ]); + + User::factory()->admin()->create(); + + $this->get($this->adminUrl.'/dashboard') + ->assertRedirect(); + }); + + it('does not affect already-authenticated user', function (): void { + config([ + 'app.env' => 'local', + 'app.screenshot_token' => $this->validToken, + ]); + + $admin = User::factory()->admin()->create(); + $customer = User::factory()->customer()->create(); + + // Already logged in as admin, token should not change that + $this->actingAs($admin) + ->get($this->adminUrl.'/dashboard?_screenshot_token='.$this->validToken) + ->assertOk(); + }); + + it('authenticates in production env when screenshot auth is explicitly enabled', function (): void { + config([ + 'app.env' => 'production', + 'app.screenshot_auth_enabled' => true, + 'app.screenshot_token' => $this->validToken, + ]); + + $admin = User::factory()->admin()->create(); + + $this->get($this->adminUrl.'/dashboard?_screenshot_token='.$this->validToken) + ->assertOk(); + }); +});