mirror of
https://github.com/twinpath/app.git
synced 2026-01-26 05:15:28 +07:00
368 lines
13 KiB
PHP
368 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use App\Models\Certificate;
|
|
use App\Models\CaCertificate;
|
|
use App\Services\OpenSslService;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Response;
|
|
use Illuminate\Support\Facades\Config;
|
|
use ZipArchive;
|
|
use Illuminate\Support\Str;
|
|
|
|
class CertificateController extends Controller
|
|
{
|
|
protected $sslService;
|
|
|
|
public function __construct(OpenSslService $sslService)
|
|
{
|
|
$this->sslService = $sslService;
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$caReady = CaCertificate::where('ca_type', 'root')->exists() &&
|
|
CaCertificate::where('ca_type', 'intermediate_2048')->exists() &&
|
|
CaCertificate::where('ca_type', 'intermediate_4096')->exists();
|
|
|
|
$perPage = $request->input('per_page', 10);
|
|
$search = $request->input('search');
|
|
|
|
$query = Certificate::where('user_id', Auth::id());
|
|
|
|
if ($search) {
|
|
$query->where(function($q) use ($search) {
|
|
$q->where('common_name', 'like', "%{$search}%")
|
|
->orWhere('serial_number', 'like', "%{$search}%")
|
|
->orWhere('san', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
$certificates = $query->latest()->paginate($perPage)->withQueryString();
|
|
|
|
if ($request->ajax()) {
|
|
return view('pages.certificate.partials.table', [
|
|
'certificates' => $certificates,
|
|
])->render();
|
|
}
|
|
|
|
return view('pages.certificate.index', [
|
|
'title' => 'Certificate Management',
|
|
'caReady' => $caReady,
|
|
'certificates' => $certificates,
|
|
'perPage' => $perPage,
|
|
'search' => $search,
|
|
'defaults' => Config::get('openssl.ca_leaf_default')
|
|
]);
|
|
}
|
|
|
|
public function downloadCa($type)
|
|
{
|
|
// map legacy or simple type to specific ca_type
|
|
$caType = match($type) {
|
|
'root' => 'root',
|
|
'intermediate', 'int_4096' => 'intermediate_4096',
|
|
'int_2048' => 'intermediate_2048',
|
|
default => $type
|
|
};
|
|
|
|
$ca = CaCertificate::where('ca_type', $caType)->firstOrFail();
|
|
|
|
$configKey = match($caType) {
|
|
'root' => 'openssl.ca_root.organizationName',
|
|
'intermediate_4096' => 'openssl.ca_4096.organizationName',
|
|
'intermediate_2048' => 'openssl.ca_2048.organizationName',
|
|
default => 'app.name'
|
|
};
|
|
|
|
$orgName = config($configKey);
|
|
$brand = Str::slug($orgName, '_');
|
|
$filename = "{$brand}_ca_{$type}.crt";
|
|
|
|
return response($ca->cert_content)
|
|
->header('Content-Type', 'application/x-x509-ca-cert')
|
|
->header('Content-Disposition', "attachment; filename={$filename}");
|
|
}
|
|
|
|
public function downloadCaBundle()
|
|
{
|
|
$root = CaCertificate::where('ca_type', 'root')->firstOrFail();
|
|
$int2048 = CaCertificate::where('ca_type', 'intermediate_2048')->firstOrFail();
|
|
$int4096 = CaCertificate::where('ca_type', 'intermediate_4096')->firstOrFail();
|
|
|
|
// Bundle includes all for convenience
|
|
$bundle = $int4096->cert_content . "\n" . $int2048->cert_content . "\n" . $root->cert_content;
|
|
|
|
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
|
|
$filename = "{$brand}_ca-bundle.crt";
|
|
|
|
return response($bundle)
|
|
->header('Content-Type', 'application/x-x509-ca-cert')
|
|
->header('Content-Disposition', "attachment; filename={$filename}");
|
|
}
|
|
|
|
public function downloadCaAndroid()
|
|
{
|
|
$root = CaCertificate::where('ca_type', 'root')->firstOrFail();
|
|
|
|
// Convert PEM to DER
|
|
$certPem = $root->cert_content;
|
|
$begin = "-----BEGIN CERTIFICATE-----";
|
|
$end = "-----END CERTIFICATE-----";
|
|
$der = base64_decode($certPem);
|
|
|
|
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
|
|
$filename = "{$brand}_root-ca.der";
|
|
|
|
return response($der)
|
|
->header('Content-Type', 'application/pkix-cert')
|
|
->header('Content-Disposition', "attachment; filename={$filename}");
|
|
}
|
|
|
|
public function downloadInstaller()
|
|
{
|
|
$appUrl = config('app.url');
|
|
$brand = Str::slug(config('openssl.ca_root.organizationName'), '_');
|
|
|
|
$script = <<<BATCH
|
|
@echo off
|
|
setlocal
|
|
|
|
:: One-Click Certificate Installer for {$brand}
|
|
:: Generated by {$appUrl}
|
|
|
|
set "SERVER_URL={$appUrl}"
|
|
set "TEMP_DIR=%TEMP%\\{$brand}_installer"
|
|
|
|
if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%"
|
|
cd /d "%TEMP_DIR%"
|
|
|
|
echo [1/5] Downloading Root CA...
|
|
curl -s -f -o "root.crt" "%SERVER_URL%/certificate/download-ca/root"
|
|
if %errorlevel% neq 0 (
|
|
echo Failed to download Root CA.
|
|
pause
|
|
exit /b %errorlevel%
|
|
)
|
|
|
|
echo [2/5] Downloading Intermediate CA 2048...
|
|
curl -s -f -o "int_2048.crt" "%SERVER_URL%/certificate/download-ca/int_2048"
|
|
if %errorlevel% neq 0 (
|
|
echo Failed to download Intermediate CA 2048.
|
|
pause
|
|
exit /b %errorlevel%
|
|
)
|
|
|
|
echo [3/5] Downloading Intermediate CA 4096...
|
|
curl -s -f -o "int_4096.crt" "%SERVER_URL%/certificate/download-ca/int_4096"
|
|
if %errorlevel% neq 0 (
|
|
echo Failed to download Intermediate CA 4096.
|
|
pause
|
|
exit /b %errorlevel%
|
|
)
|
|
|
|
echo [4/5] Installing Root CA to Trusted Root Certification Authorities...
|
|
certutil -user -addstore "Root" "root.crt"
|
|
|
|
echo [5/5] Installing Intermediate CAs to Intermediate Certification Authorities...
|
|
certutil -user -addstore "CA" "int_2048.crt"
|
|
certutil -user -addstore "CA" "int_4096.crt"
|
|
|
|
echo.
|
|
echo Cleanup...
|
|
cd ..
|
|
rmdir /s /q "%TEMP_DIR%"
|
|
|
|
echo.
|
|
echo ========================================================
|
|
echo Installation Complete!
|
|
echo You may need to restart your browser dynamically.
|
|
echo ========================================================
|
|
pause
|
|
BATCH;
|
|
|
|
return response($script)
|
|
->header('Content-Type', 'application/x-msdos-program')
|
|
->header('Content-Disposition', "attachment; filename={$brand}_install_certs.bat");
|
|
}
|
|
|
|
public function create()
|
|
{
|
|
return view('pages.certificate.create', [
|
|
'title' => 'Generate Certificate',
|
|
'defaults' => Config::get('openssl.ca_leaf_default')
|
|
]);
|
|
}
|
|
|
|
public function setupCa()
|
|
{
|
|
if (CaCertificate::count() > 0) {
|
|
return redirect()->route('certificate.index')->with('error', 'CA already initialized.');
|
|
}
|
|
|
|
if ($this->sslService->setupCa()) {
|
|
return redirect()->route('certificate.index')->with('success', 'Root and Intermediate CA successfully initialized.');
|
|
}
|
|
|
|
return redirect()->route('certificate.index')->with('error', 'Failed to initialize CA.');
|
|
}
|
|
|
|
public function generate(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'common_name' => 'required|string|max:255',
|
|
'config_mode' => 'required|in:default,manual',
|
|
'organization' => 'nullable|required_if:config_mode,manual|string|max:255',
|
|
'locality' => 'nullable|required_if:config_mode,manual|string|max:255',
|
|
'state' => 'nullable|required_if:config_mode,manual|string|max:255',
|
|
'country' => 'nullable|required_if:config_mode,manual|string|size:2',
|
|
'san' => 'nullable|string',
|
|
'key_bits' => 'required|in:2048,4096',
|
|
]);
|
|
|
|
try {
|
|
// Apply defaults if mode is 'default'
|
|
if ($validated['config_mode'] === 'default') {
|
|
$defaults = Config::get('openssl.ca_leaf_default');
|
|
$validated['organization'] = $defaults['organizationName'];
|
|
$validated['locality'] = $defaults['localityName'];
|
|
$validated['state'] = $defaults['stateOrProvinceName'];
|
|
$validated['country'] = $defaults['countryName'];
|
|
}
|
|
|
|
$result = $this->sslService->generateLeaf($validated);
|
|
|
|
Certificate::create([
|
|
'user_id' => Auth::id(),
|
|
'common_name' => $validated['common_name'],
|
|
'organization' => $validated['organization'],
|
|
'locality' => $validated['locality'],
|
|
'state' => $validated['state'],
|
|
'country' => $validated['country'],
|
|
'san' => $validated['san'],
|
|
'key_bits' => $validated['key_bits'],
|
|
'serial_number' => $this->sslService->formatSerialToHex($result['serial']),
|
|
'cert_content' => $result['cert'],
|
|
'key_content' => $result['key'],
|
|
'csr_content' => $result['csr'],
|
|
'valid_from' => $result['valid_from'] ?? null,
|
|
'valid_to' => $result['valid_to'] ?? null,
|
|
]);
|
|
|
|
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
|
|
|
|
return redirect()->route('certificate.index')->with('success', 'Certificate generated successfully.');
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->withInput()->with('error', $e->getMessage());
|
|
}
|
|
}
|
|
public function regenerate(Certificate $certificate)
|
|
{
|
|
$this->authorizeOwner($certificate);
|
|
|
|
try {
|
|
$data = [
|
|
'common_name' => $certificate->common_name,
|
|
'organization' => $certificate->organization,
|
|
'locality' => $certificate->locality,
|
|
'state' => $certificate->state,
|
|
'country' => $certificate->country,
|
|
'san' => $certificate->san,
|
|
'key_bits' => $certificate->key_bits,
|
|
];
|
|
|
|
$result = $this->sslService->generateLeaf($data);
|
|
|
|
// Update existing record with new content
|
|
$certificate->update([
|
|
'serial_number' => $this->sslService->formatSerialToHex($result['serial']),
|
|
'cert_content' => $result['cert'],
|
|
'key_content' => $result['key'],
|
|
'csr_content' => $result['csr'],
|
|
'valid_from' => $result['valid_from'] ?? null,
|
|
'valid_to' => $result['valid_to'] ?? null,
|
|
'created_at' => now(), // Refresh timestamp
|
|
]);
|
|
|
|
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
|
|
|
|
return redirect()->route('certificate.index')->with('success', 'Certificate regenerated successfully.');
|
|
} catch (\Exception $e) {
|
|
return redirect()->route('certificate.index')->with('error', 'Failed to regenerate: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
public function downloadZip(Certificate $certificate)
|
|
{
|
|
$this->authorizeOwner($certificate);
|
|
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'cert_zip');
|
|
$zip = new ZipArchive;
|
|
|
|
if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
|
|
return redirect()->back()->with('error', 'Failed to create ZIP archive.');
|
|
}
|
|
|
|
$filenameBase = preg_replace('/[^a-zA-Z0-9-_\.]/', '_', $certificate->common_name);
|
|
|
|
$zip->addFromString("{$filenameBase}.pem", $certificate->cert_content);
|
|
$zip->addFromString("{$filenameBase}.key", $certificate->key_content);
|
|
if ($certificate->csr_content) {
|
|
$zip->addFromString("{$filenameBase}.csr", $certificate->csr_content);
|
|
}
|
|
$zip->close();
|
|
|
|
return response()->download($tempFile, "cert_{$filenameBase}.zip")->deleteFileAfterSend(true);
|
|
}
|
|
|
|
public function downloadP12(Certificate $certificate)
|
|
{
|
|
$this->authorizeOwner($certificate);
|
|
|
|
$password = '112133'; // Default password from user request
|
|
if (openssl_pkcs12_export($certificate->cert_content, $p12, $certificate->key_content, $password)) {
|
|
$filenameBase = preg_replace('/[^a-zA-Z0-9-_\.]/', '_', $certificate->common_name);
|
|
return response($p12)
|
|
->header('Content-Type', 'application/x-pkcs12')
|
|
->header('Content-Disposition', "attachment; filename={$filenameBase}.p12");
|
|
}
|
|
|
|
return redirect()->back()->with('error', 'Failed to generate P12 file.');
|
|
}
|
|
|
|
public function viewFile(Certificate $certificate, $type)
|
|
{
|
|
$this->authorizeOwner($certificate);
|
|
|
|
$content = match($type) {
|
|
'cert' => $certificate->cert_content,
|
|
'key' => $certificate->key_content,
|
|
'csr' => $certificate->csr_content,
|
|
default => abort(404)
|
|
};
|
|
|
|
return response($content)->header('Content-Type', 'text/plain');
|
|
}
|
|
|
|
public function delete(Certificate $certificate)
|
|
{
|
|
$this->authorizeOwner($certificate);
|
|
$certificate->delete();
|
|
|
|
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
|
|
|
|
return redirect()->route('certificate.index')->with('success', 'Certificate deleted successfully.');
|
|
}
|
|
|
|
protected function authorizeOwner(Certificate $certificate)
|
|
{
|
|
if ($certificate->user_id !== Auth::id()) {
|
|
abort(403);
|
|
}
|
|
}
|
|
}
|