Compare commits
8 Commits
7a34f5f990
...
cf7f3ee799
| Author | SHA1 | Date | |
|---|---|---|---|
| cf7f3ee799 | |||
| b2d800a0d6 | |||
| 4e0ae2e775 | |||
| e48a5a2d2c | |||
| 941d005d6e | |||
| d830f33e23 | |||
| 6ebf2d4b45 | |||
| d6eeb9a079 |
110
GOOGLE_OAUTH_SETUP.md
Normal file
110
GOOGLE_OAUTH_SETUP.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Google OAuth Setup Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
1. A Google Cloud Platform account
|
||||||
|
2. A project created in Google Cloud Console
|
||||||
|
|
||||||
|
## Step 1: Create OAuth 2.0 Credentials
|
||||||
|
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Select your project (or create a new one)
|
||||||
|
3. Navigate to **APIs & Services** > **Credentials**
|
||||||
|
4. Click **Create Credentials** > **OAuth 2.0 Client ID**
|
||||||
|
5. If prompted, configure the OAuth consent screen:
|
||||||
|
- Choose **External** user type
|
||||||
|
- Fill in the required fields:
|
||||||
|
- App name: Tasko
|
||||||
|
- User support email: Your email
|
||||||
|
- Developer contact information: Your email
|
||||||
|
- Add scopes (optional): `userinfo.email`, `userinfo.profile`
|
||||||
|
- Add test users if needed
|
||||||
|
6. Select **Application type**: Web application
|
||||||
|
7. Add **Authorized redirect URIs**:
|
||||||
|
- For local development: `http://localhost:8000/auth/google/callback`
|
||||||
|
- For production: `https://your-domain.com/auth/google/callback`
|
||||||
|
8. Click **Create**
|
||||||
|
9. Copy the **Client ID** and **Client Secret**
|
||||||
|
|
||||||
|
## Step 2: Configure Backend
|
||||||
|
|
||||||
|
1. Copy `.env.example` to `.env` in the backend directory:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `.env` and add your Google OAuth credentials:
|
||||||
|
```env
|
||||||
|
GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
||||||
|
GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
3. For production, update the URLs:
|
||||||
|
```env
|
||||||
|
GOOGLE_REDIRECT_URI=https://api.tasko.dvirlabs.com/auth/google/callback
|
||||||
|
FRONTEND_URL=https://tasko.dvirlabs.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Test the Setup
|
||||||
|
|
||||||
|
1. Start the backend server:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the frontend:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open your browser and navigate to the frontend URL
|
||||||
|
4. Click "Continue with Google" button
|
||||||
|
5. Complete the Google authentication flow
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
### 1. Case-Insensitive Username Login
|
||||||
|
- Users can now login with usernames in any case (e.g., "Dvir", "dvir", "DVIR" all work)
|
||||||
|
- Registration checks for existing usernames case-insensitively to prevent duplicates
|
||||||
|
|
||||||
|
### 2. Google OAuth Integration
|
||||||
|
- Users can sign up and login using their Google account
|
||||||
|
- New users are automatically created with:
|
||||||
|
- Username derived from email (before @)
|
||||||
|
- Email from Google account
|
||||||
|
- Default task lists (Personal, Work, Shopping)
|
||||||
|
- Existing users (matched by email) can login with Google
|
||||||
|
- Seamless redirect back to the application after authentication
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "redirect_uri_mismatch" Error
|
||||||
|
- Ensure the redirect URI in Google Cloud Console exactly matches the one in your `.env` file
|
||||||
|
- Include the protocol (http/https), domain, port, and full path
|
||||||
|
|
||||||
|
### "Access blocked" Error
|
||||||
|
- Add your email as a test user in the OAuth consent screen
|
||||||
|
- If published, ensure your app is verified by Google
|
||||||
|
|
||||||
|
### "Invalid credentials" Error
|
||||||
|
- Check that your `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are correct
|
||||||
|
- Ensure there are no extra spaces or quotes in the `.env` file
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
1. Never commit your `.env` file to version control
|
||||||
|
2. Keep your `GOOGLE_CLIENT_SECRET` secure
|
||||||
|
3. Use HTTPS in production
|
||||||
|
4. Regularly rotate your OAuth credentials
|
||||||
|
5. Review and limit the OAuth scopes to only what's needed
|
||||||
250
PRODUCTION_OAUTH_SETUP.md
Normal file
250
PRODUCTION_OAUTH_SETUP.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# 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>
|
||||||
|
```
|
||||||
51
SETUP_COMPLETE.md
Normal file
51
SETUP_COMPLETE.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Quick Google OAuth Setup Instructions
|
||||||
|
|
||||||
|
Your Google OAuth credentials have been configured in the backend!
|
||||||
|
|
||||||
|
## ✅ Already Configured
|
||||||
|
- Client ID: `143092846986-b7fv9kucjugh9h5ojq60e1e44em57n1h.apps.googleusercontent.com`
|
||||||
|
- Client Secret: `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`
|
||||||
|
- Backend `.env` file created
|
||||||
|
- All dependencies installed
|
||||||
|
|
||||||
|
## 🔧 Required: Google Cloud Console Setup
|
||||||
|
|
||||||
|
**IMPORTANT:** You need to add the redirect URI in your Google Cloud Console:
|
||||||
|
|
||||||
|
1. Go to: https://console.cloud.google.com/apis/credentials
|
||||||
|
2. Find your OAuth 2.0 Client ID: `143092846986-b7fv9kucjugh9h5ojq60e1e44em57n1h`
|
||||||
|
3. Click **Edit** (pencil icon)
|
||||||
|
4. Under **Authorized redirect URIs**, add:
|
||||||
|
```
|
||||||
|
http://localhost:8000/auth/google/callback
|
||||||
|
```
|
||||||
|
5. Click **Save**
|
||||||
|
|
||||||
|
## 🚀 Test the Application
|
||||||
|
|
||||||
|
### Start Backend:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start Frontend (in another terminal):
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test:
|
||||||
|
1. Open http://localhost:5173
|
||||||
|
2. Try logging in with username (case-insensitive: "Dvir" = "dvir" = "DVIR")
|
||||||
|
3. Click **Continue with Google** to test OAuth login
|
||||||
|
|
||||||
|
## 📝 Features Implemented
|
||||||
|
- ✅ Case-insensitive username login
|
||||||
|
- ✅ Google OAuth login/signup
|
||||||
|
- ✅ Automatic user creation for new Google accounts
|
||||||
|
- ✅ Secure credential storage in `.env`
|
||||||
|
|
||||||
|
## ⚠️ Security Note
|
||||||
|
The `.env` file contains sensitive credentials and is excluded from git via `.gitignore`.
|
||||||
|
Never commit this file to your repository!
|
||||||
@ -1,2 +1,11 @@
|
|||||||
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
Normal file
44
backend/.gitignore
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
246
backend/main.py
246
backend/main.py
@ -1,11 +1,18 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Header, Depends
|
from fastapi import FastAPI, HTTPException, Header, Depends, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel, Field
|
from fastapi.responses import RedirectResponse
|
||||||
|
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
|
||||||
@ -15,44 +22,97 @@ 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"]
|
||||||
|
|
||||||
# Configure CORS
|
# Environment-aware configuration
|
||||||
|
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
|
||||||
|
is_production = ENVIRONMENT == 'production'
|
||||||
|
is_development = ENVIRONMENT == 'development'
|
||||||
|
|
||||||
|
# Configure Session Middleware (required for OAuth state/nonce storage)
|
||||||
|
# Generate a strong SECRET_KEY: python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
SESSION_SECRET = os.getenv('SESSION_SECRET', 'dev-secret-change-in-production')
|
||||||
|
|
||||||
|
# Session cookie configuration - environment aware
|
||||||
|
session_config = {
|
||||||
|
"secret_key": SESSION_SECRET,
|
||||||
|
"session_cookie": "tasko_session",
|
||||||
|
"max_age": 3600, # 1 hour
|
||||||
|
"path": "/", # Available on all routes (required for OAuth callback)
|
||||||
|
"same_site": "lax", # Allows OAuth redirects while preventing CSRF
|
||||||
|
"https_only": is_production, # False for HTTP (dev), True for HTTPS (prod)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Development-specific: Ensure cookies work on localhost
|
||||||
|
if is_development:
|
||||||
|
session_config["domain"] = None # Don't set domain for localhost
|
||||||
|
|
||||||
|
app.add_middleware(SessionMiddleware, **session_config)
|
||||||
|
|
||||||
|
# Log session configuration in development
|
||||||
|
if is_development:
|
||||||
|
print(f"🔐 Session Configuration (Development Mode):")
|
||||||
|
print(f" - Cookie Name: {session_config['session_cookie']}")
|
||||||
|
print(f" - Path: {session_config['path']}")
|
||||||
|
print(f" - SameSite: {session_config['same_site']}")
|
||||||
|
print(f" - HTTPS Only: {session_config['https_only']}")
|
||||||
|
print(f" - Domain: {session_config.get('domain', 'None (localhost)')}")
|
||||||
|
|
||||||
|
# Configure CORS - MUST use specific origins (not "*") when allow_credentials=True
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=allowed_origins, # Specific origins required for credentials
|
||||||
allow_credentials=True,
|
allow_credentials=True, # Required for session cookies
|
||||||
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
|
||||||
@ -60,9 +120,6 @@ 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] = "📝"
|
||||||
@ -74,6 +131,8 @@ 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
|
||||||
@ -84,9 +143,6 @@ 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
|
||||||
@ -122,9 +178,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
|
# Check if username or email exists (case-insensitive for username)
|
||||||
existing_user = db.query(db_models.User).filter(
|
existing_user = db.query(db_models.User).filter(
|
||||||
(db_models.User.username == user_data.username) |
|
(db_models.User.username.ilike(user_data.username)) |
|
||||||
(db_models.User.email == user_data.email)
|
(db_models.User.email == user_data.email)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@ -180,10 +236,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
|
# Try to find user by email or username (case-insensitive for 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 == user_data.username_or_email)),
|
(db_models.User.username.ilike(user_data.username_or_email))),
|
||||||
db_models.User.password_hash == password_hash
|
db_models.User.password_hash == password_hash
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@ -212,6 +268,154 @@ 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,5 +1,9 @@
|
|||||||
fastapi==0.109.0
|
fastapi>=0.109.0
|
||||||
uvicorn==0.27.0
|
uvicorn>=0.27.0
|
||||||
pydantic==2.5.3
|
pydantic>=2.5.0
|
||||||
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
|
||||||
@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'
|
|||||||
import Auth from './Auth'
|
import Auth from './Auth'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
|
// Use relative URLs to leverage Vite proxy (same-origin = cookies work!)
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin
|
||||||
|
|
||||||
const AVAILABLE_ICONS = [
|
const AVAILABLE_ICONS = [
|
||||||
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
'📝', '💼', '🏠', '🛒', '🎯', '💪', '📚', '✈️',
|
||||||
|
|||||||
@ -131,6 +131,61 @@
|
|||||||
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,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import './Auth.css'
|
import './Auth.css'
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
|
// Use relative URLs to leverage Vite proxy (same-origin = cookies work!)
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || '' // Empty string = same-origin
|
||||||
|
|
||||||
function Auth({ onLogin }) {
|
function Auth({ onLogin }) {
|
||||||
const [isLogin, setIsLogin] = useState(true)
|
const [isLogin, setIsLogin] = useState(true)
|
||||||
@ -12,6 +13,34 @@ 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('')
|
||||||
@ -62,6 +91,17 @@ 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">
|
||||||
@ -143,6 +183,25 @@ 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,4 +4,34 @@ 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