Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>{{ title }} - Admin Access</title> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> | |
| <style> | |
| .admin-login-container { | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background-color: var(--light-bg); | |
| } | |
| .admin-login-card { | |
| background: white; | |
| border-radius: var(--radius); | |
| box-shadow: var(--box-shadow); | |
| padding: 2rem; | |
| width: 100%; | |
| max-width: 400px; | |
| text-align: center; | |
| } | |
| .admin-login-title { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--heading-color); | |
| margin-bottom: 1rem; | |
| } | |
| .admin-login-description { | |
| color: var(--text-color); | |
| margin-bottom: 2rem; | |
| line-height: 1.6; | |
| } | |
| .admin-login-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .admin-login-input { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| font-size: 0.875rem; | |
| transition: var(--transition); | |
| } | |
| .admin-login-input:focus { | |
| outline: none; | |
| border-color: var(--ring); | |
| box-shadow: 0 0 0 2px rgba(110, 86, 207, 0.25); | |
| } | |
| .admin-login-button { | |
| background-color: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: var(--radius); | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| .admin-login-button:hover { | |
| background-color: var(--secondary-color); | |
| } | |
| .admin-login-error { | |
| color: var(--destructive); | |
| background-color: rgba(239, 68, 68, 0.1); | |
| border: 1px solid var(--destructive); | |
| border-radius: var(--radius); | |
| padding: 0.75rem; | |
| margin-top: 1rem; | |
| display: none; | |
| text-align: left; | |
| font-size: 0.875rem; | |
| } | |
| .admin-login-error.show { | |
| display: block; | |
| } | |
| .admin-login-hint { | |
| color: var(--muted-foreground); | |
| font-size: 0.75rem; | |
| text-align: left; | |
| margin: -0.5rem 0 0 0; | |
| } | |
| .admin-login-button[aria-busy="true"] { | |
| opacity: 0.7; | |
| cursor: progress; | |
| } | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="admin-login-container"> | |
| <div class="admin-login-card"> | |
| <h1 class="admin-login-title"><span aria-hidden="true">🔐</span> Admin Access Required</h1> | |
| <p class="admin-login-description"> | |
| This area requires administrator privileges. Enter your API key to access the admin dashboard. | |
| </p> | |
| <p class="admin-login-description" style="font-size: 0.85em; opacity: 0.8;"> | |
| Your admin key is saved to <code>admin_api_key.txt</code> in the task | |
| directory (it is also printed in the server log at startup). | |
| </p> | |
| <form class="admin-login-form" id="adminLoginForm"> | |
| <label class="sr-only" for="apiKey">Admin API key</label> | |
| <input | |
| type="password" | |
| id="apiKey" | |
| name="apiKey" | |
| placeholder="Admin API key" | |
| class="admin-login-input" | |
| aria-describedby="apiKeyHint" | |
| autocomplete="current-password" | |
| required | |
| > | |
| <p id="apiKeyHint" class="admin-login-hint"> | |
| The key is stored in <code>admin_api_key.txt</code> in your task directory. | |
| </p> | |
| <button type="submit" class="admin-login-button" id="adminLoginSubmit"> | |
| Access dashboard | |
| </button> | |
| </form> | |
| <div class="admin-login-error" id="errorMessage" role="alert" aria-live="polite"></div> | |
| </div> | |
| </div> | |
| <script> | |
| (function () { | |
| const form = document.getElementById('adminLoginForm'); | |
| const submitBtn = document.getElementById('adminLoginSubmit'); | |
| const errorBox = document.getElementById('errorMessage'); | |
| const keyInput = document.getElementById('apiKey'); | |
| function showError(text) { | |
| errorBox.textContent = text; | |
| errorBox.classList.add('show'); | |
| } | |
| function clearError() { | |
| errorBox.textContent = ''; | |
| errorBox.classList.remove('show'); | |
| } | |
| form.addEventListener('submit', function (e) { | |
| e.preventDefault(); | |
| if (submitBtn.getAttribute('aria-busy') === 'true') return; // guard double-submit | |
| clearError(); | |
| submitBtn.setAttribute('aria-busy', 'true'); | |
| submitBtn.disabled = true; | |
| const originalLabel = submitBtn.textContent; | |
| submitBtn.textContent = 'Verifying key…'; | |
| fetch('/admin', { | |
| method: 'GET', | |
| headers: { 'X-API-Key': keyInput.value } | |
| }) | |
| .then(function (response) { | |
| if (response.ok) { | |
| window.location.href = '/admin'; | |
| return; | |
| } | |
| if (response.status === 401 || response.status === 403) { | |
| showError("That API key wasn't recognized. Double-check the value in admin_api_key.txt."); | |
| } else if (response.status === 429) { | |
| showError("Too many sign-in attempts. Wait a moment and try again."); | |
| } else if (response.status >= 500) { | |
| showError("Something went wrong on the server. Try again in a moment."); | |
| } else { | |
| showError("Sign-in failed (HTTP " + response.status + "). Try again."); | |
| } | |
| }) | |
| .catch(function () { | |
| showError("Couldn't reach the server. Check your connection and try again."); | |
| }) | |
| .finally(function () { | |
| submitBtn.removeAttribute('aria-busy'); | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = originalLabel; | |
| }); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |