---
import Layout from '../layouts/Layout.astro';
// Server-side data fetching
let credentials = [];
let error = null;
// Get auth context from middleware
const authContext = Astro.locals.auth;
// Only fetch data if user is authenticated
if (authContext.isAuthenticated) {
try {
const baseUrl = 'http://vultr-backend:8000';
const response = await fetch(`${baseUrl}/api/vultr-credentials?limit=100`, {
headers: {
'Authorization': `Bearer ${Astro.cookies.get('auth_token')?.value}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
credentials = data.items || [];
} else {
console.error('Failed to fetch credentials:', response.status, response.statusText);
error = 'Failed to load credentials. Please try again.';
}
} catch (err) {
console.error('Error fetching credentials:', err);
error = 'Network error while loading credentials.';
}
}
---
<Layout title="Vultr Credentials">
<div class="py-10">
<header>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="md:flex md:items-center md:justify-between">
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold leading-tight text-gray-900">Vultr Credentials</h1>
<p class="mt-2 text-sm text-gray-600">
Securely manage your Vultr API keys with Fernet encryption
</p>
</div>
<div class="mt-4 flex md:mt-0 md:ml-4">
<button type="button"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
onclick="openCreateModal()">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Add Credential
</button>
</div>
</div>
</div>
</header>
<main>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mt-8">
<!-- Info Banner -->
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">Secure Credential Storage</h3>
<div class="mt-2 text-sm text-blue-700">
<p>Your Vultr API keys are encrypted with Fernet symmetric encryption. Access is controlled via ephemeral tokens with RBAC validation.</p>
</div>
</div>
</div>
</div>
{error && (
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-red-700">{error}</p>
</div>
</div>
</div>
)}
<!-- Credentials Grid -->
{credentials.length === 0 ? (
<div class="text-center py-12 bg-white rounded-lg shadow">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<h3 class="mt-2 text-sm font-semibold text-gray-900">No credentials</h3>
<p class="mt-1 text-sm text-gray-500">Get started by adding your first Vultr API credential.</p>
<div class="mt-6">
<button type="button"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700"
onclick="openCreateModal()">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Add Your First Credential
</button>
</div>
</div>
) : (
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{credentials.map((cred) => (
<div class="bg-white overflow-hidden shadow rounded-lg hover:shadow-lg transition-shadow">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex-1 min-w-0">
<h3 class="text-lg font-medium text-gray-900 truncate">{cred.label}</h3>
{cred.description && (
<p class="mt-1 text-sm text-gray-500 line-clamp-2">{cred.description}</p>
)}
</div>
<div class="ml-4 flex-shrink-0">
{cred.is_active ? (
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Active
</span>
) : (
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Inactive
</span>
)}
</div>
</div>
<dl class="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<dt class="font-medium text-gray-500">Usage Count</dt>
<dd class="mt-1 text-gray-900">{cred.usage_count}</dd>
</div>
<div>
<dt class="font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-gray-900">{new Date(cred.created_at).toLocaleDateString()}</dd>
</div>
</dl>
{cred.last_used_at && (
<p class="mt-2 text-xs text-gray-500">
Last used: {new Date(cred.last_used_at).toLocaleString()}
</p>
)}
<div class="mt-6 flex flex-col gap-2">
<button type="button"
class="w-full inline-flex justify-center items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
onclick={`requestToken('${cred.id}', '${cred.label}')`}>
<svg class="-ml-0.5 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
Request Token
</button>
<div class="flex gap-2">
<button type="button"
class="flex-1 inline-flex justify-center items-center px-3 py-2 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
onclick={`viewStatistics('${cred.id}')`}>
Statistics
</button>
<button type="button"
class="flex-1 inline-flex justify-center items-center px-3 py-2 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
onclick={`editCredential('${cred.id}')`}>
Edit
</button>
<button type="button"
class="inline-flex items-center px-3 py-2 border border-red-300 text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50"
onclick={`deleteCredential('${cred.id}', '${cred.label}')`}>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</main>
</div>
<!-- Create Credential Modal -->
<dialog id="create-modal" class="rounded-lg shadow-xl backdrop:bg-gray-900/50 max-w-lg w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium leading-6 text-gray-900">Add Vultr Credential</h3>
<button type="button" onclick="document.getElementById('create-modal').close()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form id="create-form" class="space-y-4">
<div>
<label for="label" class="block text-sm font-medium text-gray-700">Label</label>
<input type="text"
id="label"
name="label"
required
placeholder="e.g., Production Account"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
<p class="mt-1 text-sm text-gray-500">A human-readable name for this credential</p>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description (optional)</label>
<textarea id="description"
name="description"
rows="2"
placeholder="e.g., Main production Vultr account for infrastructure deployment"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
</div>
<div>
<label for="api_key" class="block text-sm font-medium text-gray-700">Vultr API Key</label>
<input type="password"
id="api_key"
name="api_key"
required
placeholder="Your Vultr API key"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm font-mono" />
<p class="mt-1 text-sm text-gray-500">The API key will be encrypted with Fernet before storage</p>
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
Your API key is encrypted before storage and can only be accessed via ephemeral token exchange.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 flex gap-3">
<button type="button"
onclick="document.getElementById('create-modal').close()"
class="flex-1 inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:text-sm">
Cancel
</button>
<button type="submit"
class="flex-1 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:text-sm">
Add Credential
</button>
</div>
</form>
</div>
</dialog>
<!-- Edit Credential Modal -->
<dialog id="edit-modal" class="rounded-lg shadow-xl backdrop:bg-gray-900/50 max-w-lg w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium leading-6 text-gray-900">Edit Credential</h3>
<button type="button" onclick="document.getElementById('edit-modal').close()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form id="edit-form" class="space-y-4">
<input type="hidden" id="edit-credential-id" />
<div>
<label for="edit-label" class="block text-sm font-medium text-gray-700">Label</label>
<input type="text"
id="edit-label"
name="label"
required
placeholder="e.g., Production Account"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
<p class="mt-1 text-sm text-gray-500">A human-readable name for this credential</p>
</div>
<div>
<label for="edit-description" class="block text-sm font-medium text-gray-700">Description (optional)</label>
<textarea id="edit-description"
name="description"
rows="2"
placeholder="e.g., Main production Vultr account for infrastructure deployment"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
</div>
<div>
<label for="edit-api-key" class="block text-sm font-medium text-gray-700">Vultr API Key (optional)</label>
<input type="password"
id="edit-api-key"
name="api_key"
placeholder="Leave blank to keep current API key"
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm font-mono" />
<p class="mt-1 text-sm text-gray-500">Only provide a new API key if you want to update it</p>
</div>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
If you update the API key, it will be re-encrypted with the latest encryption key.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-6 flex gap-3">
<button type="button"
onclick="document.getElementById('edit-modal').close()"
class="flex-1 inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:text-sm">
Cancel
</button>
<button type="submit"
class="flex-1 inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:text-sm">
Save Changes
</button>
</div>
</form>
</div>
</dialog>
<!-- Token Display Modal -->
<dialog id="token-modal" class="rounded-lg shadow-xl backdrop:bg-gray-900/50 max-w-2xl w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium leading-6 text-gray-900">Ephemeral Token</h3>
<button type="button" onclick="document.getElementById('token-modal').close()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4">
<div class="bg-green-50 border-l-4 border-green-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">
Token generated successfully! This token expires in <span id="token-ttl" class="font-semibold"></span> and can only be used once.
</p>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Ephemeral Token</label>
<div class="relative">
<input type="text"
id="token-value"
readonly
class="block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 pr-24 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm font-mono text-xs bg-gray-50" />
<button type="button"
onclick="copyToken()"
class="absolute inset-y-0 right-0 px-4 text-sm font-medium text-primary-600 hover:text-primary-700">
Copy
</button>
</div>
</div>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 text-sm text-blue-700">
<p class="font-medium">How to use this token:</p>
<ol class="mt-2 list-decimal list-inside space-y-1">
<li>Copy the token above</li>
<li>Send it to your application/service</li>
<li>Exchange it for the real API key via <code class="bg-blue-100 px-1 rounded">POST /api/vultr-credentials/exchange</code></li>
<li>The token becomes invalid after use or expiry</li>
</ol>
</div>
</div>
</div>
<div class="mt-5">
<button type="button"
onclick="document.getElementById('token-modal').close()"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:text-sm">
Done
</button>
</div>
</div>
</div>
</dialog>
<!-- Statistics Modal -->
<dialog id="stats-modal" class="rounded-lg shadow-xl backdrop:bg-gray-900/50 max-w-2xl w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium leading-6 text-gray-900">Credential Statistics</h3>
<button type="button" onclick="document.getElementById('stats-modal').close()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="stats-content" class="space-y-4">
<!-- Will be populated dynamically -->
</div>
</div>
</dialog>
<script is:inline>
function getAuthHeaders() {
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
acc[key] = value;
return acc;
}, {});
return {
'Authorization': `Bearer ${cookies.auth_token}`,
'Content-Type': 'application/json'
};
}
window.openCreateModal = function() {
document.getElementById('create-modal').showModal();
}
async function createCredential(formData) {
try {
const response = await fetch('/api/vultr-credentials', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
label: formData.get('label'),
description: formData.get('description') || null,
api_key: formData.get('api_key')
})
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert(`Failed to create credential: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error creating credential:', error);
alert('Network error while creating credential');
}
}
document.getElementById('create-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await createCredential(formData);
});
window.requestToken = async function(credentialId, label) {
try {
const response = await fetch(`/api/vultr-credentials/${credentialId}/token`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
credential_id: credentialId,
ttl_minutes: 5
})
});
if (response.ok) {
const data = await response.json();
// Show token modal
document.getElementById('token-value').value = data.token;
document.getElementById('token-ttl').textContent = `${data.ttl_seconds} seconds`;
document.getElementById('token-modal').showModal();
} else {
const error = await response.json();
alert(`Failed to request token: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error requesting token:', error);
alert('Network error while requesting token');
}
}
window.copyToken = function() {
const tokenInput = document.getElementById('token-value');
tokenInput.select();
document.execCommand('copy');
// Visual feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}
window.viewStatistics = async function(credentialId) {
try {
const response = await fetch(`/api/vultr-credentials/${credentialId}/statistics`, {
headers: getAuthHeaders()
});
if (response.ok) {
const stats = await response.json();
const statsHtml = `
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-4 rounded-lg">
<dt class="text-sm font-medium text-gray-500">Total Tokens Issued</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">${stats.total_tokens_issued}</dd>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<dt class="text-sm font-medium text-gray-500">Tokens Used</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">${stats.total_tokens_used}</dd>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<dt class="text-sm font-medium text-gray-500">Tokens Expired</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">${stats.total_tokens_expired}</dd>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<dt class="text-sm font-medium text-gray-500">API Key Usage</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">${stats.api_key_usage_count}</dd>
</div>
</div>
${stats.last_token_issued_at ? `
<div class="mt-4 text-sm text-gray-600">
Last token issued: ${new Date(stats.last_token_issued_at).toLocaleString()}
</div>
` : ''}
${stats.last_token_used_at ? `
<div class="text-sm text-gray-600">
Last token used: ${new Date(stats.last_token_used_at).toLocaleString()}
</div>
` : ''}
`;
document.getElementById('stats-content').innerHTML = statsHtml;
document.getElementById('stats-modal').showModal();
} else {
alert('Failed to load statistics');
}
} catch (error) {
console.error('Error loading statistics:', error);
alert('Network error while loading statistics');
}
}
window.editCredential = async function(credentialId) {
try {
// Fetch credential details
const response = await fetch(`/api/vultr-credentials/${credentialId}`, {
headers: getAuthHeaders()
});
if (response.ok) {
const credential = await response.json();
// Populate the edit form
document.getElementById('edit-credential-id').value = credential.id;
document.getElementById('edit-label').value = credential.label;
document.getElementById('edit-description').value = credential.description || '';
document.getElementById('edit-api-key').value = ''; // Always start empty
// Show the modal
document.getElementById('edit-modal').showModal();
} else {
const error = await response.json();
alert(`Failed to load credential: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error loading credential:', error);
alert('Network error while loading credential');
}
}
async function updateCredential(formData, credentialId) {
try {
// Build request body - only include API key if provided
const body = {
label: formData.get('label'),
description: formData.get('description') || null
};
const apiKey = formData.get('api_key');
if (apiKey && apiKey.trim()) {
body.api_key = apiKey;
}
const response = await fetch(`/api/vultr-credentials/${credentialId}`, {
method: 'PATCH',
headers: getAuthHeaders(),
body: JSON.stringify(body)
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert(`Failed to update credential: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error updating credential:', error);
alert('Network error while updating credential');
}
}
document.getElementById('edit-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const credentialId = document.getElementById('edit-credential-id').value;
await updateCredential(formData, credentialId);
});
window.deleteCredential = async function(credentialId, label) {
if (!confirm(`Are you sure you want to delete credential "${label}"? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/api/vultr-credentials/${credentialId}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert(`Failed to delete credential: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Error deleting credential:', error);
alert('Network error while deleting credential');
}
}
</script>
</Layout>