Initial commit

This commit is contained in:
2025-12-22 12:03:01 +07:00
commit 10dc345147
367 changed files with 31188 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
<?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,
]);
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
]);
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();
return redirect()->route('certificate.index')->with('success', 'Certificate deleted successfully.');
}
protected function authorizeOwner(Certificate $certificate)
{
if ($certificate->user_id !== Auth::id()) {
abort(403);
}
}
}