Compare commits

..

No commits in common. "cf7f3ee799e957df707404e77743374bb47d51c0" and "7a34f5f9901d28bb8df72ea209b998856f4b8726" have entirely different histories.

16 changed files with 31 additions and 848 deletions

View File

@ -1,110 +0,0 @@
# 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

View File

@ -1,250 +0,0 @@
# Production OAuth Setup Guide
## 🔧 Changes Made
### 1. Kubernetes Configuration Updated
**Files Modified:**
- `tasko-chart/templates/secret.yaml` - Added OAuth secrets
- `tasko-chart/templates/backend-deployment.yaml` - Added environment variables from secrets
- `tasko-chart/values.yaml` - Added OAuth configuration
**What was added:**
```yaml
backend:
env:
ENVIRONMENT: "production"
GOOGLE_REDIRECT_URI: "https://api-tasko.dvirlabs.com/auth/google/callback"
FRONTEND_URL: "https://tasko.dvirlabs.com"
oauth:
google:
clientId: "YOUR_CLIENT_ID"
clientSecret: "YOUR_CLIENT_SECRET"
sessionSecret: "YOUR_SESSION_SECRET"
```
---
## 🔐 Google Cloud Console Setup
### Step 1: Add Production Redirect URI
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Navigate to **APIs & Services** → **Credentials**
3. Click on your OAuth 2.0 Client ID (the one you created for Tasko)
4. Under **Authorized redirect URIs**, add:
```
https://api-tasko.dvirlabs.com/auth/google/callback
```
5. Keep the localhost URI for development:
```
http://localhost:8000/auth/google/callback
```
6. Click **Save**
### Step 2: Verify Authorized JavaScript Origins
Make sure these origins are authorized:
- `https://tasko.dvirlabs.com` (frontend)
- `https://api-tasko.dvirlabs.com` (backend)
- `http://localhost:5173` (local dev)
- `http://localhost:8000` (local dev)
---
## 🚀 Deploy to Kubernetes
### Option A: Using Helm Upgrade
```bash
# From the tasko-chart directory
helm upgrade tasko . --namespace my-apps --create-namespace
# Or if first deployment
helm install tasko . --namespace my-apps --create-namespace
```
### Option B: Using kubectl (if you pushed to Git)
```bash
# Your GitOps tool (ArgoCD, Flux, etc.) should auto-sync
# Or manually trigger sync if needed
```
---
## ✅ Verify Deployment
### 1. Check Backend Logs
```bash
kubectl logs -n my-apps deployment/tasko-backend -f
```
You should see:
```
🔐 Session Configuration (Development Mode): # Wait, this should say Production!
```
### 2. Check Environment Variables
```bash
kubectl exec -n my-apps deployment/tasko-backend -- env | grep GOOGLE
```
Expected output:
```
GOOGLE_CLIENT_ID=672182384838-vob26vd0qhmf0g9mru4u4sibkqre0rfa.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
GOOGLE_REDIRECT_URI=https://api-tasko.dvirlabs.com/auth/google/callback
```
### 3. Test OAuth Flow
1. Go to `https://tasko.dvirlabs.com`
2. Click "Continue with Google"
3. You should be redirected to Google login
4. After authentication, you should be redirected back to your app with a token
Watch the backend logs:
```bash
kubectl logs -n my-apps deployment/tasko-backend -f
```
Expected logs:
```
🔑 OAuth Login initiated (/auth/google):
- Redirect URI: https://api-tasko.dvirlabs.com/auth/google/callback
- Response Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=672182384838-...
🔄 OAuth Callback received (/auth/google/callback):
- Request headers Cookie: tasko_session=...
- Cookies from request.cookies: ['tasko_session']
- Session keys: ['_state_google_...']
✅ OAuth Login SUCCESS!
- User: your.email@gmail.com
```
---
## 🔒 Security Notes
### Production vs Development
The code automatically detects the environment:
**Development (`ENVIRONMENT=development`):**
- `https_only=False` (allows HTTP cookies for localhost)
- Debug logging enabled
- Session cookies work on `localhost`
**Production (`ENVIRONMENT=production`):**
- `https_only=True` (requires HTTPS for cookies)
- Debug logging disabled
- Secure session cookies
### Session Secret
The `sessionSecret` is used to sign session cookies. **Change this to a unique value!**
Generate a new secret:
```bash
python -c "import secrets; print(secrets.token_hex(32))"
```
Update in `values.yaml`:
```yaml
backend:
sessionSecret: "YOUR_NEW_SECRET_HERE"
```
---
## 🐛 Troubleshooting
### Issue: "client_id is empty"
**Cause:** Environment variables not loaded in container
**Fix:**
```bash
# Check if secrets exist
kubectl get secret -n my-apps tasko-secrets -o yaml
# Verify secret contains OAuth keys
kubectl describe secret -n my-apps tasko-secrets
# Restart deployment
kubectl rollout restart deployment/tasko-backend -n my-apps
```
### Issue: "mismatching_state: CSRF Warning"
**Cause:** Session cookies not being sent
**Possible causes:**
1. `ENVIRONMENT` not set to `production` (cookies require HTTPS)
2. Frontend and backend on different domains without proper CORS
3. Cookie `SameSite` settings
**Fix:**
- Verify `ENVIRONMENT=production` is set
- Check that `FRONTEND_URL` matches your actual frontend domain
- Ensure HTTPS is working on both frontend and backend
### Issue: "Redirect URI mismatch"
**Cause:** Google Console redirect URI doesn't match
**Fix:**
1. Check the actual redirect URI in the error message from Google
2. Add that exact URI to Google Console
3. Make sure `GOOGLE_REDIRECT_URI` in `values.yaml` matches
---
## 📝 Frontend Configuration
The frontend should automatically use the production API URL because of the proxy setup in `vite.config.js`.
### Build-time Configuration
When building the frontend Docker image, ensure `VITE_API_URL` is set:
**In `values.yaml`:**
```yaml
frontend:
env:
VITE_API_URL: "https://api-tasko.dvirlabs.com"
```
**Or in Dockerfile:**
```dockerfile
ENV VITE_API_URL=https://api-tasko.dvirlabs.com
RUN npm run build
```
---
## ✨ Quick Reference
### Backend URLs
- Production API: `https://api-tasko.dvirlabs.com`
- OAuth callback: `https://api-tasko.dvirlabs.com/auth/google/callback`
### Frontend URLs
- Production: `https://tasko.dvirlabs.com`
### Environment Variables (Backend)
```bash
ENVIRONMENT=production
GOOGLE_CLIENT_ID=672182384838-vob26vd0qhmf0g9mru4u4sibkqre0rfa.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_REDIRECT_URI=https://api-tasko.dvirlabs.com/auth/google/callback
FRONTEND_URL=https://tasko.dvirlabs.com
SESSION_SECRET=<generate-new-secret>
DATABASE_URL=<from-secret>
```

View File

@ -1,51 +0,0 @@
# 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!

View File

@ -1,11 +1,2 @@
PORT=8001 PORT=8001
HOST=0.0.0.0 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

44
backend/.gitignore vendored
View File

@ -1,44 +0,0 @@
# 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

View File

@ -21,4 +21,4 @@ COPY . .
EXPOSE 8000 EXPOSE 8000
# Run the application # Run the application
CMD ["python", "main.py"] CMD ["python", "main.py"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,18 +1,11 @@
from fastapi import FastAPI, HTTPException, Header, Depends, Request from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse from pydantic import BaseModel, Field
from starlette.middleware.sessions import SessionMiddleware
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional from typing import List, Optional
from datetime import datetime from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from authlib.integrations.starlette_client import OAuth
import uuid import uuid
import hashlib import hashlib
import os
from dotenv import load_dotenv
load_dotenv()
from database import init_db, get_db from database import init_db, get_db
import database as db_models import database as db_models
@ -22,97 +15,44 @@ app = FastAPI(title="Tasko API", version="1.0.0")
# Initialize database # Initialize database
init_db() 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"] allowed_origins = ["http://localhost:5173", "https://tasko.dvirlabs.com"]
# Environment-aware configuration # Configure CORS
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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=allowed_origins, # Specific origins required for credentials allow_origins=["*"],
allow_credentials=True, # Required for session cookies allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], 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 # Pydantic Models for API
class UserResponse(BaseModel): class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str id: str
username: str username: str
email: str email: str
created_at: datetime created_at: datetime
class Config:
from_attributes = True
class UserRegister(BaseModel): class UserRegister(BaseModel):
username: str username: str
email: str email: str
password: str password: str
class UserLogin(BaseModel): class UserLogin(BaseModel):
model_config = ConfigDict(populate_by_name=True)
username_or_email: str = Field(..., alias='usernameOrEmail') username_or_email: str = Field(..., alias='usernameOrEmail')
password: str password: str
class Config:
populate_by_name = True
class AuthResponse(BaseModel): class AuthResponse(BaseModel):
user: UserResponse user: UserResponse
token: str token: str
class TaskListResponse(BaseModel): class TaskListResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str id: str
user_id: str user_id: str
name: str name: str
@ -120,6 +60,9 @@ class TaskListResponse(BaseModel):
color: str = "#667eea" color: str = "#667eea"
created_at: datetime created_at: datetime
class Config:
from_attributes = True
class TaskListCreate(BaseModel): class TaskListCreate(BaseModel):
name: str name: str
icon: Optional[str] = "📝" icon: Optional[str] = "📝"
@ -131,8 +74,6 @@ class TaskListUpdate(BaseModel):
color: Optional[str] = None color: Optional[str] = None
class TaskResponse(BaseModel): class TaskResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str id: str
list_id: str list_id: str
user_id: str user_id: str
@ -143,6 +84,9 @@ class TaskResponse(BaseModel):
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
class Config:
from_attributes = True
class TaskCreate(BaseModel): class TaskCreate(BaseModel):
title: str title: str
list_id: str list_id: str
@ -178,9 +122,9 @@ def read_root():
@app.post("/register", response_model=AuthResponse) @app.post("/register", response_model=AuthResponse)
def register(user_data: UserRegister, db: Session = Depends(get_db)): def register(user_data: UserRegister, db: Session = Depends(get_db)):
"""Register a new user""" """Register a new user"""
# Check if username or email exists (case-insensitive for username) # Check if username or email exists
existing_user = db.query(db_models.User).filter( existing_user = db.query(db_models.User).filter(
(db_models.User.username.ilike(user_data.username)) | (db_models.User.username == user_data.username) |
(db_models.User.email == user_data.email) (db_models.User.email == user_data.email)
).first() ).first()
@ -236,10 +180,10 @@ def login(user_data: UserLogin, db: Session = Depends(get_db)):
"""Login a user with username or email""" """Login a user with username or email"""
password_hash = hash_password(user_data.password) password_hash = hash_password(user_data.password)
# Try to find user by email or username (case-insensitive for username) # Try to find user by email or username
user = db.query(db_models.User).filter( user = db.query(db_models.User).filter(
((db_models.User.email == user_data.username_or_email) | ((db_models.User.email == user_data.username_or_email) |
(db_models.User.username.ilike(user_data.username_or_email))), (db_models.User.username == user_data.username_or_email)),
db_models.User.password_hash == password_hash db_models.User.password_hash == password_hash
).first() ).first()
@ -268,154 +212,6 @@ def logout(authorization: Optional[str] = Header(None), db: Session = Depends(ge
db.commit() db.commit()
return {"message": "Logged out successfully"} return {"message": "Logged out successfully"}
@app.get("/auth/google")
async def google_login(request: Request):
"""Initiate Google OAuth login - DIRECT BROWSER REDIRECT (not fetch)"""
redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback')
# 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)
# 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()
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")
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 # Task List endpoints
@app.get("/lists", response_model=List[TaskListResponse]) @app.get("/lists", response_model=List[TaskListResponse])
@app.get("/api/lists", response_model=List[TaskListResponse]) @app.get("/api/lists", response_model=List[TaskListResponse])

View File

@ -1,9 +1,5 @@
fastapi>=0.109.0 fastapi==0.109.0
uvicorn>=0.27.0 uvicorn==0.27.0
pydantic>=2.5.0 pydantic==2.5.3
sqlalchemy>=2.0.25 sqlalchemy==2.0.25
psycopg2-binary>=2.9.9 psycopg2-binary==2.9.9
authlib>=1.3.0
httpx>=0.27.0
python-dotenv>=1.0.0
itsdangerous>=2.1.0

View File

@ -31,4 +31,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80 # Expose port 80
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -2,8 +2,7 @@ import { useState, useEffect } from 'react'
import Auth from './Auth' import Auth from './Auth'
import './App.css' import './App.css'
// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin
const AVAILABLE_ICONS = [ const AVAILABLE_ICONS = [
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️', '📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',

View File

@ -131,61 +131,6 @@
cursor: not-allowed; 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 */ /* Mobile responsive styles */
@media (max-width: 768px) { @media (max-width: 768px) {
.auth-container { .auth-container {

View File

@ -1,8 +1,7 @@
import { useState, useEffect } from 'react' import { useState } from 'react'
import './Auth.css' import './Auth.css'
// Use relative URLs to leverage Vite proxy (same-origin = cookies work!) const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin
function Auth({ onLogin }) { function Auth({ onLogin }) {
const [isLogin, setIsLogin] = useState(true) const [isLogin, setIsLogin] = useState(true)
@ -13,34 +12,6 @@ function Auth({ onLogin }) {
const [error, setError] = useState('') const [error, setError] = useState('')
const [loading, setLoading] = useState(false) 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) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()
setError('') setError('')
@ -91,17 +62,6 @@ function Auth({ onLogin }) {
} }
} }
const handleGoogleLogin = async () => {
setLoading(true)
setError('')
// 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 ( return (
<div className="auth-container"> <div className="auth-container">
<div className="auth-box"> <div className="auth-box">
@ -183,25 +143,6 @@ function Auth({ onLogin }) {
<button type="submit" className="auth-submit" disabled={loading}> <button type="submit" className="auth-submit" disabled={loading}>
{loading ? 'Please wait...' : (isLogin ? 'Login' : 'Create Account')} {loading ? 'Please wait...' : (isLogin ? 'Login' : 'Create Account')}
</button> </button>
<div className="divider">
<span>OR</span>
</div>
<button
type="button"
className="google-login-btn"
onClick={handleGoogleLogin}
disabled={loading}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="24px" height="24px">
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/>
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/>
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/>
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/>
</svg>
Continue with Google
</button>
</form> </form>
</div> </div>
</div> </div>

View File

@ -4,34 +4,4 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], 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,
},
},
},
}) })