google sign in with oauth 2.0. no more console errors or warnings

This commit is contained in:
jackiettran
2025-10-08 12:46:25 -04:00
parent 299522b3a6
commit 052781a0e6
8 changed files with 186 additions and 93 deletions

View File

@@ -19,7 +19,6 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"
/>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -6,6 +6,7 @@ import Footer from './components/Footer';
import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
import GoogleCallback from './pages/GoogleCallback';
import ItemList from './pages/ItemList';
import ItemDetail from './pages/ItemDetail';
import EditItem from './pages/EditItem';
@@ -36,6 +37,7 @@ function App() {
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/auth/google/callback" element={<GoogleCallback />} />
<Route path="/items" element={<ItemList />} />
<Route path="/items/:id" element={<ItemDetail />} />
<Route path="/users/:id" element={<PublicProfile />} />

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { useAuth } from "../contexts/AuthContext";
import PasswordStrengthMeter from "./PasswordStrengthMeter";
@@ -20,9 +20,8 @@ const AuthModal: React.FC<AuthModalProps> = ({
const [lastName, setLastName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const googleButtonRef = useRef<HTMLDivElement>(null);
const { login, register, googleLogin, updateUser } = useAuth();
const { login, register } = useAuth();
// Update mode when modal is opened with different initialMode
useEffect(() => {
@@ -31,43 +30,29 @@ const AuthModal: React.FC<AuthModalProps> = ({
}
}, [show, initialMode]);
// Initialize Google Sign-In
useEffect(() => {
if (show && window.google && process.env.REACT_APP_GOOGLE_CLIENT_ID) {
try {
window.google.accounts.id.initialize({
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
callback: handleGoogleResponse,
auto_select: false,
cancel_on_tap_outside: false,
});
} catch (error) {
console.error("Error initializing Google Sign-In:", error);
}
}
}, [show]);
const resetModal = () => {
setError("");
setEmail("");
setPassword("");
setFirstName("");
setLastName("");
};
const handleGoogleResponse = async (response: any) => {
try {
setLoading(true);
setError("");
const handleGoogleLogin = () => {
const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
const redirectUri = `${window.location.origin}/auth/google/callback`;
const scope = 'email profile';
const responseType = 'code';
if (!response?.credential) {
throw new Error("No credential received from Google");
}
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${encodeURIComponent(clientId || '')}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&response_type=${responseType}` +
`&scope=${encodeURIComponent(scope)}` +
`&access_type=offline` +
`&prompt=consent`;
await googleLogin(response.credential);
onHide();
resetModal();
} catch (err: any) {
setError(
err.response?.data?.error ||
err.message ||
"Failed to sign in with Google"
);
} finally {
setLoading(false);
}
window.location.href = googleAuthUrl;
};
const handleEmailSubmit = async (e: React.FormEvent) => {
@@ -96,27 +81,6 @@ const AuthModal: React.FC<AuthModalProps> = ({
}
};
const handleSocialLogin = (provider: string) => {
if (provider === "google") {
if (window.google) {
try {
window.google.accounts.id.prompt();
} catch (error) {
setError("Failed to open Google Sign-In. Please try again.");
}
} else {
setError("Google Sign-In is not available. Please try again later.");
}
}
};
const resetModal = () => {
setError("");
setEmail("");
setPassword("");
setFirstName("");
setLastName("");
};
if (!show) return null;
@@ -226,8 +190,9 @@ const AuthModal: React.FC<AuthModalProps> = ({
{/* Social Login Options */}
<button
className="btn btn-outline-dark w-100 mb-2 py-3 d-flex align-items-center justify-content-center"
onClick={() => handleSocialLogin("google")}
onClick={handleGoogleLogin}
disabled={loading}
type="button"
>
<i className="bi bi-google me-2"></i>
Continue with Google

View File

@@ -3,6 +3,7 @@ import React, {
useState,
useContext,
useEffect,
useRef,
ReactNode,
} from "react";
import { User } from "../types";
@@ -18,7 +19,7 @@ interface AuthContextType {
loading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (data: any) => Promise<void>;
googleLogin: (idToken: string) => Promise<void>;
googleLogin: (code: string) => Promise<void>;
logout: () => Promise<void>;
updateUser: (user: User) => void;
checkAuth: () => Promise<void>;
@@ -41,6 +42,7 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const isAuthenticating = useRef(false);
const checkAuth = async () => {
try {
@@ -62,9 +64,18 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
// Initialize authentication
const initializeAuth = async () => {
try {
await fetchCSRFToken();
// Check if user is already authenticated
await checkAuth();
// Skip CSRF token fetch and auth check if we're on the OAuth callback page
// The callback page will handle both CSRF token fetch and authentication
const isOAuthCallback = window.location.pathname === '/auth/google/callback';
if (!isOAuthCallback) {
await fetchCSRFToken();
// Skip auth check if we're in the middle of an authentication operation
if (!isAuthenticating.current) {
await checkAuth();
}
}
} catch (error) {
console.error("Failed to initialize authentication:", error);
} finally {
@@ -89,11 +100,16 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
await fetchCSRFToken();
};
const googleLogin = async (idToken: string) => {
const response = await authAPI.googleLogin({ idToken });
setUser(response.data.user);
// Fetch new CSRF token after Google login
await fetchCSRFToken();
const googleLogin = async (code: string) => {
isAuthenticating.current = true;
try {
const response = await authAPI.googleLogin(code);
setUser(response.data.user);
// Fetch new CSRF token after Google login
await fetchCSRFToken();
} finally {
isAuthenticating.current = false;
}
};
const logout = async () => {

View File

@@ -0,0 +1,98 @@
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { fetchCSRFToken } from '../services/api';
const GoogleCallback: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { googleLogin } = useAuth();
const [error, setError] = useState<string>('');
const [processing, setProcessing] = useState(true);
const hasProcessed = useRef(false);
useEffect(() => {
const handleCallback = async () => {
// Prevent double execution in React StrictMode
if (hasProcessed.current) {
return;
}
hasProcessed.current = true;
try {
const code = searchParams.get('code');
const errorParam = searchParams.get('error');
if (errorParam) {
setError('Google Sign-In was cancelled or failed. Please try again.');
setProcessing(false);
return;
}
if (!code) {
setError('No authorization code received from Google.');
setProcessing(false);
return;
}
// Fetch CSRF token before making auth request
const csrfToken = await fetchCSRFToken();
if (!csrfToken) {
console.error('Failed to fetch CSRF token');
setError('Failed to initialize security token. Please try again.');
setProcessing(false);
return;
}
// Exchange code for user session
await googleLogin(code);
// Redirect to home page on success
navigate('/', { replace: true });
} catch (err: any) {
console.error('Google OAuth callback error:', err);
setError(err.response?.data?.error || 'Failed to sign in with Google. Please try again.');
setProcessing(false);
}
};
handleCallback();
}, [searchParams, googleLogin, navigate]);
return (
<div className="container">
<div className="row justify-content-center mt-5">
<div className="col-md-6">
<div className="card">
<div className="card-body text-center py-5">
{processing ? (
<>
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<h5>Completing Google Sign-In...</h5>
<p className="text-muted">Please wait while we log you in.</p>
</>
) : error ? (
<>
<i className="bi bi-exclamation-circle text-danger" style={{ fontSize: '3rem' }}></i>
<h5 className="mt-3">Sign-In Failed</h5>
<p className="text-danger">{error}</p>
<button
className="btn btn-primary mt-3"
onClick={() => navigate('/')}
>
Return to Home
</button>
</>
) : null}
</div>
</div>
</div>
</div>
</div>
);
};
export default GoogleCallback;

View File

@@ -58,10 +58,12 @@ api.interceptors.request.use(async (config) => {
if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
// If we don't have a CSRF token yet, try to fetch it
if (!csrfToken) {
// Skip fetching for auth endpoints to avoid circular dependency
// Skip fetching for most auth endpoints to avoid circular dependency
// Exception: /auth/google needs CSRF token and should auto-fetch as fallback
const isAuthEndpoint =
config.url?.includes("/auth/") &&
!config.url?.includes("/auth/refresh");
!config.url?.includes("/auth/refresh") &&
!config.url?.includes("/auth/google");
if (!isAuthEndpoint) {
await fetchCSRFToken();
}
@@ -90,6 +92,12 @@ api.interceptors.response.use(
errorData?.code === "CSRF_TOKEN_MISMATCH" &&
!originalRequest._csrfRetry
) {
// Don't retry OAuth endpoints - the authorization code is single-use
const isOAuthEndpoint = originalRequest.url?.includes("/auth/google");
if (isOAuthEndpoint) {
return Promise.reject(error);
}
originalRequest._csrfRetry = true;
// Try to fetch a new CSRF token and retry
@@ -153,7 +161,7 @@ api.interceptors.response.use(
export const authAPI = {
register: (data: any) => api.post("/auth/register", data),
login: (data: any) => api.post("/auth/login", data),
googleLogin: (data: any) => api.post("/auth/google", data),
googleLogin: (code: string) => api.post("/auth/google", { code }),
logout: () => api.post("/auth/logout"),
refresh: () => api.post("/auth/refresh"),
getCSRFToken: () => api.get("/auth/csrf-token"),