Add Google OAuth authentication system (#4)
Some checks failed
Deploy Apartment API / deploy (push) Failing after 1s

Co-authored-by: Stephen Minakian <stephenminakian@gmail.com>
Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
This commit is contained in:
2026-01-21 19:15:22 -07:00
committed by stephen
parent 04a78a21cc
commit ef4ddc0de0
13 changed files with 1247 additions and 59 deletions

111
middleware/auth.js Normal file
View File

@ -0,0 +1,111 @@
const jwt = require('jsonwebtoken');
const authConfig = require('../config/auth');
const { findById } = require('../models/user');
/**
* Check if a token should be refreshed based on its age
* @param {Object} decoded - Decoded JWT payload
* @returns {boolean} True if token should be refreshed
*/
const shouldRefreshToken = (decoded) => {
const tokenAge = Math.floor(Date.now() / 1000) - decoded.iat;
return tokenAge > authConfig.jwt.refreshThreshold;
};
/**
* Generate a new JWT token for a user
* @param {string|ObjectId} userId - User's MongoDB _id
* @returns {string} JWT token
*/
const generateToken = (userId) => {
return jwt.sign(
{ userId: userId.toString() },
authConfig.jwt.secret,
{ expiresIn: authConfig.jwt.expiresIn }
);
};
/**
* Authentication middleware
* Validates JWT token from cookie and attaches user to request
* Implements sliding window token refresh
*
* @param {Request} req - Express request object
* @param {Response} res - Express response object
* @param {Function} next - Express next function
*/
const requireAuth = async (req, res, next) => {
const token = req.cookies[authConfig.cookie.name];
// No token present
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
// Verify the token
const decoded = jwt.verify(token, authConfig.jwt.secret);
// Get database instance
const db = req.app.locals.db;
// Fetch user from database
const user = await findById(db, decoded.userId);
// User not found
if (!user) {
res.clearCookie(authConfig.cookie.name);
return res.status(401).json({ error: 'Authentication required' });
}
// User is disabled (silent logout)
if (!user.isActive) {
res.clearCookie(authConfig.cookie.name);
return res.status(401).json({ error: 'Authentication required' });
}
// Attach user to request
req.user = user;
// Sliding window token refresh
if (shouldRefreshToken(decoded)) {
const newToken = generateToken(user._id);
res.cookie(authConfig.cookie.name, newToken, authConfig.cookie.options);
}
next();
} catch (err) {
// Token verification failed (invalid or expired)
res.clearCookie(authConfig.cookie.name);
return res.status(401).json({ error: 'Invalid token' });
}
};
/**
* Admin authorization middleware
* Must be used after requireAuth middleware
* Checks if authenticated user has admin role
*
* @param {Request} req - Express request object
* @param {Response} res - Express response object
* @param {Function} next - Express next function
*/
const requireAdmin = (req, res, next) => {
// Check if user is attached (requireAuth should be called first)
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Check if user has admin role
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
module.exports = {
requireAuth,
requireAdmin,
generateToken
};

36
middleware/passport.js Normal file
View File

@ -0,0 +1,36 @@
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const authConfig = require('../config/auth');
const { findOrCreateUser } = require('../models/user');
const configurePassport = (passport, db) => {
passport.use(new GoogleStrategy({
clientID: authConfig.google.clientID,
clientSecret: authConfig.google.clientSecret,
callbackURL: authConfig.google.callbackURL
},
async (accessToken, refreshToken, profile, done) => {
try {
// Safely extract email
const email = profile.emails?.[0]?.value;
if (!email) {
return done(new Error('No email found in Google profile'), null);
}
const userProfile = {
googleId: profile.id,
email: email,
name: profile.displayName,
picture: profile.photos?.[0]?.value || null
};
const user = await findOrCreateUser(db, userProfile);
done(null, user);
} catch (error) {
console.error('Passport strategy error:', error);
done(error, null);
}
}));
};
module.exports = { configurePassport };