File size: 13,964 Bytes
1b86d8a
4c0bb97
1b86d8a
 
448d967
4c0bb97
0effe0c
448d967
 
 
f53960f
1b86d8a
df032da
bbbddac
7f8c5ab
5da2025
86cf6aa
c050ce0
448d967
 
 
a7d3cc7
df032da
1b86d8a
4c0bb97
 
1b86d8a
87b09dd
df032da
 
 
1b86d8a
 
 
 
 
 
4c0bb97
1b86d8a
 
 
a7d3cc7
1b86d8a
 
 
 
 
df032da
 
a7d3cc7
df032da
bbbddac
 
 
35ab8fe
 
09451d2
 
46dd238
5da2025
448d967
dd3c39b
 
 
448d967
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d34eda
 
 
 
 
 
 
 
 
 
 
 
448d967
2d34eda
 
448d967
 
 
 
 
 
 
 
2d34eda
448d967
2d34eda
 
448d967
 
 
 
2d34eda
 
 
 
 
 
 
 
 
 
 
 
448d967
 
2d34eda
 
 
 
 
 
 
 
 
 
 
 
 
448d967
 
2d34eda
 
 
9a0b077
448d967
0e85bcd
cc29913
448d967
35ab8fe
1b86d8a
 
 
87b09dd
dd3c39b
5da2025
 
 
 
 
 
 
 
09451d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f53960f
 
5da2025
f53960f
 
 
 
0effe0c
f53960f
5da2025
fb023ba
5da2025
 
 
fb023ba
 
7f8c5ab
 
 
 
 
 
 
 
 
fb023ba
 
 
 
 
 
 
 
86cf6aa
fb023ba
 
 
f53960f
5da2025
4c0bb97
6905ba2
 
4c0bb97
 
 
 
 
 
 
fb023ba
0effe0c
 
 
 
 
 
 
32b6b18
0effe0c
fb023ba
 
09451d2
 
f53960f
 
5da2025
f53960f
 
 
 
0effe0c
f53960f
5da2025
fb023ba
5da2025
 
 
fb023ba
 
7f8c5ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb023ba
 
 
 
 
7f8c5ab
fb023ba
 
86cf6aa
fb023ba
 
 
f53960f
5da2025
4c0bb97
 
 
 
 
 
 
 
 
 
fb023ba
0effe0c
 
 
 
 
 
 
32b6b18
0effe0c
fb023ba
 
09451d2
 
dd3c39b
 
 
 
 
f53960f
 
09451d2
f53960f
09451d2
f53960f
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
from fastapi_users import FastAPIUsers
from fastapi_users.authentication import BearerTransport, JWTStrategy, AuthenticationBackend
from httpx_oauth.clients.google import GoogleOAuth2
from httpx_oauth.clients.github import GitHubOAuth2
from fastapi_users.manager import BaseUserManager, IntegerIDMixin
from fastapi import Depends, Request, Response, FastAPI
from fastapi.responses import JSONResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from fastapi_users.models import UP
from typing import Optional
import os
import logging
import secrets
import httpx
from datetime import datetime
import jwt

from api.database import User, OAuthAccount, CustomSQLAlchemyUserDatabase, get_user_db
from api.models import UserRead, UserCreate, UserUpdate

# إعداد اللوقينج
logger = logging.getLogger(__name__)

# استخدام BearerTransport بدل CookieTransport
bearer_transport = BearerTransport(tokenUrl="/auth/jwt/login")

SECRET = os.getenv("JWT_SECRET")
if not SECRET or len(SECRET) < 32:
    logger.error("JWT_SECRET is not set or too short.")
    raise ValueError("JWT_SECRET is required (at least 32 characters).")

def get_jwt_strategy() -> JWTStrategy:
    return JWTStrategy(secret=SECRET, lifetime_seconds=3600)

auth_backend = AuthenticationBackend(
    name="jwt",
    transport=bearer_transport,  # تغيير إلى BearerTransport
    get_strategy=get_jwt_strategy,
)

# OAuth بيانات الاعتماد
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID")
GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET")

if not all([GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET]):
    logger.error("One or more OAuth environment variables are missing.")
    raise ValueError("All OAuth credentials are required.")

GOOGLE_REDIRECT_URL = os.getenv("GOOGLE_REDIRECT_URL", "https://mgzon-mgzon-app.hf.space/auth/google/callback")
GITHUB_REDIRECT_URL = os.getenv("GITHUB_REDIRECT_URL", "https://mgzon-mgzon-app.hf.space/auth/github/callback")

google_oauth_client = GoogleOAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
github_oauth_client = GitHubOAuth2(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET)
github_oauth_client._access_token_url = "https://github.com/login/oauth/access_token"
github_oauth_client._access_token_params = {"headers": {"Accept": "application/json"}}

# مدير المستخدمين
class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
    reset_password_token_secret = SECRET
    verification_token_secret = SECRET

    async def get_by_oauth_account(self, oauth_name: str, account_id: str):
        logger.info(f"Checking OAuth account: {oauth_name}/{account_id}")
        statement = select(OAuthAccount).where(
            OAuthAccount.oauth_name == oauth_name,
            OAuthAccount.account_id == account_id
        )
        result = await self.user_db.session.execute(statement)
        return result.scalar_one_or_none()

    async def add_oauth_account(self, oauth_account: OAuthAccount):
        logger.info(f"Adding OAuth account for user {oauth_account.user_id}")
        self.user_db.session.add(oauth_account)
        await self.user_db.session.commit()
        await self.user_db.session.refresh(oauth_account)

    async def oauth_callback(
        self,
        oauth_name: str,
        access_token: str,
        account_id: str,
        account_email: str,
        expires_at: Optional[int] = None,
        refresh_token: Optional[str] = None,
        request: Optional[Request] = None,
        *,
        associate_by_email: bool = False,
        is_verified_by_default: bool = False,
    ) -> UP:
        logger.info(f"OAuth callback for {oauth_name} account {account_id}")

        oauth_account = OAuthAccount(
            oauth_name=oauth_name,
            access_token=access_token,
            account_id=account_id,
            account_email=account_email,
            expires_at=expires_at,
            refresh_token=refresh_token,
        )

        existing_oauth_account = await self.get_by_oauth_account(oauth_name, account_id)
        if existing_oauth_account:
            logger.info(f"Fetching user for OAuth account with user_id: {existing_oauth_account.user_id}")
            statement = select(User).where(User.id == existing_oauth_account.user_id)
            result = await self.user_db.session.execute(statement)
            user = result.scalar_one_or_none()

            if user:
                logger.info(f"User found: {user.email}, proceeding with on_after_login")
                await self.on_after_login(user, request)
                return user
            else:
                logger.error(f"No user found for OAuth account with user_id: {existing_oauth_account.user_id}")
                raise ValueError("User not found for existing OAuth account")

        if associate_by_email:
            logger.info(f"Associating OAuth account by email: {account_email}")
            user = await self.user_db.get_by_email(account_email)
            if user:
                oauth_account.user_id = user.id
                await self.add_oauth_account(oauth_account)
                logger.info(f"User associated: {user.email}, proceeding with on_after_login")
                await self.on_after_login(user, request)
                return user

        logger.info(f"Creating new user for email: {account_email}")
        user_dict = {
            "email": account_email,
            "hashed_password": self.password_helper.hash(secrets.token_hex(32)),
            "is_active": True,
            "is_verified": is_verified_by_default,
        }

        user = await self.user_db.create(user_dict)
        oauth_account.user_id = user.id
        await self.add_oauth_account(oauth_account)
        logger.info(f"New user created: {user.email}, proceeding with on_after_login")
        await self.on_after_login(user, request)
        return user

async def get_user_manager(user_db: CustomSQLAlchemyUserDatabase = Depends(get_user_db)):
    yield UserManager(user_db)

fastapi_users = FastAPIUsers[User, int](
    get_user_db,
    [auth_backend],
)

current_active_user = fastapi_users.current_user(active=True, optional=True)

async def generate_jwt_token(user: User, secret: str, lifetime_seconds: int) -> str:
    payload = {
        "sub": str(user.id),
        "aud": "fastapi-users:auth",
        "exp": int(datetime.utcnow().timestamp()) + lifetime_seconds,
    }
    return jwt.encode(payload, secret, algorithm="HS256")

async def custom_google_authorize(
    state: Optional[str] = None,
    oauth_client=Depends(lambda: google_oauth_client),
):
    logger.debug("Generating Google authorization URL")
    state_data = secrets.token_urlsafe(32) if state is None else state
    authorization_url = await oauth_client.get_authorization_url(
        redirect_uri=GOOGLE_REDIRECT_URL,
        state=state_data,
    )
    return JSONResponse(content={
        "authorization_url": authorization_url
    }, status_code=200)

async def custom_github_authorize(
    state: Optional[str] = None,
    oauth_client=Depends(lambda: github_oauth_client),
):
    logger.debug("Generating GitHub authorization URL")
    state_data = secrets.token_urlsafe(32) if state is None else state
    authorization_url = await oauth_client.get_authorization_url(
        redirect_uri=GITHUB_REDIRECT_URL,
        state=state_data,
    )
    return JSONResponse(content={
        "authorization_url": authorization_url
    }, status_code=200)

async def custom_oauth_callback(
    code: str,
    state: Optional[str] = None,
    user_manager: UserManager = Depends(get_user_manager),
    oauth_client=Depends(lambda: google_oauth_client),
    redirect_url: str = GOOGLE_REDIRECT_URL,
    response: Response = None,
    request: Request = None,
):
    logger.debug(f"Processing Google callback with code: {code}, state: {state}")
    try:
        if state:
            logger.debug(f"Received state: {state}")

        token_data = await oauth_client.get_access_token(code, redirect_url)
        access_token = token_data["access_token"]
        
        async with httpx.AsyncClient() as client:
            user_info_response = await client.get(
                "https://www.googleapis.com/oauth2/v3/userinfo",
                headers={"Authorization": f"Bearer {access_token}"}
            )
            if user_info_response.status_code != 200:
                raise ValueError(f"Failed to fetch user info: {user_info_response.text}")
            user_info = user_info_response.json()

        user = await user_manager.oauth_callback(
            oauth_name="google",
            access_token=access_token,
            account_id=user_info["sub"],
            account_email=user_info["email"],
            expires_at=token_data.get("expires_in"),
            refresh_token=token_data.get("refresh_token"),
            request=Request(scope={"type": "http"}),
            associate_by_email=True,
            is_verified_by_default=True,
        )

        token = await generate_jwt_token(user, SECRET, 3600)
        
        # ما نضبطش cookie لأننا بنستخدم Bearer token
        # response.set_cookie(
        #     key="fastapiusersauth",
        #     value=token,
        #     max_age=3600,
        #     httponly=True,
        #     samesite="lax",
        #     secure=True,
        # )

        is_app = request.headers.get("X-Capacitor-App", False)
        if is_app:
            return JSONResponse(content={
                "message": "Google login successful",
                "access_token": token
            }, status_code=200)
        else:
            return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)

    except Exception as e:
        logger.error(f"Error in Google OAuth callback: {str(e)}")
        return JSONResponse(content={"detail": str(e)}, status_code=400)

async def custom_github_oauth_callback(
    code: str,
    state: Optional[str] = None,
    user_manager: UserManager = Depends(get_user_manager),
    oauth_client=Depends(lambda: github_oauth_client),
    redirect_url: str = GITHUB_REDIRECT_URL,
    response: Response = None,
    request: Request = None,
):
    logger.debug(f"Processing GitHub callback with code: {code}, state: {state}")
    try:
        if state:
            logger.debug(f"Received state: {state}")

        token_data = await oauth_client.get_access_token(code, redirect_url)
        access_token = token_data["access_token"]
        
        async with httpx.AsyncClient() as client:
            user_info_response = await client.get(
                "https://api.github.com/user",
                headers={
                    "Authorization": f"Bearer {access_token}",
                    "Accept": "application/vnd.github.v3+json"
                }
            )
            if user_info_response.status_code != 200:
                raise ValueError(f"Failed to fetch user info: {user_info_response.text}")
            user_info = user_info_response.json()

            email = user_info.get("email")
            if not email:
                email_response = await client.get(
                    "https://api.github.com/user/emails",
                    headers={
                        "Authorization": f"Bearer {access_token}",
                        "Accept": "application/vnd.github.v3+json"
                    }
                )
                if email_response.status_code == 200:
                    emails = email_response.json()
                    primary_email = next((e["email"] for e in emails if e["primary"] and e["verified"]), None)
                    email = primary_email or f"{user_info['login']}@github.com"

        user = await user_manager.oauth_callback(
            oauth_name="github",
            access_token=access_token,
            account_id=str(user_info["id"]),
            account_email=email,
            expires_at=token_data.get("expires_in"),
            refresh_token=token_data.get("refresh_token"),
            request=Request(scope={"type": "http"}),
            associate_by_email=True,
            is_verified_by_default=True,
        )

        token = await generate_jwt_token(user, SECRET, 3600)
        
        # ما نضبطش cookie لأننا بنستخدم Bearer token
        # response.set_cookie(
        #     key="fastapiusersauth",
        #     value=token,
        #     max_age=3600,
        #     httponly=True,
        #     samesite="lax",
        #     secure=True,
        # )

        is_app = request.headers.get("X-Capacitor-App", False)
        if is_app:
            return JSONResponse(content={
                "message": "GitHub login successful",
                "access_token": token
            }, status_code=200)
        else:
            return RedirectResponse(url=f"/chat?access_token={token}", status_code=303)

    except Exception as e:
        logger.error(f"Error in GitHub OAuth callback: {str(e)}")
        return JSONResponse(content={"detail": str(e)}, status_code=400)

def get_auth_router(app: FastAPI):
    app.include_router(fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"])
    app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
    app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
    app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
    app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])

    app.get("/auth/google/authorize")(custom_google_authorize)
    app.get("/auth/google/callback")(custom_oauth_callback)
    app.get("/auth/github/authorize")(custom_github_authorize)
    app.get("/auth/github/callback")(custom_github_oauth_callback)