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.
112 lines
3.8 KiB
PHP
112 lines
3.8 KiB
PHP
<?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);
|
|
}
|
|
}
|
|
|