| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Admin sign in</title> |
| <style> |
| :root { |
| --bg: #120f0d; |
| --bg-deep: #080706; |
| --panel: rgba(29, 24, 21, 0.9); |
| --line: rgba(247, 239, 230, 0.12); |
| --text: #f7efe6; |
| --muted: #b6aa98; |
| --accent: #efd9bc; |
| --accent-strong: #ddbb8e; |
| --danger-bg: rgba(127, 29, 29, 0.24); |
| --danger-line: rgba(248, 113, 113, 0.28); |
| --danger-text: #fecaca; |
| --shadow: 0 34px 90px rgba(0, 0, 0, 0.45); |
| } |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| body { |
| margin: 0; |
| min-height: 100vh; |
| display: grid; |
| place-items: center; |
| padding: 20px; |
| color: var(--text); |
| font-family: "Avenir Next", "Segoe UI", ui-sans-serif, system-ui, sans-serif; |
| background: |
| radial-gradient(circle at top, rgba(239, 217, 188, 0.16), transparent 28%), |
| linear-gradient(180deg, #171210 0%, var(--bg) 44%, var(--bg-deep) 100%); |
| } |
| |
| body::before { |
| content: ""; |
| position: fixed; |
| inset: 0; |
| pointer-events: none; |
| opacity: 0.14; |
| background-image: linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px); |
| background-size: 100% 3px; |
| } |
| |
| .card { |
| width: min(460px, 100%); |
| padding: 30px; |
| border-radius: 30px; |
| border: 1px solid var(--line); |
| background: linear-gradient(180deg, rgba(39, 32, 28, 0.96), rgba(24, 20, 17, 0.94)); |
| box-shadow: var(--shadow); |
| backdrop-filter: blur(18px); |
| } |
| |
| .eyebrow { |
| display: inline-flex; |
| align-items: center; |
| gap: 10px; |
| padding: 8px 14px; |
| border-radius: 999px; |
| border: 1px solid rgba(239, 217, 188, 0.18); |
| background: rgba(239, 217, 188, 0.08); |
| color: var(--accent-strong); |
| font-size: 11px; |
| letter-spacing: 0.18em; |
| font-weight: 700; |
| text-transform: uppercase; |
| } |
| |
| h1 { |
| margin: 18px 0 10px; |
| font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif; |
| font-size: clamp(2.2rem, 7vw, 3.2rem); |
| line-height: 0.96; |
| letter-spacing: -0.04em; |
| } |
| |
| p { |
| margin: 0 0 22px; |
| color: var(--muted); |
| line-height: 1.75; |
| } |
| |
| label { |
| display: block; |
| margin-bottom: 8px; |
| color: var(--muted); |
| font-size: 13px; |
| letter-spacing: 0.06em; |
| text-transform: uppercase; |
| } |
| |
| input { |
| width: 100%; |
| padding: 14px 16px; |
| border-radius: 18px; |
| border: 1px solid var(--line); |
| background: rgba(8, 7, 6, 0.46); |
| color: var(--text); |
| font-size: 15px; |
| } |
| |
| input:focus { |
| outline: none; |
| border-color: rgba(239, 217, 188, 0.34); |
| box-shadow: 0 0 0 4px rgba(239, 217, 188, 0.08); |
| } |
| |
| .actions { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 12px; |
| margin-top: 18px; |
| } |
| |
| button, |
| .link { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| min-height: 48px; |
| padding: 0 18px; |
| border-radius: 999px; |
| font-size: 14px; |
| font-weight: 600; |
| text-decoration: none; |
| } |
| |
| button { |
| flex: 1 1 180px; |
| border: none; |
| cursor: pointer; |
| color: #1a140f; |
| background: linear-gradient(135deg, var(--accent), var(--accent-strong)); |
| } |
| |
| button:disabled { |
| cursor: wait; |
| opacity: 0.78; |
| } |
| |
| .link { |
| flex: 1 1 140px; |
| border: 1px solid var(--line); |
| color: var(--text); |
| background: rgba(255, 255, 255, 0.03); |
| } |
| |
| .note { |
| margin-top: 16px; |
| font-size: 13px; |
| } |
| |
| .error { |
| display: none; |
| margin-top: 16px; |
| padding: 12px 14px; |
| border-radius: 18px; |
| border: 1px solid var(--danger-line); |
| background: var(--danger-bg); |
| color: var(--danger-text); |
| font-size: 13px; |
| line-height: 1.6; |
| } |
| </style> |
| </head> |
| <body> |
| <form class="card" id="loginForm"> |
| <div class="eyebrow">Admin access</div> |
| <h1>Sign in to the config dashboard.</h1> |
| <p> |
| Use the current admin password for Web2API. If the password is managed by environment |
| variables, this page still accepts that live value. |
| </p> |
|
|
| <label for="secret">Admin password</label> |
| <input |
| id="secret" |
| name="secret" |
| type="password" |
| autocomplete="current-password" |
| placeholder="Enter admin password" |
| /> |
|
|
| <div class="actions"> |
| <button type="submit" id="submitBtn">Sign in</button> |
| <a class="link" href="/">Back home</a> |
| </div> |
|
|
| <p class="note">The dashboard session is stored in an HTTP-only cookie after sign-in.</p> |
| <div class="error" id="error"></div> |
| </form> |
|
|
| <script> |
| const form = document.getElementById('loginForm') |
| const secretInput = document.getElementById('secret') |
| const errorEl = document.getElementById('error') |
| const submitBtn = document.getElementById('submitBtn') |
| |
| function showError(message) { |
| errorEl.textContent = message |
| errorEl.style.display = 'block' |
| } |
| |
| function hideError() { |
| errorEl.style.display = 'none' |
| errorEl.textContent = '' |
| } |
| |
| form.addEventListener('submit', async (event) => { |
| event.preventDefault() |
| hideError() |
| |
| const secret = secretInput.value.trim() |
| if (!secret) { |
| showError('Enter the admin password.') |
| secretInput.focus() |
| return |
| } |
| |
| submitBtn.disabled = true |
| submitBtn.textContent = 'Signing in…' |
| try { |
| const res = await fetch('/api/admin/login', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ secret }), |
| }) |
| |
| const text = await res.text() |
| let data = null |
| try { |
| data = text ? JSON.parse(text) : null |
| } catch (_) {} |
| |
| if (!res.ok) { |
| throw new Error((data && data.detail) || text || 'Sign-in failed.') |
| } |
| |
| window.location.href = '/config' |
| } catch (error) { |
| showError(error && error.message ? error.message : 'Sign-in failed.') |
| } finally { |
| submitBtn.disabled = false |
| submitBtn.textContent = 'Sign in' |
| } |
| }) |
| </script> |
| </body> |
| </html> |
|
|