feat: Add Remember Me to admin login

- Added Remember Me checkbox to login form with 30-day session expiry
- Sessions default to 1 day; when remember_me=true, expire in 30 days
- User info stored in localStorage (persistent) vs sessionStorage (per-tab)
- initAuth() checks localStorage first, then sessionStorage for user info
This commit is contained in:
Leo
2026-06-01 13:10:26 -04:00
parent ed5e3ff9ec
commit 7a2d283379
2 changed files with 19 additions and 7 deletions
+4 -2
View File
@@ -834,9 +834,11 @@ def login(body: dict):
raise HTTPException(status_code=401, detail="Invalid username or password") raise HTTPException(status_code=401, detail="Invalid username or password")
token = _generate_token() token = _generate_token()
remember_me = body.get("remember_me", False)
expiry = "+30 days" if remember_me else "+1 day"
conn.execute( conn.execute(
"INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, datetime('now', '+1 day'))", "INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, datetime('now', ?))",
(row["id"], token), (row["id"], token, expiry),
) )
conn.commit() conn.commit()
_log_activity(conn, "login", "user", row["id"], _log_activity(conn, "login", "user", row["id"],
+15 -5
View File
@@ -937,6 +937,9 @@
<div id="loginError" class="login-error"></div> <div id="loginError" class="login-error"></div>
<input id="loginUser" class="input-field" type="text" placeholder="Username" autocomplete="username"> <input id="loginUser" class="input-field" type="text" placeholder="Username" autocomplete="username">
<input id="loginPass" class="input-field" type="password" placeholder="Password" autocomplete="current-password"> <input id="loginPass" class="input-field" type="password" placeholder="Password" autocomplete="current-password">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text2,#999);margin-top:4px;cursor:pointer;">
<input type="checkbox" id="loginRemember" checked> Remember me (30 days)
</label>
<button id="loginBtn" class="btn btn-primary btn-block" onclick="doLogin()">Sign In</button> <button id="loginBtn" class="btn btn-primary btn-block" onclick="doLogin()">Sign In</button>
</div> </div>
</div> </div>
@@ -1822,6 +1825,7 @@ async function doLogin() {
const username = document.getElementById('loginUser').value.trim(); const username = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value; const password = document.getElementById('loginPass').value;
const loginBtn = document.getElementById('loginBtn'); const loginBtn = document.getElementById('loginBtn');
const rememberMe = document.getElementById('loginRemember').checked;
if (!username || !password) { if (!username || !password) {
showLoginError('Please enter username and password'); showLoginError('Please enter username and password');
return; return;
@@ -1832,12 +1836,19 @@ async function doLogin() {
const data = await api('/api/auth/login', { const data = await api('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password, remember_me: rememberMe }),
}); });
AppState.currentUser = { id: data.id, username: data.username, role: data.role }; AppState.currentUser = { id: data.id, username: data.username, role: data.role };
AppState.authToken = data.token; AppState.authToken = data.token;
localStorage.setItem('adminToken', data.token); localStorage.setItem('adminToken', data.token);
sessionStorage.setItem('adminUser', JSON.stringify(AppState.currentUser)); const userInfo = JSON.stringify(AppState.currentUser);
if (rememberMe) {
localStorage.setItem('adminUser', userInfo);
sessionStorage.removeItem('adminUser');
} else {
sessionStorage.setItem('adminUser', userInfo);
localStorage.removeItem('adminUser');
}
document.getElementById('loginOverlay').classList.add('hidden'); document.getElementById('loginOverlay').classList.add('hidden');
updateUserUI(); updateUserUI();
switchPage('dashboard'); switchPage('dashboard');
@@ -1875,9 +1886,8 @@ async function initAuth() {
try { try {
// Verify token by fetching a protected endpoint that returns user info // Verify token by fetching a protected endpoint that returns user info
const users = await api('/api/users?limit=1'); const users = await api('/api/users?limit=1');
// If we got here, token is valid — but we need user info. // If we got here, token is valid — try localStorage first, then sessionStorage
// The login response stored user data, try sessionStorage fallback. const savedUser = localStorage.getItem('adminUser') || sessionStorage.getItem('adminUser');
const savedUser = sessionStorage.getItem('adminUser');
if (savedUser) { if (savedUser) {
AppState.currentUser = JSON.parse(savedUser); AppState.currentUser = JSON.parse(savedUser);
document.getElementById('loginOverlay').classList.add('hidden'); document.getElementById('loginOverlay').classList.add('hidden');