From 4127459074fbc3746f7096b97e1a92145724c58f Mon Sep 17 00:00:00 2001 From: AnishRoy50 Date: Thu, 19 Jun 2025 21:38:20 +0600 Subject: [PATCH] -Added complete Google OAuth flow for login and signup -Fixed token generation using appropriate create_access_token() function -Generated secure random passwords for Google OAuth users -Made usernames unique with random suffixes -Added proper error handling for OAuth callback -Integrated with existing authentication system --- .gitignore | 1 - backend/main.py | 4 +++ backend/models/user.py | 2 +- backend/routes/google_auth.py | 68 +++++++++++++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/App.jsx | 3 ++ frontend/src/index.jsx | 6 +++- frontend/src/pages/Login.jsx | 35 +++++++++++++++--- 8 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 backend/routes/google_auth.py diff --git a/.gitignore b/.gitignore index c58d7e9..3ac191f 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ backend/__pycache__/ *.py[cod] *.pyo backend/coverage_html/ - # Compiled binaries *.so *.dll diff --git a/backend/main.py b/backend/main.py index 8bf6108..7a5786a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,6 +8,8 @@ from fastapi.staticfiles import StaticFiles from api.v1.endpoints import search from api.v1.endpoints.chatbot import huggingface +from routes import google_auth +from starlette.middleware.sessions import SessionMiddleware app = FastAPI() @@ -26,12 +28,14 @@ allow_methods=["*"], # Allow all methods (GET, POST, etc.) allow_headers=["*"], # Allow all headers ) +app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key") # Create tables Base.metadata.create_all(bind=engine) # Include routers app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) +app.include_router(google_auth.router, prefix="/auth/google", tags=["Google Authentication"]) app.include_router(profile.router, prefix="/profile", tags=["User Profile"]) app.include_router(post.router,prefix="/posts", tags=["Posts"]) app.include_router(postReaction.router, prefix="/interactions", tags=["Post Interactions"]) diff --git a/backend/models/user.py b/backend/models/user.py index 53215fe..39a32fd 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -11,7 +11,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False) - hashed_password = Column(String, nullable=False) + hashed_password = Column(String, nullable=True) is_active = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) # Tracks email verification status otp = Column(String, nullable=True) # Stores the OTP temporarily diff --git a/backend/routes/google_auth.py b/backend/routes/google_auth.py new file mode 100644 index 0000000..97fc3af --- /dev/null +++ b/backend/routes/google_auth.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import RedirectResponse +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config +from starlette.requests import Request +from sqlalchemy.orm import Session +from core.dependencies import get_db +from models.user import User +from services.AuthHandler import AuthHandler +from core.security import create_access_token +import os + +router = APIRouter() + +config_data = { + "GOOGLE_CLIENT_ID": os.getenv("GOOGLE_CLIENT_ID"), + "GOOGLE_CLIENT_SECRET": os.getenv("GOOGLE_CLIENT_SECRET"), + "SECRET_KEY": os.getenv("SECRET_KEY", "secret"), +} +config = Config(environ=config_data) +oauth = OAuth(config) +oauth.register( + name='google', + client_id=config_data["GOOGLE_CLIENT_ID"], + client_secret=config_data["GOOGLE_CLIENT_SECRET"], + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={'scope': 'openid email profile'}, +) + +@router.get("/login") +async def google_login(request: Request): + redirect_uri = os.getenv("GOOGLE_REDIRECT_URI") + return await oauth.google.authorize_redirect(request, redirect_uri) + +@router.get("/callback") +async def google_callback(request: Request, db: Session = Depends(get_db)): + token = await oauth.google.authorize_access_token(request) + print("Google token response:", token) + user_info = token.get("userinfo") + if not user_info: + raise HTTPException(status_code=400, detail="No userinfo in Google response") # Find or create user + user = db.query(User).filter(User.email == user_info["email"]).first() + if not user: + from core.security import hash_password + # Generate a strong random password for Google OAuth users + # They won't use this password since they'll always login via Google + import secrets + random_password = secrets.token_urlsafe(16) + hashed_password = hash_password(random_password) + + # Create username from email and make it unique with a random suffix + base_username = user_info["email"].split("@")[0] + username = f"{base_username}{secrets.token_hex(3)}" + + user = User( + username=username, + email=user_info["email"], + hashed_password=hashed_password, + is_verified=True, + profile_completed=False, + ) + db.add(user) + db.commit() + db.refresh(user)# Generate JWT token + access_token = create_access_token({"sub": user.username}) + # Redirect to frontend with token as query param + frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173") + return RedirectResponse(f"{frontend_url}/login?token={access_token}") \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a1cca1d..b2b5867 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@react-oauth/google": "^0.12.2", "@tailwindcss/vite": "^4.0.9", "axios": "^1.8.1", "date-fns": "^4.1.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2121a06..6240f33 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,6 +14,9 @@ import VerifyEmail from "./pages/VerifyEmail"; import ResetPassword from "./pages/ResetPassword"; import ForgotPassword from "./pages/ForgotPassword"; + +const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; + function App() { return ( diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index ffacd6d..1bef271 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -2,8 +2,12 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; // Ensure this path is correct import './index.css'; +import { GoogleOAuthProvider } from '@react-oauth/google'; +const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID; ReactDOM.createRoot(document.getElementById("root")).render( - + + + ); diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 31b8bbe..b54c15a 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -4,6 +4,7 @@ import { login } from "../api/auth"; import axios from "axios"; import { useAuth } from "../context/AuthContext"; import Navbar from "../components/Navbar"; +import { GoogleLogin, googleLogout } from '@react-oauth/google'; const Login = () => { const [formData, setFormData] = useState({ username: "", password: "" }); @@ -14,12 +15,35 @@ const Login = () => { const { login: authLogin } = useAuth(); const location = useLocation(); + const handleGoogleSuccess = async (credentialResponse) => { + try { + // Option 1: Use backend OAuth flow (recommended) + window.location.href = `${import.meta.env.VITE_API_URL}/auth/google/login`; + + // Option 2: If using Google One Tap and want to POST token to backend: + // const { credential } = credentialResponse; + // const res = await axios.post(`${import.meta.env.VITE_API_URL}/auth/google/onetap`, { token: credential }); + // Handle login as usual with res.data.access_token + } catch (error) { + setError("Google login failed"); + } + }; useEffect(() => { if (location.state?.fromVerification) { setSuccess("Email verified! Please log in to continue."); } }, [location.state]); + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + if (token) { + localStorage.setItem("token", token); + // Optionally fetch user and redirect + window.location.href = "/dashboard/posts"; + } + }, []); + const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; @@ -101,13 +125,14 @@ const Login = () => { type="button" className="w-full flex items-center justify-center gap-2 border border-gray-300 rounded-full py-2 mb-3 hover:bg-gray-100" > - University Mail Icon setError("Google login failed")} + useOneTap /> - Continue with University mail +