mirror of
https://github.com/twinpath/app.git
synced 2026-01-26 13:21:59 +07:00
Initial commit
This commit is contained in:
160
resources/views/pages/certificate/create.blade.php
Normal file
160
resources/views/pages/certificate/create.blade.php
Normal file
@@ -0,0 +1,160 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<x-common.page-breadcrumb pageTitle="Generate SSL Certificate" />
|
||||
|
||||
<div class="max-w-3xl mx-auto mb-6">
|
||||
@if(session('error'))
|
||||
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400 border border-red-100 dark:border-red-900" role="alert">
|
||||
<span class="font-medium">Error!</span> {{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="max-w-3xl mx-auto" x-data="{
|
||||
common_name: '',
|
||||
config_mode: 'default',
|
||||
san: '',
|
||||
isValid() {
|
||||
return this.common_name.length > 3 && this.common_name.includes('.');
|
||||
}
|
||||
}">
|
||||
<x-common.component-card>
|
||||
<x-slot:header>
|
||||
<div class="mb-2">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Generate SSL Certificate</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 text-balance">Create a new self-signed certificate using your Local CA. This will be signed by your private root authority.</p>
|
||||
</div>
|
||||
</x-slot:header>
|
||||
|
||||
<form action="{{ route('certificate.generate') }}" method="POST" class="space-y-8"
|
||||
x-data="{
|
||||
common_name: '',
|
||||
config_mode: 'default',
|
||||
key_bits: '2048',
|
||||
san: '',
|
||||
isValid() {
|
||||
return this.common_name.length > 3 && this.common_name.includes('.');
|
||||
}
|
||||
}">
|
||||
@csrf
|
||||
|
||||
<!-- Configuration Mode -->
|
||||
<div>
|
||||
<label class="block mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">Configuration Mode</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label class="relative flex p-4 cursor-pointer rounded-xl border transition-all"
|
||||
:class="config_mode === 'default' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50/50 dark:bg-brand-900/10 dark:border-brand-400' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
||||
<input type="radio" name="config_mode" value="default" x-model="config_mode" class="sr-only">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center justify-center flex-shrink-0 w-5 h-5 mt-0.5 border rounded-full transition-colors"
|
||||
:class="config_mode === 'default' ? 'border-brand-500 bg-brand-500' : 'border-gray-300 dark:border-gray-600'">
|
||||
<div class="w-2 h-2 bg-white rounded-full" x-show="config_mode === 'default'"></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">Default Presets</span>
|
||||
<span class="block mt-0.5 text-xs text-gray-500 dark:text-gray-400">Use system defaults for Organization, Locality, and Country settings.</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex p-4 cursor-pointer rounded-xl border transition-all"
|
||||
:class="config_mode === 'manual' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50/50 dark:bg-brand-900/10 dark:border-brand-400' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
||||
<input type="radio" name="config_mode" value="manual" x-model="config_mode" class="sr-only">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center justify-center flex-shrink-0 w-5 h-5 mt-0.5 border rounded-full transition-colors"
|
||||
:class="config_mode === 'manual' ? 'border-brand-500 bg-brand-500' : 'border-gray-300 dark:border-gray-600'">
|
||||
<div class="w-2 h-2 bg-white rounded-full" x-show="config_mode === 'manual'"></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">Manual Configuration</span>
|
||||
<span class="block mt-0.5 text-xs text-gray-500 dark:text-gray-400">Customise all Distinguised Name (DN) attributes manually.</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Details -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Common Name (Domain) <span class="text-error-500">*</span></label>
|
||||
<input type="text" name="common_name" x-model="common_name" placeholder="e.g. example.com or e.g. 127.0.0.1" required
|
||||
class="w-full px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white transition-colors"
|
||||
:class="common_name && !isValid() ? 'border-error-500 focus:ring-error-500/20 focus:border-error-500' : ''">
|
||||
<p x-show="common_name && !isValid()" class="mt-1.5 text-xs text-error-500 animate-pulse">Please enter a valid domain name containing at least one dot.</p>
|
||||
<p class="mt-1.5 text-xs text-gray-500">The primary Fully Qualified Domain Name (FQDN) or IP Address to be secured.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Subject Alternative Names (SAN)</label>
|
||||
<input type="text" name="san" x-model="san" placeholder="e.g. api.local, 192.168.1.50"
|
||||
class="w-full px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white transition-colors">
|
||||
<p class="mt-1.5 text-xs text-gray-500">Optional comma-separated list of additional domains or IPs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Mode Fields -->
|
||||
<div x-show="config_mode === 'manual'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="p-5 border border-gray-100 rounded-xl bg-gray-50 dark:bg-gray-800/50 dark:border-gray-700/50">
|
||||
|
||||
<h4 class="mb-4 text-xs font-semibold tracking-wider text-gray-500 uppercase font-lexend">Distinguished Name Attributes</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Organization (O)</label>
|
||||
<input type="text" name="organization" value="{{ $defaults['organizationName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Country (C)</label>
|
||||
<input type="text" name="country" value="{{ $defaults['countryName'] ?? 'ID' }}" maxlength="2"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">State / Province (ST)</label>
|
||||
<input type="text" name="state" value="{{ $defaults['stateOrProvinceName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Locality (L)</label>
|
||||
<input type="text" name="locality" value="{{ $defaults['localityName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Size -->
|
||||
<div>
|
||||
<label class="block mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">Private Key Size</label>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="key_bits" value="2048" x-model="key_bits" class="peer sr-only">
|
||||
<div class="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg peer-checked:bg-brand-50 peer-checked:text-brand-700 peer-checked:border-brand-500 hover:bg-gray-50 transition-all dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:peer-checked:bg-brand-900/30 dark:peer-checked:text-brand-300 dark:peer-checked:border-brand-500">
|
||||
2048 Bit (Standard)
|
||||
</div>
|
||||
</label>
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="key_bits" value="4096" x-model="key_bits" class="peer sr-only">
|
||||
<div class="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg peer-checked:bg-brand-50 peer-checked:text-brand-700 peer-checked:border-brand-500 hover:bg-gray-50 transition-all dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:peer-checked:bg-brand-900/30 dark:peer-checked:text-brand-300 dark:peer-checked:border-brand-500">
|
||||
4096 Bit (High Security)
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end pt-6 space-x-3 border-t border-gray-100 dark:border-gray-800">
|
||||
<a href="{{ route('certificate.index') }}" class="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition shadow-sm">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" :disabled="!isValid()"
|
||||
class="px-6 py-2.5 text-sm font-semibold text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Generate Certificate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-common.component-card>
|
||||
</div>
|
||||
@endsection
|
||||
210
resources/views/pages/certificate/index.blade.php
Normal file
210
resources/views/pages/certificate/index.blade.php
Normal file
@@ -0,0 +1,210 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div x-data="{ createOpen: {{ $errors->any() ? 'true' : 'false' }} }">
|
||||
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
|
||||
Certificate Management
|
||||
</h2>
|
||||
<nav class="mt-1">
|
||||
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
<li>
|
||||
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</li>
|
||||
<li class="font-medium text-gray-800 dark:text-white/90">Certificate Management</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
@if($caReady)
|
||||
<button @click="createOpen = true" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
Generate New SSL
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$caReady)
|
||||
<div class="p-6 mb-6 border border-yellow-200 rounded-xl bg-yellow-50 dark:bg-yellow-900/10 dark:border-yellow-900/30">
|
||||
<div class="flex items-start">
|
||||
<div class="p-3 bg-yellow-100 rounded-lg dark:bg-yellow-900/30">
|
||||
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-bold text-yellow-800 dark:text-yellow-200">Setup Required</h4>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">Root CA and Intermediate CA have not been initialized in the database yet.</p>
|
||||
|
||||
@if(Auth::user()->isAdmin())
|
||||
<form action="{{ route('admin.setup-ca') }}" method="POST" class="mt-4">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-6 py-2.5 text-sm font-semibold text-white transition rounded-lg bg-yellow-600 hover:bg-yellow-700 shadow-theme-xs">
|
||||
Run CA Setup Now
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div class="mt-4 p-3 bg-yellow-200/50 rounded text-sm text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200">
|
||||
<strong>Action Required:</strong> Please contact your administrator to initialize the Root CA.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="space-y-6" x-data="{
|
||||
search: '{{ $search }}',
|
||||
perPage: '{{ $perPage }}',
|
||||
loading: false,
|
||||
updateTable(url = null) {
|
||||
this.loading = true;
|
||||
let baseUrl = url || '{{ route('certificate.index') }}';
|
||||
let finalUrl = new URL(baseUrl);
|
||||
|
||||
if (!url) {
|
||||
finalUrl.searchParams.set('search', this.search);
|
||||
finalUrl.searchParams.set('per_page', this.perPage);
|
||||
}
|
||||
|
||||
fetch(finalUrl, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(html => {
|
||||
let container = document.getElementById('certificate-table-container');
|
||||
container.innerHTML = html;
|
||||
this.loading = false;
|
||||
if (typeof Alpine !== 'undefined') {
|
||||
Alpine.initTree(container);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching content:', err);
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// View Modal Logic
|
||||
viewOpen: false,
|
||||
viewTitle: '',
|
||||
viewContent: '',
|
||||
viewUrl: '',
|
||||
loading: false,
|
||||
copied: false,
|
||||
|
||||
openViewModal(url, title) {
|
||||
this.viewUrl = url;
|
||||
this.viewTitle = title;
|
||||
this.viewContent = '';
|
||||
this.viewOpen = true;
|
||||
this.loading = true;
|
||||
this.copied = false;
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
this.viewContent = text;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching content:', err);
|
||||
this.viewContent = 'Failed to load content.';
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.viewContent).then(() => {
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 2000);
|
||||
});
|
||||
}
|
||||
}">
|
||||
<x-common.component-card>
|
||||
<x-slot:header>
|
||||
<h3 class="flex items-center text-base font-medium text-gray-800 dark:text-white/90">
|
||||
<svg class="w-5 h-5 mr-2.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
|
||||
SSL Certificates List
|
||||
</h3>
|
||||
</x-slot:header>
|
||||
<!-- DataTables Header Utility -->
|
||||
<div class="flex flex-col gap-4 mb-6 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-gray-500">Show</span>
|
||||
<select x-model="perPage" class="px-3 py-1.5 text-sm border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-brand-500">
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span class="text-sm text-gray-500">entries</span>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full md:w-64">
|
||||
<input type="text" x-model="search" placeholder="Search certificates..."
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-brand-500 focus:border-brand-500">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<svg x-show="!loading" class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<svg x-show="loading" class="w-4 h-4 text-brand-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="certificate-table-container" class="relative">
|
||||
@include('pages.certificate.partials.table')
|
||||
</div>
|
||||
</x-common.component-card>
|
||||
|
||||
@if($caReady)
|
||||
<x-common.component-card>
|
||||
<x-slot:header>
|
||||
<h3 class="flex items-center text-base font-medium text-gray-800 dark:text-white/90">
|
||||
<svg class="w-5 h-5 mr-2.5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
||||
Download Root CA Certificates
|
||||
</h3>
|
||||
</x-slot:header>
|
||||
<p class="text-sm text-gray-500 mb-6">These are the authority certificates used to sign your SSLs. Install them on your machine/browser to trust your generated certificates.</p>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a href="{{ route('certificate.download-ca', 'root') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-brand-700 bg-brand-50 rounded-lg hover:bg-brand-100 transition dark:bg-brand-900/20 dark:text-brand-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
Root CA (.crt)
|
||||
</a>
|
||||
<a href="{{ route('certificate.download-ca', 'int_2048') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition dark:bg-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
Int-2048 CA (.crt)
|
||||
</a>
|
||||
<a href="{{ route('certificate.download-ca', 'int_4096') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition dark:bg-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
Int-4096 CA (.crt)
|
||||
</a>
|
||||
<a href="{{ route('certificate.download-ca-bundle') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-success-700 bg-success-50 rounded-lg hover:bg-success-100 transition dark:bg-success-900/20 dark:text-success-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
CA Bundle (Windows)
|
||||
</a>
|
||||
<a href="{{ route('certificate.download-ca-android') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-warning-700 bg-warning-50 rounded-lg hover:bg-warning-100 transition dark:bg-warning-900/20 dark:text-warning-300">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
|
||||
CA Android (.der)
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<a href="{{ route('certificate.download-installer') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
Download Windows One-Click Installer (.bat)
|
||||
</a>
|
||||
</div>
|
||||
</x-common.component-card>
|
||||
@endif
|
||||
|
||||
@include('pages.certificate.partials.view-modal')
|
||||
</div>
|
||||
|
||||
@include('pages.certificate.partials.create-modal')
|
||||
@endsection
|
||||
@@ -0,0 +1,137 @@
|
||||
<x-ui.modal x-model="createOpen" containerClass="max-w-3xl" style="z-index: 100009;">
|
||||
<div class="p-6 sm:p-10">
|
||||
<div class="mb-6 pr-10">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Generate SSL Certificate</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 text-balance">Create a new self-signed certificate using your Local CA. This will be signed by your private root authority.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('certificate.generate') }}" method="POST" class="space-y-8"
|
||||
x-data="{
|
||||
common_name: '{{ old('common_name') }}',
|
||||
config_mode: '{{ old('config_mode', 'default') }}',
|
||||
key_bits: '2048',
|
||||
san: '{{ old('san') }}',
|
||||
isValid() {
|
||||
return this.common_name.length > 3 && this.common_name.includes('.');
|
||||
}
|
||||
}">
|
||||
@csrf
|
||||
|
||||
<!-- Configuration Mode -->
|
||||
<div>
|
||||
<label class="block mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">Configuration Mode</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label class="relative flex p-4 cursor-pointer rounded-xl border transition-all"
|
||||
:class="config_mode === 'default' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50/50 dark:bg-brand-900/10 dark:border-brand-400' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
||||
<input type="radio" name="config_mode" value="default" x-model="config_mode" class="sr-only">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center justify-center flex-shrink-0 w-5 h-5 mt-0.5 border rounded-full transition-colors"
|
||||
:class="config_mode === 'default' ? 'border-brand-500 bg-brand-500' : 'border-gray-300 dark:border-gray-600'">
|
||||
<div class="w-2 h-2 bg-white rounded-full" x-show="config_mode === 'default'"></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">Default Presets</span>
|
||||
<span class="block mt-0.5 text-xs text-gray-500 dark:text-gray-400">Use system defaults for Organization, Locality, and Country settings.</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex p-4 cursor-pointer rounded-xl border transition-all"
|
||||
:class="config_mode === 'manual' ? 'border-brand-500 ring-1 ring-brand-500 bg-brand-50/50 dark:bg-brand-900/10 dark:border-brand-400' : 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
|
||||
<input type="radio" name="config_mode" value="manual" x-model="config_mode" class="sr-only">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center justify-center flex-shrink-0 w-5 h-5 mt-0.5 border rounded-full transition-colors"
|
||||
:class="config_mode === 'manual' ? 'border-brand-500 bg-brand-500' : 'border-gray-300 dark:border-gray-600'">
|
||||
<div class="w-2 h-2 bg-white rounded-full" x-show="config_mode === 'manual'"></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">Manual Configuration</span>
|
||||
<span class="block mt-0.5 text-xs text-gray-500 dark:text-gray-400">Customise all Distinguised Name (DN) attributes manually.</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Details -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Common Name (Domain) <span class="text-error-500">*</span></label>
|
||||
<input type="text" name="common_name" x-model="common_name" placeholder="e.g. example.com or e.g. 127.0.0.1" required
|
||||
class="w-full px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white transition-colors"
|
||||
:class="common_name && !isValid() ? 'border-error-500 focus:ring-error-500/20 focus:border-error-500' : ''">
|
||||
<p x-show="common_name && !isValid()" class="mt-1.5 text-xs text-error-500 animate-pulse">Please enter a valid domain name containing at least one dot.</p>
|
||||
<p class="mt-1.5 text-xs text-gray-500">The primary Fully Qualified Domain Name (FQDN) or IP Address to be secured.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Subject Alternative Names (SAN)</label>
|
||||
<input type="text" name="san" x-model="san" placeholder="e.g. api.local, 192.168.1.50"
|
||||
class="w-full px-4 py-2.5 text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white transition-colors">
|
||||
<p class="mt-1.5 text-xs text-gray-500">Optional comma-separated list of additional domains or IPs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Mode Fields -->
|
||||
<div x-show="config_mode === 'manual'" x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2" x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="p-5 border border-gray-100 rounded-xl bg-gray-50 dark:bg-gray-800/50 dark:border-gray-700/50">
|
||||
|
||||
<h4 class="mb-4 text-xs font-semibold tracking-wider text-gray-500 uppercase font-lexend">Distinguished Name Attributes</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Organization (O)</label>
|
||||
<input type="text" name="organization" value="{{ $defaults['organizationName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Country (C)</label>
|
||||
<input type="text" name="country" value="{{ $defaults['countryName'] ?? 'ID' }}" maxlength="2"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">State / Province (ST)</label>
|
||||
<input type="text" name="state" value="{{ $defaults['stateOrProvinceName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-300">Locality (L)</label>
|
||||
<input type="text" name="locality" value="{{ $defaults['localityName'] ?? '' }}"
|
||||
class="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Size -->
|
||||
<div>
|
||||
<label class="block mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">Private Key Size</label>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="key_bits" value="2048" x-model="key_bits" class="peer sr-only">
|
||||
<div class="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg peer-checked:bg-brand-50 peer-checked:text-brand-700 peer-checked:border-brand-500 hover:bg-gray-50 transition-all dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:peer-checked:bg-brand-900/30 dark:peer-checked:text-brand-300 dark:peer-checked:border-brand-500">
|
||||
2048 Bit (Standard)
|
||||
</div>
|
||||
</label>
|
||||
<label class="cursor-pointer">
|
||||
<input type="radio" name="key_bits" value="4096" x-model="key_bits" class="peer sr-only">
|
||||
<div class="px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg peer-checked:bg-brand-50 peer-checked:text-brand-700 peer-checked:border-brand-500 hover:bg-gray-50 transition-all dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:peer-checked:bg-brand-900/30 dark:peer-checked:text-brand-300 dark:peer-checked:border-brand-500">
|
||||
4096 Bit (High Security)
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-3 pt-4 sm:pt-6 border-t border-gray-100 dark:border-gray-800">
|
||||
<button type="button" @click="createOpen = false" class="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition shadow-sm">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" :disabled="!isValid()"
|
||||
class="px-6 py-2.5 text-sm font-semibold text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Generate Certificate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-ui.modal>
|
||||
172
resources/views/pages/certificate/partials/table.blade.php
Normal file
172
resources/views/pages/certificate/partials/table.blade.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<div class="max-w-full overflow-x-auto"
|
||||
x-data="{ anyOpen: false }"
|
||||
:class="anyOpen ? 'pb-32' : 'pb-4'">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-100 dark:border-gray-800">
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider">No</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider">Common Name (CN)</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider">Serial Number</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider hidden lg:table-cell">Alt Names (SAN)</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider text-center">Status</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider text-center hidden xl:table-cell">Validity Period</th>
|
||||
<th class="px-5 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
@forelse ($certificates as $cert)
|
||||
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/50 transition-colors">
|
||||
<td class="px-5 py-4 text-sm text-gray-500">
|
||||
{{ ($certificates->currentPage() - 1) * $certificates->perPage() + $loop->iteration }}
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="text-sm font-semibold text-gray-800 dark:text-white/90">{{ $cert->common_name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ $cert->organization }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-xs font-mono text-gray-500 dark:text-gray-400 hidden sm:table-cell">
|
||||
{{ $cert->serial_number ?: '-' }}
|
||||
</td>
|
||||
<td class="px-5 py-4 hidden lg:table-cell">
|
||||
<div class="max-w-xs truncate text-xs text-gray-500 dark:text-gray-400 font-mono" title="{{ $cert->san }}">
|
||||
{{ $cert->san ?: '-' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
@php
|
||||
$isValid = $cert->valid_to && \Carbon\Carbon::parse($cert->valid_to)->isFuture();
|
||||
$statusClass = $isValid ? 'bg-success-50 text-success-700 dark:bg-success-900/30 dark:text-success-300' : 'bg-error-50 text-error-700 dark:bg-error-900/30 dark:text-error-300';
|
||||
$statusText = $isValid ? 'Valid' : 'Expired';
|
||||
if(!$cert->valid_to) {
|
||||
$statusClass = 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400';
|
||||
$statusText = 'Unknown';
|
||||
}
|
||||
@endphp
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 text-xs font-medium rounded-full {{ $statusClass }}">
|
||||
{{ $statusText }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-xs text-gray-500 dark:text-gray-400 text-center whitespace-nowrap hidden xl:table-cell">
|
||||
@if($cert->valid_from && $cert->valid_to)
|
||||
<div>{{ \Carbon\Carbon::parse($cert->valid_from)->format('Y-m-d') }}</div>
|
||||
<div class="text-gray-400">to</div>
|
||||
<div>{{ \Carbon\Carbon::parse($cert->valid_to)->format('Y-m-d') }}</div>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-1.5">
|
||||
<!-- View Dropdown -->
|
||||
<div x-data="{ open: false }" class="relative" @click.away="open = false; anyOpen = false">
|
||||
<button @click="open = !open; anyOpen = open"
|
||||
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 transition-all active:scale-90 shadow-sm"
|
||||
title="View Details">
|
||||
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
||||
<span class="hidden xl:inline">View</span>
|
||||
</button>
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="absolute left-0 mt-2 w-32 rounded-xl shadow-2xl bg-white dark:bg-gray-800 ring-1 ring-black/5 z-[110] overflow-hidden border border-gray-100 dark:border-gray-700">
|
||||
<button @click.prevent="openViewModal('{{ route('certificate.view', [$cert->uuid, 'cert']) }}', 'Certificate Content'); open = false" class="w-full flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
Cert
|
||||
</button>
|
||||
<button @click.prevent="openViewModal('{{ route('certificate.view', [$cert->uuid, 'key']) }}', 'Private Key Content'); open = false" class="w-full flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>
|
||||
Key
|
||||
</button>
|
||||
<button @click.prevent="openViewModal('{{ route('certificate.view', [$cert->uuid, 'csr']) }}', 'CSR Content'); open = false" class="w-full flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
|
||||
CSR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Dropdown -->
|
||||
<div x-data="{ open: false }" class="relative" @click.away="open = false; anyOpen = false">
|
||||
<button @click="open = !open; anyOpen = open"
|
||||
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-white bg-brand-500 rounded-lg hover:bg-brand-600 transition-all active:scale-90 shadow-md"
|
||||
title="Download Files">
|
||||
<svg class="w-3.5 h-3.5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
<span class="hidden xl:inline">Download</span>
|
||||
</button>
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="absolute left-0 mt-2 w-36 rounded-xl shadow-2xl bg-white dark:bg-gray-800 ring-1 ring-black/5 z-[110] overflow-hidden border border-gray-100 dark:border-gray-700">
|
||||
<a href="{{ route('certificate.download-zip', $cert->uuid) }}" class="flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
|
||||
ZIP Bundle
|
||||
</a>
|
||||
<a href="{{ route('certificate.download-p12', $cert->uuid) }}" class="flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
.p12 File
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Others Dropdown -->
|
||||
<div x-data="{ open: false }" class="relative" @click.away="open = false; anyOpen = false">
|
||||
<button @click="open = !open; anyOpen = open"
|
||||
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-all active:scale-90 shadow-sm"
|
||||
title="Other Actions">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"></path></svg>
|
||||
</button>
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="absolute right-0 mt-2 w-32 rounded-xl shadow-2xl bg-white dark:bg-gray-800 ring-1 ring-black/5 z-[110] overflow-hidden border border-gray-100 dark:border-gray-700">
|
||||
<form action="{{ route('certificate.regenerate', $cert->uuid) }}" method="POST" onsubmit="return confirm('Regenerate this certificate? This will replace the current certificate and key.')">
|
||||
@csrf
|
||||
<button type="submit" class="w-full flex items-center px-4 py-2 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
||||
Regen
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ route('certificate.delete', $cert->uuid) }}" method="POST" onsubmit="return confirm('Truly delete this certificate?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="w-full flex items-center px-4 py-2 text-xs text-error-600 hover:bg-error-50 dark:hover:bg-error-900/20 transition">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-5 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="w-12 h-12 text-gray-200 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||
<p class="text-sm text-gray-500 italic">No certificates found matching your criteria.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
||||
{{-- Filler rows to maintain professional layout height --}}
|
||||
@php
|
||||
$currentCount = $certificates->count();
|
||||
$fillCount = 5 - ($currentCount ?: 1);
|
||||
@endphp
|
||||
@for($i = 0; $i < $fillCount; $i++)
|
||||
<tr class="border-transparent pointer-events-none select-none">
|
||||
<td class="px-5 py-4" colspan="7"> </td>
|
||||
</tr>
|
||||
@endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($certificates->hasPages())
|
||||
<div class="px-5 py-4 border-t border-gray-100 dark:border-gray-800 ajax-pagination">
|
||||
{{ $certificates->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,42 @@
|
||||
<x-ui.modal x-model="viewOpen" containerClass="w-full max-w-[95vw] sm:w-auto sm:min-w-[500px] sm:max-w-3xl" style="z-index: 100010;">
|
||||
<div class="flex flex-col h-[80vh] sm:h-auto sm:max-h-[85vh]">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-5 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between bg-white dark:bg-gray-900 sticky top-0 z-10 gap-8">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white" x-text="viewTitle">Certificate Content</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">View and copy the raw content below.</p>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<a :href="viewUrl" target="_blank"
|
||||
class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:hover:bg-gray-700 transition">
|
||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
Raw View
|
||||
</a>
|
||||
<button @click="copyToClipboard()"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs">
|
||||
<svg x-show="!copied" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg>
|
||||
<svg x-show="copied" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
<span x-text="copied ? 'Copied!' : 'Copy Content'"></span>
|
||||
</button>
|
||||
<button @click="viewOpen = false" class="ml-2 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 transition-colors">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-hidden relative bg-gray-50 dark:bg-gray-950">
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-gray-900/50 backdrop-blur-sm z-20">
|
||||
<svg class="w-8 h-8 text-brand-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
||||
</div>
|
||||
|
||||
<!-- Editor-like View -->
|
||||
<div class="h-full overflow-auto custom-scrollbar p-0">
|
||||
<pre class="p-6 text-sm font-mono leading-relaxed text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all select-all font-fira-code" x-text="viewContent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-ui.modal>
|
||||
Reference in New Issue
Block a user