Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-#429: Added Signin with Google button #454

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ PORT=8080
MONGODB_URI="mongodb://127.0.0.1/wanderlust"
REDIS_URL="redis://127.0.0.1:6379"
FRONTEND_URL=http://localhost:5173
BACKEND_URL=http://localhost:8080
ACCESS_COOKIE_MAXAGE=120000
ACCESS_TOKEN_EXPIRES_IN='120s'
REFRESH_COOKIE_MAXAGE=120000
REFRESH_TOKEN_EXPIRES_IN='120s'
JWT_SECRET=70dd8b38486eee723ce2505f6db06f1ee503fde5eb06fc04687191a0ed665f3f98776902d2c89f6b993b1c579a87fedaf584c693a106f7cbf16e8b4e67e9d6df
NODE_ENV=Development
NODE_ENV=Development
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
5 changes: 5 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import authRouter from './routes/auth.js';
import postsRouter from './routes/posts.js';
import userRouter from './routes/user.js';
import errorMiddleware from './middlewares/error-middleware.js';
import passport from './config/passport.js';
import session from 'express-session';

const app = express();

Expand All @@ -21,6 +23,9 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(compression());
app.use(session({ secret: 'secret', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());

// API route
app.use('/api/posts', postsRouter);
Expand Down
54 changes: 54 additions & 0 deletions backend/config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import User from '../models/user.js';

passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.BACKEND_URL}/api/auth/google/callback`,
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are deploying in vercel, we can't hardcode this to 8080, kindly move this to evn variable or in this case i guess you need to use the backend_url env variable

async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ googleId: profile.id });

if (!user) {
const email = profile.emails && profile.emails[0] ? profile.emails[0].value : '';
let fullName = profile.displayName || '';
if (fullName.length > 15) {
fullName = fullName.slice(0, 15); // Ensure fullName is less than 15 characters
}
const userName = email.split('@')[0] || fullName.replace(/\s+/g, '').toLowerCase();

user = new User({
googleId: profile.id,
email,
fullName,
userName,
avatar: profile.photos && profile.photos[0] ? profile.photos[0].value : '',
});

await user.save();
}

done(null, user);
} catch (err) {
done(err, null);
}
}
)
);

passport.serializeUser((user, done) => done(null, user.id));

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err, null);
}
});

export default passport;
46 changes: 34 additions & 12 deletions backend/controllers/auth-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const signInWithEmailOrUsername = asyncHandler(async (req, res) => {
});

//Sign Out
export const signOutUser = asyncHandler(async (req, res) => {
export const signOutUser = asyncHandler(async (req, res, next) => {
await User.findByIdAndUpdate(
req.user?._id,
{
Expand All @@ -128,42 +128,64 @@ export const signOutUser = asyncHandler(async (req, res) => {
}
);

res
.status(HTTP_STATUS.OK)
.clearCookie('access_token', cookieOptions)
.clearCookie('refresh_token', cookieOptions)
.json(new ApiResponse(HTTP_STATUS.OK, '', RESPONSE_MESSAGES.USERS.SIGNED_OUT));
});
// Passport.js logout
req.logout((err) => {
if (err) {
return next(new ApiError(HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Logout failed'));
}

res
.status(HTTP_STATUS.OK)
.clearCookie('access_token', cookieOptions)
.clearCookie('refresh_token', cookieOptions)
.clearCookie('jwt', cookieOptions)
.json(new ApiResponse(HTTP_STATUS.OK, '', RESPONSE_MESSAGES.USERS.SIGNED_OUT));
});
});
// check user
export const isLoggedIn = asyncHandler(async (req, res) => {
let access_token = req.cookies?.access_token;
let refresh_token = req.cookies?.refresh_token;
const { _id } = req.params;

if (!_id) {
return res
.status(HTTP_STATUS.BAD_REQUEST)
.json(new ApiResponse(HTTP_STATUS.BAD_REQUEST, '', 'User ID is required'));
}
if (access_token) {
try {
await jwt.verify(access_token, JWT_SECRET);
return res
.status(HTTP_STATUS.OK)
.json(new ApiResponse(HTTP_STATUS.OK, access_token, RESPONSE_MESSAGES.USERS.VALID_TOKEN));
} catch (error) {
// Access token invalid, proceed to check refresh token
console.log(error);
console.log('Access token verification error:', error.message);
}
} else if (refresh_token) {
}
// If access token is not valid, check the refresh token
if (refresh_token) {
try {
await jwt.verify(refresh_token, JWT_SECRET);
const user = await User.findById(_id);
if (!user) {
return res
.status(HTTP_STATUS.NOT_FOUND)
.json(
new ApiResponse(HTTP_STATUS.NOT_FOUND, '', RESPONSE_MESSAGES.USERS.USER_NOT_EXISTS)
);
}
access_token = await user.generateAccessToken();
return res
.status(HTTP_STATUS.OK)
.cookie('access_token', access_token, cookieOptions)
.json(new ApiResponse(HTTP_STATUS.OK, access_token, RESPONSE_MESSAGES.USERS.VALID_TOKEN));
} catch (error) {
// Access token invalid, proceed to check refresh token that is in db
console.log(error);
console.log('Refresh token verification error:', error.message);
}
}

// If neither token is valid, handle accordingly
const user = await User.findById(_id);
if (!user) {
return res
Expand Down
16 changes: 8 additions & 8 deletions backend/middlewares/auth-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { ApiError } from '../utils/api-error.js';
import { HTTP_STATUS, RESPONSE_MESSAGES } from '../utils/constants.js';
import jwt from 'jsonwebtoken';
import { Role } from '../types/role-type.js';
import User from '../models/user.js';

export const authMiddleware = async (req, res, next) => {
const token = req.cookies?.access_token;
if (!token) {
return next(new ApiError(HTTP_STATUS.BAD_REQUEST, RESPONSE_MESSAGES.USERS.RE_LOGIN));
}

if (token) {
await jwt.verify(token, JWT_SECRET, (error, payload) => {
if (error) {
return new ApiError(HTTP_STATUS.FORBIDDEN, RESPONSE_MESSAGES.USERS.INVALID_TOKEN);
}
req.user = payload;
next();
});
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = await User.findById(payload.id);
next();
} catch (error) {
console.log('Token verification error:', error.message);
return next(new ApiError(HTTP_STATUS.FORBIDDEN, RESPONSE_MESSAGES.USERS.INVALID_TOKEN));
}
};

Expand Down
8 changes: 7 additions & 1 deletion backend/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const userSchema = new Schema(
},
password: {
type: String,
required: [true, 'Password is required'],
required: false,
minLength: [8, 'Password must be at least 8 character '],
match: [
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/,
Expand All @@ -60,7 +60,13 @@ const userSchema = new Schema(
refreshToken: String,
forgotPasswordToken: String,
forgotPasswordExpiry: Date,
googleId: {
type: String,
unique: true,
required: false,
},
},

{ timestamps: true }
);

Expand Down
3 changes: 3 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.0",
"nodemon": "^3.0.1",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"redis": "^4.6.13"
},
"lint-staged": {
Expand Down
32 changes: 31 additions & 1 deletion backend/routes/auth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Router } from 'express';
import { authMiddleware } from '../middlewares/auth-middleware.js';
import passport from '../config/passport.js';
import jwt from 'jsonwebtoken';
import {
signUpWithEmail,
signInWithEmailOrUsername,
Expand All @@ -13,10 +15,38 @@ const router = Router();
router.post('/email-password/signup', signUpWithEmail);
router.post('/email-password/signin', signInWithEmailOrUsername);

// Google-login
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));

router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/' }),
(req, res) => {
const token = jwt.sign({ id: req.user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.cookie('access_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
res.redirect(`${process.env.FRONTEND_URL}/signup?google-callback=true`);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, kindly use frontend url env variable

);

router.get('/check', authMiddleware, (req, res) => {
const token = req.cookies.access_token;
res.json({
token,
user: {
_id: req.user._id,
role: req.user.role,
},
});
});

//SIGN OUT
router.post('/signout', authMiddleware, signOutUser);

//CHECK USER STATUS
router.get('/check/:_id', isLoggedIn);
router.get('/check/:_id', authMiddleware, isLoggedIn);

export default router;
2 changes: 1 addition & 1 deletion frontend/src/components/require-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function RequireAuth({ allowedRole }: { allowedRole: string[] }) {
if (loading) {
return (
<>
<Loader/>
<Loader />
</>
); // Render a loading indicator
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useAuthData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const useAuthData = (): AuthData => {
loading: false,
});
} catch (error) {
console.error('Error fetching token:', error);
setData({
...data,
token: '',
Expand Down
Loading