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:
parent
ad40678b6e
commit
b5bfcc5489
7 changed files with 35 additions and 33 deletions
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(); });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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(); })
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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' }); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue