mirror of
https://github.com/dyzulk/trustlab-api.git
synced 2026-01-26 05:15:35 +07:00
feat: implement dual CDN strategy and archive management UI
This commit is contained in:
@@ -40,17 +40,35 @@ class RootCaApiController extends Controller
|
||||
try {
|
||||
$newData = $this->sslService->renewCaCertificate($certificate, $days);
|
||||
|
||||
$certificate->update([
|
||||
// 1. Unset 'is_latest' from all versions of this CA type/name
|
||||
CaCertificate::where('ca_type', $certificate->ca_type)
|
||||
->where('common_name', $certificate->common_name)
|
||||
->update(['is_latest' => false]);
|
||||
|
||||
// 2. Create NEW version record
|
||||
$newCertificate = CaCertificate::create([
|
||||
'ca_type' => $certificate->ca_type,
|
||||
'common_name' => $certificate->common_name,
|
||||
'organization' => $certificate->organization,
|
||||
'key_content' => $certificate->key_content, // Keep same private key for renewal
|
||||
'cert_content' => $newData['cert_content'],
|
||||
'serial_number' => $newData['serial_number'],
|
||||
'valid_from' => $newData['valid_from'],
|
||||
'valid_to' => $newData['valid_to'],
|
||||
'is_latest' => true,
|
||||
]);
|
||||
|
||||
// 3. Automatically sync the new version to CDN (Both latest and archive locations)
|
||||
$this->sslService->uploadPublicCertsOnly($newCertificate, 'both');
|
||||
$this->sslService->uploadIndividualInstallersOnly($newCertificate, 'both');
|
||||
|
||||
// 4. Update bundles
|
||||
$this->sslService->syncAllBundles();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Certificate renewed successfully.',
|
||||
'data' => $certificate
|
||||
'message' => 'Certificate renewed as a new version successfully.',
|
||||
'data' => $newCertificate
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
@@ -60,35 +78,37 @@ class RootCaApiController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function syncCrtOnly()
|
||||
public function syncCrtOnly(Request $request)
|
||||
{
|
||||
$this->authorizeAdminOrOwner();
|
||||
try {
|
||||
$mode = $request->input('mode', 'both');
|
||||
$certificates = CaCertificate::all();
|
||||
$count = 0;
|
||||
foreach ($certificates as $cert) {
|
||||
if ($this->sslService->uploadPublicCertsOnly($cert)) {
|
||||
if ($this->sslService->uploadPublicCertsOnly($cert, $mode)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} CRT files."]);
|
||||
return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} CRT files (Mode: {$mode})."]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Sync failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncInstallersOnly()
|
||||
public function syncInstallersOnly(Request $request)
|
||||
{
|
||||
$this->authorizeAdminOrOwner();
|
||||
try {
|
||||
$mode = $request->input('mode', 'both');
|
||||
$certificates = CaCertificate::all();
|
||||
$count = 0;
|
||||
foreach ($certificates as $cert) {
|
||||
if ($this->sslService->uploadIndividualInstallersOnly($cert)) {
|
||||
if ($this->sslService->uploadIndividualInstallersOnly($cert, $mode)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} installer sets."]);
|
||||
return response()->json(['status' => 'success', 'message' => "Successfully synced {$count} installer sets (Mode: {$mode})."]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Sync failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
@@ -107,23 +127,28 @@ class RootCaApiController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function syncToCdn()
|
||||
public function syncToCdn(Request $request)
|
||||
{
|
||||
$this->authorizeAdminOrOwner();
|
||||
$mode = $request->input('mode', 'both');
|
||||
|
||||
try {
|
||||
$certificates = CaCertificate::all();
|
||||
$count = 0;
|
||||
|
||||
foreach ($certificates as $cert) {
|
||||
if ($this->sslService->uploadToCdn($cert)) {
|
||||
if ($this->sslService->uploadPublicCertsOnly($cert, $mode)) {
|
||||
$this->sslService->uploadIndividualInstallersOnly($cert, $mode);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Also sync bundles (Always 'latest' as bundles are aggregate)
|
||||
$this->sslService->syncAllBundles();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => "Successfully synced everything ({$count} certs + bundles) to CDN."
|
||||
'message' => "Successfully synced everything ({$count} certs + bundles) to CDN (Mode: {$mode})."
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
@@ -133,6 +158,27 @@ class RootCaApiController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function promote(CaCertificate $certificate)
|
||||
{
|
||||
$this->authorizeAdminOrOwner();
|
||||
try {
|
||||
// 1. Unset 'is_latest' from all versions of this CA type/name
|
||||
CaCertificate::where('ca_type', $certificate->ca_type)
|
||||
->where('common_name', $certificate->common_name)
|
||||
->update(['is_latest' => false]);
|
||||
|
||||
// 2. Set this one as latest
|
||||
$certificate->update(['is_latest' => true]);
|
||||
|
||||
// 3. Promote on CDN
|
||||
$this->sslService->promoteToLatest($certificate);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => "Certificate version {$certificate->uuid} promoted to Latest successfully."]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Promotion failed: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeAdminOrOwner()
|
||||
{
|
||||
if (!auth()->user()->isAdminOrOwner()) {
|
||||
|
||||
@@ -23,6 +23,7 @@ class CaCertificate extends Model
|
||||
'organization',
|
||||
'valid_from',
|
||||
'valid_to',
|
||||
'is_latest',
|
||||
'cert_path',
|
||||
'der_path',
|
||||
'bat_path',
|
||||
@@ -36,6 +37,7 @@ class CaCertificate extends Model
|
||||
protected $casts = [
|
||||
'valid_from' => 'datetime',
|
||||
'valid_to' => 'datetime',
|
||||
'is_latest' => 'boolean',
|
||||
'last_synced_at' => 'datetime',
|
||||
'last_downloaded_at' => 'datetime',
|
||||
];
|
||||
|
||||
@@ -68,6 +68,7 @@ class OpenSslService
|
||||
'organization' => $rootDetails['subject']['O'] ?? null,
|
||||
'valid_from' => date('Y-m-d H:i:s', $rootDetails['validFrom_time_t']),
|
||||
'valid_to' => date('Y-m-d H:i:s', $rootDetails['validTo_time_t']),
|
||||
'is_latest' => true,
|
||||
]);
|
||||
|
||||
$this->uploadToCdn($ca);
|
||||
@@ -107,6 +108,7 @@ class OpenSslService
|
||||
'organization' => $int4096Details['subject']['O'] ?? null,
|
||||
'valid_from' => date('Y-m-d H:i:s', $int4096Details['validFrom_time_t']),
|
||||
'valid_to' => date('Y-m-d H:i:s', $int4096Details['validTo_time_t']),
|
||||
'is_latest' => true,
|
||||
]);
|
||||
|
||||
$this->uploadToCdn($ca4096);
|
||||
@@ -146,6 +148,7 @@ class OpenSslService
|
||||
'organization' => $int2048Details['subject']['O'] ?? null,
|
||||
'valid_from' => date('Y-m-d H:i:s', $int2048Details['validFrom_time_t']),
|
||||
'valid_to' => date('Y-m-d H:i:s', $int2048Details['validTo_time_t']),
|
||||
'is_latest' => true,
|
||||
]);
|
||||
|
||||
$this->uploadToCdn($ca2048);
|
||||
@@ -414,9 +417,15 @@ class OpenSslService
|
||||
/**
|
||||
* Generate Windows Installer (.bat)
|
||||
*/
|
||||
public function generateWindowsInstaller(CaCertificate $cert): string
|
||||
public function generateWindowsInstaller(CaCertificate $cert, bool $isArchive = false): string
|
||||
{
|
||||
$cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : url("/api/public/ca/{$cert->uuid}/download/pem");
|
||||
$slug = Str::slug($cert->common_name);
|
||||
if ($isArchive) {
|
||||
$cdnUrl = Storage::disk('r2-public')->url("ca/archives/{$cert->uuid}/{$slug}.crt");
|
||||
} else {
|
||||
$cdnUrl = Storage::disk('r2-public')->url("ca/{$slug}.crt");
|
||||
}
|
||||
|
||||
$typeLabel = $cert->ca_type === 'root' ? 'Root' : 'Intermediate';
|
||||
$store = $cert->ca_type === 'root' ? 'Root' : 'CA';
|
||||
|
||||
@@ -496,10 +505,16 @@ class OpenSslService
|
||||
/**
|
||||
* Generate Linux Installer (.sh)
|
||||
*/
|
||||
public function generateLinuxInstaller(CaCertificate $cert): string
|
||||
public function generateLinuxInstaller(CaCertificate $cert, bool $isArchive = false): string
|
||||
{
|
||||
$cdnUrl = $cert->cert_path ? Storage::disk('r2-public')->url($cert->cert_path) : url("/api/public/ca/{$cert->uuid}/download/pem");
|
||||
$filename = "trustlab-" . Str::slug($cert->common_name) . ".crt";
|
||||
$slug = Str::slug($cert->common_name);
|
||||
if ($isArchive) {
|
||||
$cdnUrl = Storage::disk('r2-public')->url("ca/archives/{$cert->uuid}/{$slug}.crt");
|
||||
} else {
|
||||
$cdnUrl = Storage::disk('r2-public')->url("ca/{$slug}.crt");
|
||||
}
|
||||
|
||||
$filename = "trustlab-" . $slug . ".crt";
|
||||
|
||||
return "#!/bin/bash\n" .
|
||||
"echo \"TrustLab - Installing CA Certificate: {$cert->common_name}\"\n" .
|
||||
@@ -544,36 +559,50 @@ class OpenSslService
|
||||
/**
|
||||
* Upload only PEM/DER (The CRT files) to CDN.
|
||||
*/
|
||||
public function uploadPublicCertsOnly(CaCertificate $cert)
|
||||
public function uploadPublicCertsOnly(CaCertificate $cert, string $mode = 'both')
|
||||
{
|
||||
$baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid;
|
||||
$pemFilename = $baseFilename . '.crt';
|
||||
$derFilename = $baseFilename . '.der';
|
||||
|
||||
// 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);
|
||||
$slug = Str::slug($cert->common_name);
|
||||
$paths = [];
|
||||
|
||||
Storage::disk('r2-public')->put($derFilename, $derContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'application/x-x509-ca-cert'
|
||||
]);
|
||||
if ($mode === 'archive' || $mode === 'both') {
|
||||
$paths[] = "ca/archives/{$cert->uuid}/{$slug}";
|
||||
}
|
||||
if ($mode === 'latest' || $mode === 'both') {
|
||||
$paths[] = "ca/{$slug}";
|
||||
}
|
||||
|
||||
foreach ($paths as $basePath) {
|
||||
$pemPath = $basePath . '.crt';
|
||||
$derPath = $basePath . '.der';
|
||||
|
||||
// 1. Upload PEM (.crt)
|
||||
Storage::disk('r2-public')->put($pemPath, $cert->cert_content, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'application/x-x509-ca-cert',
|
||||
'CacheControl' => 'no-cache, no-store, must-revalidate'
|
||||
]);
|
||||
|
||||
// 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($derPath, $derContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'application/x-x509-ca-cert',
|
||||
'CacheControl' => 'no-cache, no-store, must-revalidate'
|
||||
]);
|
||||
}
|
||||
|
||||
// Always point model paths to the 'latest' version for public UI
|
||||
$cert->update([
|
||||
'cert_path' => $pemFilename,
|
||||
'der_path' => $derFilename,
|
||||
'cert_path' => "ca/{$slug}.crt",
|
||||
'der_path' => "ca/{$slug}.der",
|
||||
'last_synced_at' => now()
|
||||
]);
|
||||
|
||||
@@ -583,52 +612,74 @@ class OpenSslService
|
||||
/**
|
||||
* Upload individual installers (SH, BAT, MAC) to CDN.
|
||||
*/
|
||||
public function uploadIndividualInstallersOnly(CaCertificate $cert)
|
||||
public function uploadIndividualInstallersOnly(CaCertificate $cert, string $mode = 'both')
|
||||
{
|
||||
$baseFilename = 'ca/' . Str::slug($cert->common_name) . '-' . $cert->uuid;
|
||||
$batFilename = $baseFilename . '.bat';
|
||||
$macFilename = $baseFilename . '.mobileconfig';
|
||||
$linuxFilename = $baseFilename . '.sh';
|
||||
|
||||
$slug = Str::slug($cert->common_name);
|
||||
$cacheControl = 'no-cache, no-store, must-revalidate';
|
||||
|
||||
$syncs = [];
|
||||
if ($mode === 'archive' || $mode === 'both') {
|
||||
$syncs[] = ['base' => "ca/archives/{$cert->uuid}/installers/trustlab-{$slug}", 'isArchive' => true];
|
||||
}
|
||||
if ($mode === 'latest' || $mode === 'both') {
|
||||
$syncs[] = ['base' => "ca/installers/trustlab-{$slug}", 'isArchive' => false];
|
||||
}
|
||||
|
||||
// 3. Generate and Upload Windows Installer (.bat)
|
||||
$batContent = $this->generateWindowsInstaller($cert);
|
||||
Storage::disk('r2-public')->delete($batFilename);
|
||||
Storage::disk('r2-public')->put($batFilename, $batContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'text/plain',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
foreach ($syncs as $sync) {
|
||||
$batPath = $sync['base'] . '.bat';
|
||||
$macPath = $sync['base'] . '.mobileconfig';
|
||||
$linuxPath = $sync['base'] . '.sh';
|
||||
|
||||
// 4. Generate and Upload macOS Profile (.mobileconfig)
|
||||
$macContent = $this->generateMacInstaller($cert);
|
||||
Storage::disk('r2-public')->delete($macFilename);
|
||||
Storage::disk('r2-public')->put($macFilename, $macContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'application/x-apple-aspen-config',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
// 3. Generate and Upload Windows Installer (.bat)
|
||||
$batContent = $this->generateWindowsInstaller($cert, $sync['isArchive']);
|
||||
Storage::disk('r2-public')->put($batPath, $batContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'text/plain',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
|
||||
// 5. Generate and Upload Linux Script (.sh)
|
||||
$linuxContent = $this->generateLinuxInstaller($cert);
|
||||
Storage::disk('r2-public')->delete($linuxFilename);
|
||||
Storage::disk('r2-public')->put($linuxFilename, $linuxContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'text/plain',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
// 4. Generate and Upload macOS Profile (.mobileconfig)
|
||||
$macContent = $this->generateMacInstaller($cert); // macOS profiles are self-contained
|
||||
Storage::disk('r2-public')->put($macPath, $macContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'application/x-apple-aspen-config',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
|
||||
// 5. Generate and Upload Linux Script (.sh)
|
||||
$linuxContent = $this->generateLinuxInstaller($cert, $sync['isArchive']);
|
||||
Storage::disk('r2-public')->put($linuxPath, $linuxContent, [
|
||||
'visibility' => 'public',
|
||||
'ContentType' => 'text/plain',
|
||||
'CacheControl' => $cacheControl
|
||||
]);
|
||||
}
|
||||
|
||||
$cert->update([
|
||||
'bat_path' => $batFilename,
|
||||
'mac_path' => $macFilename,
|
||||
'linux_path' => $linuxFilename,
|
||||
'bat_path' => "ca/installers/trustlab-{$slug}.bat",
|
||||
'mac_path' => "ca/installers/trustlab-{$slug}.mobileconfig",
|
||||
'linux_path' => "ca/installers/trustlab-{$slug}.sh",
|
||||
'last_synced_at' => now()
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote an archived certificate version to 'Latest' (public root)
|
||||
*/
|
||||
public function promoteToLatest(CaCertificate $cert)
|
||||
{
|
||||
// Simply re-sync this specific certificate version as 'latest'
|
||||
$this->uploadPublicCertsOnly($cert, 'latest');
|
||||
$this->uploadIndividualInstallersOnly($cert, 'latest');
|
||||
|
||||
// Also sync all bundles to ensure global installers are updated with this promoted version
|
||||
$this->syncAllBundles();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Global Bundles (Installer Sapujagat)
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ return new class extends Migration
|
||||
$table->string('bat_path')->nullable();
|
||||
$table->string('mac_path')->nullable();
|
||||
$table->string('linux_path')->nullable();
|
||||
$table->boolean('is_latest')->default(false);
|
||||
$table->timestamp('last_synced_at')->nullable();
|
||||
|
||||
$table->string('common_name')->nullable();
|
||||
@@ -58,6 +59,9 @@ return new class extends Migration
|
||||
if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'last_synced_at')) {
|
||||
$table->timestamp('last_synced_at')->nullable()->after('linux_path');
|
||||
}
|
||||
if (!Schema::connection('mysql_ca')->hasColumn('ca_certificates', 'is_latest')) {
|
||||
$table->boolean('is_latest')->default(false)->after('last_synced_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ Route::middleware(['auth:sanctum'])->group(function () {
|
||||
Route::post('/admin/ca-certificates/sync-installers', [RootCaApiController::class, 'syncInstallersOnly']);
|
||||
Route::post('/admin/ca-certificates/sync-bundles', [RootCaApiController::class, 'syncBundlesOnly']);
|
||||
Route::post('/admin/ca-certificates/{certificate}/renew', [RootCaApiController::class, 'renew']);
|
||||
Route::post('/admin/ca-certificates/{certificate}/promote', [RootCaApiController::class, 'promote']);
|
||||
|
||||
// API Keys Management
|
||||
Route::get('/api-keys', [ApiKeyController::class, 'index']);
|
||||
|
||||
Reference in New Issue
Block a user