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 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 16:52:15 -05:00
parent 6f39c32270
commit a4cf7026bc
8 changed files with 233 additions and 36 deletions

View File

@@ -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

View File

@@ -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}"

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class ScreenshotAuthMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! $this->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();
}
}

View File

@@ -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',
];
}

View File

@@ -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');

View File

@@ -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'),
];

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('support_tickets', function (Blueprint $table): void {
// Drop unique index first, then column
$table->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');
});
}
};

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
use App\Models\User;
use Database\Seeders\RoleAndPermissionSeeder;
beforeEach(function (): void {
$this->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();
});
});