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:
32
TASKS.md
32
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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
78
website/app/Http/Middleware/ScreenshotAuthMiddleware.php
Normal file
78
website/app/Http/Middleware/ScreenshotAuthMiddleware.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
104
website/tests/Feature/ScreenshotAuthTest.php
Normal file
104
website/tests/Feature/ScreenshotAuthTest.php
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user