- 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

@@ -0,0 +1,111 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class OAuthRotateKeysCommand extends Command
{
protected $signature = 'oauth:rotate-keys {--kid=} {--force : Do not prompt for confirmation}';
protected $description = 'Generate a new JWT signing key pair for tenant OAuth tokens.';
public function handle(): int
{
$storage = rtrim(config('oauth.keys.storage_path', storage_path('app/oauth-keys')), DIRECTORY_SEPARATOR);
$currentKid = config('oauth.keys.current_kid', 'fotospiel-jwt');
$newKid = $this->option('kid') ?: 'kid-'.now()->format('YmdHis');
if (! $this->option('force') &&
! $this->confirm("Rotate JWT keys? Current kid: {$currentKid}. New kid: {$newKid}", true)
) {
$this->info('Rotation cancelled.');
return self::SUCCESS;
}
File::ensureDirectoryExists($storage);
$archiveDir = $this->archiveExistingKeys($storage, $currentKid);
$newDirectory = $storage.DIRECTORY_SEPARATOR.$newKid;
if (File::exists($newDirectory)) {
$this->error("Target directory already exists: {$newDirectory}");
return self::FAILURE;
}
File::makeDirectory($newDirectory, 0700, true);
$this->generateKeyPair($newDirectory);
$this->info('New signing keys generated.');
$this->line("Path: {$newDirectory}");
if ($archiveDir) {
$this->line("Previous keys archived at: {$archiveDir}");
}
$this->warn("Update OAUTH_JWT_KID in your environment configuration to: {$newKid}");
return self::SUCCESS;
}
private function archiveExistingKeys(string $storage, string $kid): ?string
{
$existingDir = $storage.DIRECTORY_SEPARATOR.$kid;
$legacyPublic = storage_path('app/public.key');
$legacyPrivate = storage_path('app/private.key');
if (File::exists($existingDir)) {
$archiveDir = $storage.DIRECTORY_SEPARATOR.'archive'.DIRECTORY_SEPARATOR.$kid.'-'.now()->format('YmdHis');
File::ensureDirectoryExists(dirname($archiveDir));
File::moveDirectory($existingDir, $archiveDir);
return $archiveDir;
}
if (File::exists($legacyPublic) || File::exists($legacyPrivate)) {
$archiveDir = $storage.DIRECTORY_SEPARATOR.'archive'.DIRECTORY_SEPARATOR.'legacy-'.now()->format('YmdHis');
File::ensureDirectoryExists($archiveDir);
if (File::exists($legacyPublic)) {
File::move($legacyPublic, $archiveDir.DIRECTORY_SEPARATOR.'public.key');
}
if (File::exists($legacyPrivate)) {
File::move($legacyPrivate, $archiveDir.DIRECTORY_SEPARATOR.'private.key');
}
return $archiveDir;
}
return null;
}
private function generateKeyPair(string $directory): void
{
$config = [
'digest_alg' => OPENSSL_ALGO_SHA256,
'private_key_bits' => 4096,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$resource = openssl_pkey_new($config);
if (! $resource) {
throw new \RuntimeException('Failed to generate key pair');
}
openssl_pkey_export($resource, $privateKey);
$details = openssl_pkey_get_details($resource);
$publicKey = $details['key'] ?? null;
if (! $publicKey) {
throw new \RuntimeException('Unable to extract public key');
}
File::put($directory.DIRECTORY_SEPARATOR.'private.key', $privateKey);
File::chmod($directory.DIRECTORY_SEPARATOR.'private.key', 0600);
File::put($directory.DIRECTORY_SEPARATOR.'public.key', $publicKey);
File::chmod($directory.DIRECTORY_SEPARATOR.'public.key', 0644);
}
}