MukeshKapoor25 commited on
Commit
8ee3f02
·
1 Parent(s): 3eb72ad

Refactor user model and service; implement OAuth and OTP login features, enhance JWT handling, and update requirements

Browse files
app/models/user_model.py CHANGED
@@ -1,9 +1,10 @@
1
  from fastapi import HTTPException
2
  from app.core.nosql_client import db
3
  from app.utils.common_utils import is_email # Assumes utility exists
 
4
 
5
  class BookMyServiceUserModel:
6
- collection = db["book_my_service_users"]
7
 
8
  @staticmethod
9
  async def find_by_email(email: str):
@@ -31,8 +32,9 @@ class BookMyServiceUserModel:
31
  }) is not None
32
 
33
  @staticmethod
34
- async def create(user_data: dict):
35
- result = await BookMyServiceUserModel.collection.insert_one(user_data)
 
36
  return result.inserted_id
37
 
38
  @staticmethod
 
1
  from fastapi import HTTPException
2
  from app.core.nosql_client import db
3
  from app.utils.common_utils import is_email # Assumes utility exists
4
+ from app.schemas.user_schema import UserRegisterRequest
5
 
6
  class BookMyServiceUserModel:
7
+ collection = db["customers"]
8
 
9
  @staticmethod
10
  async def find_by_email(email: str):
 
32
  }) is not None
33
 
34
  @staticmethod
35
+ async def create(user_data: UserRegisterRequest):
36
+ user_dict = user_data.dict()
37
+ result = await BookMyServiceUserModel.collection.insert_one(user_dict)
38
  return result.inserted_id
39
 
40
  @staticmethod
app/routers/user_router.py CHANGED
@@ -1,29 +1,96 @@
1
- ## bookmyservice-ums/app/routers/user_router.py
 
2
 
3
- from fastapi import APIRouter, HTTPException
4
- from app.schemas.user_schema import OTPRequest, OTPVerifyRequest, UserRegisterRequest, OAuthLoginRequest, TokenResponse
 
 
 
 
 
5
  from app.services.user_service import UserService
6
- from app.utils.jwt import create_access_token
 
7
 
8
  router = APIRouter()
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  @router.post("/send-otp")
11
  async def send_otp_handler(payload: OTPRequest):
12
  identifier = payload.email or payload.phone
13
  if not identifier:
14
  raise HTTPException(status_code=400, detail="Email or phone required")
 
15
  await UserService.send_otp(identifier, payload.phone or "N/A")
16
- return {"message": "OTP sent"}
 
 
 
 
 
 
17
 
18
- @router.post("/verify-otp", response_model=TokenResponse)
19
- async def verify_otp_handler(payload: OTPVerifyRequest):
20
- return await UserService.verify_otp_and_login(payload.login_input, payload.otp)
 
 
 
 
 
 
 
 
21
 
 
22
  @router.post("/oauth-login", response_model=TokenResponse)
23
  async def oauth_login_handler(payload: OAuthLoginRequest):
24
- user_id = f"{payload.provider}_{payload.token}" # In production: validate token and extract user info
25
- return await UserService.verify_otp_and_login(user_id, otp="") # simulate OTP match via token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
 
27
  @router.post("/register", response_model=TokenResponse)
28
- async def register_user(payload: UserRegisterRequest):
29
- return await UserService.register(payload)
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Security
2
+ from fastapi.security import APIKeyHeader
3
 
4
+ from app.schemas.user_schema import (
5
+ OTPRequest,
6
+ OTPVerifyRequest,
7
+ UserRegisterRequest,
8
+ OAuthLoginRequest,
9
+ TokenResponse,
10
+ )
11
  from app.services.user_service import UserService
12
+ from app.utils.jwt import create_temp_token, decode_token
13
+ from app.utils.social_utils import verify_google_token, verify_apple_token
14
 
15
  router = APIRouter()
16
 
17
+ # 🔐 Declare API key header scheme (Swagger shows a single token input box)
18
+ api_key_scheme = APIKeyHeader(name="Authorization", auto_error=True)
19
+
20
+ # 🔍 Bearer token parser
21
+ # More flexible bearer token parser
22
+ def get_bearer_token(api_key: str = Security(api_key_scheme)) -> str:
23
+ try:
24
+ # If "Bearer " prefix is included, strip it
25
+ if api_key.lower().startswith("bearer "):
26
+ return api_key[7:] # Remove "Bearer " prefix
27
+
28
+ # Else, assume it's already a raw JWT
29
+ return api_key
30
+
31
+ except Exception as e:
32
+ raise HTTPException(
33
+ status_code=status.HTTP_401_UNAUTHORIZED,
34
+ detail="Invalid or missing Authorization token"
35
+ )
36
+
37
+
38
+
39
  @router.post("/send-otp")
40
  async def send_otp_handler(payload: OTPRequest):
41
  identifier = payload.email or payload.phone
42
  if not identifier:
43
  raise HTTPException(status_code=400, detail="Email or phone required")
44
+
45
  await UserService.send_otp(identifier, payload.phone or "N/A")
46
+
47
+ temp_token = create_temp_token({
48
+ "sub": identifier,
49
+ "type": "otp_verification"
50
+ }, expires_minutes=10)
51
+
52
+ return {"message": "OTP sent", "temp_token": temp_token}
53
 
54
+ # 🔐 OTP Login using temporary token
55
+ @router.post("/otp-login", response_model=TokenResponse)
56
+ async def otp_login_handler(
57
+ payload: OTPVerifyRequest,
58
+ temp_token: str = Depends(get_bearer_token)
59
+ ):
60
+ decoded = decode_token(temp_token)
61
+ if not decoded or decoded.get("sub") != payload.login_input or decoded.get("type") != "otp_verification":
62
+ raise HTTPException(status_code=401, detail="Invalid or expired OTP session token")
63
+
64
+ return await UserService.otp_login_handler(payload.login_input, payload.otp)
65
 
66
+ # 🌐 OAuth Login for Google / Apple
67
  @router.post("/oauth-login", response_model=TokenResponse)
68
  async def oauth_login_handler(payload: OAuthLoginRequest):
69
+ if payload.provider == "google":
70
+ user_info = await verify_google_token(payload.token)
71
+ user_id = f"google_{user_info['id']}"
72
+ elif payload.provider == "apple":
73
+ user_info = await verify_apple_token(payload.token)
74
+ user_id = f"apple_{user_info['id']}"
75
+ else:
76
+ raise HTTPException(status_code=400, detail="Unsupported OAuth provider")
77
+
78
+ temp_token = create_temp_token({
79
+ "sub": user_id,
80
+ "type": "oauth_session",
81
+ "verified": True
82
+ }, expires_minutes=10)
83
+
84
+ return {"access_token": temp_token, "token_type": "bearer"}
85
 
86
+ # 👤 Final user registration after OTP or OAuth
87
  @router.post("/register", response_model=TokenResponse)
88
+ async def register_user(
89
+ payload: UserRegisterRequest,
90
+ temp_token: str = Depends(get_bearer_token)
91
+ ):
92
+ decoded = decode_token(temp_token)
93
+ if not decoded or decoded.get("type") not in ["otp_verification", "oauth_session"]:
94
+ raise HTTPException(status_code=401, detail="Invalid or expired registration token")
95
+
96
+ return await UserService.register(payload, decoded)
app/schemas/user_schema.py CHANGED
@@ -3,7 +3,7 @@ from typing import Optional, Literal
3
 
4
  # Used for OTP-based or OAuth-based user registration
5
  class UserRegisterRequest(BaseModel):
6
- full_name: str
7
  email: Optional[EmailStr] = None
8
  phone: Optional[str] = None
9
  otp: Optional[str] = None
 
3
 
4
  # Used for OTP-based or OAuth-based user registration
5
  class UserRegisterRequest(BaseModel):
6
+ name: str
7
  email: Optional[EmailStr] = None
8
  phone: Optional[str] = None
9
  otp: Optional[str] = None
app/services/user_service.py CHANGED
@@ -19,7 +19,7 @@ class UserService:
19
  logger.debug(f"OTP sent to {identifier}: {otp}")
20
 
21
  @staticmethod
22
- async def verify_otp_and_login(identifier: str, otp: str):
23
  if not await BookMyServiceOTPModel.verify_otp(identifier, otp):
24
  logger.debug(f"Invalid or expired OTP for identifier: {identifier}")
25
  raise HTTPException(status_code=400, detail="Invalid or expired OTP")
 
19
  logger.debug(f"OTP sent to {identifier}: {otp}")
20
 
21
  @staticmethod
22
+ async def otp_login_handler(identifier: str, otp: str):
23
  if not await BookMyServiceOTPModel.verify_otp(identifier, otp):
24
  logger.debug(f"Invalid or expired OTP for identifier: {identifier}")
25
  raise HTTPException(status_code=400, detail="Invalid or expired OTP")
app/utils/jwt.py CHANGED
@@ -1,14 +1,24 @@
1
 
2
  ## bookmyservice-ums/app/utils/jwt.py
3
 
4
- from jose import jwt
5
  from datetime import datetime, timedelta
6
 
7
  SECRET_KEY = "secret-key-placeholder"
8
  ALGORITHM = "HS256"
9
 
 
10
  def create_access_token(data: dict, expires_minutes: int = 60):
11
  to_encode = data.copy()
12
  expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
13
  to_encode.update({"exp": expire})
14
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
 
 
 
 
 
 
 
 
 
 
1
 
2
  ## bookmyservice-ums/app/utils/jwt.py
3
 
4
+ from jose import jwt, JWTError
5
  from datetime import datetime, timedelta
6
 
7
  SECRET_KEY = "secret-key-placeholder"
8
  ALGORITHM = "HS256"
9
 
10
+
11
  def create_access_token(data: dict, expires_minutes: int = 60):
12
  to_encode = data.copy()
13
  expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
14
  to_encode.update({"exp": expire})
15
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
16
+
17
+ def create_temp_token(data: dict, expires_minutes: int = 10):
18
+ return create_access_token(data, expires_minutes=expires_minutes)
19
+
20
+ def decode_token(token: str) -> dict:
21
+ try:
22
+ return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
23
+ except JWTError:
24
+ return {}
app/utils/social_utils.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from google.oauth2 import id_token as google_id_token
2
+ from google.auth.transport import requests as google_requests
3
+ from jose import jwt as jose_jwt, JWTError, jwk
4
+ from jose.utils import base64url_decode
5
+ from typing import Dict
6
+ import httpx
7
+ import json
8
+
9
+ # Async wrapper to run Google’s sync function in a thread pool
10
+ import asyncio
11
+ from functools import partial
12
+
13
+ # ✅ Async Google token verification
14
+ async def verify_google_token(token: str, client_id: str) -> Dict:
15
+ """
16
+ Asynchronously verifies a Google ID token and returns the payload if valid.
17
+ """
18
+ try:
19
+ loop = asyncio.get_event_loop()
20
+ # Run the sync method in a thread to avoid blocking
21
+ idinfo = await loop.run_in_executor(
22
+ None,
23
+ partial(google_id_token.verify_oauth2_token, token, google_requests.Request(), client_id)
24
+ )
25
+ if idinfo.get('iss') not in ['accounts.google.com', 'https://accounts.google.com']:
26
+ raise ValueError('Wrong issuer.')
27
+ return idinfo
28
+ except Exception as e:
29
+ raise ValueError(f"Invalid Google token: {str(e)}")
30
+
31
+
32
+ # ✅ Async Apple token verification
33
+ async def verify_apple_token(token: str, audience: str) -> Dict:
34
+ """
35
+ Asynchronously verifies an Apple identity token and returns the decoded payload.
36
+ """
37
+ try:
38
+ # Fetch Apple's public keys (JWKS)
39
+ async with httpx.AsyncClient() as client:
40
+ response = await client.get('https://appleid.apple.com/auth/keys')
41
+ response.raise_for_status()
42
+ keys = response.json().get('keys', [])
43
+
44
+ # Decode header to get kid and alg
45
+ header = jose_jwt.get_unverified_header(token)
46
+ kid = header.get('kid')
47
+ alg = header.get('alg')
48
+
49
+ key = next((k for k in keys if k['kid'] == kid and k['alg'] == alg), None)
50
+ if not key:
51
+ raise ValueError("Public key not found for Apple token")
52
+
53
+ public_key = jwk.construct(key)
54
+ message, encoded_sig = token.rsplit('.', 1)
55
+ decoded_sig = base64url_decode(encoded_sig.encode())
56
+
57
+ if not public_key.verify(message.encode(), decoded_sig):
58
+ raise ValueError("Invalid Apple token signature")
59
+
60
+ claims = jose_jwt.decode(
61
+ token,
62
+ key,
63
+ algorithms=['RS256'],
64
+ audience=audience,
65
+ issuer='https://appleid.apple.com'
66
+ )
67
+ return claims
68
+
69
+ except Exception as e:
70
+ raise ValueError(f"Invalid Apple token: {str(e)}")
requirements.txt CHANGED
@@ -16,4 +16,6 @@ twilio
16
 
17
  # Optional useful utilities (highly recommended)
18
  httpx # async HTTP client (e.g., for calling 3rd party APIs)
19
- passlib[bcrypt] # password hashing (if needed)
 
 
 
16
 
17
  # Optional useful utilities (highly recommended)
18
  httpx # async HTTP client (e.g., for calling 3rd party APIs)
19
+ passlib[bcrypt] # password hashing (if needed)
20
+
21
+ httpx