Revert auth to localStorage tokens, fix slide padding, AI panel options

REVERT: httpOnly cookie auth removed — reverts to JWT in localStorage.
The cookie implementation broke the login page (always-hidden auth screen).
localStorage token approach is restored fully:
- Login/register return token in JSON body
- getAuthHeaders() sends Authorization: Bearer header
- Session check reads localStorage token on load
- clearSession() removes localStorage token

Fix: slide preview padding — section elements now have 40px 48px padding
so text is not flush against the frame edges.

Kept from v3.14: AI panel context-aware options (word count, slide count,
quiz question toggle), delete confirmation wording per content type.
This commit is contained in:
Daniel Onyejesi 2026-03-24 01:57:59 -04:00
parent ad40678b6e
commit b5bfcc5489
7 changed files with 35 additions and 33 deletions

View file

@ -731,7 +731,8 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.slide-preview-close:hover{background:rgba(255,255,255,0.25);}
.slide-preview-stage{flex:1;display:flex;align-items:center;justify-content:center;gap:12px;min-height:0;overflow:hidden;}
.slide-preview-frame{flex:1;max-width:900px;background:white;border-radius:8px;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,0.6);max-height:100%;display:flex;flex-direction:column;}
.slide-preview-frame section{all:unset;display:block;}
.slide-preview-frame section{all:unset;display:block;padding:40px 48px !important;box-sizing:border-box;}
.slide-preview-frame section *{box-sizing:border-box;}
.slide-nav-btn{background:rgba(255,255,255,0.15);border:none;color:white;font-size:36px;line-height:1;padding:10px 14px;border-radius:10px;cursor:pointer;flex-shrink:0;transition:background 0.15s;user-select:none;}
.slide-nav-btn:hover{background:rgba(255,255,255,0.25);}
.slide-preview-dots{display:flex;justify-content:center;gap:6px;padding-top:10px;flex-shrink:0;flex-wrap:wrap;}

View file

@ -21,8 +21,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16.png">
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512.png">
<!-- Auth screen hidden optimistically on load; auth.js verifies via /api/auth/me cookie check -->
<script>document.documentElement.classList.add('has-session');</script>
<script>if (localStorage.getItem('ped_scribe_token')) { document.documentElement.classList.add('has-session'); }</script>
<style>html.has-session #auth-screen { display: none !important; }</style>
</head>
<body>

View file

@ -447,7 +447,7 @@ function transcribeAudio(blob) {
formData.append('audio', blob, 'audio.webm');
return fetch('/api/transcribe', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '') },
body: formData
}).then(function(r) { return r.json(); });
}

View file

@ -24,17 +24,20 @@ document.addEventListener('DOMContentLoaded', function() {
// Make sure forms are visible/hidden correctly on load
showLoginForm();
// Check for saved session via httpOnly cookie (cookie sent automatically)
// Optimistically hide auth screen — shows briefly if no session exists
authScreen.classList.add('hidden');
fetch('/api/auth/me', { credentials: 'same-origin' })
// Check for saved session via localStorage token
var savedToken = localStorage.getItem(TOKEN_KEY);
if (savedToken) {
authScreen.classList.add('hidden');
fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + savedToken }
})
.then(function(r) {
if (r.ok) return r.json();
throw new Error('not authenticated');
throw new Error('expired');
})
.then(function(data) {
if (data && data.user) {
enterApp(data.user);
enterApp(data.user, savedToken);
} else {
authScreen.classList.remove('hidden');
clearSession();
@ -44,6 +47,7 @@ document.addEventListener('DOMContentLoaded', function() {
authScreen.classList.remove('hidden');
clearSession();
});
}
// ---- HELPER FUNCTIONS ----
@ -65,8 +69,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (forgotForm) forgotForm.style.display = 'block';
}
function enterApp(user) {
// Token is now in httpOnly cookie — do NOT store in localStorage/window
function enterApp(user, token) {
window.AUTH_TOKEN = token;
localStorage.setItem(TOKEN_KEY, token);
localStorage.setItem(USER_KEY, JSON.stringify(user));
if (authScreen) authScreen.style.display = 'none';
@ -127,16 +132,16 @@ document.addEventListener('DOMContentLoaded', function() {
}
function clearSession() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
window.AUTH_TOKEN = null;
// Tell server to clear the httpOnly cookie (fire-and-forget)
fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(function() {});
}
// Auth headers — cookie is sent automatically for same-origin requests
// No Authorization header needed; cookie handles authentication
window.getAuthHeaders = function() {
return { 'Content-Type': 'application/json' };
return {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (window.AUTH_TOKEN || localStorage.getItem(TOKEN_KEY) || '')
};
};
// ---- FORM TOGGLE LINKS ----
@ -279,8 +284,8 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
if (data.success && data.user) {
enterApp(data.user);
if (data.success && data.token && data.user) {
enterApp(data.user, data.token);
showToast('Welcome, ' + data.user.name + '!', 'success');
} else {
showToast(data.error || 'Login failed', 'error');
@ -328,8 +333,8 @@ document.addEventListener('DOMContentLoaded', function() {
hideLoading();
console.log('[Auth] Register response:', JSON.stringify(data).substring(0, 200));
if (data.success && data.user) {
enterApp(data.user);
if (data.success && data.token && data.user) {
enterApp(data.user, data.token);
showToast(data.message || 'Account created!', 'success');
} else if (data.success && data.needsVerification) {
showToast(data.message || 'Check email to verify', 'success');

View file

@ -718,10 +718,11 @@
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...'; }
showLoading('AI is generating content…');
// FormData: no Content-Type header (browser sets it with boundary); cookie sent automatically
// FormData: no Content-Type header (browser sets it with boundary)
var token = window.AUTH_TOKEN || localStorage.getItem('ped_scribe_token') || '';
fetch('/api/admin/learning/ai-generate', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Authorization': 'Bearer ' + token },
body: formData
})
.then(function(r) { return r.json(); })

View file

@ -6,14 +6,12 @@ const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
async function authMiddleware(req, res, next) {
var token = null;
// 1. httpOnly cookie (primary — JS cannot read, immune to XSS token theft)
if (req.cookies && req.cookies.ped_auth) {
var authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
} else if (req.cookies && req.cookies.ped_auth) {
token = req.cookies.ped_auth;
}
// 2. Authorization header (backward compat for API clients)
else if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
token = req.headers.authorization.substring(7);
}
if (!token) {
return res.status(401).json({ error: 'Authentication required' });

View file

@ -160,9 +160,8 @@ router.post('/register', async (req, res) => {
if (!smtpHost) {
await db.run('UPDATE users SET email_verified = true, verify_token = NULL WHERE id = ?', [userId]);
var token = jwt.sign({ userId: userId }, JWT_SECRET, { expiresIn: '7d' });
setAuthCookie(res, token);
return res.json({
success: true,
success: true, token: token,
user: { id: userId, email: email.toLowerCase(), name: name, role: role, email_verified: true },
message: role === 'admin' ? 'Account created as ADMIN (first user). Auto-verified.' : 'Account created (auto-verified).'
});
@ -247,10 +246,9 @@ router.post('/login', async (req, res) => {
var token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
await db.run('INSERT INTO audit_log (user_id, action, ip_address) VALUES (?, ?, ?)', [user.id, 'login', req.ip]);
setAuthCookie(res, token);
res.json({
success: true,
success: true, token: token,
user: { id: user.id, email: user.email, name: user.name, role: user.role, totp_enabled: user.totp_enabled, email_verified: user.email_verified }
});
} catch (err) { console.error('[Auth] Login error:', err.message); res.status(500).json({ error: 'Login failed' }); }