Skip to content

Commit 0138f26

Browse files
authored
Merge pull request #39 from dikamjit-borah/main
Set up Google Single Sign-On (SSO) in the CoreX backend
2 parents 941f6d8 + d517c54 commit 0138f26

6 files changed

Lines changed: 347 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@
5656
"eslint": "^9.37.0",
5757
"nodemon": "^3.1.10"
5858
}
59-
}
59+
}

src/app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import express from 'express';
22
import cors from 'cors';
3+
import passport from './config/passport.config.js';
34
import productRoutes from './routes/product.routes.js';
45
import cartRoutes from './routes/cart.routes.js';
56
import collectionRoutes from './routes/collection.routes.js';
7+
import authRoutes from './routes/auth.routes.js';
68
import errorHandler from './middleware/error-handler.middleware.js';
79
import notFound from './middleware/notFound.middleware.js'
810
const app = express();
911
app.use(cors());
1012
app.use(express.json());
1113

14+
// Initialize Passport
15+
app.use(passport.initialize());
16+
1217
app.get('/',(req,res)=>{
1318
res.send("Welcome to Homepage");
1419
})
@@ -17,6 +22,7 @@ app.get('/',(req,res)=>{
1722
app.use('/api/products', productRoutes);
1823
app.use('/api/cart', cartRoutes);
1924
app.use('/api/collections', collectionRoutes);
25+
app.use('/auth', authRoutes);
2026

2127
// Middleware for not found 404
2228
app.use(notFound);

src/config/passport.config.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import passport from 'passport';
2+
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
3+
import User from '../models/user.model.js';
4+
import jwt from 'jsonwebtoken';
5+
6+
// Configure Google OAuth Strategy
7+
passport.use(new GoogleStrategy({
8+
clientID: process.env.GOOGLE_CLIENT_ID,
9+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
10+
callbackURL: process.env.GOOGLE_CALLBACK_URL || '/auth/google/callback'
11+
}, async (accessToken, refreshToken, profile, done) => {
12+
try {
13+
// Find or create user from Google profile
14+
const user = await User.findOrCreateFromGoogle(profile);
15+
16+
// Generate JWT token
17+
const token = jwt.sign(
18+
{
19+
userId: user._id,
20+
email: user.email,
21+
authProvider: user.authProvider
22+
},
23+
process.env.JWT_SECRET,
24+
{ expiresIn: '7d' }
25+
);
26+
27+
// Attach token to user object
28+
user.token = token;
29+
30+
return done(null, user);
31+
} catch (error) {
32+
console.error('Google OAuth error:', error);
33+
return done(error, null);
34+
}
35+
}));
36+
37+
// Serialize user for session
38+
passport.serializeUser((user, done) => {
39+
done(null, user._id);
40+
});
41+
42+
// Deserialize user from session
43+
passport.deserializeUser(async (id, done) => {
44+
try {
45+
const user = await User.findById(id);
46+
done(null, user);
47+
} catch (error) {
48+
done(error, null);
49+
}
50+
});
51+
52+
// JWT verification middleware
53+
export const verifyJWT = (req, res, next) => {
54+
const token = req.header('Authorization')?.replace('Bearer ', '');
55+
56+
if (!token) {
57+
return res.status(401).json({
58+
success: false,
59+
message: 'Access denied. No token provided.'
60+
});
61+
}
62+
63+
try {
64+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
65+
req.user = decoded;
66+
next();
67+
} catch (error) {
68+
res.status(400).json({
69+
success: false,
70+
message: 'Invalid token.'
71+
});
72+
}
73+
};
74+
75+
// Generate JWT token utility
76+
export const generateToken = (user) => {
77+
return jwt.sign(
78+
{
79+
userId: user._id,
80+
email: user.email,
81+
authProvider: user.authProvider
82+
},
83+
process.env.JWT_SECRET,
84+
{ expiresIn: '7d' }
85+
);
86+
};
87+
88+
export default passport;

src/controllers/auth.controller.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import passport from '../config/passport.config.js';
2+
import User from '../models/user.model.js';
3+
import { generateToken } from '../config/passport.config.js';
4+
5+
// Redirect to Google OAuth
6+
export const googleAuth = passport.authenticate('google', {
7+
scope: ['profile', 'email']
8+
});
9+
10+
// Handle Google OAuth callback
11+
export const googleCallback = (req, res, next) => {
12+
passport.authenticate('google', (err, user, info) => {
13+
if (err) {
14+
console.error('Google OAuth error:', err);
15+
return res.status(500).json({
16+
success: false,
17+
message: 'Authentication failed',
18+
error: err.message
19+
});
20+
}
21+
22+
if (!user) {
23+
return res.status(401).json({
24+
success: false,
25+
message: 'Authentication failed'
26+
});
27+
}
28+
29+
// Generate JWT token
30+
const token = generateToken(user);
31+
32+
// Return success response with token and user info
33+
res.json({
34+
success: true,
35+
message: 'Authentication successful',
36+
token,
37+
user: {
38+
id: user._id,
39+
email: user.email,
40+
name: user.name,
41+
firstName: user.firstName,
42+
lastName: user.lastName,
43+
profilePicture: user.profilePicture,
44+
authProvider: user.authProvider,
45+
lastLogin: user.lastLogin
46+
}
47+
});
48+
})(req, res, next);
49+
};
50+
51+
// Get current user profile
52+
export const getProfile = async (req, res) => {
53+
try {
54+
const user = await User.findById(req.user.userId).select('-__v');
55+
56+
if (!user) {
57+
return res.status(404).json({
58+
success: false,
59+
message: 'User not found'
60+
});
61+
}
62+
63+
res.json({
64+
success: true,
65+
user: {
66+
id: user._id,
67+
email: user.email,
68+
name: user.name,
69+
firstName: user.firstName,
70+
lastName: user.lastName,
71+
profilePicture: user.profilePicture,
72+
authProvider: user.authProvider,
73+
isActive: user.isActive,
74+
createdAt: user.createdAt,
75+
lastLogin: user.lastLogin
76+
}
77+
});
78+
} catch (error) {
79+
console.error('Get profile error:', error);
80+
res.status(500).json({
81+
success: false,
82+
message: 'Failed to get user profile',
83+
error: error.message
84+
});
85+
}
86+
};
87+
88+
// Logout (invalidate token on client side)
89+
export const logout = (req, res) => {
90+
res.json({
91+
success: true,
92+
message: 'Logout successful. Please remove the token from client storage.'
93+
});
94+
};
95+
96+
// Refresh token
97+
export const refreshToken = async (req, res) => {
98+
try {
99+
const user = await User.findById(req.user.userId);
100+
101+
if (!user) {
102+
return res.status(404).json({
103+
success: false,
104+
message: 'User not found'
105+
});
106+
}
107+
108+
// Generate new token
109+
const token = generateToken(user);
110+
111+
res.json({
112+
success: true,
113+
message: 'Token refreshed successfully',
114+
token
115+
});
116+
} catch (error) {
117+
console.error('Refresh token error:', error);
118+
res.status(500).json({
119+
success: false,
120+
message: 'Failed to refresh token',
121+
error: error.message
122+
});
123+
}
124+
};

src/models/user.model.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import mongoose from 'mongoose';
2+
3+
const userSchema = new mongoose.Schema({
4+
googleId: {
5+
type: String,
6+
unique: true,
7+
sparse: true
8+
},
9+
email: {
10+
type: String,
11+
required: true,
12+
unique: true,
13+
lowercase: true,
14+
trim: true
15+
},
16+
name: {
17+
type: String,
18+
required: true,
19+
trim: true
20+
},
21+
firstName: {
22+
type: String,
23+
trim: true
24+
},
25+
lastName: {
26+
type: String,
27+
trim: true
28+
},
29+
profilePicture: {
30+
type: String,
31+
default: null
32+
},
33+
authProvider: {
34+
type: String,
35+
enum: ['google'],
36+
required: true,
37+
default: 'google'
38+
},
39+
isActive: {
40+
type: Boolean,
41+
default: true
42+
},
43+
lastLogin: {
44+
type: Date,
45+
default: Date.now
46+
}
47+
}, {
48+
timestamps: true
49+
});
50+
51+
// Indexes
52+
userSchema.index({ email: 1 });
53+
userSchema.index({ googleId: 1 });
54+
55+
// Method to update last login
56+
userSchema.methods.updateLastLogin = function() {
57+
this.lastLogin = new Date();
58+
return this.save();
59+
};
60+
61+
// Static method to find or create user from Google profile
62+
userSchema.statics.findOrCreateFromGoogle = async function(profile) {
63+
try {
64+
// Try to find existing user by Google ID first
65+
let user = await this.findOne({ googleId: profile.id });
66+
67+
if (user) {
68+
// Update last login and return user
69+
await user.updateLastLogin();
70+
return user;
71+
}
72+
73+
// Try to find existing user by email
74+
user = await this.findOne({ email: profile.emails[0].value });
75+
76+
if (user) {
77+
// Link Google account to existing user
78+
user.googleId = profile.id;
79+
user.authProvider = 'google';
80+
await user.save();
81+
await user.updateLastLogin();
82+
return user;
83+
}
84+
85+
// Create new user
86+
const nameParts = profile.displayName ? profile.displayName.split(' ') : ['', ''];
87+
user = new this({
88+
googleId: profile.id,
89+
email: profile.emails[0].value,
90+
name: profile.displayName || profile.emails[0].value,
91+
firstName: nameParts[0] || '',
92+
lastName: nameParts.slice(1).join(' ') || '',
93+
profilePicture: profile.photos && profile.photos[0] ? profile.photos[0].value : null,
94+
authProvider: 'google'
95+
});
96+
97+
await user.save();
98+
return user;
99+
} catch (error) {
100+
throw new Error(`Failed to find or create user: ${error.message}`);
101+
}
102+
};
103+
104+
const User = mongoose.model('User', userSchema);
105+
106+
export default User;

src/routes/auth.routes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import express from 'express';
2+
import {
3+
googleAuth,
4+
googleCallback,
5+
getProfile,
6+
logout,
7+
refreshToken
8+
} from '../controllers/auth.controller.js';
9+
import { verifyJWT } from '../config/passport.config.js';
10+
11+
const router = express.Router();
12+
13+
// Google OAuth routes
14+
router.get('/google', googleAuth);
15+
router.get('/google/callback', googleCallback);
16+
17+
// Protected routes (require JWT authentication)
18+
router.get('/profile', verifyJWT, getProfile);
19+
router.post('/logout', verifyJWT, logout);
20+
router.post('/refresh', verifyJWT, refreshToken);
21+
22+
export default router;

0 commit comments

Comments
 (0)