Skip to content

Commit

Permalink
feat-#429: Added Google sign-in button to login page
Browse files Browse the repository at this point in the history
  • Loading branch information
siddharthisrani committed Jul 20, 2024
1 parent db4b416 commit ba42d33
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 41 deletions.
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: 'http://localhost:8080/api/auth/google/callback',
},
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('http://localhost:5173/signup?google-callback=true');
}
);

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;
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
59 changes: 49 additions & 10 deletions frontend/src/pages/signin-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, useNavigate } from 'react-router-dom';
// import AddGoogleIcon from '@/assets/svg/google-color-icon.svg';
import AddGoogleIcon from '@/assets/svg/google-color-icon.svg';
// import AddGithubIcon from '@/assets/svg/github-icon.svg';
import { useForm } from 'react-hook-form';
import type { FieldValues } from 'react-hook-form';
Expand All @@ -11,12 +11,13 @@ import { AxiosError, isAxiosError } from 'axios';
import axiosInstance from '@/helpers/axios-instance';
import userState from '@/utils/user-state';
import ThemeToggle from '@/components/theme-toggle-button';
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import EyeIcon from '@/assets/svg/eye.svg';
import EyeOffIcon from '@/assets/svg/eye-off.svg';
function signin() {
const navigate = useNavigate();
const [passwordVisible, setPasswordVisible] = useState(false);
const toastShownRef = useRef(false);
const {
register,
handleSubmit,
Expand Down Expand Up @@ -68,11 +69,49 @@ function signin() {
}
};

useEffect(() => {
const handleGoogleCallback = async () => {
const searchParams = new URLSearchParams(location.search);
const isGoogleCallback = searchParams.get('google-callback') === 'true';

if (isGoogleCallback && !toastShownRef.current) {
try {
const response = await axiosInstance.get('/api/auth/check');
const { token, user } = response.data;
localStorage.setItem('token', token);
if (user && user._id && user.role) {
userState.setUser({ _id: user._id, role: user.role });
navigate('/');
if (!toastShownRef.current) {
toast.success('Successfully logged in with Google');
toastShownRef.current = true;
}
} else {
console.error('User data is incomplete:', user);
}
window.history.replaceState({}, document.title, window.location.pathname);
} catch (error) {
console.error('Error handling Google login:', error);
if (!toastShownRef.current) {
toast.error('Failed to log in with Google');
toastShownRef.current = true;
}
}
}
};

handleGoogleCallback();
}, [location, navigate]);

const handleGoogleLogin = () => {
window.location.href = 'http://localhost:8080/api/auth/google';
};

return (
<div className="flex-grow cursor-default bg-white py-4 dark:bg-dark-card">
<div className="m-4 mb-4 flex justify-center">
<div className="flex w-full items-center justify-center">
<h2 className="text-center text-lg font-bold text-black dark:text-dark-primary w-2/4 pl-2 sm:text-xl md:w-3/4 md:pl-48">
<h2 className="w-2/4 pl-2 text-center text-lg font-bold text-black dark:text-dark-primary sm:text-xl md:w-3/4 md:pl-48">
Sign in to WanderLust
</h2>
<div className="flex items-center justify-end px-4 sm:px-20">
Expand Down Expand Up @@ -138,21 +177,21 @@ function signin() {
{/* <span>OR</span> */}
</div>

{/* <Link
to={'/google-auth'}
className="flex w-full items-center justify-center space-x-2 rounded-lg border-2 border-b-4 border-gray-300 p-3 text-center hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 md:w-3/4 lg:w-2/5"
<button
className="flex w-full items-center justify-center space-x-2 rounded-lg border-2 border-b-4 border-gray-300 p-3 text-center hover:bg-gray-50 dark:border-gray-700 dark:text-dark-primary dark:hover:bg-gray-700 md:w-3/4 lg:w-2/5"
onClick={handleGoogleLogin}
>
<img className="h-4 w-6 pl-1 sm:h-5 sm:w-10" src={AddGoogleIcon} />
<span className="text-sm dark:text-dark-primary sm:text-base">Continue with Google</span>
</Link>
<span className="text-sm sm:text-base">Continue with Google</span>
</button>

<Link
{/* <Link
to={'/github-auth'}
className="flex w-full items-center justify-center space-x-2 rounded-lg border-2 border-b-4 border-gray-300 p-3 text-center hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 md:w-3/4 lg:w-2/5"
>
<img className="h-4 w-6 sm:h-5 sm:w-10" src={AddGithubIcon} />
<span className="text-sm dark:text-dark-primary sm:text-base">Continue with Github</span>
</Link> */}
</Link> */}
</div>
</div>
);
Expand Down
Loading

0 comments on commit ba42d33

Please sign in to comment.