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,100 @@
<div class="mt-8 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
x-data="{
apiMode: 'private',
lang: 'curl',
baseUrl: window.location.origin,
privateEndpoint: '/api/v1/certificates',
publicEndpoint: '/api/public/ca-certificates'
}">
<div class="p-6 border-b border-gray-100 dark:border-gray-700">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
API Documentation
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Learn how to integrate DyDev APP API into your application.</p>
</div>
<!-- API Selection -->
<div class="flex p-1 bg-gray-100 dark:bg-gray-900 rounded-lg">
<button @click="apiMode = 'private'"
:class="apiMode === 'private' ? 'bg-white dark:bg-gray-800 text-brand-600 dark:text-brand-400 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700'"
class="px-4 py-1.5 text-sm font-medium rounded-md transition-all flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
Private API
</button>
<button @click="apiMode = 'public'"
:class="apiMode === 'public' ? 'bg-white dark:bg-gray-800 text-brand-600 dark:text-brand-400 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700'"
class="px-4 py-1.5 text-sm font-medium rounded-md transition-all flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 002 2 2 2 0 012 2v.654M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Public API
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12">
<!-- Sidebar Tabs -->
<div class="lg:col-span-3 border-r border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/50 p-4">
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-4 px-2">Language</h3>
<div class="space-y-1">
<button @click="lang = 'curl'" :class="lang === 'curl' ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors">
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>
cURL
</button>
<button @click="lang = 'js'" :class="lang === 'js' ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors">
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3zm14.53 14.12c-.52-.31-.76-.6-.76-1.12 0-.49.33-.82.8-.82.46 0 .76.24.96.59l1.1-.64c-.33-.59-.88-1.07-2.01-1.07-1.11 0-1.99.71-1.99 1.9 0 1.13.68 1.72 1.7 2.3.56.32.78.61.78 1.13 0 .58-.45.92-1.05.92-.72 0-1.15-.36-1.38-.9l-1.14.66c.38.86 1.15 1.34 2.54 1.34 1.48 0 2.21-.83 2.21-1.95 0-1.29-.75-1.93-1.76-2.51zm-5.71 1.63c-.38-.6-.62-.83-.99-.83-.41 0-.61.2-.61.53v4.2c0 .33.22.54.56.54.34 0 .56-.21.56-.54V17.5c.34 0 .53.18.89.73l1.15-.7c-.55-.88-1.04-1.22-2.02-1.22-1.24 0-1.76.85-1.76 1.72 0 .84.44 1.51 1.09 1.94-.58.39-.77.92-.77 1.63 0 1.05.73 1.71 1.75 1.71.97 0 1.57-.42 2.05-1.2l-1.12-.66c-.34.56-.59.78-1 .78-.44 0-.68-.26-.68-.61v-2.29z"/></svg>
JavaScript
</button>
<button @click="lang = 'php'" :class="lang === 'php' ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors">
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm6.36 14.83c-.08.23-.29.33-.6.33-.31 0-.58-.1-.7-.34l-.87-1.84h-2.12l-.46 1.83c-.08.31-.3.43-.63.43-.32 0-.54-.12-.54-.42 0-.04.01-.09.02-.15l1.65-6.19c.12-.46.39-.68.86-.68.46 0 .71.21.82.63l2.6 6.42zm-5-3.35h1.56l-.78-1.63-.78 1.63zM8.5 15.17l.02.16c0 .3-.22.42-.54.42-.33 0-.55-.12-.63-.43L5.7 8.16C5.69 8.1 5.68 8.05 5.68 8c0-.3.22-.42.54-.42.33 0 .55.12.63.43l1.65 6.16z"/></svg>
PHP
</button>
<button @click="lang = 'python'" :class="lang === 'python' ? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'" class="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg transition-colors">
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"><path d="M11.97 2c-2.47 0-2.31 1.07-2.31 1.07l.01 1.11h2.36v.34H8.64s-2.15-.26-2.15 2.1c0 2.37 1.86 2.22 1.86 2.22h1.11v-1.55c0 0-.08-1.86 1.86-1.86h3.1s1.84 0 1.84-1.78V5.21s.11-1.79-1.84-1.79c-1.95-.01-2.45-.42-2.45-.42S13.44 2 11.97 2zM7.46 10.45s-1.84 0-1.84 1.78v1.44s-.11 1.79 1.84 1.79c1.95 0 2.45.42 2.45.42s.53 1.02 2 1.02c2.47 0 2.31-1.07 2.31-1.07l-.01-1.11H12.1v-.34h3.64s2.15.26 2.15-2.1c0-2.37-1.86-2.22-1.86-2.22H7.46zm1.39 1.11c.31 0 .56.25.56.56s-.25.56-.56.56-.56-.25-.56-.56.25-.56.56-.56zm3.33 7.22c-.31 0-.56-.25-.56-.56s.25-.56.56-.56.56.25.56.56-.25.56-.56.56z"/></svg>
Python
</button>
</div>
</div>
<!-- Code Content -->
<div class="lg:col-span-9 bg-gray-50 dark:bg-gray-900/30">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-white flex items-center gap-2">
<span class="px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 text-[10px] font-bold uppercase" x-text="apiMode === 'private' ? 'Authenticated' : 'Public'"></span>
<code class="text-brand-600 dark:text-brand-400" x-text="apiMode === 'private' ? privateEndpoint : publicEndpoint"></code>
</h4>
<button @click="navigator.clipboard.writeText($refs.codeContent.innerText)" class="p-1 px-2 text-[10px] font-bold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 uppercase flex items-center gap-1 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Copy Snippet
</button>
</div>
<div class="relative group">
<pre class="rounded-xl bg-white dark:bg-gray-950 p-6 overflow-x-auto border border-gray-200 dark:border-gray-800 shadow-inner"><code class="text-sm font-mono leading-relaxed" x-ref="codeContent"><template x-if="lang === 'curl'"><span class="text-gray-800 dark:text-gray-300" x-text="'curl -X GET ' + baseUrl + (apiMode === 'private' ? privateEndpoint : publicEndpoint) + '\n -H \'Accept: application/json\'' + (apiMode === 'private' ? '\n -H \'X-API-KEY: your_api_key_here\'' : '')"></span></template><template x-if="lang === 'js'"><span class="text-gray-800 dark:text-gray-300" x-text="'fetch(\'' + baseUrl + (apiMode === 'private' ? privateEndpoint : publicEndpoint) + '\', {\n method: \'GET\', \n headers: {\n \'Accept\': \'application/json\'' + (apiMode === 'private' ? ',\n \'X-API-KEY\': \'your_api_key_here\'' : '') + '\n }\n})\n.then(response => response.json())\n.then(data => console.log(data));'"></span></template><template x-if="lang === 'php'"><span class="text-gray-800 dark:text-gray-300" x-text="'$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, \'' + baseUrl + (apiMode === 'private' ? privateEndpoint : publicEndpoint) + '\');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\ncurl_setopt($ch, CURLOPT_HTTPHEADER, [\n \'Accept: application/json\'' + (apiMode === 'private' ? ',\n \'X-API-KEY: your_api_key_here\'' : '') + '\n]);\n$response = curl_exec($ch);\ncurl_close($ch);\necho $response;'"></span></template><template x-if="lang === 'python'"><span class="text-gray-800 dark:text-gray-300" x-text="'import requests\n\nurl = \'' + baseUrl + (apiMode === 'private' ? privateEndpoint : publicEndpoint) + '\'\nheaders = {\n \'Accept\': \'application/json\'' + (apiMode === 'private' ? ',\n \'X-API-KEY\': \'your_api_key_here\'' : '') + '\n}\n\nresponse = requests.get(url, headers=headers)\nprint(response.json())'"></span></template></code></pre>
</div>
<div class="mt-4 p-4 rounded-lg bg-orange-50 dark:bg-orange-900/10 border border-orange-100 dark:border-orange-900/20" x-show="apiMode === 'private'">
<div class="flex gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-600 dark:text-orange-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-xs text-orange-800 dark:text-orange-300 leading-normal">
<strong>Security Note:</strong> Your API Key should never be shared or exposed in client-side code (browsers). Always use environment variables and call these endpoints from your backend servers.
</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,43 @@
<div x-data="{ selectedKey: null }">
<x-ui.modal
x-on:open-modal.window="if ($event.detail.name === 'delete-api-key') { open = true; selectedKey = $event.detail.apiKey; }"
:isOpen="false"
containerClass="max-w-[700px]">
<div class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Delete API Key
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Are you sure you want to delete this API key? This action cannot be undone.
</p>
</div>
<div class="px-2 text-center mb-8">
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/10">
<svg class="h-10 w-10 text-red-600 dark:text-red-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
Any applications using the key <span x-text="selectedKey ? selectedKey.name : ''" class="font-bold text-gray-900 dark:text-white"></span> will stop working immediately.
</p>
</div>
<form x-bind:action="selectedKey ? '{{ route('api-keys.index') }}/' + selectedKey.id : '#'" method="POST" class="flex flex-col">
@csrf
@method('DELETE')
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto">
Cancel
</button>
<button type="submit"
class="flex w-full justify-center rounded-lg bg-red-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-red-700 sm:w-auto">
Delete API Key
</button>
</div>
</form>
</div>
</x-ui.modal>
</div>

View File

@@ -0,0 +1,71 @@
<div x-data="{ selectedKey: null }">
<x-ui.modal
x-on:open-modal.window="if ($event.detail.name === 'edit-api-key') { open = true; selectedKey = $event.detail.apiKey; }"
:isOpen="false"
containerClass="max-w-[700px]">
<div class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Edit API Key
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Update the name of your API key to keep your applications organized.
</p>
</div>
<form x-bind:action="selectedKey ? '{{ route('api-keys.index') }}/' + selectedKey.id : '#'" method="POST"
@submit.prevent="submitEditForm" class="flex flex-col">
@csrf
@method('PUT')
<div class="px-2">
<div class="space-y-5">
<div>
<label for="edit_name" class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Application Name
</label>
<input type="text" name="name" id="edit_name" required x-model="selectedKey ? selectedKey.name : ''"
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
</div>
</div>
</div>
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto">
Cancel
</button>
<button type="submit"
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto">
Save Changes
</button>
</div>
</form>
</div>
<script>
function submitEditForm(e) {
const form = e.target;
const action = form.getAttribute('action');
const formData = new FormData(form);
fetch(action, {
method: 'POST', // Method spoofing is handled by _method field
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Error updating API key');
}
})
.catch(error => console.error('Error:', error));
}
</script>
</x-ui.modal>
</div>

View File

@@ -0,0 +1,42 @@
<x-ui.modal @open-modal.window="if ($event.detail === 'generate-api-key') open = true" :isOpen="false" containerClass="max-w-[700px]">
<div class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Generate API Key
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
To enable secure access to the web services, your app requires an API key with permissions for resources.
</p>
</div>
<form action="{{ route('api-keys.store') }}" method="POST" class="flex flex-col">
@csrf
<div class="px-2">
<div class="space-y-5">
<div>
<label for="name" class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Application Name
</label>
<input type="text" name="name" id="name" required
placeholder="e.g. My Awesome App"
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
<p class="mt-2 text-xs text-gray-500 dark:text-gray-500">
Naming your application makes it easier to recognize your API key in the future.
</p>
</div>
</div>
</div>
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto">
Close
</button>
<button type="submit"
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto">
Generate API Key
</button>
</div>
</form>
</div>
</x-ui.modal>

View File

@@ -0,0 +1,123 @@
<div x-data="{
show: false,
selectedKey: null,
newKey: '',
step: 'confirm',
loading: false,
async regenerate() {
if (this.loading) return;
this.loading = true;
try {
const response = await fetch(`{{ route('api-keys.index') }}/${this.selectedKey.id}/regenerate`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.success) {
this.newKey = data.new_key;
this.step = 'success';
// Update the key in the table if possible
if (this.selectedKey) {
this.selectedKey.key = data.new_key;
}
} else {
alert(data.message || 'Error regenerating API key');
}
} catch (error) {
console.error('Error:', error);
alert('Error regenerating API key. Please try again.');
} finally {
this.loading = false;
}
}
}"
x-on:open-modal.window="if ($event.detail.name === 'confirm-regenerate-api-key') { show = true; selectedKey = $event.detail.apiKey; step = 'confirm'; newKey = ''; loading = false; }">
<x-ui.modal x-model="show" :isOpen="false" containerClass="max-w-[700px]">
<div class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<!-- Step: Confirm -->
<template x-if="step === 'confirm'">
<div>
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Regenerate API Key
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Are you sure you want to regenerate this API key?
</p>
</div>
<div class="px-2 text-center mb-8">
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-brand-100 dark:bg-brand-900/10">
<svg class="h-10 w-10 text-brand-600 dark:text-brand-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</div>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
The old key for <span x-text="selectedKey ? selectedKey.name : ''" class="font-bold text-gray-900 dark:text-white"></span> will stop working immediately.
</p>
</div>
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button" :disabled="loading"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto disabled:opacity-50">
Cancel
</button>
<button @click="regenerate()" type="button" :disabled="loading"
class="flex w-full items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto disabled:opacity-50">
<svg x-show="loading" class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span x-text="loading ? 'Regenerating...' : 'Regenerate Key'"></span>
</button>
</div>
</div>
</template>
<!-- Step: Success -->
<template x-if="step === 'success'">
<div>
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
New API Key Generated
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Please copy your new API key now. You won't be able to see it again!
</p>
</div>
<div class="px-2 mb-8">
<div class="flex items-center gap-2">
<code x-text="newKey"
class="flex-1 bg-gray-50 dark:bg-gray-800 p-4 rounded-xl border border-gray-200 dark:border-gray-700 font-mono text-brand-600 dark:text-brand-400 break-all text-sm">
</code>
<button x-data="{ copied: false }"
@click="navigator.clipboard.writeText(newKey); copied = true; setTimeout(() => copied = false, 2000)"
class="p-4 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-xl hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors border border-gray-200 dark:border-gray-700">
<svg x-show="!copied" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" x-cloak xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button"
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto">
Done
</button>
</div>
</div>
</template>
</div>
</x-ui.modal>
</div>

View File

@@ -0,0 +1,98 @@
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400 uppercase">
<th class="px-6 py-4 font-medium">Name</th>
<th class="px-6 py-4 font-medium">Status</th>
<th class="px-6 py-4 font-medium">Created</th>
<th class="px-6 py-4 font-medium">Last used</th>
<th class="px-6 py-4 font-medium">Disable/Enable</th>
<th class="px-6 py-4 font-medium">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($apiKeys as $key)
<tr class="bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" x-data="{ editingKey: { ...@js($key), regenerating: false } }">
<td class="px-6 py-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-white" x-text="editingKey.name"></div>
<div class="flex items-center gap-2">
<div class="relative flex-1 max-w-xs">
<input type="text" readonly :value="maskKey(editingKey.key)"
class="w-full bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg py-1.5 pl-3 pr-10 text-xs font-mono text-gray-600 dark:text-gray-400 focus:outline-none focus:ring-0">
</div>
<button x-data="{ copied: false }"
@click="navigator.clipboard.writeText(editingKey.key); copied = true; setTimeout(() => copied = false, 2000)"
class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors">
<svg x-show="!copied" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" x-cloak xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<button @click="$dispatch('open-modal', { name: 'confirm-regenerate-api-key', apiKey: editingKey })"
class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
</div>
</div>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="editingKey.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-500' : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-500'"
x-text="editingKey.is_active ? 'Active' : 'Disabled'">
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $key->created_at->format('M d, Y') }}
</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $key->last_used_at ? $key->last_used_at->format('m/d/Y, h:i:s A') : 'Never' }}
</td>
<td class="px-6 py-4">
<button @click="toggleStatus(editingKey)" class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
:class="editingKey.is_active ? 'bg-brand-500' : 'bg-gray-200 dark:bg-gray-700'" role="switch" :aria-checked="editingKey.is_active">
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="editingKey.is_active ? 'translate-x-5' : 'translate-x-0'"></span>
</button>
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<button @click="$dispatch('open-modal', { name: 'delete-api-key', apiKey: editingKey })"
class="text-gray-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
<button @click="$dispatch('open-modal', { name: 'edit-api-key', apiKey: editingKey })"
class="text-gray-400 hover:text-brand-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
@if(request('search'))
No API keys found matching "{{ request('search') }}".
@else
No API keys found. Generate one to get started.
@endif
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($apiKeys->hasPages())
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/50">
{{ $apiKeys->links() }}
</div>
@endif