394 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TIP — Login</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0d0d0d;
--surface: #161616;
--surface2: #1f1f1f;
--border: #2a2a2a;
--text: #e0e0e0;
--text-dim: #666;
--accent: #FF8100;
--accent-dark: #e07000;
--accent-glow: rgba(255,129,0,0.15);
--red: #c1121f;
--red-light: rgba(193,18,31,0.12);
--mono: 'JetBrains Mono', monospace;
--font: 'DM Sans', system-ui, sans-serif;
--radius: 10px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
/* Subtle grid background */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255,129,0,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,129,0,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* Glow orb */
body::after {
content: '';
position: fixed;
top: -20%;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(255,129,0,0.08) 0%, transparent 70%);
pointer-events: none;
}
.card {
position: relative;
z-index: 1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
box-shadow: 0 24px 64px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,129,0,0.05);
}
.logo-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 2rem;
}
.logo-mark {
width: 40px; height: 40px;
border-radius: 10px;
background: var(--accent);
display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: 1rem; color: #fff;
letter-spacing: -0.02em;
box-shadow: 0 4px 16px rgba(255,129,0,0.3);
}
.logo-text {
font-size: 1.1rem;
font-weight: 700;
color: #fff;
letter-spacing: -0.02em;
}
.logo-sub {
font-size: 0.72rem;
color: var(--text-dim);
font-family: var(--mono);
margin-top: 1px;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
color: #fff;
letter-spacing: -0.03em;
margin-bottom: 0.4rem;
}
.subtitle {
font-size: 0.82rem;
color: var(--text-dim);
margin-bottom: 2rem;
}
.field {
margin-bottom: 1.25rem;
}
label {
display: block;
font-size: 0.78rem;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 0.5rem;
}
.input-wrap {
position: relative;
}
input[type="password"] {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem 2.75rem 0.75rem 0.9rem;
font-family: var(--mono);
font-size: 0.9rem;
color: var(--text);
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
letter-spacing: 0.1em;
}
input[type="password"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
input[type="password"].error {
border-color: var(--red);
box-shadow: 0 0 0 3px var(--red-light);
}
input[type="password"]::placeholder {
letter-spacing: 0.05em;
color: #444;
}
.toggle-pw {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-dim);
cursor: pointer;
padding: 0;
font-size: 1rem;
line-height: 1;
transition: color 0.2s;
}
.toggle-pw:hover { color: var(--text); }
.error-msg {
display: none;
align-items: center;
gap: 0.4rem;
background: var(--red-light);
border: 1px solid rgba(193,18,31,0.3);
border-radius: var(--radius);
padding: 0.6rem 0.85rem;
font-size: 0.8rem;
color: #f87171;
margin-bottom: 1.25rem;
}
.error-msg.show { display: flex; }
button[type="submit"] {
width: 100%;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
padding: 0.85rem 1rem;
font-family: var(--font);
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s, transform 0.1s;
letter-spacing: -0.01em;
}
button[type="submit"]:hover {
background: var(--accent-dark);
box-shadow: 0 4px 16px rgba(255,129,0,0.25);
}
button[type="submit"]:active { transform: scale(0.98); }
button[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.spinner {
display: none;
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin: 0 auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
.footer-note {
margin-top: 1.75rem;
text-align: center;
font-size: 0.72rem;
color: var(--text-dim);
font-family: var(--mono);
}
.footer-note span {
color: var(--accent);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="card">
<div class="logo-row">
<div class="logo-mark">TIP</div>
<div>
<div class="logo-text">Transceiver Intelligence</div>
<div class="logo-sub">Platform · v0.3</div>
</div>
</div>
<h1>Sign in</h1>
<p class="subtitle">Enter your access password to continue.</p>
<div id="errorBox" class="error-msg">
<span></span>
<span id="errorText">Invalid password. Please try again.</span>
</div>
<form id="loginForm" autocomplete="off">
<div class="field">
<label for="password">Password</label>
<div class="input-wrap">
<input
id="password"
type="password"
placeholder="••••••••••••"
autofocus
autocomplete="current-password"
/>
<button type="button" class="toggle-pw" id="togglePw" title="Show / hide password">
<span id="eyeIcon">👁</span>
</button>
</div>
</div>
<button type="submit" id="submitBtn">
<span id="btnLabel">Sign in</span>
<div class="spinner" id="spinner"></div>
</button>
</form>
<div class="footer-note">
<span>Flexoptix</span> · Internal use only
</div>
</div>
<script>
var API = '/api/auth/login';
var REDIRECT = '/dashboard/';
// ── Token storage helpers — never store plaintext ───────────────────
(function() {
var _K = 'tip_v3_tk';
var _x = 'fx9z2mq8';
function _enc(s) {
var r = '';
for (var i = 0; i < s.length; i++) r += String.fromCharCode(s.charCodeAt(i) ^ _x.charCodeAt(i % _x.length));
return btoa(r);
}
function _dec(s) {
try {
var b = atob(s); var r = '';
for (var i = 0; i < b.length; i++) r += String.fromCharCode(b.charCodeAt(i) ^ _x.charCodeAt(i % _x.length));
return r;
} catch(e) { return ''; }
}
window.saveToken = function(t) { localStorage.setItem(_K, _enc(t)); localStorage.removeItem('tip_token'); };
window.loadToken = function() {
var v = localStorage.getItem(_K);
if (v) return _dec(v) || '';
var old = localStorage.getItem('tip_token');
if (old) { window.saveToken(old); return old; }
return '';
};
})();
// If already logged in → skip straight to dashboard
(function() {
var t = window.loadToken();
if (!t) return;
fetch('/api/auth/verify', { headers: { Authorization: 'Bearer ' + t } })
.then(function(r) { if (r.ok) window.location.replace(REDIRECT); })
.catch(function() {});
})();
var form = document.getElementById('loginForm');
var pwInput = document.getElementById('password');
var errBox = document.getElementById('errorBox');
var errText = document.getElementById('errorText');
var btn = document.getElementById('submitBtn');
var lbl = document.getElementById('btnLabel');
var spin = document.getElementById('spinner');
var toggle = document.getElementById('togglePw');
var eye = document.getElementById('eyeIcon');
// Toggle password visibility
toggle.addEventListener('click', function() {
var isText = pwInput.type === 'text';
pwInput.type = isText ? 'password' : 'text';
eye.textContent = isText ? '👁' : '🙈';
});
// Clear error on typing
pwInput.addEventListener('input', function() {
pwInput.classList.remove('error');
errBox.classList.remove('show');
});
form.addEventListener('submit', async function(e) {
e.preventDefault();
var pw = pwInput.value;
if (!pw) { showError('Please enter your password.'); return; }
// Loading state
btn.disabled = true;
lbl.style.display = 'none';
spin.style.display = 'block';
errBox.classList.remove('show');
try {
var res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw })
});
var data = await res.json();
if (res.ok && data.token) {
window.saveToken(data.token);
window.location.replace(REDIRECT);
} else {
showError(data.error || 'Invalid password. Please try again.');
}
} catch (err) {
showError('Network error — please try again.');
} finally {
btn.disabled = false;
lbl.style.display = '';
spin.style.display = 'none';
}
});
function showError(msg) {
errText.textContent = msg;
errBox.classList.add('show');
pwInput.classList.add('error');
pwInput.focus();
}
// Enter key on input
pwInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') form.dispatchEvent(new Event('submit'));
});
</script>
</body>
</html>