Files
jason 69b7262535 Fix 401 Unauthorized on all API calls after login (HTTP installs)
Root cause: cookie was set with Secure=true whenever NODE_ENV=production.
Browsers refuse to send Secure cookies over plain HTTP, so the session
cookie was dropped on every request after login — causing every protected
endpoint to return 401.

Fix: replace the NODE_ENV check with an explicit COOKIE_SECURE env var
(default false). Set COOKIE_SECURE=true only when running behind an HTTPS
reverse proxy. Direct HTTP installs (standard Unraid setup) work as-is.

Also updated UNRAID.md to document COOKIE_SECURE with a warning explaining
why it must stay false for plain-HTTP access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:40:08 -05:00

59 lines
2.0 KiB
TypeScript

import { Router, Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { AppError, ok } from '../types/index';
import { authMiddleware } from '../middleware/authMiddleware';
export const authRouter = Router();
// secure:true requires HTTPS — for plain-HTTP homelab installs (Unraid, etc.)
// this must be false so the browser actually sends the cookie back.
// Set COOKIE_SECURE=true in your env only if you're behind an HTTPS reverse proxy.
const COOKIE_OPTS = {
httpOnly: true,
sameSite: 'strict' as const,
secure: process.env.COOKIE_SECURE === 'true',
path: '/',
};
authRouter.post('/login', (req: Request, res: Response, next: NextFunction) => {
try {
const { username, password } = req.body as { username?: string; password?: string };
if (!username || !password) {
throw new AppError('Username and password are required', 400, 'MISSING_FIELDS');
}
const adminUsername = process.env.ADMIN_USERNAME;
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminUsername || !adminPassword) {
throw new AppError('Server not configured: admin credentials missing', 500, 'CONFIG_ERROR');
}
if (username !== adminUsername || password !== adminPassword) {
throw new AppError('Invalid username or password', 401, 'INVALID_CREDENTIALS');
}
const secret = process.env.JWT_SECRET;
if (!secret) throw new AppError('Server not configured: JWT_SECRET missing', 500, 'CONFIG_ERROR');
const token = jwt.sign({ sub: 'admin' }, secret, {
expiresIn: (process.env.JWT_EXPIRY ?? '8h') as jwt.SignOptions['expiresIn'],
});
res.cookie('token', token, COOKIE_OPTS);
res.json(ok({ success: true }));
} catch (e) {
next(e);
}
});
authRouter.post('/logout', (_req: Request, res: Response) => {
res.clearCookie('token', COOKIE_OPTS);
res.json(ok({ success: true }));
});
authRouter.get('/me', authMiddleware, (_req: Request, res: Response) => {
res.json(ok({ authenticated: true }));
});