Spaces:
Running
Running
| /** | |
| * Authentication Middleware | |
| * Provides token verification and user population. | |
| * Supports both Firebase ID tokens (RS256) and legacy HS256 JWTs. | |
| */ | |
| import jwt from "jsonwebtoken"; | |
| import User from "../models/auth.model.js"; | |
| /** | |
| * Verify token (Firebase or legacy JWT) and populate req.user | |
| */ | |
| export const verifyToken = async (req, res, next) => { | |
| try { | |
| // Get token from cookie or Authorization header | |
| let token = req.cookies?.token; | |
| if (!token && req.headers.authorization) { | |
| const authHeader = req.headers.authorization; | |
| if (authHeader.startsWith("Bearer ")) { | |
| token = authHeader.substring(7); | |
| } | |
| } | |
| if (!token) { | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Access denied. No token provided.", | |
| }); | |
| } | |
| // Decode header to determine token type | |
| const header = jwt.decode(token, { complete: true })?.header; | |
| if (header?.kid) { | |
| // Firebase ID token (RS256 with key ID) β verify via Firebase Admin | |
| const admin = (await import('firebase-admin')).default; | |
| if (admin.apps?.length > 0) { | |
| const decodedToken = await admin.auth().verifyIdToken(token); | |
| req.userId = decodedToken.uid; | |
| req.userEmail = decodedToken.email; | |
| req.firebaseUser = decodedToken; | |
| // Look up the MongoDB user record for this Firebase/Google user | |
| let mongoUser = await User.findOne({ firebaseUid: decodedToken.uid }).select('-password'); | |
| if (!mongoUser && decodedToken.email) { | |
| mongoUser = await User.findOne({ email: decodedToken.email.trim().toLowerCase() }).select('-password'); | |
| // If found by email but missing firebaseUid, backfill it | |
| if (mongoUser && !mongoUser.firebaseUid) { | |
| mongoUser.firebaseUid = decodedToken.uid; | |
| await mongoUser.save(); | |
| } | |
| } | |
| if (mongoUser) { | |
| req.user = mongoUser; | |
| req.userRole = mongoUser.role; | |
| } else { | |
| req.userRole = decodedToken.role || decodedToken.custom_claims?.role || 'farmer'; | |
| } | |
| return next(); | |
| } | |
| // Firebase Admin not initialised β decode WITHOUT verification to at least | |
| // extract the uid so createListing can store sellerFirebaseUid. | |
| // This is safe for optional-auth routes; verifyToken routes will reject above. | |
| console.warn('[auth/verifyToken] Firebase token but Admin SDK not initialised β cannot verify.'); | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Firebase authentication unavailable.", | |
| }); | |
| } | |
| // Legacy HS256 JWT β verify with shared secret | |
| const decoded = jwt.verify(token, process.env.JWT_KEY || process.env.JWT_SECRET); | |
| // Populate req.user with user data from DB when available | |
| const user = await User.findById(decoded.id).select("-password"); | |
| if (user) { | |
| req.user = user; | |
| } | |
| req.userId = decoded.id; | |
| req.userRole = decoded.role || user?.role || "farmer"; | |
| next(); | |
| } catch (error) { | |
| console.error("Token verification error:", error.message); | |
| if (error.name === "TokenExpiredError" || error.code === "auth/id-token-expired") { | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Token expired. Please login again.", | |
| }); | |
| } | |
| if (error.name === "JsonWebTokenError") { | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Invalid token.", | |
| }); | |
| } | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Authentication error.", | |
| }); | |
| } | |
| }; | |
| /** | |
| * Optional auth β populates req.user / req.userId if a valid token is present, | |
| * but NEVER blocks the request. | |
| * | |
| * Firebase token path: | |
| * 1. If Firebase Admin SDK is initialised β verify properly, set req.userId + req.user | |
| * 2. If Firebase Admin SDK is NOT initialised β decode the JWT payload WITHOUT | |
| * signature verification (safe for optional-auth routes) and still set | |
| * req.userId = uid. This ensures sellerFirebaseUid is always stored. | |
| * | |
| * Legacy HS256 JWT path: verify with JWT_KEY / JWT_SECRET, populate req.user. | |
| */ | |
| export const optionalAuth = async (req, res, next) => { | |
| try { | |
| let token = req.cookies?.token; | |
| if (!token && req.headers.authorization) { | |
| const authHeader = req.headers.authorization; | |
| if (authHeader.startsWith("Bearer ")) { | |
| token = authHeader.substring(7); | |
| } | |
| } | |
| if (token) { | |
| const decoded_header = jwt.decode(token, { complete: true }); | |
| const header = decoded_header?.header; | |
| if (header?.kid) { | |
| // --- Firebase ID token --- | |
| let firebaseUid = null; | |
| let firebaseEmail = null; | |
| try { | |
| const admin = (await import('firebase-admin')).default; | |
| if (admin.apps?.length > 0) { | |
| // Proper full verification | |
| const decodedToken = await admin.auth().verifyIdToken(token); | |
| firebaseUid = decodedToken.uid; | |
| firebaseEmail = decodedToken.email; | |
| req.firebaseUser = decodedToken; | |
| } else { | |
| // Admin SDK not available β decode payload only (no signature check) | |
| // This is intentionally unverified; it is safe here because | |
| // optionalAuth never gates access, it only identifies the caller. | |
| const payload = decoded_header?.payload; | |
| firebaseUid = payload?.sub || payload?.user_id || null; | |
| firebaseEmail = payload?.email || null; | |
| console.warn('[auth/optionalAuth] Firebase Admin not initialised β using unverified uid:', firebaseUid); | |
| } | |
| } catch (firebaseErr) { | |
| // Token expired or malformed β still try to extract uid gracefully | |
| const payload = decoded_header?.payload; | |
| firebaseUid = payload?.sub || payload?.user_id || null; | |
| firebaseEmail = payload?.email || null; | |
| console.warn('[auth/optionalAuth] Firebase token error, falling back to decoded uid:', firebaseErr.message); | |
| } | |
| if (firebaseUid) { | |
| req.userId = firebaseUid; | |
| req.userEmail = firebaseEmail; | |
| // Try to link to a MongoDB user | |
| let mongoUser = await User.findOne({ firebaseUid }).select('-password'); | |
| if (!mongoUser && firebaseEmail) { | |
| mongoUser = await User.findOne({ email: firebaseEmail.trim().toLowerCase() }).select('-password'); | |
| if (mongoUser && !mongoUser.firebaseUid) { | |
| mongoUser.firebaseUid = firebaseUid; | |
| await mongoUser.save(); | |
| } | |
| } | |
| if (mongoUser) { | |
| req.user = mongoUser; | |
| req.userRole = mongoUser.role; | |
| } else { | |
| req.userRole = 'farmer'; | |
| } | |
| } | |
| return next(); | |
| } | |
| // --- Legacy HS256 JWT --- | |
| try { | |
| const decoded = jwt.verify(token, process.env.JWT_KEY || process.env.JWT_SECRET); | |
| const user = await User.findById(decoded.id).select("-password"); | |
| if (user) { | |
| req.user = user; | |
| req.userId = decoded.id; | |
| req.userRole = decoded.role || user.role; | |
| } | |
| } catch { | |
| // Invalid legacy token β ignore silently | |
| } | |
| } | |
| next(); | |
| } catch (error) { | |
| // Silently continue without auth | |
| next(); | |
| } | |
| }; | |
| /** | |
| * Require specific role(s) | |
| */ | |
| export const requireRole = (...roles) => { | |
| return (req, res, next) => { | |
| if (!req.user) { | |
| return res.status(401).json({ | |
| success: false, | |
| message: "Authentication required.", | |
| }); | |
| } | |
| if (!roles.includes(req.userRole)) { | |
| return res.status(403).json({ | |
| success: false, | |
| message: `Access denied. Required roles: ${roles.join(", ")}`, | |
| }); | |
| } | |
| next(); | |
| }; | |
| }; | |
| export default { verifyToken, optionalAuth, requireRole }; | |