diff --git a/package.json b/package.json index 61a354a..3dcf329 100644 --- a/package.json +++ b/package.json @@ -56,4 +56,4 @@ "eslint": "^9.37.0", "nodemon": "^3.1.10" } -} +} \ No newline at end of file diff --git a/src/app.js b/src/app.js index 4d5d643..00e4e37 100644 --- a/src/app.js +++ b/src/app.js @@ -1,14 +1,19 @@ import express from 'express'; import cors from 'cors'; +import passport from './config/passport.config.js'; import productRoutes from './routes/product.routes.js'; import cartRoutes from './routes/cart.routes.js'; import collectionRoutes from './routes/collection.routes.js'; +import authRoutes from './routes/auth.routes.js'; import errorHandler from './middleware/error-handler.middleware.js'; import notFound from './middleware/notFound.middleware.js' const app = express(); app.use(cors()); app.use(express.json()); +// Initialize Passport +app.use(passport.initialize()); + app.get('/',(req,res)=>{ res.send("Welcome to Homepage"); }) @@ -17,6 +22,7 @@ app.get('/',(req,res)=>{ app.use('/api/products', productRoutes); app.use('/api/cart', cartRoutes); app.use('/api/collections', collectionRoutes); +app.use('/auth', authRoutes); // Middleware for not found 404 app.use(notFound); diff --git a/src/config/passport.config.js b/src/config/passport.config.js new file mode 100644 index 0000000..97d1494 --- /dev/null +++ b/src/config/passport.config.js @@ -0,0 +1,88 @@ +import passport from 'passport'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import User from '../models/user.model.js'; +import jwt from 'jsonwebtoken'; + +// Configure Google OAuth Strategy +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL || '/auth/google/callback' +}, async (accessToken, refreshToken, profile, done) => { + try { + // Find or create user from Google profile + const user = await User.findOrCreateFromGoogle(profile); + + // Generate JWT token + const token = jwt.sign( + { + userId: user._id, + email: user.email, + authProvider: user.authProvider + }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + // Attach token to user object + user.token = token; + + return done(null, user); + } catch (error) { + console.error('Google OAuth error:', error); + return done(error, null); + } +})); + +// Serialize user for session +passport.serializeUser((user, done) => { + done(null, user._id); +}); + +// Deserialize user from session +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findById(id); + done(null, user); + } catch (error) { + done(error, null); + } +}); + +// JWT verification middleware +export const verifyJWT = (req, res, next) => { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + success: false, + message: 'Access denied. No token provided.' + }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; + next(); + } catch (error) { + res.status(400).json({ + success: false, + message: 'Invalid token.' + }); + } +}; + +// Generate JWT token utility +export const generateToken = (user) => { + return jwt.sign( + { + userId: user._id, + email: user.email, + authProvider: user.authProvider + }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); +}; + +export default passport; diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 0000000..debcf09 --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,124 @@ +import passport from '../config/passport.config.js'; +import User from '../models/user.model.js'; +import { generateToken } from '../config/passport.config.js'; + +// Redirect to Google OAuth +export const googleAuth = passport.authenticate('google', { + scope: ['profile', 'email'] +}); + +// Handle Google OAuth callback +export const googleCallback = (req, res, next) => { + passport.authenticate('google', (err, user, info) => { + if (err) { + console.error('Google OAuth error:', err); + return res.status(500).json({ + success: false, + message: 'Authentication failed', + error: err.message + }); + } + + if (!user) { + return res.status(401).json({ + success: false, + message: 'Authentication failed' + }); + } + + // Generate JWT token + const token = generateToken(user); + + // Return success response with token and user info + res.json({ + success: true, + message: 'Authentication successful', + token, + user: { + id: user._id, + email: user.email, + name: user.name, + firstName: user.firstName, + lastName: user.lastName, + profilePicture: user.profilePicture, + authProvider: user.authProvider, + lastLogin: user.lastLogin + } + }); + })(req, res, next); +}; + +// Get current user profile +export const getProfile = async (req, res) => { + try { + const user = await User.findById(req.user.userId).select('-__v'); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + res.json({ + success: true, + user: { + id: user._id, + email: user.email, + name: user.name, + firstName: user.firstName, + lastName: user.lastName, + profilePicture: user.profilePicture, + authProvider: user.authProvider, + isActive: user.isActive, + createdAt: user.createdAt, + lastLogin: user.lastLogin + } + }); + } catch (error) { + console.error('Get profile error:', error); + res.status(500).json({ + success: false, + message: 'Failed to get user profile', + error: error.message + }); + } +}; + +// Logout (invalidate token on client side) +export const logout = (req, res) => { + res.json({ + success: true, + message: 'Logout successful. Please remove the token from client storage.' + }); +}; + +// Refresh token +export const refreshToken = async (req, res) => { + try { + const user = await User.findById(req.user.userId); + + if (!user) { + return res.status(404).json({ + success: false, + message: 'User not found' + }); + } + + // Generate new token + const token = generateToken(user); + + res.json({ + success: true, + message: 'Token refreshed successfully', + token + }); + } catch (error) { + console.error('Refresh token error:', error); + res.status(500).json({ + success: false, + message: 'Failed to refresh token', + error: error.message + }); + } +}; diff --git a/src/models/user.model.js b/src/models/user.model.js new file mode 100644 index 0000000..a0a78c8 --- /dev/null +++ b/src/models/user.model.js @@ -0,0 +1,106 @@ +import mongoose from 'mongoose'; + +const userSchema = new mongoose.Schema({ + googleId: { + type: String, + unique: true, + sparse: true + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true + }, + name: { + type: String, + required: true, + trim: true + }, + firstName: { + type: String, + trim: true + }, + lastName: { + type: String, + trim: true + }, + profilePicture: { + type: String, + default: null + }, + authProvider: { + type: String, + enum: ['google'], + required: true, + default: 'google' + }, + isActive: { + type: Boolean, + default: true + }, + lastLogin: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Indexes +userSchema.index({ email: 1 }); +userSchema.index({ googleId: 1 }); + +// Method to update last login +userSchema.methods.updateLastLogin = function() { + this.lastLogin = new Date(); + return this.save(); +}; + +// Static method to find or create user from Google profile +userSchema.statics.findOrCreateFromGoogle = async function(profile) { + try { + // Try to find existing user by Google ID first + let user = await this.findOne({ googleId: profile.id }); + + if (user) { + // Update last login and return user + await user.updateLastLogin(); + return user; + } + + // Try to find existing user by email + user = await this.findOne({ email: profile.emails[0].value }); + + if (user) { + // Link Google account to existing user + user.googleId = profile.id; + user.authProvider = 'google'; + await user.save(); + await user.updateLastLogin(); + return user; + } + + // Create new user + const nameParts = profile.displayName ? profile.displayName.split(' ') : ['', '']; + user = new this({ + googleId: profile.id, + email: profile.emails[0].value, + name: profile.displayName || profile.emails[0].value, + firstName: nameParts[0] || '', + lastName: nameParts.slice(1).join(' ') || '', + profilePicture: profile.photos && profile.photos[0] ? profile.photos[0].value : null, + authProvider: 'google' + }); + + await user.save(); + return user; + } catch (error) { + throw new Error(`Failed to find or create user: ${error.message}`); + } +}; + +const User = mongoose.model('User', userSchema); + +export default User; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..424ae54 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,22 @@ +import express from 'express'; +import { + googleAuth, + googleCallback, + getProfile, + logout, + refreshToken +} from '../controllers/auth.controller.js'; +import { verifyJWT } from '../config/passport.config.js'; + +const router = express.Router(); + +// Google OAuth routes +router.get('/google', googleAuth); +router.get('/google/callback', googleCallback); + +// Protected routes (require JWT authentication) +router.get('/profile', verifyJWT, getProfile); +router.post('/logout', verifyJWT, logout); +router.post('/refresh', verifyJWT, refreshToken); + +export default router;