- 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:
111
app/Console/Commands/OAuthRotateKeysCommand.php
Normal file
111
app/Console/Commands/OAuthRotateKeysCommand.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user