Download templates/api_keys.html from dn-scribe/attstore: direct link, hf CLI and curl.
- Browser
- Download file 18.7 kB
-
https://huggingface.co/spaces/dn-scribe/attstore/resolve/main/templates/api_keys.html
- Command line
-
hf download hf://spaces/dn-scribe/attstore/templates/api_keys.html
-
curl -L -o api_keys.html https://huggingface.co/spaces/dn-scribe/attstore/resolve/main/templates/api_keys.html
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 %} | |