File size: 6,713 Bytes
9882d96 45b1ef5 9882d96 45b1ef5 9882d96 45b1ef5 9882d96 45b1ef5 9882d96 45b1ef5 9882d96 b5482e9 9882d96 b5482e9 9882d96 b5482e9 9882d96 b5482e9 9882d96 b5482e9 9882d96 b5482e9 9882d96 b5482e9 9882d96 45b1ef5 9882d96 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
"""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 OAuth verification URL
GOOGLE_VERIFY_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
# JWT settings
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
"""
# Verify token with Google
async with httpx.AsyncClient() as client:
try:
# Get user info using access token
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()
# Verify the token was issued for our client
# Note: For access tokens from Token Client, we trust Google's validation
# The token is already validated by Google if we get a 200 response
except httpx.RequestError as e:
raise HTTPException(
status_code=500,
detail=f"Failed to verify token with Google: {str(e)}"
)
# Extract user info from Google response
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"
)
# Check if user exists by email in profiles table
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 exists - update avatar if changed
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:
# Create new user using Supabase Admin API
from app.shared.integrations.supabase_client import supabase
try:
# Create user in auth.users using Supabase Admin API
auth_response = supabase.auth.admin.create_user({
"email": email,
"email_confirm": True, # Auto-confirm email for OAuth users
"user_metadata": {
"full_name": full_name,
"avatar_url": avatar_url,
"provider": "google"
}
})
user_id = auth_response.user.id
except Exception as e:
# If user already exists in auth.users, try to get their ID
try:
# Query auth.users to get existing user
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)}"
)
# Create profile in profiles table
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()
# Generate JWT token
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
"""
# Optional: Log logout event
await db.execute(
text("""
INSERT INTO auth.audit_log (user_id, action, timestamp)
VALUES (:user_id, 'logout', NOW())
"""),
{"user_id": user_id}
)
# Note: The above will fail if audit_log table doesn't exist
# Comment it out if not needed or create the table
return {"message": "Logout successful"}
|