feat: Add case-insensitive username login and Google OAuth authentication
- Make username login case-insensitive (Dvir = dvir = DVIR) - Add Google OAuth login/signup functionality - Install required dependencies (authlib, httpx, python-dotenv) - Create OAuth endpoints: /auth/google/url, /auth/google/callback - Add Google login button to frontend with styled UI - Configure environment variables for OAuth credentials - Add comprehensive setup documentation - Secure .env file with .gitignore
This commit is contained in:
parent
7a34f5f990
commit
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
|
||||||
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
|
||||||
122
backend/main.py
122
backend/main.py
@ -1,11 +1,17 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Header, Depends
|
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 pydantic import BaseModel, Field
|
||||||
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,6 +21,18 @@ 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
|
# Configure CORS
|
||||||
@ -122,9 +140,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 +198,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 +230,102 @@ 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():
|
||||||
|
"""Initiate Google OAuth login"""
|
||||||
|
redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback')
|
||||||
|
return await oauth.google.authorize_redirect(redirect_uri)
|
||||||
|
|
||||||
|
@app.get("/auth/google/callback")
|
||||||
|
async def google_callback(code: str, db: Session = Depends(get_db)):
|
||||||
|
"""Handle Google OAuth callback"""
|
||||||
|
try:
|
||||||
|
# Get access token from Google
|
||||||
|
token = await oauth.google.authorize_access_token()
|
||||||
|
|
||||||
|
# Get user info from Google
|
||||||
|
user_info = token.get('userinfo')
|
||||||
|
if not user_info:
|
||||||
|
raise HTTPException(status_code=400, detail="Failed to get user info from Google")
|
||||||
|
|
||||||
|
email = user_info.get('email')
|
||||||
|
google_id = user_info.get('sub')
|
||||||
|
name = user_info.get('name', email.split('@')[0])
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
user = db.query(db_models.User).filter(db_models.User.email == email).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
# Create new user
|
||||||
|
user_id = str(uuid.uuid4())
|
||||||
|
# Use email username part as username, make it unique if needed
|
||||||
|
username = email.split('@')[0]
|
||||||
|
counter = 1
|
||||||
|
original_username = username
|
||||||
|
while db.query(db_models.User).filter(db_models.User.username.ilike(username)).first():
|
||||||
|
username = f"{original_username}{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
user = db_models.User(
|
||||||
|
id=user_id,
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password_hash=hashlib.sha256(google_id.encode()).hexdigest() # Use Google ID as password hash
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
|
||||||
|
# Create default lists for new user
|
||||||
|
default_lists = [
|
||||||
|
{"name": "Personal", "icon": "🏠", "color": "#667eea"},
|
||||||
|
{"name": "Work", "icon": "💼", "color": "#f093fb"},
|
||||||
|
{"name": "Shopping", "icon": "🛒", "color": "#4facfe"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for list_data in default_lists:
|
||||||
|
list_id = str(uuid.uuid4())
|
||||||
|
new_list = db_models.TaskList(
|
||||||
|
id=list_id,
|
||||||
|
user_id=user_id,
|
||||||
|
name=list_data["name"],
|
||||||
|
icon=list_data["icon"],
|
||||||
|
color=list_data["color"]
|
||||||
|
)
|
||||||
|
db.add(new_list)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
# Create auth token
|
||||||
|
token_str = str(uuid.uuid4())
|
||||||
|
new_token = db_models.Token(token=token_str, user_id=user.id)
|
||||||
|
db.add(new_token)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Redirect to frontend with token
|
||||||
|
frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:5173')
|
||||||
|
return RedirectResponse(url=f"{frontend_url}?token={token_str}&user={user.id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Authentication failed: {str(e)}")
|
||||||
|
|
||||||
|
@app.get("/auth/google/url")
|
||||||
|
def get_google_auth_url():
|
||||||
|
"""Get Google OAuth URL for frontend"""
|
||||||
|
redirect_uri = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:8000/auth/google/callback')
|
||||||
|
authorization_url = f"https://accounts.google.com/o/oauth2/v2/auth?client_id={os.getenv('GOOGLE_CLIENT_ID', '')}&redirect_uri={redirect_uri}&response_type=code&scope=openid%20email%20profile&access_type=offline&prompt=consent"
|
||||||
|
return {"url": authorization_url}
|
||||||
|
|
||||||
|
@app.get("/api/user", response_model=UserResponse)
|
||||||
|
def get_current_user(authorization: Optional[str] = Header(None), db: Session = Depends(get_db)):
|
||||||
|
"""Get current user details"""
|
||||||
|
user_id = verify_token(authorization, db)
|
||||||
|
user = db.query(db_models.User).filter(db_models.User.id == user_id).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
return UserResponse.model_validate(user)
|
||||||
|
|
||||||
# Task List endpoints
|
# 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,8 @@
|
|||||||
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
|
||||||
|
|||||||
@ -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,4 +1,4 @@
|
|||||||
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'
|
const API_URL = import.meta.env.VITE_API_URL || 'http://10.188.50.221:8000'
|
||||||
@ -12,6 +12,34 @@ function Auth({ onLogin }) {
|
|||||||
const [error, setError] = useState('')
|
const [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 +90,27 @@ function Auth({ onLogin }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGoogleLogin = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/auth/google/url`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.url) {
|
||||||
|
window.location.href = data.url
|
||||||
|
} else {
|
||||||
|
setError('Failed to initiate Google login')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError('Failed to connect to Google login')
|
||||||
|
setLoading(false)
|
||||||
|
console.error('Google login error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-container">
|
<div className="auth-container">
|
||||||
<div className="auth-box">
|
<div className="auth-box">
|
||||||
@ -143,6 +192,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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user