File size: 7,906 Bytes
8a6248c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/**
 * 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 };