Mercurial
view mrjunejune/src/login/index.html @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
<!DOCTYPE html> <html lang="en" data-zen-theme="cyberpunk"> <head> {{/parts/base_head.html}} <title>Sign in — MrJuneJune</title> <style> .login-page main { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: var(--zenbu-sys-padding-lg) var(--zenbu-sys-padding-md); } .login-card { width: 100%; max-width: 360px; } .login-card h2 { margin: 0 0 var(--zenbu-sys-padding-md); font-size: 1.4rem; font-weight: 700; text-align: center; letter-spacing: 0.08em; } .login-form { display: flex; flex-direction: column; gap: var(--zenbu-sys-padding-sm); } .login-actions { display: flex; justify-content: flex-end; gap: var(--zenbu-sys-space-control); margin-top: var(--zenbu-sys-padding-xs, 0.5rem); } #loginError { display: none; padding: 0.6rem 0.75rem; border-left: var(--zenbu-sys-stroke-width-emphasis, 2px) solid var(--zenbu-sys-color-danger-foreground); background: color-mix(in srgb, var(--zenbu-sys-color-danger-foreground) 10%, transparent); color: var(--zenbu-sys-color-danger-foreground); font-size: 0.875rem; } #loginError[aria-hidden="false"] { display: block; } </style> </head> <body class="login-page"> {{/parts/header.html}} <main> <div class="login-card"> <zen-heading size="xl"> <h2>Sign in</h2> </zen-heading> <div id="loginError" role="alert" aria-live="assertive" aria-hidden="true"></div> <form class="login-form" id="loginForm" novalidate> <zen-field appearance="plain" size="md"> <label for="username">Username</label> <input id="username" name="username" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" required maxlength="32" > </zen-field> <zen-field appearance="plain" size="md"> <label for="password">Password</label> <input id="password" name="password" type="password" autocomplete="current-password" required minlength="12" maxlength="1024" > </zen-field> <div class="login-actions"> <zen-button appearance="plain" size="md"> <button type="submit" id="loginSubmit">Sign in</button> </zen-button> </div> </form> </div> </main> <script> (function () { 'use strict'; const form = document.getElementById('loginForm'); const errorEl = document.getElementById('loginError'); const submitBtn = document.getElementById('loginSubmit'); function showError(message) { errorEl.textContent = message; errorEl.setAttribute('aria-hidden', 'false'); } function hideError() { errorEl.textContent = ''; errorEl.setAttribute('aria-hidden', 'true'); } async function fetchSession() { const resp = await fetch('/api/auth/session', { credentials: 'same-origin', }); if (!resp.ok) throw new Error('session unavailable'); return resp.json(); } form.addEventListener('submit', async function (evt) { evt.preventDefault(); hideError(); const username = form.username.value.trim(); const password = form.password.value; if (!username || !password) { showError('Please enter your username and password.'); return; } submitBtn.disabled = true; try { const session = await fetchSession(); const csrfToken = session.csrfToken; const resp = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Origin': window.location.origin, }, credentials: 'same-origin', body: JSON.stringify({ username, password, csrfToken }), }); form.password.value = ''; let data = await resp.json().catch(function () { return null; }); if (!resp.ok) { showError(resp.status === 429 ? 'Too many attempts. Please try again later.' : 'Invalid username or password.'); return; } if (!data) { data = await fetchSession(); if (data.kind !== 'user') { window.location.reload(); return; } } if (data.mustChangePassword) { window.location.href = '/account/password'; return; } function safeNext(n) { if (!n || typeof n !== 'string') return null; if (!n.startsWith('/') || n.startsWith('//') || n.startsWith('/\\')) return null; if (n.includes('\\')) return null; try { const url = new URL(n, window.location.origin); if (url.origin !== window.location.origin) return null; return url.pathname + url.search + url.hash; } catch (_) { return null; } } const next = safeNext(new URLSearchParams(window.location.search).get('next')); window.location.href = next || '/jrpg'; } catch (_) { form.password.value = ''; showError('Sign-in failed. Please try again.'); } finally { submitBtn.disabled = false; } }); })(); </script> </body> </html>