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"}