355 lines
18 KiB
HTML
355 lines
18 KiB
HTML
{{define "login.html"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Chat Switchboard</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/favicon.svg?v={{.Version}}">
|
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}/favicon.ico?v={{.Version}}">
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg: #0e0e10; --bg-surface: #18181b; --bg-raised: #222227;
|
|
--border: #2e2e35; --border-light: #3a3a42;
|
|
--text: #e8e8ed; --text-2: #9898a8; --text-3: #6b6b7b;
|
|
--accent: #6c9fff; --accent-hover: #84b0ff; --accent-dim: rgba(108,159,255,0.12);
|
|
--purple: #a78bfa; --purple-dim: rgba(167,139,250,0.10);
|
|
--danger: #ef4444; --success: #22c55e;
|
|
--input-bg: #1a1a1f;
|
|
--grid-line: rgba(108,159,255,0.04);
|
|
--glow1: rgba(108,159,255,0.06); --glow2: rgba(167,139,250,0.05);
|
|
--orb: rgba(167,139,250,0.08);
|
|
--font: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
--mono: 'JetBrains Mono', monospace;
|
|
--banner-h: 28px;
|
|
--banner-top-h: {{if .Banner.Visible}}var(--banner-h){{else}}0px{{end}};
|
|
--banner-bot-h: {{if .Banner.Visible}}var(--banner-h){{else}}0px{{end}};
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: var(--font); background: var(--bg); color: var(--text); overflow: hidden; }
|
|
|
|
.login-shell { display: flex; flex-direction: column; height: 100vh; }
|
|
.login-main { display: flex; flex: 1; min-height: 0; }
|
|
|
|
/* ── Hero Panel (left) ─────────────────── */
|
|
.hero {
|
|
flex: 1; display: flex; flex-direction: column; justify-content: center;
|
|
padding: 4rem 3.5rem; position: relative; overflow: hidden;
|
|
background:
|
|
radial-gradient(ellipse 80% 60% at 30% 40%, var(--glow1), transparent),
|
|
radial-gradient(ellipse 60% 50% at 70% 70%, var(--glow2), transparent),
|
|
var(--bg);
|
|
}
|
|
.hero-grid {
|
|
position: absolute; inset: -50%;
|
|
background-image:
|
|
linear-gradient(var(--grid-line) 1px, transparent 1px),
|
|
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
|
|
background-size: 48px 48px;
|
|
animation: gridDrift 30s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
.hero-orb {
|
|
position: absolute; width: 320px; height: 320px; bottom: -80px; right: -60px;
|
|
background: radial-gradient(circle, var(--orb), transparent 70%);
|
|
border-radius: 50%; pointer-events: none;
|
|
}
|
|
.hero-content { position: relative; z-index: 1; max-width: 520px; }
|
|
|
|
.hero-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 1.5rem; }
|
|
.hero-brand-icon { width: 40px; height: 40px; }
|
|
.hero-brand-text { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.03em; line-height: 1.1; }
|
|
.hero-brand-text .hl { color: var(--accent); }
|
|
|
|
.hero h1 {
|
|
font-size: 2.4rem; font-weight: 700; line-height: 1.15;
|
|
letter-spacing: -0.03em; margin-bottom: 1rem;
|
|
background: linear-gradient(135deg, var(--text) 0%, var(--text-2) 100%);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
|
}
|
|
.hero-sub {
|
|
font-size: 1.05rem; color: var(--text-2); line-height: 1.6;
|
|
margin-bottom: 2.5rem; max-width: 440px;
|
|
}
|
|
.hero-pills { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
.hero-pill {
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
padding: 7px 14px; background: var(--bg-surface);
|
|
border-radius: 100px; font-size: 0.8rem; font-weight: 500;
|
|
transition: all 0.15s;
|
|
}
|
|
.hero-pill.ac { border: 1px solid rgba(108,159,255,0.2); color: var(--accent); }
|
|
.hero-pill.pu { border: 1px solid rgba(167,139,250,0.2); color: var(--purple); }
|
|
.hero-pill-icon { font-size: 0.95rem; line-height: 1; }
|
|
|
|
.hero-version {
|
|
margin-top: 3rem; font-size: 0.72rem; font-family: var(--mono);
|
|
color: var(--text-3); letter-spacing: 0.04em;
|
|
}
|
|
|
|
/* ── Auth Panel (right) ────────────────── */
|
|
.auth-panel {
|
|
width: 440px; flex-shrink: 0; display: flex; align-items: center;
|
|
justify-content: center; padding: 2rem; background: var(--bg-surface);
|
|
border-left: 1px solid var(--border); overflow-y: auto;
|
|
}
|
|
.auth-inner { width: 100%; max-width: 340px; }
|
|
|
|
.auth-heading { margin-bottom: 2rem; }
|
|
.auth-heading h2 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 4px; }
|
|
.auth-heading p { font-size: 0.85rem; color: var(--text-3); }
|
|
|
|
.auth-tabs { display: flex; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border); }
|
|
.auth-tab {
|
|
flex: 1; background: none; border: none; border-bottom: 2px solid transparent;
|
|
color: var(--text-3); padding: 10px 0; cursor: pointer; font-family: var(--font);
|
|
font-size: 0.85rem; font-weight: 500; transition: color 0.15s;
|
|
}
|
|
.auth-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
.auth-tab:hover { color: var(--text-2); }
|
|
|
|
.form-field { margin-bottom: 16px; }
|
|
.form-field label { display: block; font-size: 13px; font-weight: 500; color: var(--text-2); margin-bottom: 6px; }
|
|
.form-field input {
|
|
width: 100%; box-sizing: border-box; background: var(--input-bg);
|
|
border: 1px solid var(--border); color: var(--text);
|
|
padding: 10px 14px; border-radius: 10px; font-size: 14px; font-family: var(--font);
|
|
outline: none; transition: border-color 0.15s, box-shadow 0.15s;
|
|
}
|
|
.form-field input:focus {
|
|
border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim);
|
|
}
|
|
|
|
.auth-error { color: var(--danger); font-size: 0.78rem; margin: 0 0 0.5rem; }
|
|
|
|
.btn-login {
|
|
width: 100%; background: var(--accent); color: #fff; border: none;
|
|
padding: 10px 18px; font-size: 14px; border-radius: 10px; cursor: pointer;
|
|
font-family: var(--font); font-weight: 600; letter-spacing: -0.01em;
|
|
transition: all 0.18s; margin-top: 1.5rem;
|
|
}
|
|
.btn-login:hover { background: var(--accent-hover); }
|
|
.btn-login:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
|
|
.auth-footer {
|
|
margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border);
|
|
text-align: center;
|
|
}
|
|
.auth-footer p { font-size: 0.75rem; color: var(--text-3); line-height: 1.6; }
|
|
|
|
/* ── Banner ─────────────────────────────── */
|
|
.login-banner {
|
|
text-align: center; font-size: 11px; font-weight: 700;
|
|
letter-spacing: 1.2px; padding: 3px 0; text-transform: uppercase; flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Animations ─────────────────────────── */
|
|
@keyframes gridDrift { to { transform: translate(48px, 48px); } }
|
|
@keyframes fadeUp { to { opacity: 1; transform: translateY(0); } }
|
|
.anim { opacity: 0; transform: translateY(12px); animation: fadeUp 0.5s ease forwards; }
|
|
.anim-1 { animation-delay: .05s; } .anim-2 { animation-delay: .12s; }
|
|
.anim-3 { animation-delay: .19s; } .anim-4 { animation-delay: .26s; }
|
|
.anim-5 { animation-delay: .33s; }
|
|
.anim-a1 { animation-delay: .15s; } .anim-a2 { animation-delay: .22s; }
|
|
.anim-a3 { animation-delay: .29s; }
|
|
|
|
/* ── Responsive ─────────────────────────── */
|
|
@media (max-width: 900px) {
|
|
.hero { display: none; }
|
|
.auth-panel { width: 100%; border-left: none; }
|
|
}
|
|
|
|
::-webkit-scrollbar { width: 5px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { border-radius: 3px; background: var(--border); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="login-shell">
|
|
|
|
{{if .Banner.Visible}}
|
|
<div class="login-banner" style="background:{{.Banner.Background}};color:{{.Banner.Color}};">
|
|
{{.Banner.Text}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="login-main">
|
|
|
|
<!-- ═══ HERO (left) ═══ -->
|
|
<div class="hero">
|
|
<div class="hero-grid"></div>
|
|
<div class="hero-orb"></div>
|
|
<div class="hero-content">
|
|
<div class="hero-brand anim anim-1">
|
|
<img class="hero-brand-icon" src="{{.BasePath}}/favicon.svg?v={{.Version}}" alt="Chat Switchboard">
|
|
<span class="hero-brand-text">Chat <span class="hl">Switchboard</span></span>
|
|
</div>
|
|
<h1 class="anim anim-2">Your AI conversations,<br>your infrastructure</h1>
|
|
<p class="hero-sub anim anim-3">
|
|
Self-hosted multi-provider chat with team collaboration, BYOK privacy, and an extensible plugin system.
|
|
</p>
|
|
<div class="hero-pills anim anim-4">
|
|
<span class="hero-pill ac"><span class="hero-pill-icon">🤖</span> Multi-Provider AI</span>
|
|
<span class="hero-pill pu"><span class="hero-pill-icon">🔑</span> Bring Your Own Key</span>
|
|
<span class="hero-pill ac"><span class="hero-pill-icon">👥</span> Teams & Projects</span>
|
|
<span class="hero-pill pu"><span class="hero-pill-icon">🧩</span> Extensions</span>
|
|
<span class="hero-pill ac"><span class="hero-pill-icon">🔍</span> Web Search Tools</span>
|
|
<span class="hero-pill pu"><span class="hero-pill-icon">📝</span> Notes & Memory</span>
|
|
</div>
|
|
<p class="hero-version anim anim-5">v{{.Version}}{{if .Environment}} · {{.Environment}}{{end}}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ AUTH (right) ═══ -->
|
|
<div class="auth-panel">
|
|
<div class="auth-inner">
|
|
<div class="auth-heading anim anim-a1">
|
|
<h2 id="authTitle">Welcome back</h2>
|
|
<p id="authSubtitle">Sign in to your Switchboard instance</p>
|
|
</div>
|
|
|
|
<div class="auth-tabs anim anim-a2">
|
|
<button class="auth-tab active" id="tabLogin" onclick="_switchTab('login')">Log In</button>
|
|
{{if .RegistrationOpen}}<button class="auth-tab" id="tabRegister" onclick="_switchTab('register')">Register</button>{{end}}
|
|
</div>
|
|
|
|
<!-- LOGIN FORM -->
|
|
<div id="loginForm" class="anim anim-a3">
|
|
<div class="form-field">
|
|
<label for="loginUsername">Username</label>
|
|
<input type="text" id="loginUsername" placeholder="Enter username" autocomplete="username" autofocus>
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="loginPassword">Password</label>
|
|
<input type="password" id="loginPassword" placeholder="Enter password" autocomplete="current-password">
|
|
</div>
|
|
<p class="auth-error" id="loginError" style="display:none;"></p>
|
|
<button class="btn-login" id="loginBtn" onclick="Pages.doLogin()">Log In</button>
|
|
</div>
|
|
|
|
<!-- REGISTER FORM -->
|
|
{{if .RegistrationOpen}}
|
|
<div id="registerForm" style="display:none;">
|
|
<div class="form-field">
|
|
<label for="regUsername">Username</label>
|
|
<input type="text" id="regUsername" placeholder="Choose a username" autocomplete="username">
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="regEmail">Email</label>
|
|
<input type="email" id="regEmail" placeholder="you@example.com" autocomplete="email">
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="regPassword">Password</label>
|
|
<input type="password" id="regPassword" placeholder="Min 8 characters" autocomplete="new-password">
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="regConfirm">Confirm Password</label>
|
|
<input type="password" id="regConfirm" placeholder="Re-enter password" autocomplete="new-password">
|
|
</div>
|
|
<p class="auth-error" id="regError" style="display:none;"></p>
|
|
<button class="btn-login" id="regBtn" onclick="_doRegister()">Create Account</button>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="auth-footer">
|
|
<p>Self-hosted instance · Your data stays on your infrastructure</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .Banner.Visible}}
|
|
<div class="login-banner" style="background:{{.Banner.Background}};color:{{.Banner.Color}};">
|
|
{{.Banner.Text}}
|
|
</div>
|
|
{{end}}
|
|
|
|
</div>
|
|
|
|
<script>
|
|
window.__BASE__ = '{{.BasePath}}';
|
|
</script>
|
|
<script src="{{.BasePath}}/js/pages.js?v={{.Version}}"></script>
|
|
<script>
|
|
function _switchTab(tab) {
|
|
document.getElementById('loginForm').style.display = tab === 'login' ? '' : 'none';
|
|
const regForm = document.getElementById('registerForm');
|
|
if (regForm) regForm.style.display = tab === 'register' ? '' : 'none';
|
|
document.getElementById('tabLogin').classList.toggle('active', tab === 'login');
|
|
const regTab = document.getElementById('tabRegister');
|
|
if (regTab) regTab.classList.toggle('active', tab === 'register');
|
|
document.getElementById('authTitle').textContent = tab === 'login' ? 'Welcome back' : 'Create account';
|
|
document.getElementById('authSubtitle').textContent = tab === 'login'
|
|
? 'Sign in to your Switchboard instance'
|
|
: 'Join your team on Switchboard';
|
|
document.getElementById('loginError').textContent = '';
|
|
const regErr = document.getElementById('regError');
|
|
if (regErr) { regErr.textContent = ''; regErr.style.display = 'none'; }
|
|
}
|
|
|
|
async function _doRegister() {
|
|
const username = document.getElementById('regUsername').value.trim();
|
|
const email = document.getElementById('regEmail').value.trim();
|
|
const password = document.getElementById('regPassword').value;
|
|
const confirm = document.getElementById('regConfirm').value;
|
|
const errEl = document.getElementById('regError');
|
|
const btn = document.getElementById('regBtn');
|
|
|
|
if (!username || !password) { errEl.textContent = 'Username and password are required'; errEl.style.display = ''; return; }
|
|
if (password.length < 8) { errEl.textContent = 'Password must be at least 8 characters'; errEl.style.display = ''; return; }
|
|
if (password !== confirm) { errEl.textContent = 'Passwords do not match'; errEl.style.display = ''; return; }
|
|
|
|
errEl.style.display = 'none';
|
|
errEl.textContent = '';
|
|
btn.disabled = true; btn.textContent = 'Creating account\u2026';
|
|
|
|
const base = window.__BASE__ || '';
|
|
try {
|
|
const resp = await fetch(base + '/api/v1/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, email, password }),
|
|
});
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Registration failed');
|
|
}
|
|
const data = await resp.json();
|
|
if (data.pending) {
|
|
errEl.textContent = 'Account created — pending admin approval.';
|
|
errEl.style.color = 'var(--success)';
|
|
errEl.style.display = '';
|
|
setTimeout(() => { errEl.style.color = ''; errEl.style.display = 'none'; _switchTab('login'); }, 3000);
|
|
} else {
|
|
// Auto-login
|
|
const storageKey = base ? 'sb_auth_' + base.replace(/\//g, '') : 'sb_auth';
|
|
localStorage.setItem(storageKey, JSON.stringify({
|
|
accessToken: data.access_token, refreshToken: data.refresh_token, user: data.user,
|
|
}));
|
|
document.cookie = 'sb_token=' + data.access_token + '; path=/; max-age=900; SameSite=Strict';
|
|
window.location.href = base + '/';
|
|
}
|
|
} catch (e) {
|
|
errEl.textContent = e.message;
|
|
errEl.style.display = '';
|
|
} finally {
|
|
btn.disabled = false; btn.textContent = 'Create Account';
|
|
}
|
|
}
|
|
|
|
// Enter key handlers
|
|
document.getElementById('loginPassword').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') Pages.doLogin();
|
|
});
|
|
document.getElementById('loginUsername').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') document.getElementById('loginPassword').focus();
|
|
});
|
|
document.getElementById('regConfirm')?.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') _doRegister();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|