- Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env

hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads,
attach packages, and surface localized success/error states.
- Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/
PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent
creation, webhooks, and the wizard CTA.
- Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/
useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages,
Checkout) with localized copy and experiment tracking.
- Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing
localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations.
- Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke
test for the hero CTA while reconciling outstanding checklist items.
This commit is contained in:
Codex Agent
2025-10-19 11:41:03 +02:00
parent ae9b9160ac
commit a949c8d3af
113 changed files with 5169 additions and 712 deletions

View File

@@ -5,7 +5,7 @@ namespace Tests\Feature;
use App\Models\OAuthClient;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Tests\TestCase;
@@ -60,8 +60,22 @@ KEY;
{
parent::setUp();
file_put_contents(storage_path('app/public.key'), self::PUBLIC_KEY);
file_put_contents(storage_path('app/private.key'), self::PRIVATE_KEY);
config()->set('oauth.keys.current_kid', 'test-kid');
config()->set('oauth.keys.storage_path', storage_path('app/oauth-keys-tests'));
$paths = $this->keyPaths('test-kid');
File::ensureDirectoryExists($paths['directory']);
File::put($paths['public'], self::PUBLIC_KEY);
File::put($paths['private'], self::PRIVATE_KEY);
File::chmod($paths['private'], 0600);
File::chmod($paths['public'], 0644);
}
protected function tearDown(): void
{
File::deleteDirectory(storage_path('app/oauth-keys-tests'));
parent::tearDown();
}
public function test_authorization_code_flow_and_refresh(): void
@@ -150,5 +164,121 @@ KEY;
'error' => 'Refresh token cannot be used from this IP address',
]);
}
public function test_refresh_token_ip_binding_can_be_disabled(): void
{
config()->set('oauth.refresh_tokens.enforce_ip_binding', false);
$tenant = Tenant::factory()->create([
'slug' => 'ip-free',
]);
OAuthClient::create([
'id' => (string) Str::uuid(),
'client_id' => 'tenant-admin-app',
'tenant_id' => $tenant->id,
'redirect_uris' => ['http://localhost/callback'],
'scopes' => ['tenant:read'],
'is_active' => true,
]);
$codeVerifier = 'unit-test-code-verifier-abcdef';
$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
$codeResponse = $this->get('/api/v1/oauth/authorize?' . http_build_query([
'client_id' => 'tenant-admin-app',
'redirect_uri' => 'http://localhost/callback',
'response_type' => 'code',
'scope' => 'tenant:read',
'state' => 'state',
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
]));
$location = $codeResponse->headers->get('Location');
parse_str(parse_url($location, PHP_URL_QUERY) ?? '', $query);
$code = $query['code'];
$tokenResponse = $this->post('/api/v1/oauth/token', [
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => 'tenant-admin-app',
'redirect_uri' => 'http://localhost/callback',
'code_verifier' => $codeVerifier,
]);
$token = $tokenResponse->json('refresh_token');
$this->withServerVariables(['REMOTE_ADDR' => '203.0.113.33'])
->post('/api/v1/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $token,
'client_id' => 'tenant-admin-app',
])
->assertOk();
}
public function test_refresh_token_allows_same_subnet_when_enabled(): void
{
config()->set('oauth.refresh_tokens.allow_subnet_match', true);
$tenant = Tenant::factory()->create([
'slug' => 'subnet-tenant',
]);
OAuthClient::create([
'id' => (string) Str::uuid(),
'client_id' => 'tenant-admin-app',
'tenant_id' => $tenant->id,
'redirect_uris' => ['http://localhost/callback'],
'scopes' => ['tenant:read'],
'is_active' => true,
]);
$codeVerifier = 'unit-test-code-verifier-subnet';
$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
$codeResponse = $this->get('/api/v1/oauth/authorize?' . http_build_query([
'client_id' => 'tenant-admin-app',
'redirect_uri' => 'http://localhost/callback',
'response_type' => 'code',
'scope' => 'tenant:read',
'state' => 'state',
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
]));
$location = $codeResponse->headers->get('Location');
parse_str(parse_url($location, PHP_URL_QUERY) ?? '', $query);
$code = $query['code'];
$tokenResponse = $this->withServerVariables(['REMOTE_ADDR' => '198.51.100.24'])->post('/api/v1/oauth/token', [
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => 'tenant-admin-app',
'redirect_uri' => 'http://localhost/callback',
'code_verifier' => $codeVerifier,
]);
$token = $tokenResponse->json('refresh_token');
$this->withServerVariables(['REMOTE_ADDR' => '198.51.100.55'])
->post('/api/v1/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $token,
'client_id' => 'tenant-admin-app',
])
->assertOk();
}
private function keyPaths(string $kid): array
{
$base = storage_path('app/oauth-keys-tests');
return [
'directory' => $base . DIRECTORY_SEPARATOR . $kid,
'public' => $base . DIRECTORY_SEPARATOR . $kid . DIRECTORY_SEPARATOR . 'public.key',
'private' => $base . DIRECTORY_SEPARATOR . $kid . DIRECTORY_SEPARATOR . 'private.key',
];
}
}