diff --git a/client/src/api/index.ts b/client/src/api/index.ts index 96f6dca..d2bd7d9 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -18,10 +18,13 @@ async function req(path: string, options: RequestInit = {}): Promise { const res = await fetch(`${BASE}${path}`, { ...options, headers }); if (res.status === 401) { - // Token expired — clear storage and reload to login + const hadToken = Boolean(localStorage.getItem(TOKEN_KEY)); localStorage.removeItem(TOKEN_KEY); localStorage.removeItem('codedump_user'); - window.location.href = '/login'; + // Only force-redirect if the user had an active session that expired. + // Without this guard, unauthenticated requests (e.g. settings fetch on + // the login page) trigger a redirect loop: 401 → /login → fetch → 401 → ... + if (hadToken) window.location.href = '/login'; throw new Error('Session expired'); } diff --git a/server/src/index.ts b/server/src/index.ts index 86b35af..0b24373 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -30,7 +30,7 @@ app.use('/api/uploads', express.static(UPLOAD_PATH)); app.use('/api/projects', requireAuth, projectsRouter); app.use('/api/tools', requireAuth, toolsRouter); app.use('/api/uploads', requireAuth, uploadsRouter); // handles POST + DELETE only -app.use('/api/settings', requireAuth, settingsRouter); +app.use('/api/settings', settingsRouter); // GET is public (branding on login page); PUT/POST require admin (per-method in router) app.use('/api/users', usersRouter); // requireAdmin applied inside router // Serve built React client in production diff --git a/server/src/routes/settings.ts b/server/src/routes/settings.ts index e46f760..610c477 100644 --- a/server/src/routes/settings.ts +++ b/server/src/routes/settings.ts @@ -3,6 +3,7 @@ import multer from 'multer'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import db, { UPLOAD_PATH } from '../db/schema'; +import { requireAdmin } from '../middleware/auth'; const router = Router(); @@ -27,7 +28,7 @@ router.get('/', (_req: Request, res: Response) => { res.json(settings); }); -router.put('/', (req: Request, res: Response) => { +router.put('/', requireAdmin, (req: Request, res: Response) => { const allowed = ['app_title', 'logo_url', 'accent_color', 'company_name']; const upsert = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'); const update = db.transaction((body: Record) => { @@ -45,7 +46,7 @@ router.put('/', (req: Request, res: Response) => { res.json(settings); }); -router.post('/logo', logoUpload.single('logo'), (req: Request, res: Response) => { +router.post('/logo', requireAdmin, logoUpload.single('logo'), (req: Request, res: Response) => { if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const url = `/api/uploads/${req.file.filename}`; db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run('logo_url', JSON.stringify(url));