attstore / templates /api_keys.html
dn-scribe's picture
fix: same error on api_key modal
36c4cd1
Raw History Blame Contribute Delete
18.7 kB
{% extends "base.html" %}
{% block title %}API Keys - Attestation Store{% endblock %}
{% block content %}
<div class="container my-4">
<div class="row mb-4">
<div class="col">
<h1 class="h2">
<i data-feather="key" class="me-2"></i>
API Keys Management
{% if filtered_user %}
<small class="text-muted">- {{ filtered_user.username }}</small>
{% endif %}
</h1>
<p class="text-muted">Generate and manage API keys for programmatic access</p>
</div>
<div class="col-auto">
{% if filtered_user %}
<a href="{{ url_for('web.api_keys') }}" class="btn btn-outline-secondary me-2">
<i data-feather="list" class="me-1"></i>
View All Keys
</a>
{% endif %}
<button class="btn btn-primary" id="generateApiKeyBtn" onclick="showCreateModal()">
<i data-feather="plus" class="me-1"></i>
Generate API Key
</button>
</div>
</div>
<!-- Filters and Search -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
{% if is_admin %}
<div class="col-md-3">
<label for="user_id" class="form-label">Filter by User</label>
<select class="form-select" id="user_id" name="user_id" onchange="this.form.submit()">
<option value="">All Users</option>
{% for user in users %}
<option value="{{ user.id }}" {{ 'selected' if filter_user_id == user.id|string else '' }}>
{{ user.username }}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<div class="col-md-3">
<label for="sort" class="form-label">Sort by</label>
<select class="form-select" id="sort" name="sort" onchange="this.form.submit()">
<option value="created_at" {{ 'selected' if sort_by == 'created_at' else '' }}>Created Date</option>
<option value="name" {{ 'selected' if sort_by == 'name' else '' }}>Name</option>
<option value="user" {{ 'selected' if sort_by == 'user' else '' }}>User</option>
<option value="expiration_date" {{ 'selected' if sort_by == 'expiration_date' else '' }}>Expiration</option>
</select>
</div>
<div class="col-md-2">
<label for="order" class="form-label">Order</label>
<select class="form-select" id="order" name="order" onchange="this.form.submit()">
<option value="desc" {{ 'selected' if sort_order == 'desc' else '' }}>Descending</option>
<option value="asc" {{ 'selected' if sort_order == 'asc' else '' }}>Ascending</option>
</select>
</div>
<div class="col-md-4">
<label for="search" class="form-label">Search</label>
<div class="input-group">
<input type="text" class="form-control" id="search" name="search" value="{{ search }}"
placeholder="Search by name, key, or user...">
<button class="btn btn-outline-secondary" type="submit">
<i data-feather="search"></i>
</button>
{% if search or filter_user_id %}
<a href="{{ url_for('web.api_keys') }}" class="btn btn-outline-danger">
<i data-feather="x"></i>
</a>
{% endif %}
</div>
</div>
<!-- Hidden fields to preserve other filters -->
{% if filter_user_id %}
<input type="hidden" name="user_id" value="{{ filter_user_id }}">
{% endif %}
<input type="hidden" name="sort" value="{{ sort_by }}">
<input type="hidden" name="order" value="{{ sort_order }}">
</form>
</div>
</div>
<!-- API Keys Table -->
<div class="card">
<div class="card-body">
{% if api_keys %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a href="?sort=name&order={{ 'asc' if sort_by == 'name' and sort_order == 'desc' else 'desc' }}&user_id={{ filter_user_id or '' }}&search={{ search }}"
class="text-decoration-none text-white">
Name
{% if sort_by == 'name' %}
<i data-feather="chevron-{{ 'up' if sort_order == 'asc' else 'down' }}" width="16" height="16"></i>
{% endif %}
</a>
</th>
<th>
<a href="?sort=user&order={{ 'asc' if sort_by == 'user' and sort_order == 'desc' else 'desc' }}&user_id={{ filter_user_id or '' }}&search={{ search }}"
class="text-decoration-none text-white">
User
{% if sort_by == 'user' %}
<i data-feather="chevron-{{ 'up' if sort_order == 'asc' else 'down' }}" width="16" height="16"></i>
{% endif %}
</a>
</th>
<th>Key</th>
<th>
<a href="?sort=expiration_date&order={{ 'asc' if sort_by == 'expiration_date' and sort_order == 'desc' else 'desc' }}&user_id={{ filter_user_id or '' }}&search={{ search }}"
class="text-decoration-none text-white">
Expires
{% if sort_by == 'expiration_date' %}
<i data-feather="chevron-{{ 'up' if sort_order == 'asc' else 'down' }}" width="16" height="16"></i>
{% endif %}
</a>
</th>
<th>
<a href="?sort=created_at&order={{ 'asc' if sort_by == 'created_at' and sort_order == 'desc' else 'desc' }}&user_id={{ filter_user_id or '' }}&search={{ search }}"
class="text-decoration-none text-white">
Created
{% if sort_by == 'created_at' %}
<i data-feather="chevron-{{ 'up' if sort_order == 'asc' else 'down' }}" width="16" height="16"></i>
{% endif %}
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for key in api_keys %}
<tr>
<td>
{% if key.name %}
<strong>{{ key.name }}</strong>
{% else %}
<span class="text-muted">Unnamed</span>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ key.user.username }}</span>
</td>
<td>
<div class="d-flex flex-column">
<div class="mb-1">
<strong>API Token:</strong>
<code class="api-key-display">{{ key.key[:12] }}...{{ key.key[-8:] }}</code>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('{{ key.key }}', 'API Token')">
<i data-feather="copy" width="12" height="12"></i>
</button>
</div>
{% if key.get_client_credentials()[0] %}
<div class="small text-muted">
<div><strong>Client ID:</strong> {{ key.get_client_credentials()[0] }}
<button class="btn btn-sm btn-outline-secondary ms-1" onclick="copyToClipboard('{{ key.get_client_credentials()[0] }}', 'Client ID')">
<i data-feather="copy" width="10" height="10"></i>
</button>
</div>
<div><strong>Client Secret:</strong> {{ key.get_client_credentials()[1][:8] }}...
<button class="btn btn-sm btn-outline-secondary ms-1" onclick="copyToClipboard('{{ key.get_client_credentials()[1] }}', 'Client Secret')">
<i data-feather="copy" width="10" height="10"></i>
</button>
</div>
</div>
{% endif %}
</div>
</td>
<td>
{% if key.expiration_date %}
{% if key.is_expired() %}
<span class="badge bg-danger">Expired</span><br>
<small class="text-muted">{{ key.expiration_date.strftime('%Y-%m-%d') }}</small>
{% else %}
<span class="badge bg-success">{{ (key.expiration_date - key.created_at).days }} days</span><br>
<small class="text-muted">{{ key.expiration_date.strftime('%Y-%m-%d') }}</small>
{% endif %}
{% else %}
<span class="badge bg-warning">Never</span>
{% endif %}
</td>
<td>
<small class="text-muted">{{ key.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
</td>
<td>
<form method="POST" action="{{ url_for('web.revoke_api_key', key_id=key.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure you want to revoke this API key?')">
<i data-feather="trash-2" width="12" height="12"></i>
Revoke
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i data-feather="key" width="48" height="48" class="text-muted mb-3"></i>
<h5 class="text-muted">No API Keys</h5>
{% if filtered_user %}
<p class="text-muted">No API keys found for user {{ filtered_user.username }}.</p>
{% elif search %}
<p class="text-muted">No API keys match your search criteria.</p>
{% else %}
<p class="text-muted">No API keys have been generated yet.</p>
{% endif %}
<button class="btn btn-primary" onclick="showCreateModal()">
<i data-feather="plus" class="me-1"></i>
Generate {% if not api_keys %}First {% endif %}API Key
</button>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Create API Key Modal -->
<div class="modal fade" id="createApiKeyModal" tabindex="-1" inert>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i data-feather="key" class="me-2"></i>
Generate API Key
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('web.create_api_key') }}">
<div class="modal-body">
<div class="mb-3">
<label for="target_user_id" class="form-label">User</label>
<select class="form-select" id="target_user_id" name="target_user_id" required {% if not is_admin %}disabled{% endif %}>
<option value="">Select a user</option>
{% for user in users %}
<option value="{{ user.id }}" {{ 'selected' if (filter_user_id == user.id|string) or (not is_admin and user.id == current_user.id) else '' }}>
{{ user.username }} ({{ user.email }})
</option>
{% endfor %}
</select>
<div class="form-text">
{% if is_admin %}
The user this API key will authenticate as
{% else %}
<input type="hidden" name="target_user_id" value="{{ current_user.id }}">
API keys can only be created for your own account
{% endif %}
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name (Optional)</label>
<input type="text" class="form-control" id="name" name="name"
placeholder="e.g., CI/CD Pipeline, Integration Service">
<div class="form-text">A descriptive name to identify this API key</div>
</div>
<div class="mb-3">
<label for="expiration_days" class="form-label">Expiration</label>
<select class="form-select" id="expiration_days" name="expiration_days">
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="365" selected>1 year (default)</option>
<option value="0">Never expires</option>
</select>
<div class="form-text">When this API key should expire</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Generate API Key</button>
</div>
</form>
</div>
</div>
</div>
<script>
function showCreateModal() {
const modal = new bootstrap.Modal(document.getElementById('createApiKeyModal'));
modal.show();
}
function copyToClipboard(text, label = 'Key') {
navigator.clipboard.writeText(text).then(function() {
// Show a simple feedback
const button = event.target.closest('button');
const originalHTML = button.innerHTML;
button.innerHTML = '<i data-feather="check" width="10" height="10"></i>';
feather.replace();
// Show a toast-like message
showToast(`${label} copied to clipboard!`);
setTimeout(() => {
button.innerHTML = originalHTML;
feather.replace();
}, 2000);
});
}
function showToast(message) {
// Create a simple toast notification
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '1050';
toast.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header">
<i data-feather="info" width="16" height="16" class="me-2"></i>
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">${message}</div>
</div>
`;
document.body.appendChild(toast);
feather.replace();
// Auto remove after 3 seconds
setTimeout(() => {
toast.remove();
}, 3000);
}
// Initialize Feather icons when page loads
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
});
</script>
{% endblock %}
{% block scripts %}
<script>
// Accessibility: manage focus and inert for create API Key modal
const createApiKeyModalEl = document.getElementById('createApiKeyModal');
let lastFocusedApiKeyTrigger;
// ensure modal is inert when hidden to prevent aria-hidden focus issues
createApiKeyModalEl.setAttribute('inert', '');
createApiKeyModalEl.addEventListener('show.bs.modal', () => {
lastFocusedApiKeyTrigger = document.activeElement;
createApiKeyModalEl.removeAttribute('inert');
});
createApiKeyModalEl.addEventListener('hide.bs.modal', () => {
createApiKeyModalEl.setAttribute('inert', '');
if (lastFocusedApiKeyTrigger && typeof lastFocusedApiKeyTrigger.focus === 'function') {
lastFocusedApiKeyTrigger.focus();
}
});
</script>
{% endblock %}