Skip to content

Commit e188218

Browse files
committed
google auth
1 parent de7c227 commit e188218

17 files changed

Lines changed: 538 additions & 109 deletions

File tree

backend/bun.lock

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"cors": "^2.8.5",
1515
"dotenv": "^17.3.1",
1616
"express": "^5.1.0",
17+
"google-auth-library": "^10.6.2",
1718
"jsonwebtoken": "^9.0.2",
1819
"mongoose": "^9.2.3",
1920
"morgan": "^1.10.1",

backend/src/app.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,22 @@ app.use(express.json());
1616
app.use(express.urlencoded({ extended: true }));
1717
app.use(cookieParser());
1818

19+
const allowedOrigins = (process.env.FRONTEND_URL || "http://localhost:5173")
20+
.split(",")
21+
.map((origin) => origin.trim())
22+
.filter(Boolean);
23+
1924
app.use(
2025
cors({
21-
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
26+
origin: (origin, callback) => {
27+
if (!origin || allowedOrigins.includes(origin)) {
28+
return callback(null, true);
29+
}
30+
return callback(new Error("Not allowed by CORS"));
31+
},
2232
credentials: true,
33+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
34+
allowedHeaders: ["Content-Type", "Authorization"],
2335
})
2436
);
2537

Lines changed: 176 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,207 @@
1-
// controllers/auth.controller.js
2-
import User from '../models/user.model.js';
3-
import generateToken from '../utils/generateToken.js';
1+
import { OAuth2Client } from "google-auth-library";
2+
import User from "../models/user.model.js";
3+
import { clearAuthCookie, setAuthCookie } from "../utils/authCookie.js";
4+
import generateToken from "../utils/generateToken.js";
45

5-
// REGISTER (only for normal users, admin comes from seed file)
66
const allowedRoles = new Set(["developer", "client"]);
77

8+
const getSafeRole = (role) => (allowedRoles.has(role) ? role : "client");
9+
10+
const serializeUser = (user) => ({
11+
_id: user._id,
12+
name: user.name,
13+
email: user.email,
14+
role: user.role,
15+
profilePic: user.profilePic || user.avatar || "",
16+
googleId: user.googleId || null,
17+
});
18+
19+
const sendAuthResponse = (res, user, statusCode = 200) => {
20+
const token = generateToken(user._id);
21+
setAuthCookie(res, token);
22+
23+
res.status(statusCode).json({
24+
...serializeUser(user),
25+
token,
26+
});
27+
};
28+
29+
const getGoogleAudienceList = () => {
30+
const rawIds =
31+
process.env.GOOGLE_CLIENT_IDS || process.env.GOOGLE_CLIENT_ID || "";
32+
const audience = rawIds
33+
.split(",")
34+
.map((id) => id.trim())
35+
.filter(Boolean);
36+
37+
if (!audience.length) {
38+
const error = new Error(
39+
"Google OAuth is not configured. Set GOOGLE_CLIENT_ID (or GOOGLE_CLIENT_IDS)."
40+
);
41+
error.statusCode = 500;
42+
throw error;
43+
}
44+
45+
return audience;
46+
};
47+
48+
const getGoogleClient = () => {
49+
return new OAuth2Client();
50+
};
51+
852
export const registerUser = async (req, res, next) => {
953
try {
1054
const { name, email, password, role } = req.body;
1155

12-
const userExists = await User.findOne({ email });
13-
if (userExists) {
56+
if (!name || !email || !password) {
1457
res.status(400);
15-
throw new Error('User already exists');
58+
throw new Error("Name, email and password are required.");
1659
}
1760

18-
const safeRole = allowedRoles.has(role) ? role : "client";
61+
const existingUser = await User.findOne({ email: email.toLowerCase() });
62+
if (existingUser) {
63+
res.status(400);
64+
throw new Error("User already exists");
65+
}
1966

2067
const user = await User.create({
21-
name,
22-
email,
68+
name: name.trim(),
69+
email: email.toLowerCase(),
2370
password,
24-
role: safeRole,
71+
role: getSafeRole(role),
2572
});
2673

27-
res.status(201).json({
28-
_id: user._id,
29-
name: user.name,
30-
email: user.email,
31-
role: user.role,
32-
token: generateToken(user._id),
33-
});
74+
sendAuthResponse(res, user, 201);
3475
} catch (error) {
3576
next(error);
3677
}
3778
};
3879

39-
// LOGIN (works for both user & admin)
4080
export const loginUser = async (req, res, next) => {
4181
try {
4282
const { email, password } = req.body;
4383

44-
const user = await User.findOne({ email });
45-
if (user && (await user.matchPassword(password))) {
46-
res.json({
47-
_id: user._id,
48-
name: user.name,
49-
email: user.email,
50-
role: user.role, // tells frontend if this is "admin" or "user"
51-
token: generateToken(user._id),
84+
if (!email || !password) {
85+
res.status(400);
86+
throw new Error("Email and password are required.");
87+
}
88+
89+
const user = await User.findOne({ email: email.toLowerCase() });
90+
if (!user) {
91+
res.status(401);
92+
throw new Error("Invalid email or password");
93+
}
94+
95+
if (!user.password) {
96+
res.status(400);
97+
throw new Error(
98+
"This account uses Google sign-in. Continue with Google to log in."
99+
);
100+
}
101+
102+
const isPasswordValid = await user.matchPassword(password);
103+
if (!isPasswordValid) {
104+
res.status(401);
105+
throw new Error("Invalid email or password");
106+
}
107+
108+
sendAuthResponse(res, user);
109+
} catch (error) {
110+
next(error);
111+
}
112+
};
113+
114+
export const googleAuth = async (req, res, next) => {
115+
try {
116+
const { token: googleIdToken, role } = req.body;
117+
118+
if (!googleIdToken) {
119+
res.status(400);
120+
throw new Error("Google ID token is required.");
121+
}
122+
123+
const audience = getGoogleAudienceList();
124+
const oauthClient = getGoogleClient();
125+
126+
const ticket = await oauthClient.verifyIdToken({
127+
idToken: googleIdToken,
128+
audience,
129+
});
130+
131+
const payload = ticket.getPayload();
132+
if (!payload) {
133+
res.status(401);
134+
throw new Error("Invalid Google token payload.");
135+
}
136+
137+
const googleId = payload.sub;
138+
const email = payload.email?.toLowerCase();
139+
const name = payload.name?.trim();
140+
const profilePic = payload.picture || "";
141+
const isEmailVerified = Boolean(payload.email_verified);
142+
143+
if (!googleId || !email || !isEmailVerified) {
144+
res.status(401);
145+
throw new Error("Google account must have a verified email.");
146+
}
147+
148+
if (!allowedRoles.has(role)) {
149+
res.status(400);
150+
throw new Error("Account type must be either client or developer.");
151+
}
152+
153+
const selectedRole = role;
154+
let user = await User.findOne({ email });
155+
let isNewUser = false;
156+
157+
if (!user) {
158+
user = await User.create({
159+
name: name || email.split("@")[0],
160+
email,
161+
googleId,
162+
profilePic,
163+
avatar: profilePic,
164+
role: selectedRole,
165+
isVerified: true,
52166
});
167+
isNewUser = true;
53168
} else {
54-
res.status(401);
55-
throw new Error('Invalid email or password');
169+
if (user.googleId && user.googleId !== googleId) {
170+
res.status(409);
171+
throw new Error("This email is linked to a different Google account.");
172+
}
173+
174+
if (!user.googleId) user.googleId = googleId;
175+
if (name && !user.name) user.name = name;
176+
if (profilePic) user.profilePic = profilePic;
177+
if (profilePic && !user.avatar) user.avatar = profilePic;
178+
if (!user.isVerified && isEmailVerified) user.isVerified = true;
179+
180+
await user.save();
56181
}
182+
183+
const token = generateToken(user._id);
184+
setAuthCookie(res, token);
185+
186+
res.status(200).json({
187+
...serializeUser(user),
188+
token,
189+
isNewUser,
190+
});
57191
} catch (error) {
192+
if (error?.message?.includes("Wrong recipient")) {
193+
error.message =
194+
"Google verification failed: client ID mismatch. Ensure frontend VITE_GOOGLE_CLIENT_ID and backend GOOGLE_CLIENT_ID/GOOGLE_CLIENT_IDS are aligned.";
195+
}
196+
197+
if (!res.statusCode || res.statusCode === 200) {
198+
res.status(error.statusCode || 401);
199+
}
58200
next(error);
59201
}
60202
};
203+
204+
export const logoutUser = async (req, res) => {
205+
clearAuthCookie(res);
206+
res.status(200).json({ message: "Logged out successfully." });
207+
};

0 commit comments

Comments
 (0)