|
|
"""Authentication control functions.""" |
|
|
|
|
|
import httpx |
|
|
from fastapi import HTTPException |
|
|
from sqlalchemy.ext.asyncio import AsyncSession |
|
|
from sqlalchemy import text |
|
|
from datetime import datetime, timedelta |
|
|
import jwt |
|
|
import os |
|
|
from uuid import uuid4 |
|
|
from app.core.config import settings |
|
|
|
|
|
|
|
|
GOOGLE_VERIFY_URL = "https://www.googleapis.com/oauth2/v3/userinfo" |
|
|
|
|
|
|
|
|
JWT_ALGORITHM = "HS256" |
|
|
JWT_EXPIRATION_HOURS = 24 |
|
|
|
|
|
|
|
|
async def login_control(access_token: str, db: AsyncSession) -> dict: |
|
|
""" |
|
|
Login with Google OAuth access token. |
|
|
|
|
|
Steps: |
|
|
1. Verify access token with Google |
|
|
2. Get user info from Google |
|
|
3. Check if user exists in database |
|
|
4. Create user if not exists |
|
|
5. Generate JWT token |
|
|
6. Return user info and token |
|
|
|
|
|
Args: |
|
|
access_token: Google OAuth access token |
|
|
db: Database session |
|
|
|
|
|
Returns: |
|
|
dict: User info and JWT token |
|
|
|
|
|
Raises: |
|
|
HTTPException: If token is invalid or verification fails |
|
|
""" |
|
|
|
|
|
async with httpx.AsyncClient() as client: |
|
|
try: |
|
|
|
|
|
response = await client.get( |
|
|
GOOGLE_VERIFY_URL, |
|
|
headers={"Authorization": f"Bearer {access_token}"} |
|
|
) |
|
|
|
|
|
if response.status_code != 200: |
|
|
raise HTTPException( |
|
|
status_code=401, |
|
|
detail="Invalid access token" |
|
|
) |
|
|
|
|
|
google_user_info = response.json() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except httpx.RequestError as e: |
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail=f"Failed to verify token with Google: {str(e)}" |
|
|
) |
|
|
|
|
|
|
|
|
email = google_user_info.get("email") |
|
|
full_name = google_user_info.get("name", "") |
|
|
avatar_url = google_user_info.get("picture") |
|
|
|
|
|
if not email: |
|
|
raise HTTPException( |
|
|
status_code=400, |
|
|
detail="Email not provided by Google" |
|
|
) |
|
|
|
|
|
|
|
|
result = await db.execute( |
|
|
text(""" |
|
|
SELECT p.id, p.full_name, p.avatar_url, p.role, p.email |
|
|
FROM profiles p |
|
|
WHERE p.email = :email |
|
|
"""), |
|
|
{"email": email} |
|
|
) |
|
|
row = result.fetchone() |
|
|
|
|
|
if row: |
|
|
|
|
|
user_id = str(row.id) |
|
|
|
|
|
if avatar_url and avatar_url != row.avatar_url: |
|
|
await db.execute( |
|
|
text(""" |
|
|
UPDATE profiles |
|
|
SET avatar_url = :avatar_url, updated_at = NOW() |
|
|
WHERE id = :user_id |
|
|
"""), |
|
|
{"avatar_url": avatar_url, "user_id": user_id} |
|
|
) |
|
|
await db.commit() |
|
|
else: |
|
|
|
|
|
from app.shared.integrations.supabase_client import supabase |
|
|
|
|
|
try: |
|
|
|
|
|
auth_response = supabase.auth.admin.create_user({ |
|
|
"email": email, |
|
|
"email_confirm": True, |
|
|
"user_metadata": { |
|
|
"full_name": full_name, |
|
|
"avatar_url": avatar_url, |
|
|
"provider": "google" |
|
|
} |
|
|
}) |
|
|
|
|
|
user_id = auth_response.user.id |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
try: |
|
|
|
|
|
auth_result = await db.execute( |
|
|
text("SELECT id FROM auth.users WHERE email = :email"), |
|
|
{"email": email} |
|
|
) |
|
|
auth_row = auth_result.fetchone() |
|
|
|
|
|
if auth_row: |
|
|
user_id = str(auth_row.id) |
|
|
else: |
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail=f"Failed to create or retrieve user: {str(e)}" |
|
|
) |
|
|
except Exception as inner_e: |
|
|
raise HTTPException( |
|
|
status_code=500, |
|
|
detail=f"Failed to create user: {str(e)}, {str(inner_e)}" |
|
|
) |
|
|
|
|
|
|
|
|
await db.execute( |
|
|
text(""" |
|
|
INSERT INTO profiles (id, email, full_name, avatar_url, role, locale, created_at, updated_at) |
|
|
VALUES (:id, :email, :full_name, :avatar_url, 'tourist', 'vi_VN', NOW(), NOW()) |
|
|
ON CONFLICT (id) DO UPDATE |
|
|
SET email = :email, full_name = :full_name, avatar_url = :avatar_url, updated_at = NOW() |
|
|
"""), |
|
|
{ |
|
|
"id": user_id, |
|
|
"email": email, |
|
|
"full_name": full_name, |
|
|
"avatar_url": avatar_url |
|
|
} |
|
|
) |
|
|
|
|
|
await db.commit() |
|
|
|
|
|
|
|
|
token_payload = { |
|
|
"user_id": user_id, |
|
|
"email": email, |
|
|
"exp": datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS) |
|
|
} |
|
|
token = jwt.encode(token_payload, settings.jwt_secret, algorithm=JWT_ALGORITHM) |
|
|
|
|
|
return { |
|
|
"user_id": user_id, |
|
|
"email": email, |
|
|
"full_name": full_name, |
|
|
"avatar_url": avatar_url, |
|
|
"token": token |
|
|
} |
|
|
|
|
|
|
|
|
async def logout_control(user_id: str, db: AsyncSession) -> dict: |
|
|
""" |
|
|
Logout user. |
|
|
|
|
|
For now, this is a simple logout that just confirms the action. |
|
|
In a production system, you might want to: |
|
|
- Blacklist the JWT token |
|
|
- Clear server-side sessions |
|
|
- Log the logout event |
|
|
|
|
|
Args: |
|
|
user_id: User ID |
|
|
db: Database session |
|
|
|
|
|
Returns: |
|
|
dict: Logout confirmation message |
|
|
""" |
|
|
|
|
|
await db.execute( |
|
|
text(""" |
|
|
INSERT INTO auth.audit_log (user_id, action, timestamp) |
|
|
VALUES (:user_id, 'logout', NOW()) |
|
|
"""), |
|
|
{"user_id": user_id} |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {"message": "Logout successful"} |
|
|
|