Compare commits
No commits in common. "cf7f3ee799e957df707404e77743374bb47d51c0" and "7a34f5f9901d28bb8df72ea209b998856f4b8726" have entirely different histories.
cf7f3ee799
...
7a34f5f990
@ -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
|
||||
@ -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>
|
||||
```
|
||||
@ -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!
|
||||
@ -1,11 +1,2 @@
|
||||
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
|
||||
|
||||
44
backend/.gitignore
vendored
44
backend/.gitignore
vendored
@ -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
|
||||
@ -21,4 +21,4 @@ COPY . .
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
CMD ["python", "main.py"]
|
||||
|
||||
BIN
backend/__pycache__/database.cpython-312.pyc
Normal file
BIN
backend/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
246
backend/main.py
246
backend/main.py
@ -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.responses import RedirectResponse
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
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
|
||||
@ -22,97 +15,44 @@ 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"]
|
||||
|
||||
# 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
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=allowed_origins, # Specific origins required for credentials
|
||||
allow_credentials=True, # Required for session cookies
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
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)
|
||||
|
||||
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
|
||||
@ -120,6 +60,9 @@ class TaskListResponse(BaseModel):
|
||||
color: str = "#667eea"
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TaskListCreate(BaseModel):
|
||||
name: str
|
||||
icon: Optional[str] = "📝"
|
||||
@ -131,8 +74,6 @@ class TaskListUpdate(BaseModel):
|
||||
color: Optional[str] = None
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
list_id: str
|
||||
user_id: str
|
||||
@ -143,6 +84,9 @@ class TaskResponse(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
title: str
|
||||
list_id: str
|
||||
@ -178,9 +122,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 (case-insensitive for username)
|
||||
# Check if username or email exists
|
||||
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)
|
||||
).first()
|
||||
|
||||
@ -236,10 +180,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 (case-insensitive for username)
|
||||
# Try to find user by email or username
|
||||
user = db.query(db_models.User).filter(
|
||||
((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
|
||||
).first()
|
||||
|
||||
@ -268,154 +212,6 @@ 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(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
|
||||
@app.get("/lists", response_model=List[TaskListResponse])
|
||||
@app.get("/api/lists", response_model=List[TaskListResponse])
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
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
|
||||
itsdangerous>=2.1.0
|
||||
fastapi==0.109.0
|
||||
uvicorn==0.27.0
|
||||
pydantic==2.5.3
|
||||
sqlalchemy==2.0.25
|
||||
psycopg2-binary==2.9.9
|
||||
|
||||
@ -31,4 +31,4 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@ -2,8 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import Auth from './Auth'
|
||||
import './App.css'
|
||||
|
||||
// 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 API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
|
||||
|
||||
const AVAILABLE_ICONS = [
|
||||
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
||||
|
||||
@ -131,61 +131,6 @@
|
||||
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 {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import './Auth.css'
|
||||
|
||||
// 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 API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
|
||||
|
||||
function Auth({ onLogin }) {
|
||||
const [isLogin, setIsLogin] = useState(true)
|
||||
@ -13,34 +12,6 @@ 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('')
|
||||
@ -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 (
|
||||
<div className="auth-container">
|
||||
<div className="auth-box">
|
||||
@ -183,25 +143,6 @@ function Auth({ onLogin }) {
|
||||
<button type="submit" className="auth-submit" disabled={loading}>
|
||||
{loading ? 'Please wait...' : (isLogin ? 'Login' : 'Create Account')}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,34 +4,4 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user