Spaces:
Sleeping
Sleeping
Commit ·
fe2f3e5
1
Parent(s): 0011d75
feat(logging): add comprehensive logging across application layers
Browse filesImplement detailed logging in models, services, and routers to improve debugging and monitoring
Configure logging setup in app.py with appropriate log levels and handlers
Add error handling with logging for critical operations in user flows
- app/app.py +23 -0
- app/models/otp_model.py +62 -17
- app/models/user_model.py +53 -10
- app/routers/user_router.py +88 -18
- app/services/user_service.py +61 -22
app/app.py
CHANGED
|
@@ -3,6 +3,29 @@
|
|
| 3 |
from fastapi import FastAPI
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from app.routers import user_router, profile_router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
app = FastAPI(title="BookMyService User Management Service")
|
| 8 |
|
|
|
|
| 3 |
from fastapi import FastAPI
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from app.routers import user_router, profile_router
|
| 6 |
+
import logging
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
# Configure logging
|
| 10 |
+
logging.basicConfig(
|
| 11 |
+
level=logging.DEBUG,
|
| 12 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 13 |
+
handlers=[
|
| 14 |
+
logging.StreamHandler(sys.stdout),
|
| 15 |
+
logging.FileHandler('app.log')
|
| 16 |
+
]
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Set specific logger levels
|
| 20 |
+
logging.getLogger("user_router").setLevel(logging.DEBUG)
|
| 21 |
+
logging.getLogger("user_service").setLevel(logging.DEBUG)
|
| 22 |
+
logging.getLogger("otp_model").setLevel(logging.DEBUG)
|
| 23 |
+
logging.getLogger("user_model").setLevel(logging.DEBUG)
|
| 24 |
+
logging.getLogger("profile_service").setLevel(logging.DEBUG)
|
| 25 |
+
|
| 26 |
+
# Reduce noise from other libraries
|
| 27 |
+
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
| 28 |
+
logging.getLogger("fastapi").setLevel(logging.INFO)
|
| 29 |
|
| 30 |
app = FastAPI(title="BookMyService User Management Service")
|
| 31 |
|
app/models/otp_model.py
CHANGED
|
@@ -3,6 +3,9 @@ from app.core.cache_client import get_redis
|
|
| 3 |
from app.utils.sms_utils import send_sms_otp
|
| 4 |
from app.utils.email_utils import send_email_otp
|
| 5 |
from app.utils.common_utils import is_email
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
class BookMyServiceOTPModel:
|
| 8 |
OTP_TTL = 300 # 5 minutes
|
|
@@ -11,18 +14,37 @@ class BookMyServiceOTPModel:
|
|
| 11 |
|
| 12 |
@staticmethod
|
| 13 |
async def store_otp(identifier: str, phone: str, otp: str, ttl: int = OTP_TTL):
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
await redis.
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
'''
|
| 27 |
# Send OTP via SMS, fallback to Email if identifier is email
|
| 28 |
try:
|
|
@@ -41,13 +63,36 @@ class BookMyServiceOTPModel:
|
|
| 41 |
'''
|
| 42 |
@staticmethod
|
| 43 |
async def verify_otp(identifier: str, otp: str):
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
await
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
@staticmethod
|
| 53 |
async def read_otp(identifier: str):
|
|
|
|
| 3 |
from app.utils.sms_utils import send_sms_otp
|
| 4 |
from app.utils.email_utils import send_email_otp
|
| 5 |
from app.utils.common_utils import is_email
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger("otp_model")
|
| 9 |
|
| 10 |
class BookMyServiceOTPModel:
|
| 11 |
OTP_TTL = 300 # 5 minutes
|
|
|
|
| 14 |
|
| 15 |
@staticmethod
|
| 16 |
async def store_otp(identifier: str, phone: str, otp: str, ttl: int = OTP_TTL):
|
| 17 |
+
logger.info(f"Storing OTP for identifier: {identifier}")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
redis = await get_redis()
|
| 21 |
+
logger.debug(f"Redis connection established for OTP storage")
|
| 22 |
|
| 23 |
+
# Rate limit: max 3 OTPs per 10 minutes
|
| 24 |
+
rate_key = f"otp_rate_limit:{identifier}"
|
| 25 |
+
logger.debug(f"Checking rate limit with key: {rate_key}")
|
| 26 |
+
|
| 27 |
+
attempts = await redis.incr(rate_key)
|
| 28 |
+
logger.debug(f"Current OTP attempts for {identifier}: {attempts}")
|
| 29 |
+
|
| 30 |
+
if attempts == 1:
|
| 31 |
+
await redis.expire(rate_key, BookMyServiceOTPModel.RATE_LIMIT_WINDOW)
|
| 32 |
+
logger.debug(f"Set rate limit expiry for {identifier}")
|
| 33 |
+
elif attempts > BookMyServiceOTPModel.RATE_LIMIT_MAX:
|
| 34 |
+
logger.warning(f"Rate limit exceeded for {identifier}: {attempts} attempts")
|
| 35 |
+
raise HTTPException(status_code=429, detail="Too many OTP requests. Try again later.")
|
| 36 |
|
| 37 |
+
# Store OTP
|
| 38 |
+
otp_key = f"bms_otp:{identifier}"
|
| 39 |
+
await redis.setex(otp_key, ttl, otp)
|
| 40 |
+
logger.info(f"OTP stored successfully for {identifier} with key: {otp_key}, TTL: {ttl}")
|
| 41 |
+
|
| 42 |
+
except HTTPException as e:
|
| 43 |
+
logger.error(f"HTTP error storing OTP for {identifier}: {e.status_code} - {e.detail}")
|
| 44 |
+
raise e
|
| 45 |
+
except Exception as e:
|
| 46 |
+
logger.error(f"Unexpected error storing OTP for {identifier}: {str(e)}", exc_info=True)
|
| 47 |
+
raise HTTPException(status_code=500, detail="Failed to store OTP")
|
| 48 |
'''
|
| 49 |
# Send OTP via SMS, fallback to Email if identifier is email
|
| 50 |
try:
|
|
|
|
| 63 |
'''
|
| 64 |
@staticmethod
|
| 65 |
async def verify_otp(identifier: str, otp: str):
|
| 66 |
+
logger.info(f"Verifying OTP for identifier: {identifier}")
|
| 67 |
+
logger.debug(f"Provided OTP: {otp}")
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
redis = await get_redis()
|
| 71 |
+
logger.debug("Redis connection established for OTP verification")
|
| 72 |
+
|
| 73 |
+
key = f"bms_otp:{identifier}"
|
| 74 |
+
logger.debug(f"Looking up OTP with key: {key}")
|
| 75 |
+
|
| 76 |
+
stored = await redis.get(key)
|
| 77 |
+
logger.debug(f"Stored OTP value: {stored}")
|
| 78 |
+
|
| 79 |
+
if stored:
|
| 80 |
+
logger.debug(f"OTP found in Redis. Comparing: provided='{otp}' vs stored='{stored}'")
|
| 81 |
+
if stored == otp:
|
| 82 |
+
logger.info(f"OTP verification successful for {identifier}")
|
| 83 |
+
await redis.delete(key)
|
| 84 |
+
logger.debug(f"OTP deleted from Redis after successful verification")
|
| 85 |
+
return True
|
| 86 |
+
else:
|
| 87 |
+
logger.warning(f"OTP mismatch for {identifier}: provided='{otp}' vs stored='{stored}'")
|
| 88 |
+
return False
|
| 89 |
+
else:
|
| 90 |
+
logger.warning(f"No OTP found in Redis for identifier: {identifier} with key: {key}")
|
| 91 |
+
return False
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
logger.error(f"Error verifying OTP for {identifier}: {str(e)}", exc_info=True)
|
| 95 |
+
return False
|
| 96 |
|
| 97 |
@staticmethod
|
| 98 |
async def read_otp(identifier: str):
|
app/models/user_model.py
CHANGED
|
@@ -2,28 +2,71 @@ 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):
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
@staticmethod
|
| 14 |
async def find_by_mobile(mobile: str):
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
@staticmethod
|
| 18 |
async def find_by_identifier(identifier: str):
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
@staticmethod
|
| 29 |
async def exists_by_email_or_phone(email: str, phone: str) -> bool:
|
|
|
|
| 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 |
+
import logging
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger("user_model")
|
| 8 |
|
| 9 |
class BookMyServiceUserModel:
|
| 10 |
collection = db["customers"]
|
| 11 |
|
| 12 |
@staticmethod
|
| 13 |
async def find_by_email(email: str):
|
| 14 |
+
logger.debug(f"Searching for user by email: {email}")
|
| 15 |
+
try:
|
| 16 |
+
user = await BookMyServiceUserModel.collection.find_one({"email": email})
|
| 17 |
+
if user:
|
| 18 |
+
logger.debug(f"User found by email: {email}")
|
| 19 |
+
else:
|
| 20 |
+
logger.debug(f"No user found with email: {email}")
|
| 21 |
+
return user
|
| 22 |
+
except Exception as e:
|
| 23 |
+
logger.error(f"Error finding user by email {email}: {str(e)}", exc_info=True)
|
| 24 |
+
return None
|
| 25 |
|
| 26 |
@staticmethod
|
| 27 |
async def find_by_mobile(mobile: str):
|
| 28 |
+
logger.debug(f"Searching for user by mobile: {mobile}")
|
| 29 |
+
try:
|
| 30 |
+
user = await BookMyServiceUserModel.collection.find_one({"phone": mobile})
|
| 31 |
+
if user:
|
| 32 |
+
logger.debug(f"User found by mobile: {mobile}")
|
| 33 |
+
else:
|
| 34 |
+
logger.debug(f"No user found with mobile: {mobile}")
|
| 35 |
+
return user
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logger.error(f"Error finding user by mobile {mobile}: {str(e)}", exc_info=True)
|
| 38 |
+
return None
|
| 39 |
|
| 40 |
@staticmethod
|
| 41 |
async def find_by_identifier(identifier: str):
|
| 42 |
+
logger.info(f"Finding user by identifier: {identifier}")
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Determine if identifier is email or mobile
|
| 46 |
+
is_email_identifier = is_email(identifier)
|
| 47 |
+
logger.debug(f"Identifier type - is_email: {is_email_identifier}")
|
| 48 |
+
|
| 49 |
+
if is_email_identifier:
|
| 50 |
+
logger.debug(f"Searching by email for identifier: {identifier}")
|
| 51 |
+
user = await BookMyServiceUserModel.find_by_email(identifier)
|
| 52 |
+
else:
|
| 53 |
+
logger.debug(f"Searching by mobile for identifier: {identifier}")
|
| 54 |
+
user = await BookMyServiceUserModel.find_by_mobile(identifier)
|
| 55 |
+
|
| 56 |
+
if not user:
|
| 57 |
+
logger.warning(f"User not found with identifier: {identifier}")
|
| 58 |
+
raise HTTPException(status_code=404, detail="User not found with this email or mobile")
|
| 59 |
+
|
| 60 |
+
logger.info(f"User found successfully for identifier: {identifier}")
|
| 61 |
+
logger.debug(f"User data keys: {list(user.keys()) if user else 'None'}")
|
| 62 |
+
return user
|
| 63 |
+
|
| 64 |
+
except HTTPException as e:
|
| 65 |
+
logger.error(f"HTTP error finding user by identifier {identifier}: {e.status_code} - {e.detail}")
|
| 66 |
+
raise e
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error(f"Unexpected error finding user by identifier {identifier}: {str(e)}", exc_info=True)
|
| 69 |
+
raise HTTPException(status_code=500, detail="Failed to find user")
|
| 70 |
|
| 71 |
@staticmethod
|
| 72 |
async def exists_by_email_or_phone(email: str, phone: str) -> bool:
|
app/routers/user_router.py
CHANGED
|
@@ -13,17 +13,25 @@ from app.utils.jwt import create_temp_token, decode_token
|
|
| 13 |
from app.utils.social_utils import verify_google_token, verify_apple_token
|
| 14 |
import logging
|
| 15 |
|
| 16 |
-
logger = logging.getLogger(
|
| 17 |
|
| 18 |
router = APIRouter()
|
| 19 |
|
| 20 |
# 🔐 Declare API key header scheme (Swagger shows a single token input box)
|
| 21 |
-
api_key_scheme = APIKeyHeader(name="Authorization", auto_error=
|
| 22 |
|
| 23 |
# 🔍 Bearer token parser
|
| 24 |
# More flexible bearer token parser
|
| 25 |
def get_bearer_token(api_key: str = Security(api_key_scheme)) -> str:
|
| 26 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# If "Bearer " prefix is included, strip it
|
| 28 |
logger.debug(f"Received Authorization header: {api_key}")
|
| 29 |
if api_key.lower().startswith("bearer "):
|
|
@@ -32,28 +40,54 @@ def get_bearer_token(api_key: str = Security(api_key_scheme)) -> str:
|
|
| 32 |
# Else, assume it's already a raw JWT
|
| 33 |
return api_key
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
|
|
|
| 36 |
raise HTTPException(
|
| 37 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 38 |
-
detail="Invalid
|
| 39 |
)
|
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
@router.post("/send-otp")
|
| 44 |
async def send_otp_handler(payload: OTPRequest):
|
| 45 |
-
|
| 46 |
-
if not identifier:
|
| 47 |
-
raise HTTPException(status_code=400, detail="Email or phone required")
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
# 🔐 OTP Login using temporary token
|
| 59 |
@router.post("/otp-login", response_model=TokenResponse)
|
|
@@ -61,11 +95,47 @@ async def otp_login_handler(
|
|
| 61 |
payload: OTPVerifyRequest,
|
| 62 |
temp_token: str = Depends(get_bearer_token)
|
| 63 |
):
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
raise HTTPException(status_code=401, detail="Invalid or expired OTP session token")
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
# 🌐 OAuth Login for Google / Apple
|
| 71 |
@router.post("/oauth-login", response_model=TokenResponse)
|
|
|
|
| 13 |
from app.utils.social_utils import verify_google_token, verify_apple_token
|
| 14 |
import logging
|
| 15 |
|
| 16 |
+
logger = logging.getLogger("user_router")
|
| 17 |
|
| 18 |
router = APIRouter()
|
| 19 |
|
| 20 |
# 🔐 Declare API key header scheme (Swagger shows a single token input box)
|
| 21 |
+
api_key_scheme = APIKeyHeader(name="Authorization", auto_error=False)
|
| 22 |
|
| 23 |
# 🔍 Bearer token parser
|
| 24 |
# More flexible bearer token parser
|
| 25 |
def get_bearer_token(api_key: str = Security(api_key_scheme)) -> str:
|
| 26 |
try:
|
| 27 |
+
# Check if Authorization header is missing
|
| 28 |
+
if not api_key:
|
| 29 |
+
logger.warning("Missing Authorization header")
|
| 30 |
+
raise HTTPException(
|
| 31 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 32 |
+
detail="Missing Authorization header"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
# If "Bearer " prefix is included, strip it
|
| 36 |
logger.debug(f"Received Authorization header: {api_key}")
|
| 37 |
if api_key.lower().startswith("bearer "):
|
|
|
|
| 40 |
# Else, assume it's already a raw JWT
|
| 41 |
return api_key
|
| 42 |
|
| 43 |
+
except HTTPException:
|
| 44 |
+
# Re-raise HTTP exceptions
|
| 45 |
+
raise
|
| 46 |
except Exception as e:
|
| 47 |
+
logger.error(f"Error processing Authorization header: {str(e)}")
|
| 48 |
raise HTTPException(
|
| 49 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 50 |
+
detail="Invalid Authorization header format"
|
| 51 |
)
|
| 52 |
|
| 53 |
|
| 54 |
|
| 55 |
@router.post("/send-otp")
|
| 56 |
async def send_otp_handler(payload: OTPRequest):
|
| 57 |
+
logger.info(f"OTP request started - email: {payload.email}, phone: {payload.phone}")
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
try:
|
| 60 |
+
# Validate input
|
| 61 |
+
identifier = payload.email or payload.phone
|
| 62 |
+
if not identifier:
|
| 63 |
+
logger.warning("OTP request failed - neither email nor phone provided")
|
| 64 |
+
raise HTTPException(status_code=400, detail="Email or phone required")
|
| 65 |
+
|
| 66 |
+
logger.debug(f"Using identifier: {identifier}")
|
| 67 |
+
|
| 68 |
+
# Send OTP via service
|
| 69 |
+
logger.debug(f"Calling UserService.send_otp with identifier: {identifier}, phone: {payload.phone or 'N/A'}")
|
| 70 |
+
await UserService.send_otp(identifier, payload.phone or "N/A")
|
| 71 |
+
logger.info(f"OTP sent successfully to: {identifier}")
|
| 72 |
+
|
| 73 |
+
# Create temporary token
|
| 74 |
+
logger.debug("Creating temporary token for OTP verification")
|
| 75 |
+
temp_token = create_temp_token({
|
| 76 |
+
"sub": identifier,
|
| 77 |
+
"type": "otp_verification"
|
| 78 |
+
}, expires_minutes=10)
|
| 79 |
+
|
| 80 |
+
logger.info(f"Temporary token created for: {identifier}")
|
| 81 |
+
logger.debug(f"Temp token (first 20 chars): {temp_token[:20]}...")
|
| 82 |
+
|
| 83 |
+
return {"message": "OTP sent", "temp_token": temp_token}
|
| 84 |
+
|
| 85 |
+
except HTTPException as e:
|
| 86 |
+
logger.error(f"OTP request failed - HTTP {e.status_code}: {e.detail}")
|
| 87 |
+
raise e
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Unexpected error during OTP request: {str(e)}", exc_info=True)
|
| 90 |
+
raise HTTPException(status_code=500, detail="Internal server error during OTP request")
|
| 91 |
|
| 92 |
# 🔐 OTP Login using temporary token
|
| 93 |
@router.post("/otp-login", response_model=TokenResponse)
|
|
|
|
| 95 |
payload: OTPVerifyRequest,
|
| 96 |
temp_token: str = Depends(get_bearer_token)
|
| 97 |
):
|
| 98 |
+
logger.info(f"OTP login attempt started - login_input: {payload.login_input}")
|
| 99 |
+
logger.debug(f"Received temp_token: {temp_token[:20]}..." if temp_token else "No temp_token received")
|
|
|
|
| 100 |
|
| 101 |
+
try:
|
| 102 |
+
# Decode and validate temporary token
|
| 103 |
+
logger.debug("Attempting to decode temporary token")
|
| 104 |
+
decoded = decode_token(temp_token)
|
| 105 |
+
logger.debug(f"Decoded token payload: {decoded}")
|
| 106 |
+
|
| 107 |
+
if not decoded:
|
| 108 |
+
logger.warning("Failed to decode temporary token - token is invalid or expired")
|
| 109 |
+
raise HTTPException(status_code=401, detail="Invalid or expired OTP session token")
|
| 110 |
+
|
| 111 |
+
# Validate token subject matches login input
|
| 112 |
+
token_sub = decoded.get("sub")
|
| 113 |
+
token_type = decoded.get("type")
|
| 114 |
+
logger.debug(f"Token subject: {token_sub}, Token type: {token_type}")
|
| 115 |
+
|
| 116 |
+
if token_sub != payload.login_input:
|
| 117 |
+
logger.warning(f"Token subject mismatch - token_sub: {token_sub}, login_input: {payload.login_input}")
|
| 118 |
+
raise HTTPException(status_code=401, detail="Invalid or expired OTP session token")
|
| 119 |
+
|
| 120 |
+
if token_type != "otp_verification":
|
| 121 |
+
logger.warning(f"Invalid token type - expected: otp_verification, got: {token_type}")
|
| 122 |
+
raise HTTPException(status_code=401, detail="Invalid or expired OTP session token")
|
| 123 |
+
|
| 124 |
+
logger.info(f"Temporary token validation successful for: {payload.login_input}")
|
| 125 |
+
|
| 126 |
+
# Call user service for OTP verification and login
|
| 127 |
+
logger.debug(f"Calling UserService.otp_login_handler with identifier: {payload.login_input}, otp: {payload.otp}")
|
| 128 |
+
result = await UserService.otp_login_handler(payload.login_input, payload.otp)
|
| 129 |
+
|
| 130 |
+
logger.info(f"OTP login successful for: {payload.login_input}")
|
| 131 |
+
return result
|
| 132 |
+
|
| 133 |
+
except HTTPException as e:
|
| 134 |
+
logger.error(f"OTP login failed for {payload.login_input} - HTTP {e.status_code}: {e.detail}")
|
| 135 |
+
raise e
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logger.error(f"Unexpected error during OTP login for {payload.login_input}: {str(e)}", exc_info=True)
|
| 138 |
+
raise HTTPException(status_code=500, detail="Internal server error during OTP login")
|
| 139 |
|
| 140 |
# 🌐 OAuth Login for Google / Apple
|
| 141 |
@router.post("/oauth-login", response_model=TokenResponse)
|
app/services/user_service.py
CHANGED
|
@@ -14,31 +14,70 @@ logger = logging.getLogger("user_service")
|
|
| 14 |
class UserService:
|
| 15 |
@staticmethod
|
| 16 |
async def send_otp(identifier: str, phone: str):
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
@staticmethod
|
| 23 |
async def otp_login_handler(identifier: str, otp: str):
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
logger.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
"
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
@staticmethod
|
| 44 |
async def register(data: UserRegisterRequest, decoded):
|
|
|
|
| 14 |
class UserService:
|
| 15 |
@staticmethod
|
| 16 |
async def send_otp(identifier: str, phone: str):
|
| 17 |
+
logger.info(f"UserService.send_otp called - identifier: {identifier}, phone: {phone}")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
# Using dummy OTP for testing
|
| 21 |
+
otp = "777777"
|
| 22 |
+
logger.debug(f"Generated OTP: {otp} for identifier: {identifier}")
|
| 23 |
+
|
| 24 |
+
await BookMyServiceOTPModel.store_otp(identifier, phone, otp)
|
| 25 |
+
logger.info(f"OTP stored successfully for identifier: {identifier}")
|
| 26 |
+
logger.debug(f"OTP sent to {identifier}: {otp}")
|
| 27 |
+
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.error(f"Error in send_otp for identifier {identifier}: {str(e)}", exc_info=True)
|
| 30 |
+
raise HTTPException(status_code=500, detail="Failed to send OTP")
|
| 31 |
|
| 32 |
@staticmethod
|
| 33 |
async def otp_login_handler(identifier: str, otp: str):
|
| 34 |
+
logger.info(f"UserService.otp_login_handler called - identifier: {identifier}, otp: {otp}")
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
# Verify OTP
|
| 38 |
+
logger.debug(f"Verifying OTP for identifier: {identifier}")
|
| 39 |
+
otp_valid = await BookMyServiceOTPModel.verify_otp(identifier, otp)
|
| 40 |
+
logger.debug(f"OTP verification result: {otp_valid}")
|
| 41 |
+
|
| 42 |
+
if not otp_valid:
|
| 43 |
+
logger.warning(f"Invalid or expired OTP for identifier: {identifier}")
|
| 44 |
+
raise HTTPException(status_code=400, detail="Invalid or expired OTP")
|
| 45 |
+
|
| 46 |
+
logger.info(f"OTP verification successful for identifier: {identifier}")
|
| 47 |
+
|
| 48 |
+
# Find user by identifier
|
| 49 |
+
logger.debug(f"Looking up user by identifier: {identifier}")
|
| 50 |
+
user = await BookMyServiceUserModel.find_by_identifier(identifier)
|
| 51 |
+
logger.debug(f"User lookup result: {user is not None}")
|
| 52 |
+
|
| 53 |
+
if not user:
|
| 54 |
+
logger.warning(f"No user found for identifier: {identifier}")
|
| 55 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 56 |
+
|
| 57 |
+
logger.info(f"User found for identifier: {identifier}, user_id: {user.get('user_id')}")
|
| 58 |
+
|
| 59 |
+
# Create JWT token
|
| 60 |
+
logger.debug("Creating JWT token for authenticated user")
|
| 61 |
+
token_data = {
|
| 62 |
+
"sub": user.get("user_id"),
|
| 63 |
+
"user_id": user.get("user_id"),
|
| 64 |
+
"email": user.get("email"),
|
| 65 |
+
"role": "user",
|
| 66 |
+
"exp": datetime.utcnow() + timedelta(hours=8)
|
| 67 |
+
}
|
| 68 |
+
logger.debug(f"Token data: {token_data}")
|
| 69 |
+
|
| 70 |
+
access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 71 |
+
logger.info(f"JWT token created successfully for user: {user.get('user_id')}")
|
| 72 |
+
|
| 73 |
+
return {"access_token": access_token, "token_type": "bearer", "name": user.get("name")}
|
| 74 |
+
|
| 75 |
+
except HTTPException as e:
|
| 76 |
+
logger.error(f"HTTP error in otp_login_handler for {identifier}: {e.status_code} - {e.detail}")
|
| 77 |
+
raise e
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"Unexpected error in otp_login_handler for {identifier}: {str(e)}", exc_info=True)
|
| 80 |
+
raise HTTPException(status_code=500, detail="Internal server error during login")
|
| 81 |
|
| 82 |
@staticmethod
|
| 83 |
async def register(data: UserRegisterRequest, decoded):
|