jwt-auth.js 3.07 KB
/**
 * JWT Authentication Middleware
 *
 * - Signs JWT with { userId, email, role }
 * - Stores in httpOnly cookie "lemon_token" (7-day expiry)
 * - requireAuth: blocks unauthenticated requests
 * - optionalAuth: attaches user if present, doesn't block
 */

const jwt = require('jsonwebtoken');
const userDb = require('../services/user-db');

const JWT_SECRET = process.env.JWT_SECRET || 'lemon-dev-secret-change-in-production';
const COOKIE_NAME = 'lemon_token';
const TOKEN_EXPIRY = '7d';

/**
 * Sign a JWT and set it as httpOnly cookie
 */
function signAndSetCookie(res, user) {
  const payload = {
    userId: user.id,
    email: user.email,
    role: user.role || 'user',
  };

  const token = jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });

  res.cookie(COOKIE_NAME, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    path: '/',
  });

  return token;
}

/**
 * Clear the auth cookie
 */
function clearCookie(res) {
  res.clearCookie(COOKIE_NAME, { path: '/' });
}

/**
 * Extract and verify JWT from cookie or Authorization header
 */
function extractUser(req) {
  let token = null;

  // Cookie first
  if (req.cookies && req.cookies[COOKIE_NAME]) {
    token = req.cookies[COOKIE_NAME];
  }

  // Authorization: Bearer <token> fallback
  if (!token) {
    const auth = req.headers['authorization'];
    if (auth && auth.startsWith('Bearer ')) {
      token = auth.slice(7).trim();
    }
  }

  if (!token) return null;

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    const user = userDb.getUserById(decoded.userId);
    if (!user) return null;
    return userDb.toPublicUser(user);
  } catch (err) {
    return null;
  }
}

/**
 * Middleware: require authentication (401 if not logged in)
 */
function requireAuth(req, res, next) {
  const user = extractUser(req);
  if (!user) {
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'Please log in to access this resource.',
    });
  }
  req.user = user;
  next();
}

/**
 * Middleware: optional authentication (attach user if present)
 */
function optionalAuth(req, res, next) {
  req.user = extractUser(req) || null;
  next();
}

/**
 * Middleware: require admin role (403 if not admin)
 */
function requireAdmin(req, res, next) {
  const user = extractUser(req);
  if (!user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  if (user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden', message: 'Admin access required.' });
  }
  req.user = user;
  next();
}

/**
 * 直接从 token 字符串提取用户(供 WebSocket handshake 使用)
 */
function extractUserFromToken(token) {
  if (!token) return null;
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    const user = userDb.getUserById(decoded.userId);
    return user ? userDb.toPublicUser(user) : null;
  } catch {
    return null;
  }
}

module.exports = {
  signAndSetCookie,
  clearCookie,
  requireAuth,
  requireAdmin,
  optionalAuth,
  extractUser,
  extractUserFromToken,
  JWT_SECRET,
  COOKIE_NAME,
};