From d6eeb9a079fb19d9d86e657127168e726aefc8d0 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 20 Feb 2026 16:30:27 +0200 Subject: [PATCH 1/5] feat: Add case-insensitive username login and Google OAuth authentication - Make username login case-insensitive (Dvir = dvir = DVIR) - Add Google OAuth login/signup functionality - Install required dependencies (authlib, httpx, python-dotenv) - Create OAuth endpoints: /auth/google/url, /auth/google/callback - Add Google login button to frontend with styled UI - Configure environment variables for OAuth credentials - Add comprehensive setup documentation - Secure .env file with .gitignore --- GOOGLE_OAUTH_SETUP.md | 110 +++++++++++++++++++++++++++++++++++ SETUP_COMPLETE.md | 51 ++++++++++++++++ backend/.env.example | 9 +++ backend/.gitignore | 44 ++++++++++++++ backend/main.py | 122 +++++++++++++++++++++++++++++++++++++-- backend/requirements.txt | 13 +++-- frontend/src/Auth.css | 55 ++++++++++++++++++ frontend/src/Auth.jsx | 70 +++++++++++++++++++++- 8 files changed, 464 insertions(+), 10 deletions(-) create mode 100644 GOOGLE_OAUTH_SETUP.md create mode 100644 SETUP_COMPLETE.md create mode 100644 backend/.gitignore diff --git a/GOOGLE_OAUTH_SETUP.md b/GOOGLE_OAUTH_SETUP.md new file mode 100644 index 0000000..f6c905f --- /dev/null +++ b/GOOGLE_OAUTH_SETUP.md @@ -0,0 +1,110 @@ +# Google OAuth Setup Guide + +## Prerequisites +1. A Google Cloud Platform account +2. A project created in Google Cloud Console + +## Step 1: Create OAuth 2.0 Credentials + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Select your project (or create a new one) +3. Navigate to **APIs & Services** > **Credentials** +4. Click **Create Credentials** > **OAuth 2.0 Client ID** +5. If prompted, configure the OAuth consent screen: + - Choose **External** user type + - Fill in the required fields: + - App name: Tasko + - User support email: Your email + - Developer contact information: Your email + - Add scopes (optional): `userinfo.email`, `userinfo.profile` + - Add test users if needed +6. Select **Application type**: Web application +7. Add **Authorized redirect URIs**: + - For local development: `http://localhost:8000/auth/google/callback` + - For production: `https://your-domain.com/auth/google/callback` +8. Click **Create** +9. Copy the **Client ID** and **Client Secret** + +## Step 2: Configure Backend + +1. Copy `.env.example` to `.env` in the backend directory: + ```bash + cd backend + cp .env.example .env + ``` + +2. Edit `.env` and add your Google OAuth credentials: + ```env + GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com + GOOGLE_CLIENT_SECRET=your_client_secret_here + GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback + FRONTEND_URL=http://localhost:5173 + ``` + +3. For production, update the URLs: + ```env + GOOGLE_REDIRECT_URI=https://api.tasko.dvirlabs.com/auth/google/callback + FRONTEND_URL=https://tasko.dvirlabs.com + ``` + +## Step 3: Install Dependencies + +```bash +cd backend +pip install -r requirements.txt +``` + +## Step 4: Test the Setup + +1. Start the backend server: + ```bash + cd backend + uvicorn main:app --reload + ``` + +2. Start the frontend: + ```bash + cd frontend + npm run dev + ``` + +3. Open your browser and navigate to the frontend URL +4. Click "Continue with Google" button +5. Complete the Google authentication flow + +## Features Implemented + +### 1. Case-Insensitive Username Login +- Users can now login with usernames in any case (e.g., "Dvir", "dvir", "DVIR" all work) +- Registration checks for existing usernames case-insensitively to prevent duplicates + +### 2. Google OAuth Integration +- Users can sign up and login using their Google account +- New users are automatically created with: + - Username derived from email (before @) + - Email from Google account + - Default task lists (Personal, Work, Shopping) +- Existing users (matched by email) can login with Google +- Seamless redirect back to the application after authentication + +## Troubleshooting + +### "redirect_uri_mismatch" Error +- Ensure the redirect URI in Google Cloud Console exactly matches the one in your `.env` file +- Include the protocol (http/https), domain, port, and full path + +### "Access blocked" Error +- Add your email as a test user in the OAuth consent screen +- If published, ensure your app is verified by Google + +### "Invalid credentials" Error +- Check that your `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are correct +- Ensure there are no extra spaces or quotes in the `.env` file + +## Security Notes + +1. Never commit your `.env` file to version control +2. Keep your `GOOGLE_CLIENT_SECRET` secure +3. Use HTTPS in production +4. Regularly rotate your OAuth credentials +5. Review and limit the OAuth scopes to only what's needed diff --git a/SETUP_COMPLETE.md b/SETUP_COMPLETE.md new file mode 100644 index 0000000..b450e95 --- /dev/null +++ b/SETUP_COMPLETE.md @@ -0,0 +1,51 @@ +# Quick Google OAuth Setup Instructions + +Your Google OAuth credentials have been configured in the backend! + +## āœ… Already Configured +- Client ID: `143092846986-b7fv9kucjugh9h5ojq60e1e44em57n1h.apps.googleusercontent.com` +- Client Secret: `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` +- Backend `.env` file created +- All dependencies installed + +## šŸ”§ Required: Google Cloud Console Setup + +**IMPORTANT:** You need to add the redirect URI in your Google Cloud Console: + +1. Go to: https://console.cloud.google.com/apis/credentials +2. Find your OAuth 2.0 Client ID: `143092846986-b7fv9kucjugh9h5ojq60e1e44em57n1h` +3. Click **Edit** (pencil icon) +4. Under **Authorized redirect URIs**, add: + ``` + http://localhost:8000/auth/google/callback + ``` +5. Click **Save** + +## šŸš€ Test the Application + +### Start Backend: +```bash +cd backend +python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +### Start Frontend (in another terminal): +```bash +cd frontend +npm run dev +``` + +### Test: +1. Open http://localhost:5173 +2. Try logging in with username (case-insensitive: "Dvir" = "dvir" = "DVIR") +3. Click **Continue with Google** to test OAuth login + +## šŸ“ Features Implemented +- āœ… Case-insensitive username login +- āœ… Google OAuth login/signup +- āœ… Automatic user creation for new Google accounts +- āœ… Secure credential storage in `.env` + +## āš ļø Security Note +The `.env` file contains sensitive credentials and is excluded from git via `.gitignore`. +Never commit this file to your repository! diff --git a/backend/.env.example b/backend/.env.example index 84fc4b7..2d0d36d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,2 +1,11 @@ PORT=8001 HOST=0.0.0.0 + +# Database Configuration +DATABASE_URL=postgresql://tasko_user:tasko_password@localhost:5432/tasko_db + +# Google OAuth Configuration +GOOGLE_CLIENT_ID=your_google_client_id_here.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=your_google_client_secret_here +GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback +FRONTEND_URL=http://localhost:5173 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..98916e9 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environment variables +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Database +*.db +*.sqlite +*.sqlite3 + +# OS +.DS_Store +Thumbs.db diff --git a/backend/main.py b/backend/main.py index c73c5e9..5a652f4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,11 +1,17 @@ from fastapi import FastAPI, HTTPException, Header, Depends from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse from pydantic import BaseModel, Field from typing import List, Optional from datetime import datetime from sqlalchemy.orm import Session +from authlib.integrations.starlette_client import OAuth import uuid import hashlib +import os +from dotenv import load_dotenv + +load_dotenv() from database import init_db, get_db import database as db_models @@ -15,6 +21,18 @@ app = FastAPI(title="Tasko API", version="1.0.0") # Initialize database init_db() +# OAuth Configuration +oauth = OAuth() +oauth.register( + name='google', + client_id=os.getenv('GOOGLE_CLIENT_ID', ''), + client_secret=os.getenv('GOOGLE_CLIENT_SECRET', ''), + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': 'openid email profile' + } +) + allowed_origins = ["http://localhost:5173", "https://tasko.dvirlabs.com"] # Configure CORS @@ -122,9 +140,9 @@ def read_root(): @app.post("/register", response_model=AuthResponse) def register(user_data: UserRegister, db: Session = Depends(get_db)): """Register a new user""" - # Check if username or email exists + # Check if username or email exists (case-insensitive for username) existing_user = db.query(db_models.User).filter( - (db_models.User.username == user_data.username) | + (db_models.User.username.ilike(user_data.username)) | (db_models.User.email == user_data.email) ).first() @@ -180,10 +198,10 @@ def login(user_data: UserLogin, db: Session = Depends(get_db)): """Login a user with username or email""" password_hash = hash_password(user_data.password) - # Try to find user by email or username + # Try to find user by email or username (case-insensitive for username) user = db.query(db_models.User).filter( ((db_models.User.email == user_data.username_or_email) | - (db_models.User.username == user_data.username_or_email)), + (db_models.User.username.ilike(user_data.username_or_email))), db_models.User.password_hash == password_hash ).first() @@ -212,6 +230,102 @@ def logout(authorization: Optional[str] = Header(None), db: Session = Depends(ge db.commit() return {"message": "Logged out successfully"} +@app.get("/auth/google") +async def google_login(): + """Initiate Google OAuth login""" + redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback') + return await oauth.google.authorize_redirect(redirect_uri) + +@app.get("/auth/google/callback") +async def google_callback(code: str, db: Session = Depends(get_db)): + """Handle Google OAuth callback""" + try: + # Get access token from Google + token = await oauth.google.authorize_access_token() + + # Get user info from Google + user_info = token.get('userinfo') + if not user_info: + raise HTTPException(status_code=400, detail="Failed to get user info from Google") + + email = user_info.get('email') + google_id = user_info.get('sub') + name = user_info.get('name', email.split('@')[0]) + + # Check if user exists + user = db.query(db_models.User).filter(db_models.User.email == email).first() + + if not user: + # Create new user + user_id = str(uuid.uuid4()) + # Use email username part as username, make it unique if needed + username = email.split('@')[0] + counter = 1 + original_username = username + while db.query(db_models.User).filter(db_models.User.username.ilike(username)).first(): + username = f"{original_username}{counter}" + counter += 1 + + user = db_models.User( + id=user_id, + username=username, + email=email, + password_hash=hashlib.sha256(google_id.encode()).hexdigest() # Use Google ID as password hash + ) + db.add(user) + + # Create default lists for new user + default_lists = [ + {"name": "Personal", "icon": "šŸ ", "color": "#667eea"}, + {"name": "Work", "icon": "šŸ’¼", "color": "#f093fb"}, + {"name": "Shopping", "icon": "šŸ›’", "color": "#4facfe"}, + ] + + for list_data in default_lists: + list_id = str(uuid.uuid4()) + new_list = db_models.TaskList( + id=list_id, + user_id=user_id, + name=list_data["name"], + icon=list_data["icon"], + color=list_data["color"] + ) + db.add(new_list) + + db.commit() + db.refresh(user) + + # Create auth token + token_str = str(uuid.uuid4()) + new_token = db_models.Token(token=token_str, user_id=user.id) + db.add(new_token) + db.commit() + + # Redirect to frontend with token + frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173') + return RedirectResponse(url=f"{frontend_url}?token={token_str}&user={user.id}") + + except Exception as e: + raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}") + +@app.get("/auth/google/url") +def get_google_auth_url(): + """Get Google OAuth URL for frontend""" + redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback') + authorization_url = f"https://accounts.google.com/o/oauth2/v2/auth?client_id={os.getenv('GOOGLE_CLIENT_ID', '')}&redirect_uri={redirect_uri}&response_type=code&scope=openid%20email%20profile&access_type=offline&prompt=consent" + return {"url": authorization_url} + +@app.get("/api/user", response_model=UserResponse) +def get_current_user(authorization: Optional[str] = Header(None), db: Session = Depends(get_db)): + """Get current user details""" + user_id = verify_token(authorization, db) + user = db.query(db_models.User).filter(db_models.User.id == user_id).first() + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return UserResponse.model_validate(user) + # Task List endpoints @app.get("/lists", response_model=List[TaskListResponse]) @app.get("/api/lists", response_model=List[TaskListResponse]) diff --git a/backend/requirements.txt b/backend/requirements.txt index c4bdd0a..b73a9a6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,8 @@ -fastapi==0.109.0 -uvicorn==0.27.0 -pydantic==2.5.3 -sqlalchemy==2.0.25 -psycopg2-binary==2.9.9 +fastapi>=0.109.0 +uvicorn>=0.27.0 +pydantic>=2.5.0 +sqlalchemy>=2.0.25 +psycopg2-binary>=2.9.9 +authlib>=1.3.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 diff --git a/frontend/src/Auth.css b/frontend/src/Auth.css index b813435..5874ca8 100644 --- a/frontend/src/Auth.css +++ b/frontend/src/Auth.css @@ -131,6 +131,61 @@ cursor: not-allowed; } +.divider { + display: flex; + align-items: center; + text-align: center; + margin: 0.5rem 0; +} + +.divider::before, +.divider::after { + content: ''; + flex: 1; + border-bottom: 1px solid #e0e0e0; +} + +.divider span { + padding: 0 1rem; + color: #999; + font-size: 0.85rem; + font-weight: 600; +} + +.google-login-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1rem 2rem; + background: white; + color: #333; + border: 2px solid #e0e0e0; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.google-login-btn:hover:not(:disabled) { + background: #f8f9fa; + border-color: #d0d0d0; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.google-login-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.google-login-btn svg { + width: 24px; + height: 24px; +} + /* Mobile responsive styles */ @media (max-width: 768px) { .auth-container { diff --git a/frontend/src/Auth.jsx b/frontend/src/Auth.jsx index 14360d7..4e5ad8f 100644 --- a/frontend/src/Auth.jsx +++ b/frontend/src/Auth.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import './Auth.css' const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000' @@ -12,6 +12,34 @@ function Auth({ onLogin }) { const [error, setError] = useState('') const [loading, setLoading] = useState(false) + // Check for Google OAuth callback + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search) + const token = urlParams.get('token') + const userId = urlParams.get('user') + + if (token && userId) { + // Fetch user details with token + fetch(`${API_URL}/api/user`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.json()) + .then(user => { + localStorage.setItem('token', token) + localStorage.setItem('user', JSON.stringify(user)) + onLogin(user, token) + // Clean up URL + window.history.replaceState({}, document.title, '/') + }) + .catch(err => { + console.error('Failed to fetch user:', err) + setError('Failed to complete Google login') + }) + } + }, [onLogin]) + const handleSubmit = async (e) => { e.preventDefault() setError('') @@ -62,6 +90,27 @@ function Auth({ onLogin }) { } } + const handleGoogleLogin = async () => { + setLoading(true) + setError('') + + try { + const response = await fetch(`${API_URL}/auth/google/url`) + const data = await response.json() + + if (data.url) { + window.location.href = data.url + } else { + setError('Failed to initiate Google login') + setLoading(false) + } + } catch (error) { + setError('Failed to connect to Google login') + setLoading(false) + console.error('Google login error:', error) + } + } + return (
@@ -143,6 +192,25 @@ function Auth({ onLogin }) { + +
+ OR +
+ +
From 6ebf2d4b45767afef0b8c1f7023b654c9f0547aa Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 20 Feb 2026 16:32:29 +0200 Subject: [PATCH 2/5] fix: Update Pydantic models to use ConfigDict instead of deprecated class-based config Replace deprecated class Config with model_config = ConfigDict() to fix Pydantic v2 deprecation warnings --- backend/main.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/backend/main.py b/backend/main.py index 5a652f4..f0d1d4d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, HTTPException, Header, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional from datetime import datetime from sqlalchemy.orm import Session @@ -46,31 +46,31 @@ app.add_middleware( # Pydantic Models for API class UserResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: str username: str email: str created_at: datetime - class Config: - from_attributes = True - class UserRegister(BaseModel): username: str email: str password: str class UserLogin(BaseModel): + model_config = ConfigDict(populate_by_name=True) + username_or_email: str = Field(..., alias='usernameOrEmail') password: str - - class Config: - populate_by_name = True class AuthResponse(BaseModel): user: UserResponse token: str class TaskListResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: str user_id: str name: str @@ -78,9 +78,6 @@ class TaskListResponse(BaseModel): color: str = "#667eea" created_at: datetime - class Config: - from_attributes = True - class TaskListCreate(BaseModel): name: str icon: Optional[str] = "šŸ“" @@ -92,6 +89,8 @@ class TaskListUpdate(BaseModel): color: Optional[str] = None class TaskResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: str list_id: str user_id: str @@ -102,9 +101,6 @@ class TaskResponse(BaseModel): created_at: datetime updated_at: datetime - class Config: - from_attributes = True - class TaskCreate(BaseModel): title: str list_id: str From d830f33e2390138464ee0a973ecfadfb4c7553f7 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 20 Feb 2026 16:53:34 +0200 Subject: [PATCH 3/5] fix: Add Request parameter to Google OAuth endpoints Pass the FastAPI Request object to authorize_redirect() and authorize_access_token() methods as required by Authlib's Starlette integration --- backend/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/main.py b/backend/main.py index f0d1d4d..89375ac 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, HTTPException, Header, Depends +from fastapi import FastAPI, HTTPException, Header, Depends, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse from pydantic import BaseModel, Field, ConfigDict @@ -227,17 +227,17 @@ def logout(authorization: Optional[str] = Header(None), db: Session = Depends(ge return {"message": "Logged out successfully"} @app.get("/auth/google") -async def google_login(): +async def google_login(request: Request): """Initiate Google OAuth login""" redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback') - return await oauth.google.authorize_redirect(redirect_uri) + return await oauth.google.authorize_redirect(request, redirect_uri) @app.get("/auth/google/callback") -async def google_callback(code: str, db: Session = Depends(get_db)): +async def google_callback(request: Request, db: Session = Depends(get_db)): """Handle Google OAuth callback""" try: # Get access token from Google - token = await oauth.google.authorize_access_token() + token = await oauth.google.authorize_access_token(request) # Get user info from Google user_info = token.get('userinfo') From 941d005d6e99181ac9fadce8e723994ffee7ed69 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 20 Feb 2026 16:56:44 +0200 Subject: [PATCH 4/5] ADD login --- backend/__pycache__/database.cpython-312.pyc | Bin 4078 -> 0 bytes backend/__pycache__/database.cpython-313.pyc | Bin 4019 -> 0 bytes backend/__pycache__/main.cpython-312.pyc | Bin 21953 -> 0 bytes frontend/src/App.jsx | 2 +- frontend/src/Auth.jsx | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 backend/__pycache__/database.cpython-312.pyc delete mode 100644 backend/__pycache__/database.cpython-313.pyc delete mode 100644 backend/__pycache__/main.cpython-312.pyc diff --git a/backend/__pycache__/database.cpython-312.pyc b/backend/__pycache__/database.cpython-312.pyc deleted file mode 100644 index 71b60f86de2e18bb4a23372600fa329c02d4708d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4078 zcmb_fO>7j&6|SD?p6Ti7`SIA;{1{>d0z0e4P5^@;s|}d37Xlb*Z8p$rY1E!B?6$k7 zhw2{8SQ}}GkisTP)=8wm9#%n04n{bNFOfKsQx3Th(aGY9R-09FNN#M*VTE$Yd)+hR z!E1Ioq@;dT_3Bl1)qCH2_3BTlltQ3fFprv=O2|L3(=1Y>v6d4ExkU_O2o|vgO|T;nq5xZi#E6&z?JWX9irtTxt2_4 zz*T{pYU6eV+%#}AZQLyZHw)a3Hg30(yFs)bqZ8=Xnw0PQH~ujnagUkhX&xVT9E<8@E*C&*!n7$@Mjb}Y>GBy`%L_bf(2}Jy-7~M!;*{>v ze1xkmbzRdb+xi@3TxHaPVW;fQm=%~I8?cfGD~)lraAM-b@QLw};)S=)F1%53TyL6D z_lk9FV8GMeIj2~4DLWQ6E4u5>JIpw4IVIhi0oh}NhYlWS2pUtP`6yTPs^^p(+crI( z!1iUU>dtU+$}AhDhK_?O*66JH(6~h?(FB8N5kt_TU=NW>M#P9LM_U$%t#b`=S-K~N z^R@U3_;Q0UF2`DUNCZ5ECTF)c{@zwCuj2;Ml?I$73}rdl+5w^|ZKxZ#h^e%Z+Q99o zrSlmsUI68B303G$+>P*Lg_*X_YQ;HP;}U3uUPGP5s%7&^m9lQw4VTMR%hIPTdRJh$ z%ew-P8}zbXwLC6QfJx4puE#}O^I1-}tk?PsYEh5wcUWacFT-EbQ+jExSaB*0Fv{=4g2=pW|||Sk`S?EOMn-v>l^rVOuR0uT*ubF_J14J={&$WvF$A zuFWH+!DZY9j&OlhpoIEy(Ds(4}t0uE1&zEU|0n8^0)SdI3%H*`^QGGH%=Omt*mW@dO zw~lVmU#YPitP+q;Xt;GCH^}3z9rbgcCNhh$JB1a|PYl+FS2J4{FW;G1>3SgfnbG>G zRW-MG_|Bds~XkbE6JcN7R3v= zrE7k2=z1ai1uk5IuT@$>>aW21Kbl1fOx09(7?va>zAU#)pnzF02qxNi$$+PBe(|-* zVW7$yijiDaTkGK^mxVBuHZsfE7D#fnj(m>cHOdwAqGI5Up}pB4utQT<(S%Qa^@|S! zn=iz7A3l79QayC9ei&nik)Ta@qO}g&h0L!38OwKmIWpKQD2ljXLqNC~1QpRNIb|-D z9Lr%}0h0lox&bSAtl(@2DYpg$vC{Vi=idY?$&SUncLx1rzFt@j+Iz!x0J5XLx*s6+ z#W)b)Y2HsBfmuNiNIo378Y zn_!Ey@fu(Qpss@w@5POvOb5IS&Ca6vH&AOG03t(3gh*aUwJ_Nhz7XC4o_WlshFP`q5w-((Tz1OU3Z_UQz5{5Z>BjOs zFN~0Di<*Uj@MD;9gtWpwX4$Mb(U?{`cDS7y7bWP5LG_v3!{`1RAF zpWqRm^OGnr@LM|zh2 z67t))0xuTbD$P*4#!wIIIY;1Up-Tm6$M$u}etOO8Z+kqk1kB{=HUWp(3}3imJ`yCd z5eKqVI0r3XWhop&NV4-tP9wp5D9lnYWQ36jF%TSj14^FBuZLOp8|)&8pr+iP0eKP? z1mP1h{t+4fm>l_p41P>@{*5G_$RxY%CVRj3UhT1%dn^{#q-Zu$S3gsT(zE#Pqr{$i zY&EmvPW8U@VARi?sjHvHdmhEN)y1cxDqIw99(qc!e!5E*j@~RhB~U%>R)u$jn;wq5 Y10xyXL?DOtfA&O#SJ#x7&=~;qe-%-#p8x;= diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc deleted file mode 100644 index d438edaa0cb865620dd379f2b62ba8873ed5bbe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4019 zcmb_fO>7j&74Dwtp6Ti7`SlosG3+c5*dYNYfWfvl8}N>u#pXwH+Zz@}X|-p{cFR58 zLv;^eD~Cyt60%W(l?aKfv9s|f$eF#>CG}}%5rMewvG8K-e%T*2aP*IMWCWC(1swZ^~zLDj8YJ<;lKE1)` z^$hKp$(C|VngpY)cEDUrM*DaZ_aiLXpjO?a^Tr&RXEJP;+(3iGnvQj~NoW^pD^An4 z%_*DQ7wInOxG%DJjZB+O+h@uI*!!I2`Aot&KhK-C{k39^*u*DCT-um59r#Lm%B;>A z4Y$z*mlBV~tEN{qYlKNSoXHdJ9C7Xo7fJ=D7=~lkiD59+FzRltX=7V6jH^x4Zc8$T zF>O)Lw*h*N3*wYv__)AOP9<0*m2v42tHu;u7)qE->ZWD0)H)=LS<{=P=u-+QK!9EQ zRmZ)~)DYY?!}Mtq1+oo0^q7o@@>tBr<$A2sFgzb1S_M6T{ZtdSW*GDkQr&cv!dk}Pu@GQRC_QPXm7WM zR|?&CPc97yg=4MrE4i+_%F-Ku=)KB8c>zr4JpKHhWu0SVIO*0;Cu9XKntbR$Ui z-zFKY2@geu`LVW+p zlcxwV!}$3I$mk%F{Xke^V;=e%b{3I@DCm3z6wt4tK4O5r34}>Jvq)Ceb(marZI^xt zB#z^##dNtGvS72g5c8)Igve0XV%J_Z&Od zgXAd~#e+WiX!yNAyL@vvgc2@jdnl!OUG!?F-xoxAjLVcQ86Z3Z*I~8}tSCPp+rR<{ zBIwdDfG)#10Mge7alt5U0hl@-Ob%@R=fH1NEJqY|nCmsH9D!TlzKMVM&Yv&->0*$Z;D_JlAjtOIz5E~^WKZ8b8%6+b;e`mmJQIcg z8m8?#!WE$5Gv2P-*Li%8%bU5deFaUsY>J+SURo`m?0CUILUv zng!hpC5cHxP!ceuPJ9!k>m`Z-HT9p`|*ht*t5_=lwY==}H3 ze6xU7m&98qo(cGVwpS4cZk3-2@OrjG6R(K3e3V=PNk%-w_3-`A0}=7?ni>;3 HI6D6Wl+db% diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 67fafe725c8f02f902fc363ea250918948e765a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21953 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%0AX(o0fe;?+Q zgP*8Q6D{%Ef9SH+&|C5nZa&qW3@gn%uOQiEoLyG3#Z|3Pv;GXPM`cD=X8dtXbwdDWu)~;EMX4-0 zHDcFVYZM>A784j;!+@{{t#9mB?{HFi1WXEvOUEN*CloRd`FafD7zq1}#Fcr5E6ex9 z{~eURgKXoUA)yz!pi09L0Xc+P#joo7LFH$a;79#G@U6b+;!>`_czq~j(a+^6ycwB_ zEc(h)z6R0Puvl2SSWtYcYNl!_-%eayzL#)uDXi*B(zQ9kY|gm2h&Wxz#kF3~vi|7e z(%mZkw{>yFkt)d(F<@y5!x*;&RhA4+F#}hYQWIE^HA(Ee-dABVDKFb zG>+krwTi#T++5W{W+ii2a0i3`1c8y~OYxH-sqhb=(iLPJPavUZ9RJ$-#!I-n8Z>to zGtJ$8>q~n*y1N?SdPix^t>{ej!=Z`J8z-MAcQ+I_cpPrf8LKlnnhfe;fAn_&5bzLv zWlb9-x~kET1|-l9C129tm6zl1(h-q?V{2vl(HvWU3)9g%w&1fyyw*V+Tl{~83i#oQ z@xkN2vSyvZ&q?;i$roH%_|p^kV-UgMm>uJtq2b{%8~pGG{tNE~UKcP+4F1Z8#0@(ksWemAGN}Gq3H%ZWFYu^Jz!#mPYzV*H z;qg?He9tdgd?P_%b|8yszk@q>IGmP}x976S6iCZUV)8z?-o zXdchMWjBNw79!=7pBRB3>QH`j!~-{xgW8nj3u(>RXcGq8F(4;$2QhUR1H%1IW9keB z0SwM#FbsiY2IGl*(1PHBFMs|5OKxI-*LBF<33BR8&gLZZ@JO6TVZjsJ`5~szVL>b} zqDK*)NYI7%Ljim>|2}*l&kspveq>1ZwcpqF-STgRyvBbCbpgF7p$4E2Jc_1&OYKTh zyM9A${Vmn_TdMXqRMl^&x?fYSUsIm{PBr|Rs{AKw-t@7#(go|52{YU!qU|>vPm0zj znvWz3yC(8MZKIhKRVY%0i%j8SQOPuXb6O$C_>y)}bkP!JrAW?l_5G|FM>Bi}^Sif<}X@>eF?O$@bGCBCq9r#&*URbpw40`P+&++|Aeue!9HX?=Nte&s=ZZedOH+`X-%g_Ya9a@G z(q)v%^4-#FrISV1E1#NZded_=W%8sk1vi+Xtn6CZ`XKG4mY*sQ4)?diDtR4%m@ibO=9z2?0w&(*AG5me>N_@doj@) zPQDv?O1bH6w_DQ~&0V=~zH^l%6W@5}8D?Ss`{kk147{f+6)-d<>RTTaA^Z;FD7X#H zu(k)MFstj)$+r?`dOjZ)FYyUsGS-d+oSzQ9{PUKEM9=|qsa?{ zPbn|$#i1aYZ@S<5BiJlRCQiTm3}YOEtZ_@4fmd~LlYkJj<8Tia_oh7yALe$8Ycoi) zZo@ARB~HEl`EhX|mKc6NIdB=*wQKsyta*kkWY_&84^I8`1g>dU;sS?j+Jyt|#9&*R zfmeA+Kt_NYbH{No1^ECq=G~aF=h4o+4}3ps7SEhdTws%DIK+&N)379n=9=bP@4%8E z`4G?#BJ5kiK6uT-i`m`x2@dR2(PDNygD8*o9e&9CqEbB1Cb&z<^U0H##j?vdb1rc{lst1$ Date: Sun, 22 Feb 2026 05:24:40 +0200 Subject: [PATCH 5/5] Fix google auth --- backend/main.py | 104 ++++++++++++++++++++++++++++++++++++++-- frontend/src/App.jsx | 3 +- frontend/src/Auth.jsx | 23 +++------ frontend/vite.config.js | 30 ++++++++++++ 4 files changed, 138 insertions(+), 22 deletions(-) diff --git a/backend/main.py b/backend/main.py index 89375ac..2347c46 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, HTTPException, Header, Depends, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse +from starlette.middleware.sessions import SessionMiddleware from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional from datetime import datetime @@ -35,15 +36,56 @@ oauth.register( allowed_origins = ["http://localhost:5173", "https://tasko.dvirlabs.com"] -# Configure CORS +# Environment-aware configuration +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') +is_production = ENVIRONMENT == 'production' +is_development = ENVIRONMENT == 'development' + +# Configure Session Middleware (required for OAuth state/nonce storage) +# Generate a strong SECRET_KEY: python -c "import secrets; print(secrets.token_hex(32))" +SESSION_SECRET = os.getenv('SESSION_SECRET', 'dev-secret-change-in-production') + +# Session cookie configuration - environment aware +session_config = { + "secret_key": SESSION_SECRET, + "session_cookie": "tasko_session", + "max_age": 3600, # 1 hour + "path": "/", # Available on all routes (required for OAuth callback) + "same_site": "lax", # Allows OAuth redirects while preventing CSRF + "https_only": is_production, # False for HTTP (dev), True for HTTPS (prod) +} + +# Development-specific: Ensure cookies work on localhost +if is_development: + session_config["domain"] = None # Don't set domain for localhost + +app.add_middleware(SessionMiddleware, **session_config) + +# Log session configuration in development +if is_development: + print(f"šŸ” Session Configuration (Development Mode):") + print(f" - Cookie Name: {session_config['session_cookie']}") + print(f" - Path: {session_config['path']}") + print(f" - SameSite: {session_config['same_site']}") + print(f" - HTTPS Only: {session_config['https_only']}") + print(f" - Domain: {session_config.get('domain', 'None (localhost)')}") + +# Configure CORS - MUST use specific origins (not "*") when allow_credentials=True app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, + allow_origins=allowed_origins, # Specific origins required for credentials + allow_credentials=True, # Required for session cookies allow_methods=["*"], allow_headers=["*"], + expose_headers=["*"], # Allow frontend to read all response headers ) +# Log startup info +if is_development: + print(f"šŸš€ Tasko API starting in DEVELOPMENT mode") + print(f" - CORS Origins: {allowed_origins}") + print(f" - Allow Credentials: True (session cookies enabled)") + # Pydantic Models for API class UserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -228,14 +270,56 @@ def logout(authorization: Optional[str] = Header(None), db: Session = Depends(ge @app.get("/auth/google") async def google_login(request: Request): - """Initiate Google OAuth login""" + """Initiate Google OAuth login - DIRECT BROWSER REDIRECT (not fetch)""" redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback') - return await oauth.google.authorize_redirect(request, redirect_uri) + + # Debug logging for development + if is_development: + print(f"\nšŸ”‘ OAuth Login initiated (/auth/google):") + print(f" - Redirect URI: {redirect_uri}") + print(f" - Request URL: {request.url}") + print(f" - Request method: {request.method}") + print(f" - Request headers Cookie: {request.headers.get('cookie', 'NONE')}") + print(f" - Session available: {hasattr(request, 'session')}") + if hasattr(request, 'session'): + print(f" - Session keys BEFORE: {list(request.session.keys())}") + + # This will set session state and redirect to Google + response = await oauth.google.authorize_redirect(request, redirect_uri) + + if is_development: + print(f" - Session keys AFTER: {list(request.session.keys())}") + print(f" - Response status: {response.status_code}") + print(f" - Response Set-Cookie: {response.headers.get('set-cookie', 'NONE')}") + print(f" - Response Location: {response.headers.get('location', 'NONE')[:100]}...") + + return response @app.get("/auth/google/callback") async def google_callback(request: Request, db: Session = Depends(get_db)): """Handle Google OAuth callback""" try: + # Debug logging for development + if is_development: + print(f"\nšŸ”„ OAuth Callback received (/auth/google/callback):") + print(f" - Request URL: {request.url}") + print(f" - Request method: {request.method}") + print(f" - Request headers Cookie: {request.headers.get('cookie', 'NONE')}") + print(f" - Request query params: {dict(request.query_params)}") + print(f" - Cookies from request.cookies: {list(request.cookies.keys())}") + print(f" - Session available: {hasattr(request, 'session')}") + if hasattr(request, 'session'): + session_keys = list(request.session.keys()) + print(f" - Session keys: {session_keys}") + # Print state for debugging + for key in session_keys: + if 'state' in key.lower(): + value_str = str(request.session[key]) + if len(value_str) > 100: + print(f" - Session[{key}]: {value_str[:100]}...") + else: + print(f" - Session[{key}]: {value_str}") + # Get access token from Google token = await oauth.google.authorize_access_token(request) @@ -297,11 +381,21 @@ async def google_callback(request: Request, db: Session = Depends(get_db)): db.add(new_token) db.commit() + if is_development: + print(f"āœ… OAuth Login SUCCESS!") + print(f" - User: {user.email} (ID: {user.id})") + print(f" - Token generated: {token_str[:20]}...") + print(f" - Redirecting to frontend with token") + # Redirect to frontend with token frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173') return RedirectResponse(url=f"{frontend_url}?token={token_str}&user={user.id}") except Exception as e: + if is_development: + print(f"āŒ OAuth Login FAILED: {str(e)}") + import traceback + traceback.print_exc() raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}") @app.get("/auth/google/url") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 027209c..a5d89ae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react' import Auth from './Auth' import './App.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) +const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin const AVAILABLE_ICONS = [ 'šŸ“', 'šŸ’¼', 'šŸ ', 'šŸ›’', 'šŸŽÆ', 'šŸ’Ŗ', 'šŸ“š', 'āœˆļø', diff --git a/frontend/src/Auth.jsx b/frontend/src/Auth.jsx index 4c36f04..79a8de8 100644 --- a/frontend/src/Auth.jsx +++ b/frontend/src/Auth.jsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import './Auth.css' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' +// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) +const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin function Auth({ onLogin }) { const [isLogin, setIsLogin] = useState(true) @@ -94,21 +95,11 @@ function Auth({ onLogin }) { setLoading(true) setError('') - try { - const response = await fetch(`${API_URL}/auth/google/url`) - const data = await response.json() - - if (data.url) { - window.location.href = data.url - } else { - setError('Failed to initiate Google login') - setLoading(false) - } - } catch (error) { - setError('Failed to connect to Google login') - setLoading(false) - console.error('Google login error:', error) - } + // CORRECT OAuth flow: Direct browser redirect to /auth/google + // This allows the backend to set session cookie and redirect to Google + // DO NOT use fetch() - OAuth requires browser redirects to preserve cookies + console.log('Redirecting to Google OAuth...') + window.location.href = `${API_URL}/auth/google` } return ( diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..e23eec4 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,4 +4,34 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + // Proxy API requests to backend - enables same-origin for session cookies + '/auth': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/login': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/register': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + '/logout': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false, + }, + }, + }, })