From 07b76bf0489b1bbad1366459682104e03e095178 Mon Sep 17 00:00:00 2001 From: "Jenola.base.eth" Date: Sat, 30 May 2026 00:10:14 +0000 Subject: [PATCH] feat: enhance authentication flow and cookie management - Updated CORS configuration to allow credentials. - Modified authentication middleware to check for token in cookies. - Implemented access token cookie management in auth routes. - Refactored frontend AuthContext to remove token state management. - Adjusted API service to remove token parameter from requests. - Updated various components to handle user state without direct token access. - Added cookie handling for access tokens in login and registration flows. - Improved admin dashboard and campaign management to align with new auth structure. --- backend/src/index.js | 7 +- backend/src/middleware/auth.js | 5 +- backend/src/routes/auth.js | 42 ++++++- frontend/src/context/AuthContext.jsx | 20 +-- frontend/src/pages/AcceptInvite.jsx | 6 +- frontend/src/pages/AdminDashboard.jsx | 50 ++++---- frontend/src/pages/CreateCampaign.jsx | 19 ++- frontend/src/pages/Dashboard.jsx | 10 +- frontend/src/pages/Developer.jsx | 19 ++- frontend/src/pages/Login.jsx | 4 +- frontend/src/pages/Register.jsx | 4 +- frontend/src/services/api.js | 171 ++++++++++++-------------- package-lock.json | 106 ++++++++++++++++ 13 files changed, 286 insertions(+), 177 deletions(-) create mode 100644 package-lock.json diff --git a/backend/src/index.js b/backend/src/index.js index 73a7388..67d7452 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -21,7 +21,12 @@ const rateLimit = require('express-rate-limit'); const app = express(); app.use(helmet()); -app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:5173' })); +app.use( + cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:5173', + credentials: true, + }) +); app.use(express.json({ limit: '50kb' })); app.use(cookieParser()); app.use(requestIdMiddleware); diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index e1e6b4b..dc4af17 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -12,10 +12,7 @@ function hashApiKey(rawKey) { async function authenticate(req) { const header = req.headers.authorization; - if (!header || !header.startsWith('Bearer ')) { - throw new Error('Missing token'); - } - const token = header.slice(7).trim(); + const token = req.cookies?.cp_token || (header && header.startsWith('Bearer ') ? header.slice(7).trim() : null); if (!token) throw new Error('Missing token'); if (token.startsWith('cp_live_')) { diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 4cc41d2..5d95be8 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -27,12 +27,42 @@ const { * description: User registration and login */ +const ACCESS_TOKEN_COOKIE_NAME = 'cp_token'; const REFRESH_TOKEN_COOKIE_NAME = 'cp_refresh_token'; const REFRESH_TOKEN_COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000; const FORGOT_PASSWORD_MESSAGE = 'If that email exists, a password reset link has been sent.'; +function parseJwtExpiresIn(value) { + const match = String(value).match(/^(\d+)([smhd])$/); + if (!match) return 15 * 60; + const num = parseInt(match[1], 10); + const unit = match[2]; + if (unit === 's') return num; + if (unit === 'm') return num * 60; + if (unit === 'h') return num * 60 * 60; + if (unit === 'd') return num * 24 * 60 * 60; + return 15 * 60; +} + +function setAccessTokenCookie(res, token) { + res.cookie(ACCESS_TOKEN_COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: parseJwtExpiresIn(process.env.JWT_EXPIRES_IN || '15m') * 1000, + }); +} + +function clearAccessTokenCookie(res) { + res.clearCookie(ACCESS_TOKEN_COOKIE_NAME, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + }); +} + const isTest = process.env.NODE_ENV === 'test'; const registerLimiter = rateLimit({ windowMs: 60 * 60 * 1000, @@ -252,6 +282,7 @@ router.post('/register', registerLimiter, registerValidation, validateRequest, a const { token: refreshToken, expiresAt } = await createRefreshToken(user.id); setRefreshTokenCookie(res, refreshToken, expiresAt); + setAccessTokenCookie(res, accessToken); const requestId = req.id; setImmediate(() => { @@ -269,7 +300,7 @@ router.post('/register', registerLimiter, registerValidation, validateRequest, a }); }); - res.status(201).json({ token: accessToken, user }); + res.status(201).json({ user }); }); router.post('/login', loginLimiter, loginValidation, validateRequest, async (req, res) => { @@ -349,9 +380,9 @@ router.post('/login', loginLimiter, loginValidation, validateRequest, async (req const { token: refreshToken, expiresAt } = await createRefreshToken(user.id); setRefreshTokenCookie(res, refreshToken, expiresAt); + setAccessTokenCookie(res, accessToken); res.json({ - token: accessToken, user: { id: user.id, email: user.email, @@ -381,9 +412,9 @@ router.post('/refresh', async (req, res) => { const { token: newRefreshToken, expiresAt } = await rotateRefreshToken(token, user.id); setRefreshTokenCookie(res, newRefreshToken, expiresAt); + setAccessTokenCookie(res, accessToken); res.json({ - token: accessToken, user: { id: user.id, email: user.email, @@ -398,13 +429,14 @@ router.post('/refresh', async (req, res) => { }); }); -router.post('/logout', requireAuth, async (req, res) => { +router.post('/logout', async (req, res) => { const token = req.cookies?.[REFRESH_TOKEN_COOKIE_NAME]; if (token) { await revokeRefreshToken(token); } clearRefreshTokenCookie(res); - res.json({ message: 'Logged out successfully' }); + clearAccessTokenCookie(res); + res.json({ ok: true }); }); router.post( diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 5efca33..bf35d14 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -18,31 +18,25 @@ export function AuthProvider({ children }) { return null; } }); - const [token, setToken] = useState(() => api.getToken()); const [ready, setReady] = useState(false); useEffect(() => { let active = true; async function restoreSession() { - if (!user) { - setReady(true); - return; - } - try { const data = await api.refresh(); if (!active) return; - setToken(data.token); if (data.user) { setUser(data.user); localStorage.setItem('cp_user', JSON.stringify(data.user)); + } else { + setUser(null); + localStorage.removeItem('cp_user'); } } catch { if (!active) return; setUser(null); - setToken(null); - api.setToken(null); localStorage.removeItem('cp_user'); } finally { if (active) { @@ -58,11 +52,9 @@ export function AuthProvider({ children }) { }; }, []); - const login = useCallback(async (userData, jwt) => { + const login = useCallback(async (userData) => { const normalized = { ...userData, role: userData.role || (userData.is_admin ? 'admin' : 'contributor') }; setUser(normalized); - setToken(jwt); - api.setToken(jwt); localStorage.setItem('cp_user', JSON.stringify(normalized)); setReady(true); }, []); @@ -73,8 +65,6 @@ export function AuthProvider({ children }) { } catch { } setUser(null); - setToken(null); - api.setToken(null); localStorage.removeItem('cp_user'); setReady(true); }, []); @@ -85,7 +75,7 @@ export function AuthProvider({ children }) { }, []); return ( - + {children} ); diff --git a/frontend/src/pages/AcceptInvite.jsx b/frontend/src/pages/AcceptInvite.jsx index e1e2cfd..d35e9a3 100644 --- a/frontend/src/pages/AcceptInvite.jsx +++ b/frontend/src/pages/AcceptInvite.jsx @@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext'; export default function AcceptInvite() { const { id, token } = useParams(); - const { user, token: authToken, ready } = useAuth(); + const { user, ready } = useAuth(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -15,7 +15,7 @@ export default function AcceptInvite() { setLoading(true); setError(''); try { - await api.acceptCampaignInvitation(id, { token }, authToken); + await api.acceptCampaignInvitation(id, { token }); setSuccess(true); setLoading(false); setTimeout(() => { @@ -35,7 +35,7 @@ export default function AcceptInvite() { ); } - if (!authToken) { + if (!user) { return (

Join Campaign Team

diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 9708717..dfc779e 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -5,18 +5,18 @@ import { useNavigate } from 'react-router-dom'; const DISPUTE_STATUSES = ['open', 'under_review', 'resolved_creator', 'resolved_contributor', 'closed']; -function DisputeQueue({ token }) { +function DisputeQueue() { const [disputes, setDisputes] = useState([]); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(null); useEffect(() => { // Load open/under_review disputes across all campaigns via admin endpoint - api.getAdminCampaigns(token) + api.getAdminCampaigns() .then(async (campaigns) => { const all = await Promise.all( campaigns.map((c) => - api.getCampaignDisputes(c.id, token) + api.getCampaignDisputes(c.id) .then((ds) => ds.map((d) => ({ ...d, campaign_title: c.title }))) .catch(() => []) ) @@ -24,14 +24,14 @@ function DisputeQueue({ token }) { setDisputes(all.flat().sort((a, b) => new Date(b.created_at) - new Date(a.created_at))); }) .finally(() => setLoading(false)); - }, [token]); + }, []); async function resolve(dispute, status) { const note = window.prompt(`Resolution note (${status}):`, ''); if (note === null) return; setBusyId(dispute.id); try { - const updated = await api.updateDispute(dispute.id, { status, resolution_note: note || undefined }, token); + const updated = await api.updateDispute(dispute.id, { status, resolution_note: note || undefined }); setDisputes((prev) => prev.map((d) => (d.id === updated.id ? { ...d, ...updated } : d))); } catch (err) { alert(err.message || 'Could not update dispute'); @@ -98,7 +98,7 @@ function DisputeQueue({ token }) { } export default function AdminDashboard() { - const { user, token, ready } = useAuth(); + const { user, ready } = useAuth(); const navigate = useNavigate(); const [stats, setStats] = useState(null); const [campaigns, setCampaigns] = useState([]); @@ -121,11 +121,11 @@ export default function AdminDashboard() { } Promise.all([ - api.getAdminStats(token), - api.getAdminCampaigns(token), - api.getAdminMilestones(token), - api.getAdminUsers(token), - api.getAdminAuditLog(token) + api.getAdminStats(), + api.getAdminCampaigns(), + api.getAdminMilestones(), + api.getAdminUsers(true), + api.getAdminAuditLog() ]).then(([st, camp, milestoneRows, usrs, audit]) => { setStats(st); setCampaigns(camp); @@ -138,34 +138,34 @@ export default function AdminDashboard() { navigate('/'); }); - }, [ready, user, token, navigate]); + }, [ready, user, navigate]); if (!ready || loading) return
Loading admin panel...
; async function refreshCampaigns() { - const camp = await api.getAdminCampaigns(token); + const camp = await api.getAdminCampaigns(); setCampaigns(camp); } async function refreshUsers() { - const usrs = await api.getAdminUsers(token, true); + const usrs = await api.getAdminUsers(true); setUsers(usrs); } async function refreshAuditLog() { - const audit = await api.getAdminAuditLog(token); + const audit = await api.getAdminAuditLog(); setAuditLog(audit); } async function refreshMilestones() { - const rows = await api.getAdminMilestones(token); + const rows = await api.getAdminMilestones(); setMilestones(rows); } async function approveMilestone(id) { setBusyMilestoneId(id); try { - await api.approveMilestone(id, {}, token); + await api.approveMilestone(id, {}); await refreshMilestones(); await refreshCampaigns(); } finally { @@ -178,7 +178,7 @@ export default function AdminDashboard() { if (reason === null) return; setBusyMilestoneId(id); try { - await api.rejectMilestone(id, { reason: reason || 'Rejected by platform' }, token); + await api.rejectMilestone(id, { reason: reason || 'Rejected by platform' }); await refreshMilestones(); } finally { setBusyMilestoneId(null); @@ -190,7 +190,7 @@ export default function AdminDashboard() { if (reason === null) return; setBusyCampaignId(campaignId); try { - await api.adminSuspendCampaign(campaignId, { reason }, token); + await api.adminSuspendCampaign(campaignId, { reason }); await refreshCampaigns(); await refreshAuditLog(); alert('Campaign suspended'); @@ -205,7 +205,7 @@ export default function AdminDashboard() { if (!window.confirm('Restore this campaign to active?')) return; setBusyCampaignId(campaignId); try { - await api.adminRestoreCampaign(campaignId, token); + await api.adminRestoreCampaign(campaignId); await refreshCampaigns(); await refreshAuditLog(); alert('Campaign restored'); @@ -222,7 +222,7 @@ export default function AdminDashboard() { if (!window.confirm('This will permanently delete the campaign. Are you sure?')) return; setBusyCampaignId(campaignId); try { - await api.adminDeleteCampaign(campaignId, { reason }, token); + await api.adminDeleteCampaign(campaignId, { reason }); await refreshCampaigns(); await refreshAuditLog(); alert('Campaign deleted'); @@ -238,7 +238,7 @@ export default function AdminDashboard() { if (reason === null) return; setBusyUserId(userId); try { - await api.adminBanUser(userId, { reason }, token); + await api.adminBanUser(userId, { reason }); await refreshUsers(); await refreshAuditLog(); alert('User banned'); @@ -253,7 +253,7 @@ export default function AdminDashboard() { if (!window.confirm('Unban this user?')) return; setBusyUserId(userId); try { - await api.adminUnbanUser(userId, token); + await api.adminUnbanUser(userId); await refreshUsers(); await refreshAuditLog(); alert('User unbanned'); @@ -453,7 +453,7 @@ export default function AdminDashboard() { {activeTab === 'disputes' && ( <>

Dispute Queue

- + )} @@ -499,7 +499,7 @@ export default function AdminDashboard() { {c.status}