Prabha-AIMLOPS commited on
Commit
adb1c6b
·
1 Parent(s): aeac0d3

added 4 endpoints to support merchant login

Browse files
app/controllers/merchant_controller.py CHANGED
@@ -83,7 +83,7 @@ async def get_otps(key: str = Query(..., description="The email or mobile number
83
  logger.info("Retrieving OTP for key: %s", key)
84
 
85
  # Retrieve OTP from Redis
86
- otp_data = await get_otp_from_cache(key)
87
  if not otp_data:
88
  raise HTTPException(status_code=404, detail="OTP not found or expired")
89
 
 
83
  logger.info("Retrieving OTP for key: %s", key)
84
 
85
  # Retrieve OTP from Redis
86
+ otp_data = await get_otp_from_cache(f"otp:reg:{key}")
87
  if not otp_data:
88
  raise HTTPException(status_code=404, detail="OTP not found or expired")
89
 
app/controllers/merchant_login_controller.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.services.merchant_services import (
2
+ generate_login_otp_service,
3
+ login_service,
4
+ refresh_token_service,
5
+ logout_service,
6
+ )
7
+ from fastapi.security import OAuth2PasswordRequestForm
8
+ from fastapi import APIRouter, HTTPException, Query, Depends, status, Body
9
+ import logging
10
+
11
+ # Initialize router and logger
12
+ router = APIRouter(prefix="/merchant")
13
+ logger = logging.getLogger(__name__)
14
+
15
+ @router.post("/login/otp")
16
+ async def generate_login_otp(identifier: str = Body(..., description="Email or Mobile number")):
17
+ """
18
+ Generate and send login OTP to the provided email or mobile number.
19
+
20
+ Args:
21
+ identifier (str): Email or mobile number.
22
+
23
+ Returns:
24
+ dict: Confirmation message indicating OTP has been sent.
25
+ """
26
+ try:
27
+ result = await generate_login_otp_service(identifier)
28
+ return result
29
+ except HTTPException as e:
30
+ logger.error("Failed to generate login OTP: %s", e.detail)
31
+ raise e
32
+ except Exception as e:
33
+ logger.error("Unexpected error while generating login OTP: %s", e)
34
+ raise HTTPException(status_code=500, detail="Failed to generate login OTP") from e
35
+
36
+
37
+ @router.post("/login")
38
+ async def login(user: OAuth2PasswordRequestForm = Depends()):
39
+ """
40
+ Login using email/mobile and OTP.
41
+
42
+ Args:
43
+ user (OAuth2PasswordRequestForm): Contains username (email/mobile) and password (OTP).
44
+
45
+ Returns:
46
+ dict: Access and refresh tokens.
47
+ """
48
+ try:
49
+ result = await login_service(user.username, user.password)
50
+ return result
51
+ except HTTPException as e:
52
+ logger.error("Login failed: %s", e.detail)
53
+ raise e
54
+ except Exception as e:
55
+ logger.error("Unexpected error during login: %s", e)
56
+ raise HTTPException(status_code=500, detail="Login failed") from e
57
+
58
+
59
+ @router.post("/token/refresh")
60
+ async def refresh_token(
61
+ identifier: str = Body(..., description="Email or Mobile number"),
62
+ refresh_token: str = Body(..., description="Refresh token")
63
+ ):
64
+ """
65
+ Refresh access and refresh tokens.
66
+
67
+ Args:
68
+ identifier (str): Email or mobile number.
69
+ refresh_token (str): The refresh token.
70
+
71
+ Returns:
72
+ dict: New access and refresh tokens.
73
+ """
74
+ try:
75
+ result = await refresh_token_service(identifier, refresh_token)
76
+ return result
77
+ except HTTPException as e:
78
+ logger.error("Failed to refresh token for identifier %s: %s", identifier, e.detail)
79
+ raise e
80
+ except Exception as e:
81
+ logger.error("Unexpected error while refreshing token for identifier %s: %s", identifier, e)
82
+ raise HTTPException(status_code=500, detail="Failed to refresh token") from e
83
+
84
+
85
+ @router.post("/logout")
86
+ async def logout(identifier: str = Body(..., description="Email or Mobile number")):
87
+ """
88
+ Logout the user by invalidating access and refresh tokens.
89
+
90
+ Args:
91
+ identifier (str): Email or mobile number.
92
+
93
+ Returns:
94
+ dict: Confirmation message.
95
+ """
96
+ try:
97
+ result = await logout_service(identifier)
98
+ return result
99
+ except HTTPException as e:
100
+ logger.error("Failed to logout: %s", e.detail)
101
+ raise e
102
+ except Exception as e:
103
+ logger.error("Unexpected error during logout: %s", e)
104
+ raise HTTPException(status_code=500, detail="Failed to logout") from e
app/repositories/cache_repository.py CHANGED
@@ -16,7 +16,7 @@ async def get_otp(key: str) -> dict:
16
  """
17
  try:
18
  # Construct the Redis key
19
- redis_key = f"otp:{key}"
20
 
21
  # Retrieve the value from Redis
22
  value = await redis_client.get(redis_key)
@@ -42,7 +42,7 @@ async def set_otp(key: str, otp_data: dict):
42
  """
43
  try:
44
  # Construct the Redis key
45
- redis_key = f"otp:{key}"
46
 
47
  # Extract expiry duration in seconds
48
  expiry_duration = otp_data.get("expiry_duration", 15 * 60) # Default to 15 minutes if not provided
@@ -52,4 +52,26 @@ async def set_otp(key: str, otp_data: dict):
52
  except Exception as e:
53
  # Log the error and re-raise it
54
  logger.error(f"Failed to store OTP for key {key}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  raise
 
16
  """
17
  try:
18
  # Construct the Redis key
19
+ redis_key = f"{key}"
20
 
21
  # Retrieve the value from Redis
22
  value = await redis_client.get(redis_key)
 
42
  """
43
  try:
44
  # Construct the Redis key
45
+ redis_key = f"{key}"
46
 
47
  # Extract expiry duration in seconds
48
  expiry_duration = otp_data.get("expiry_duration", 15 * 60) # Default to 15 minutes if not provided
 
52
  except Exception as e:
53
  # Log the error and re-raise it
54
  logger.error(f"Failed to store OTP for key {key}: {e}")
55
+ raise
56
+
57
+ async def delete_cache(key: str) -> None:
58
+ """
59
+ Delete a key from Redis.
60
+
61
+ Args:
62
+ key (str): The key to delete from the cache.
63
+
64
+ Returns:
65
+ None
66
+ """
67
+ try:
68
+ # Construct the Redis key
69
+ redis_key = f"{key}"
70
+
71
+ # Delete the key from Redis
72
+ await redis_client.delete(redis_key)
73
+ logger.info(f"Successfully deleted key {redis_key} from cache.")
74
+ except Exception as e:
75
+ # Log the error and re-raise it
76
+ logger.error(f"Failed to delete key {key} from cache: {e}")
77
  raise
app/routers/router.py CHANGED
@@ -1,7 +1,9 @@
1
  from fastapi import APIRouter
2
  from app.controllers.customer_controller import router as customer
3
  from app.controllers.merchant_controller import router as merchant
 
4
 
5
  router = APIRouter()
6
  router.include_router(customer, prefix="", tags=["Customer"])
7
- router.include_router(merchant, prefix="", tags=["Merchant"])
 
 
1
  from fastapi import APIRouter
2
  from app.controllers.customer_controller import router as customer
3
  from app.controllers.merchant_controller import router as merchant
4
+ from app.controllers.merchant_login_controller import router as merchant_login
5
 
6
  router = APIRouter()
7
  router.include_router(customer, prefix="", tags=["Customer"])
8
+ router.include_router(merchant, prefix="", tags=["Merchant"])
9
+ router.include_router(merchant_login, prefix="", tags=["Merchant Login"])
app/services/merchant_services.py CHANGED
@@ -1,11 +1,11 @@
 
 
 
1
  from app.models.merchant import MerchantRegister
2
  from app.repositories.db_repository import save_merchant, get_merchant_by_name, get_merchant_by_email, get_merchant_by_mobile
3
  from app.utils.merchant_utils import generate_tenant_id
4
- from app.utils.auth_utils import validate_email, validate_mobile
5
- from app.repositories.cache_repository import get_otp, set_otp
6
- import logging
7
- import random
8
-
9
 
10
  logger = logging.getLogger(__name__)
11
 
@@ -62,8 +62,8 @@ async def send_sms_email_verification(email: str, mobile: str):
62
  # Generate a 6-digit OTP
63
  otp = ''.join(random.choices("0123456789", k=6))
64
  # Store OTP in cache with a 15-minute expiry
65
- await set_otp(email, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
66
- await set_otp(mobile, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
67
  # Send the OTP to the email address
68
  # (You would typically use an email service here)
69
  # For demonstration, we'll just log the OTP
@@ -83,8 +83,8 @@ async def verify_otp_and_save_merchant(merchant: MerchantRegister) -> dict:
83
  errors = []
84
 
85
  # Retrieve OTPs from cache
86
- email_otp_data = await get_otp_from_cache(merchant.email)
87
- mobile_otp_data = await get_otp_from_cache(merchant.mobile)
88
 
89
  # Verify email OTP
90
  email_verified = email_otp_data and email_otp_data["otp"] == merchant.email_ver_code
@@ -143,12 +143,114 @@ async def generate_and_send_otp(email: str, mobile: str) -> None:
143
  otp = ''.join(random.choices("0123456789", k=6))
144
 
145
  # Store OTP in cache with a 15-minute expiry
146
- await set_otp(email, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
147
- await set_otp(mobile, {"otp": otp, "expiry_duration": 15 * 60})
148
 
149
  # Log the OTP generation
150
  logger.info("Generated OTP for %s & %s: %s", email, mobile, otp)
151
 
152
  except Exception as e:
153
  logger.error("Failed to generate and send OTP for %s, %s: %s", email, mobile, e)
154
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import random
3
+ from fastapi import HTTPException
4
  from app.models.merchant import MerchantRegister
5
  from app.repositories.db_repository import save_merchant, get_merchant_by_name, get_merchant_by_email, get_merchant_by_mobile
6
  from app.utils.merchant_utils import generate_tenant_id
7
+ from app.utils.auth_utils import validate_email, validate_mobile, generate_tokens
8
+ from app.repositories.cache_repository import get_otp, set_otp, delete_cache
 
 
 
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
62
  # Generate a 6-digit OTP
63
  otp = ''.join(random.choices("0123456789", k=6))
64
  # Store OTP in cache with a 15-minute expiry
65
+ await set_otp(f"otp:reg:{email}", {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
66
+ await set_otp(f"otp:reg:{mobile}", {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
67
  # Send the OTP to the email address
68
  # (You would typically use an email service here)
69
  # For demonstration, we'll just log the OTP
 
83
  errors = []
84
 
85
  # Retrieve OTPs from cache
86
+ email_otp_data = await get_otp(f"otp:reg:{merchant.email}")
87
+ mobile_otp_data = await get_otp(f"otp:reg:{merchant.mobile}")
88
 
89
  # Verify email OTP
90
  email_verified = email_otp_data and email_otp_data["otp"] == merchant.email_ver_code
 
143
  otp = ''.join(random.choices("0123456789", k=6))
144
 
145
  # Store OTP in cache with a 15-minute expiry
146
+ await set_otp(f"otp:reg:{email}", {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
147
+ await set_otp(f"otp:reg:{mobile}", {"otp": otp, "expiry_duration": 15 * 60})
148
 
149
  # Log the OTP generation
150
  logger.info("Generated OTP for %s & %s: %s", email, mobile, otp)
151
 
152
  except Exception as e:
153
  logger.error("Failed to generate and send OTP for %s, %s: %s", email, mobile, e)
154
+ raise
155
+
156
+ async def generate_login_otp_service(identifier: str) -> dict:
157
+ """
158
+ Generate and send login OTP to email or mobile after validating the identifier.
159
+
160
+ Args:
161
+ identifier (str): Email or mobile number.
162
+
163
+ Returns:
164
+ dict: Confirmation message or error message.
165
+ """
166
+ try:
167
+ # Determine if the identifier is an email or mobile number
168
+ if "@" in identifier:
169
+ # Check if the email exists in the database
170
+ merchant = await get_merchant_by_email(identifier)
171
+ if not merchant:
172
+ raise HTTPException(status_code=404, detail="Email not registered as a valid merchant")
173
+ else:
174
+ # Check if the mobile number exists in the database
175
+ merchant = await get_merchant_by_mobile(identifier)
176
+ if not merchant:
177
+ raise HTTPException(status_code=404, detail="Mobile number not registered as a valid merchant")
178
+
179
+ # Generate a 6-digit OTP
180
+ otp = ''.join(random.choices("0123456789", k=6))
181
+
182
+ # Store OTP in cache with a 3-minute expiry
183
+ await set_otp(f"otp:login:{identifier}", {"otp": otp, "expiry_duration": 3 * 60}) # 3 minutes
184
+
185
+ # Simulate sending OTP (replace with actual email/SMS service)
186
+ if "@" in identifier:
187
+ logger.info("Sending OTP to email: %s", identifier)
188
+ else:
189
+ logger.info("Sending OTP to mobile: %s", identifier)
190
+
191
+ return {"message": "OTP sent successfully to the provided identifier."}
192
+
193
+ except HTTPException as e:
194
+ logger.error("Failed to generate login OTP for identifier %s: %s", identifier, e.detail)
195
+ raise e
196
+ except Exception as e:
197
+ logger.error("Unexpected error while generating login OTP for identifier %s: %s", identifier, e)
198
+ raise HTTPException(status_code=500, detail="Failed to generate login OTP") from e
199
+
200
+ async def login_service(identifier: str, otp: str) -> dict:
201
+ """
202
+ Verify OTP and generate access and refresh tokens.
203
+
204
+ Args:
205
+ identifier (str): Email or mobile number.
206
+ otp (str): OTP entered by the user.
207
+
208
+ Returns:
209
+ dict: Access and refresh tokens.
210
+ """
211
+ otp_data = await get_otp(f"otp:login:{identifier}")
212
+ if not otp_data or otp_data.get("otp") != otp:
213
+ raise HTTPException(status_code=400, detail="Invalid or expired OTP")
214
+ # Generate tokens
215
+ tokens = generate_tokens(identifier)
216
+
217
+ await set_otp(f"token:login:access:{identifier}", {"access_token": tokens['access_token'], "expiry_duration": 30 * 60}) # 30 minutes
218
+ await set_otp(f"token:login:refresh:{identifier}", {"refresh_token": tokens['refresh_token'], "expiry_duration": 1 * 24 * 60 * 60})# 1day
219
+
220
+ return tokens
221
+
222
+ async def refresh_token_service(identifier: str, refresh_token: str) -> dict:
223
+ """
224
+ Refresh access and refresh tokens.
225
+
226
+ Args:
227
+ identifier (str): Email or mobile number.
228
+
229
+ Returns:
230
+ dict: New access and refresh tokens.
231
+ """
232
+ token_data = await get_otp(f"token:login:refresh:{identifier}")
233
+
234
+ if not token_data or token_data["refresh_token"] != refresh_token:
235
+ raise HTTPException(status_code=400, detail="Invalid or expired refresh token")
236
+
237
+ tokens = generate_tokens(identifier)
238
+ await set_otp(f"token:login:access:{identifier}", {"access_token": tokens['access_token'], "expiry_duration": 30 * 60}) # 30 minutes
239
+ await set_otp(f"token:login:refresh:{identifier}", {"refresh_token": tokens['refresh_token'], "expiry_duration": 1 * 24 * 60 * 60})# 1day
240
+
241
+ return tokens
242
+
243
+ async def logout_service(identifier: str) -> dict:
244
+ """
245
+ Invalidate access and refresh tokens.
246
+
247
+ Args:
248
+ identifier (str): Email or mobile number.
249
+
250
+ Returns:
251
+ dict: Confirmation message.
252
+ """
253
+ await delete_cache(f"token:login:access:{identifier}")
254
+ await delete_cache(f"token:login:refresh:{identifier}")
255
+
256
+ return {"message": "Successfully logged out."}
app/utils/auth_utils.py CHANGED
@@ -1,5 +1,11 @@
1
  import re
2
  import logging
 
 
 
 
 
 
3
 
4
  def validate_email(email: str) -> bool:
5
  """
@@ -24,4 +30,40 @@ def validate_mobile(mobile: str) -> bool:
24
  Returns:
25
  bool: True if valid, False otherwise.
26
  """
27
- return mobile.isdigit() and len(mobile) == 10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import re
2
  import logging
3
+ from fastapi import HTTPException
4
+ from jose import JWTError, jwt
5
+ from datetime import datetime, timedelta
6
+
7
+ SECRET_KEY = "B@@kMy$er^!(e"
8
+ ALGORITHM = "HS256"
9
 
10
  def validate_email(email: str) -> bool:
11
  """
 
30
  Returns:
31
  bool: True if valid, False otherwise.
32
  """
33
+ return mobile.isdigit() and len(mobile) == 10
34
+
35
+ def generate_tokens(identifier: str) -> dict:
36
+ """
37
+ Generate access and refresh tokens.
38
+
39
+ Args:
40
+ identifier (str): Email or mobile number.
41
+
42
+ Returns:
43
+ dict: Access and refresh tokens.
44
+ """
45
+ access_token_expiry = datetime.utcnow() + timedelta(minutes=30)
46
+ refresh_token_expiry = datetime.utcnow() + timedelta(days=1)
47
+
48
+ access_token = jwt.encode({"sub": identifier, "exp": access_token_expiry}, SECRET_KEY, algorithm=ALGORITHM)
49
+ refresh_token = jwt.encode({"sub": identifier, "exp": refresh_token_expiry}, SECRET_KEY, algorithm=ALGORITHM)
50
+
51
+ return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
52
+
53
+
54
+ def verify_token(token: str) -> dict:
55
+ """
56
+ Verify and decode a JWT token.
57
+
58
+ Args:
59
+ token (str): The JWT token.
60
+
61
+ Returns:
62
+ dict: Decoded token data.
63
+ """
64
+ try:
65
+ return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
66
+ except jwt.ExpiredSignatureError:
67
+ raise HTTPException(status_code=401, detail="Token has expired")
68
+ except jwt.InvalidTokenError:
69
+ raise HTTPException(status_code=401, detail="Invalid token")
requirements.txt CHANGED
@@ -9,4 +9,5 @@ python-jose
9
  passlib
10
  python-multipart
11
  bcrypt
12
- nanoid
 
 
9
  passlib
10
  python-multipart
11
  bcrypt
12
+ nanoid
13
+ jose