| <!DOCTYPE html> |
| <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; |
| 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> |