From b5bfcc5489bc76319767da8552dc1e53ea661646 Mon Sep 17 00:00:00 2001 From: Daniel Onyejesi Date: Tue, 24 Mar 2026 01:57:59 -0400 Subject: [PATCH] Revert auth to localStorage tokens, fix slide padding, AI panel options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/css/styles.css | 3 ++- public/index.html | 3 +-- public/js/app.js | 2 +- public/js/auth.js | 39 ++++++++++++++++++++++----------------- public/js/learningHub.js | 5 +++-- src/middleware/auth.js | 10 ++++------ src/routes/auth.js | 6 ++---- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/public/css/styles.css b/public/css/styles.css index 9286080..6382c07 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -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;} diff --git a/public/index.html b/public/index.html index c862eaa..430fa82 100644 --- a/public/index.html +++ b/public/index.html @@ -21,8 +21,7 @@ - - + diff --git a/public/js/app.js b/public/js/app.js index 7743e15..502c47e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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(); }); } diff --git a/public/js/auth.js b/public/js/auth.js index 686992b..4deb809 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -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'); diff --git a/public/js/learningHub.js b/public/js/learningHub.js index a333c47..0385db7 100644 --- a/public/js/learningHub.js +++ b/public/js/learningHub.js @@ -718,10 +718,11 @@ if (btn) { btn.disabled = true; btn.innerHTML = ' 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(); }) diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 9c66a49..eea8541 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -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' }); diff --git a/src/routes/auth.js b/src/routes/auth.js index 474185f..6c32804 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -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' }); }