diff --git a/backend/.env.sample b/backend/.env.sample
index ee9fc7e0..5457d313 100644
--- a/backend/.env.sample
+++ b/backend/.env.sample
@@ -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
\ No newline at end of file
+NODE_ENV=Development
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
\ No newline at end of file
diff --git a/backend/app.js b/backend/app.js
index f198513e..38e4e7a6 100644
--- a/backend/app.js
+++ b/backend/app.js
@@ -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();
@@ -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);
diff --git a/backend/config/passport.js b/backend/config/passport.js
new file mode 100644
index 00000000..728b377e
--- /dev/null
+++ b/backend/config/passport.js
@@ -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`,
+ },
+ 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;
diff --git a/backend/controllers/auth-controller.js b/backend/controllers/auth-controller.js
index bdeb3fea..e5dd9300 100644
--- a/backend/controllers/auth-controller.js
+++ b/backend/controllers/auth-controller.js
@@ -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,
{
@@ -128,19 +128,31 @@ 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);
@@ -148,22 +160,32 @@ export const isLoggedIn = asyncHandler(async (req, 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
diff --git a/backend/middlewares/auth-middleware.js b/backend/middlewares/auth-middleware.js
index 6e091962..363645f3 100644
--- a/backend/middlewares/auth-middleware.js
+++ b/backend/middlewares/auth-middleware.js
@@ -3,6 +3,7 @@ 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;
@@ -10,14 +11,13 @@ export const authMiddleware = async (req, res, next) => {
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));
}
};
diff --git a/backend/models/user.js b/backend/models/user.js
index 344289f6..c564fba4 100644
--- a/backend/models/user.js
+++ b/backend/models/user.js
@@ -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,}$/,
@@ -60,7 +60,13 @@ const userSchema = new Schema(
refreshToken: String,
forgotPasswordToken: String,
forgotPasswordExpiry: Date,
+ googleId: {
+ type: String,
+ unique: true,
+ required: false,
+ },
},
+
{ timestamps: true }
);
diff --git a/backend/package.json b/backend/package.json
index 7f3f03be..9d666138 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -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": {
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index c11b3394..85b498b3 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -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,
@@ -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`);
+ }
+);
+
+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;
diff --git a/frontend/src/components/require-auth.tsx b/frontend/src/components/require-auth.tsx
index 06f6febc..68647121 100644
--- a/frontend/src/components/require-auth.tsx
+++ b/frontend/src/components/require-auth.tsx
@@ -8,7 +8,7 @@ function RequireAuth({ allowedRole }: { allowedRole: string[] }) {
if (loading) {
return (
<>
-