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
|
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
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
|
EXPOSE 8000
|
||||||
|
|
||||||
# Run the application
|
# 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.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])
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
@ -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;"]
|
||||||
|
|||||||
@ -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 = [
|
||||||
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user