102 lines
3.5 KiB
HTML
102 lines
3.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Sanad — Sign in</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0c0f14; --panel:#141922; --border:#222a37; --text:#e8eef7;
|
|
--dim:#6b7585; --muted:#8a95a7; --accent:#3aa9ff; --err:#ff5d5d; --ok:#5fdc8a;
|
|
}
|
|
*{box-sizing:border-box}
|
|
html,body{height:100%;margin:0}
|
|
body{
|
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,sans-serif;
|
|
background:radial-gradient(ellipse at top,#162033 0%,var(--bg) 60%);
|
|
color:var(--text);
|
|
display:flex;align-items:center;justify-content:center;
|
|
padding:1rem;
|
|
}
|
|
.card{
|
|
width:100%;max-width:360px;background:var(--panel);
|
|
border:1px solid var(--border);border-radius:14px;
|
|
padding:1.8rem 1.6rem 1.4rem;
|
|
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
|
}
|
|
.brand{font-size:1.5rem;font-weight:700;margin-bottom:.1rem}
|
|
.brand .accent{color:var(--accent)}
|
|
.sub{color:var(--muted);font-size:.78rem;margin-bottom:1.3rem}
|
|
label{display:block;font-size:.72rem;color:var(--dim);margin:.5rem 0 .25rem;letter-spacing:.04em;text-transform:uppercase}
|
|
input{
|
|
width:100%;padding:.55rem .7rem;
|
|
background:#0d121a;border:1px solid var(--border);border-radius:8px;
|
|
color:var(--text);font:inherit;font-size:.92rem;outline:none;
|
|
}
|
|
input:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(58,169,255,.18)}
|
|
.btn{
|
|
width:100%;margin-top:1rem;padding:.6rem .8rem;
|
|
background:var(--accent);color:#031426;border:0;border-radius:8px;
|
|
font-weight:600;font-size:.92rem;cursor:pointer;
|
|
transition:filter .12s,opacity .12s;
|
|
}
|
|
.btn:hover{filter:brightness(1.08)}
|
|
.btn:disabled{opacity:.55;cursor:wait}
|
|
.msg{margin-top:.85rem;font-size:.78rem;min-height:1em}
|
|
.msg.err{color:var(--err)}
|
|
.msg.ok{color:var(--ok)}
|
|
.hint{margin-top:1rem;font-size:.65rem;color:var(--dim);text-align:center}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<form class="card" id="loginForm" autocomplete="on">
|
|
<div class="brand">Sanad <span class="accent">Dashboard</span></div>
|
|
<div class="sub">Sign in to continue</div>
|
|
|
|
<label for="u">Username</label>
|
|
<input id="u" name="username" autocomplete="username" required autofocus>
|
|
|
|
<label for="p">Password</label>
|
|
<input id="p" name="password" type="password" autocomplete="current-password" required>
|
|
|
|
<button class="btn" id="submit" type="submit">Sign in</button>
|
|
<div class="msg" id="msg"></div>
|
|
<div class="hint">Robot control — local network only</div>
|
|
</form>
|
|
|
|
<script>
|
|
const form=document.getElementById('loginForm');
|
|
const msg=document.getElementById('msg');
|
|
const btn=document.getElementById('submit');
|
|
form.addEventListener('submit',async ev=>{
|
|
ev.preventDefault();
|
|
msg.className='msg';msg.textContent='';
|
|
btn.disabled=true;btn.textContent='Signing in…';
|
|
try{
|
|
const r=await fetch('/api/auth/login',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({
|
|
username:document.getElementById('u').value,
|
|
password:document.getElementById('p').value,
|
|
}),
|
|
});
|
|
if(r.ok){
|
|
msg.className='msg ok';msg.textContent='Signed in — redirecting…';
|
|
const next=new URL(location.href).searchParams.get('next')||'/';
|
|
location.href=next;
|
|
return;
|
|
}
|
|
let detail='Invalid credentials';
|
|
try{const j=await r.json();detail=j.detail||detail;}catch(e){}
|
|
msg.className='msg err';msg.textContent=detail;
|
|
}catch(e){
|
|
msg.className='msg err';msg.textContent='Network error: '+(e&&e.message||e);
|
|
}finally{
|
|
btn.disabled=false;btn.textContent='Sign in';
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|