Skip to content

Commit 0d00e86

Browse files
authored
Implement Google sign-in along with password accounts (#27)
* feat(auth): implement Google sign-in and linking with password accounts * Change Contact Mail ID
1 parent 6e4cd57 commit 0d00e86

9 files changed

Lines changed: 851 additions & 149 deletions

File tree

Firebase.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ const firebaseConfig = {
2121
const app = initializeApp(firebaseConfig);
2222
const auth = getAuth(app);
2323

24+
// Shared Google provider. `select_account` forces the account chooser every time
25+
// so a user signed into multiple Google accounts isn't silently logged into the
26+
// wrong one.
27+
const googleProvider = new GoogleAuthProvider();
28+
googleProvider.setCustomParameters({ prompt: "select_account" });
29+
2430
let analytics: Analytics | undefined;
2531
if (typeof window !== "undefined") {
2632
isSupported().then((supported) => {
@@ -30,4 +36,4 @@ if (typeof window !== "undefined") {
3036
});
3137
}
3238

33-
export { app, auth, analytics, logEvent};
39+
export { app, auth, googleProvider, analytics, logEvent};

app/api/user/register/route.ts

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import dbConnect from "@/lib/db";
99
import User, { IUser } from "@/models/User";
1010
import { verifyRecaptcha } from "@/lib/recaptcha";
1111
import { checkRateLimit, getClientIp } from "@/lib/rate-limit";
12+
import { getAuth as getAdminAuth } from "@/lib/firebase-admin";
1213

1314
// Configure route
1415
export const dynamic = "force-dynamic";
@@ -75,7 +76,10 @@ export async function POST(request: Request) {
7576
{
7677
success: false,
7778
message: "Too many requests. Please try again later.",
78-
error: { code: "rate_limit_exceeded", message: "Rate limit exceeded" },
79+
error: {
80+
code: "rate_limit_exceeded",
81+
message: "Rate limit exceeded",
82+
},
7983
},
8084
{ status: 429 },
8185
);
@@ -105,17 +109,78 @@ export async function POST(request: Request) {
105109
const recaptchaToken = formData.get("recaptcha_token") as string | null;
106110
const captcha = await verifyRecaptcha(recaptchaToken, "register");
107111
if (!captcha.ok) {
108-
console.warn("[register] reCAPTCHA rejected:", captcha.reason, captcha.score);
112+
console.warn(
113+
"[register] reCAPTCHA rejected:",
114+
captcha.reason,
115+
captcha.score,
116+
);
109117
return NextResponse.json(
110118
{
111119
success: false,
112120
message: "Security check failed. Please try again.",
113-
error: { code: "recaptcha_failed", message: "reCAPTCHA verification failed" },
121+
error: {
122+
code: "recaptcha_failed",
123+
message: "reCAPTCHA verification failed",
124+
},
114125
},
115126
{ status: 400 },
116127
);
117128
}
118129

130+
const isGoogle = formData.get("auth_provider") === "google";
131+
let googleUid: string | undefined;
132+
let googleEmail: string | undefined;
133+
if (isGoogle) {
134+
const idToken = formData.get("id_token") as string | null;
135+
if (!idToken) {
136+
return NextResponse.json(
137+
{
138+
success: false,
139+
message: "Missing Google sign-in token",
140+
error: {
141+
code: "missing_id_token",
142+
message: "Missing Google sign-in token",
143+
},
144+
},
145+
{ status: 400 },
146+
);
147+
}
148+
try {
149+
const decoded = await getAdminAuth().verifyIdToken(idToken);
150+
googleUid = decoded.uid;
151+
googleEmail = decoded.email?.toLowerCase();
152+
} catch (tokenError) {
153+
console.error(
154+
"[register] Google ID token verification failed:",
155+
tokenError,
156+
);
157+
return NextResponse.json(
158+
{
159+
success: false,
160+
message: "Invalid Google sign-in token. Please sign in again.",
161+
error: {
162+
code: "invalid_id_token",
163+
message: "Invalid Google sign-in token",
164+
},
165+
},
166+
{ status: 401 },
167+
);
168+
}
169+
if (!googleEmail) {
170+
return NextResponse.json(
171+
{
172+
success: false,
173+
message: "Your Google account has no email address.",
174+
error: {
175+
code: "google_no_email",
176+
message: "Google account has no email address",
177+
},
178+
},
179+
{ status: 400 },
180+
);
181+
}
182+
}
183+
119184
// Helper to convert File to base64
120185
const fileToBase64 = async (file: File): Promise<string> => {
121186
const arrayBuffer = await file.arrayBuffer();
@@ -125,7 +190,10 @@ export async function POST(request: Request) {
125190

126191
// Extract fields
127192
const name = formData.get("name") as string;
128-
const email = formData.get("email") as string;
193+
// For Google, the email comes from the verified ID token, never the client.
194+
const email = isGoogle
195+
? (googleEmail as string)
196+
: (formData.get("email") as string);
129197
const password = formData.get("password") as string;
130198
const discord_username = formData.get("discord_username") as string;
131199
const phone = formData.get("phone") as string;
@@ -166,7 +234,8 @@ export async function POST(request: Request) {
166234
errors.email = "Invalid email format";
167235
}
168236

169-
if (!password || !validatePassword(password)) {
237+
// Google users have no password — Firebase handles their credential.
238+
if (!isGoogle && (!password || !validatePassword(password))) {
170239
errors.password =
171240
"Password must be at least 8 characters and contain uppercase, lowercase, number, and special character";
172241
}
@@ -268,40 +337,62 @@ export async function POST(request: Request) {
268337
);
269338
}
270339

271-
// Create Firebase Auth user
272-
let firebaseUser;
273-
try {
274-
const userCredential = await createUserWithEmailAndPassword(
275-
auth,
276-
email,
277-
password,
278-
);
279-
firebaseUser = userCredential.user;
280-
281-
// Send email verification
282-
await sendEmailVerification(firebaseUser);
283-
} catch (firebaseError: any) {
284-
if (firebaseError.code === "auth/email-already-in-use") {
340+
if (isGoogle) {
341+
const existingUid = await User.findOne({ uid: googleUid });
342+
if (existingUid) {
285343
return NextResponse.json(
286344
{
287345
success: false,
288-
message: "Email already exists",
289-
error: { code: "email_exists", message: "Email already exists" },
346+
message:
347+
"This Google account is already registered. Please log in.",
348+
error: {
349+
code: "already_registered",
350+
message: "Account already exists",
351+
},
290352
},
291353
{ status: 409 },
292354
);
293355
}
294-
if (firebaseError.code === "auth/weak-password") {
295-
return NextResponse.json(
296-
{
297-
success: false,
298-
message: "Password is too weak",
299-
error: { code: "weak_password", message: "Password is too weak" },
300-
},
301-
{ status: 400 },
356+
}
357+
358+
// Resolve the Firebase uid. Email/password users are created here (and sent
359+
// a verification email); Google users already exist in Firebase with a
360+
// verified email, so we reuse the uid from their ID token and skip both.
361+
let uid: string;
362+
if (isGoogle) {
363+
uid = googleUid!;
364+
} else {
365+
try {
366+
const userCredential = await createUserWithEmailAndPassword(
367+
auth,
368+
email,
369+
password,
302370
);
371+
await sendEmailVerification(userCredential.user);
372+
uid = userCredential.user.uid;
373+
} catch (firebaseError: any) {
374+
if (firebaseError.code === "auth/email-already-in-use") {
375+
return NextResponse.json(
376+
{
377+
success: false,
378+
message: "Email already exists",
379+
error: { code: "email_exists", message: "Email already exists" },
380+
},
381+
{ status: 409 },
382+
);
383+
}
384+
if (firebaseError.code === "auth/weak-password") {
385+
return NextResponse.json(
386+
{
387+
success: false,
388+
message: "Password is too weak",
389+
error: { code: "weak_password", message: "Password is too weak" },
390+
},
391+
{ status: 400 },
392+
);
393+
}
394+
throw firebaseError;
303395
}
304-
throw firebaseError;
305396
}
306397

307398
// Upload files to Cloudinary if provided
@@ -336,7 +427,7 @@ export async function POST(request: Request) {
336427

337428
// Create user in MongoDB
338429
const newUser = new User({
339-
uid: firebaseUser.uid,
430+
uid,
340431
name: name.trim(),
341432
email: email.toLowerCase().trim(),
342433
phone: phone.trim(),
@@ -353,17 +444,19 @@ export async function POST(request: Request) {
353444
isLooking: Boolean(isLooking),
354445
role: "user",
355446
teamCode: undefined,
447+
authProvider: isGoogle ? "google" : "password",
356448
});
357449

358450
await newUser.save();
359451

360452
return NextResponse.json(
361453
{
362454
message: "Registration successful",
363-
uid: firebaseUser.uid,
364-
status: "pending_verification",
455+
uid,
456+
// Google emails arrive verified, so there's nothing pending for them.
457+
status: isGoogle ? "active" : "pending_verification",
365458
user: {
366-
uid: firebaseUser.uid,
459+
uid,
367460
email: newUser.email,
368461
name: newUser.name,
369462
isAdmin: false,

0 commit comments

Comments
 (0)