diff --git a/app/Http/Controllers/Api/PublicCaController.php b/app/Http/Controllers/Api/PublicCaController.php index b41cad7..041489e 100644 --- a/app/Http/Controllers/Api/PublicCaController.php +++ b/app/Http/Controllers/Api/PublicCaController.php @@ -27,6 +27,7 @@ class PublicCaController extends Controller 'serial' => $cert->serial_number, 'expires_at' => $cert->valid_to->toIso8601String(), 'cdn_url' => $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : null, + 'der_cdn_url' => $cert->der_path ? Storage::disk('r2-public')->url($cert->der_path) : null, ]; }); @@ -52,6 +53,11 @@ class PublicCaController extends Controller } if ($format === 'der') { + // Redirect to CDN if path exists and format is DER + if ($cert->der_path) { + return redirect()->away(Storage::disk('r2-public')->url($cert->der_path)); + } + // Convert PEM to DER (Base64 decode the body) $pem = $cert->cert_content; $lines = explode("\n", trim($pem)); diff --git a/app/Models/CaCertificate.php b/app/Models/CaCertificate.php index e1a8220..9ce9a34 100644 --- a/app/Models/CaCertificate.php +++ b/app/Models/CaCertificate.php @@ -24,6 +24,7 @@ class CaCertificate extends Model 'valid_from', 'valid_to', 'cert_path', + 'der_path', 'last_synced_at', 'download_count', 'last_downloaded_at' diff --git a/app/Services/OpenSslService.php b/app/Services/OpenSslService.php index b1ed1f3..869aa77 100644 --- a/app/Services/OpenSslService.php +++ b/app/Services/OpenSslService.php @@ -412,20 +412,39 @@ class OpenSslService } } /** - * Upload CA certificate (public) to R2 CDN. + * Upload CA certificate (public) to R2 CDN in both PEM and DER formats. */ public function uploadToCdn(CaCertificate $cert) { try { - $filename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid . '.crt'; + $baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid; + $pemFilename = $baseFilename . '.crt'; + $derFilename = $baseFilename . '.der'; - Storage::disk('r2-public')->put($filename, $cert->cert_content, [ + // 1. Upload PEM (.crt) + Storage::disk('r2-public')->put($pemFilename, $cert->cert_content, [ + 'visibility' => 'public', + 'ContentType' => 'application/x-x509-ca-cert' + ]); + + // 2. Convert to DER and Upload (.der) + $lines = explode("\n", trim($cert->cert_content)); + $payload = ''; + foreach ($lines as $line) { + if (!str_starts_with($line, '-----')) { + $payload .= trim($line); + } + } + $derContent = base64_decode($payload); + + Storage::disk('r2-public')->put($derFilename, $derContent, [ 'visibility' => 'public', 'ContentType' => 'application/x-x509-ca-cert' ]); $cert->update([ - 'cert_path' => $filename, + 'cert_path' => $pemFilename, + 'der_path' => $derFilename, 'last_synced_at' => now() ]); diff --git a/database/migrations/2025_12_23_123913_create_ca_certificates_table.php b/database/migrations/2025_12_23_123913_create_ca_certificates_table.php index 7083e35..8f516a0 100644 --- a/database/migrations/2025_12_23_123913_create_ca_certificates_table.php +++ b/database/migrations/2025_12_23_123913_create_ca_certificates_table.php @@ -11,23 +11,42 @@ return new class extends Migration */ public function up(): void { + // 1. Create table if not exists (Clean Slate for fresh install) if (!Schema::connection('mysql_ca')->hasTable('ca_certificates')) { Schema::connection('mysql_ca')->create('ca_certificates', function (Blueprint $table) { $table->string('uuid', 32)->primary(); - $table->string('ca_type'); // root, intermediate_4096, intermediate_2048 + $table->string('ca_type'); $table->longText('cert_content')->nullable(); $table->longText('key_content')->nullable(); $table->string('serial_number')->nullable(); + + // CDN Integration Columns + $table->string('cert_path')->nullable(); + $table->string('der_path')->nullable(); + $table->timestamp('last_synced_at')->nullable(); + $table->string('common_name')->nullable(); $table->string('organization')->nullable(); $table->dateTime('valid_from')->nullable(); $table->dateTime('valid_to')->nullable(); - // Tracking $table->unsignedBigInteger('download_count')->default(0); $table->timestamp('last_downloaded_at')->nullable(); $table->timestamps(); }); + } else { + // 2. Self-Healing: Add missing columns if table already exists (Production Sync) + Schema::connection('mysql_ca')->table('ca_certificates', function (Blueprint $table) { + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'cert_path')) { + $table->string('cert_path')->nullable()->after('serial_number'); + } + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'der_path')) { + $table->string('der_path')->nullable()->after('cert_path'); + } + if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'last_synced_at')) { + $table->timestamp('last_synced_at')->nullable()->after('der_path'); + } + }); } } diff --git a/database/migrations/2026_01_06_000002_add_cert_path_to_ca_certificates_table.php b/database/migrations/2026_01_06_000002_add_cert_path_to_ca_certificates_table.php deleted file mode 100644 index f47181b..0000000 --- a/database/migrations/2026_01_06_000002_add_cert_path_to_ca_certificates_table.php +++ /dev/null @@ -1,29 +0,0 @@ -table('ca_certificates', function (Blueprint $table) { - $table->string('cert_path')->nullable()->after('serial_number'); - $table->timestamp('last_synced_at')->nullable()->after('cert_path'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::connection('mysql_ca')->table('ca_certificates', function (Blueprint $table) { - $table->dropColumn(['cert_path', 'last_synced_at']); - }); - } -};