Spaces:
Sleeping
Sleeping
Commit ·
4e94087
1
Parent(s): 79d7b1c
notfication ms integration
Browse files- app/auth/controllers/service_partners_router.py +1 -1
- app/auth/controllers/staff_router.py +2 -2
- app/auth/services/customer_auth_service.py +1 -3
- app/auth/services/service_partners_auth_service.py +1 -3
- app/auth/services/staff_auth_service.py +0 -1
- app/core/config.py +4 -12
- app/system_users/services/service.py +0 -2
- app/utils/notification_queue.py +44 -42
app/auth/controllers/service_partners_router.py
CHANGED
|
@@ -31,7 +31,7 @@ async def send_service_partners_otp(request: ServiceProfessionalSendOTPRequest):
|
|
| 31 |
2. Verifies service professional exists with this phone in scm_service_partners collection
|
| 32 |
3. Validates professional is active
|
| 33 |
4. Generates random 6-digit OTP
|
| 34 |
-
5. Sends OTP via
|
| 35 |
6. Stores OTP in Redis with 5-minute expiration
|
| 36 |
|
| 37 |
**Security:**
|
|
|
|
| 31 |
2. Verifies service professional exists with this phone in scm_service_partners collection
|
| 32 |
3. Validates professional is active
|
| 33 |
4. Generates random 6-digit OTP
|
| 34 |
+
5. Sends OTP via notification-ms (WhatsApp/SMS)
|
| 35 |
6. Stores OTP in Redis with 5-minute expiration
|
| 36 |
|
| 37 |
**Security:**
|
app/auth/controllers/staff_router.py
CHANGED
|
@@ -35,7 +35,7 @@ async def send_staff_otp(request: StaffSendOTPRequest):
|
|
| 35 |
2. Verifies staff user exists with this phone
|
| 36 |
3. Validates user is staff role (not admin)
|
| 37 |
4. Generates random 6-digit OTP
|
| 38 |
-
5. Sends OTP via
|
| 39 |
6. Stores OTP in database with 5-minute expiration
|
| 40 |
|
| 41 |
**Security:**
|
|
@@ -123,7 +123,7 @@ async def staff_login_mobile_otp(
|
|
| 123 |
|
| 124 |
**Security:**
|
| 125 |
- Only allows staff/employee roles (not admin/super_admin)
|
| 126 |
-
- OTP validation via
|
| 127 |
- OTP expires in 5 minutes
|
| 128 |
- Maximum 3 verification attempts
|
| 129 |
- One-time use only
|
|
|
|
| 35 |
2. Verifies staff user exists with this phone
|
| 36 |
3. Validates user is staff role (not admin)
|
| 37 |
4. Generates random 6-digit OTP
|
| 38 |
+
5. Sends OTP via notification-ms (WhatsApp/SMS)
|
| 39 |
6. Stores OTP in database with 5-minute expiration
|
| 40 |
|
| 41 |
**Security:**
|
|
|
|
| 123 |
|
| 124 |
**Security:**
|
| 125 |
- Only allows staff/employee roles (not admin/super_admin)
|
| 126 |
+
- OTP validation via notification-ms
|
| 127 |
- OTP expires in 5 minutes
|
| 128 |
- Maximum 3 verification attempts
|
| 129 |
- One-time use only
|
app/auth/services/customer_auth_service.py
CHANGED
|
@@ -55,7 +55,7 @@ class CustomerAuthService:
|
|
| 55 |
|
| 56 |
async def send_otp(self, mobile: str) -> Tuple[bool, str, int]:
|
| 57 |
"""
|
| 58 |
-
Send OTP to customer mobile number via
|
| 59 |
|
| 60 |
Args:
|
| 61 |
mobile: Customer mobile number
|
|
@@ -84,7 +84,6 @@ class CustomerAuthService:
|
|
| 84 |
"expires_at": expires_at.isoformat(),
|
| 85 |
"attempts": 0,
|
| 86 |
"verified": False,
|
| 87 |
-
"wati_message_id": None,
|
| 88 |
"delivery_status": "pending"
|
| 89 |
}
|
| 90 |
|
|
@@ -106,7 +105,6 @@ class CustomerAuthService:
|
|
| 106 |
otp=otp,
|
| 107 |
expiry_minutes=expiry_minutes,
|
| 108 |
merchant_id=merchant_id or "",
|
| 109 |
-
template_name=settings.WATI_OTP_TEMPLATE_NAME,
|
| 110 |
)
|
| 111 |
|
| 112 |
if queued:
|
|
|
|
| 55 |
|
| 56 |
async def send_otp(self, mobile: str) -> Tuple[bool, str, int]:
|
| 57 |
"""
|
| 58 |
+
Send OTP to customer mobile number via notification-ms.
|
| 59 |
|
| 60 |
Args:
|
| 61 |
mobile: Customer mobile number
|
|
|
|
| 84 |
"expires_at": expires_at.isoformat(),
|
| 85 |
"attempts": 0,
|
| 86 |
"verified": False,
|
|
|
|
| 87 |
"delivery_status": "pending"
|
| 88 |
}
|
| 89 |
|
|
|
|
| 105 |
otp=otp,
|
| 106 |
expiry_minutes=expiry_minutes,
|
| 107 |
merchant_id=merchant_id or "",
|
|
|
|
| 108 |
)
|
| 109 |
|
| 110 |
if queued:
|
app/auth/services/service_partners_auth_service.py
CHANGED
|
@@ -71,7 +71,7 @@ class ServiceProfessionalAuthService:
|
|
| 71 |
|
| 72 |
async def send_otp(self, phone: str) -> Tuple[bool, str, int]:
|
| 73 |
"""
|
| 74 |
-
Send OTP to service professional mobile number via
|
| 75 |
|
| 76 |
Args:
|
| 77 |
phone: Service professional mobile number
|
|
@@ -116,7 +116,6 @@ class ServiceProfessionalAuthService:
|
|
| 116 |
"expires_at": expires_at.isoformat(),
|
| 117 |
"attempts": 0,
|
| 118 |
"verified": False,
|
| 119 |
-
"wati_message_id": None,
|
| 120 |
"delivery_status": "pending"
|
| 121 |
}
|
| 122 |
|
|
@@ -135,7 +134,6 @@ class ServiceProfessionalAuthService:
|
|
| 135 |
otp=otp,
|
| 136 |
expiry_minutes=expiry_minutes,
|
| 137 |
merchant_id=merchant_id,
|
| 138 |
-
template_name=settings.WATI_STAFF_OTP_TEMPLATE_NAME,
|
| 139 |
)
|
| 140 |
|
| 141 |
if queued:
|
|
|
|
| 71 |
|
| 72 |
async def send_otp(self, phone: str) -> Tuple[bool, str, int]:
|
| 73 |
"""
|
| 74 |
+
Send OTP to service professional mobile number via notification-ms.
|
| 75 |
|
| 76 |
Args:
|
| 77 |
phone: Service professional mobile number
|
|
|
|
| 116 |
"expires_at": expires_at.isoformat(),
|
| 117 |
"attempts": 0,
|
| 118 |
"verified": False,
|
|
|
|
| 119 |
"delivery_status": "pending"
|
| 120 |
}
|
| 121 |
|
|
|
|
| 134 |
otp=otp,
|
| 135 |
expiry_minutes=expiry_minutes,
|
| 136 |
merchant_id=merchant_id,
|
|
|
|
| 137 |
)
|
| 138 |
|
| 139 |
if queued:
|
app/auth/services/staff_auth_service.py
CHANGED
|
@@ -148,7 +148,6 @@ class StaffAuthService:
|
|
| 148 |
otp=otp,
|
| 149 |
expiry_minutes=expiry_minutes,
|
| 150 |
merchant_id=staff["merchant_id"],
|
| 151 |
-
template_name=settings.WATI_STAFF_OTP_TEMPLATE_NAME,
|
| 152 |
)
|
| 153 |
|
| 154 |
if queued:
|
|
|
|
| 148 |
otp=otp,
|
| 149 |
expiry_minutes=expiry_minutes,
|
| 150 |
merchant_id=staff["merchant_id"],
|
|
|
|
| 151 |
)
|
| 152 |
|
| 153 |
if queued:
|
app/core/config.py
CHANGED
|
@@ -57,18 +57,10 @@ class Settings(BaseSettings):
|
|
| 57 |
TWILIO_AUTH_TOKEN: Optional[str] = os.getenv("TWILIO_AUTH_TOKEN")
|
| 58 |
TWILIO_PHONE_NUMBER: Optional[str] = os.getenv("TWILIO_PHONE_NUMBER")
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# SMTP Configuration (email is routed through notification-ms per-merchant)
|
| 65 |
-
# Kept as optional stubs so existing env files don't cause validation errors
|
| 66 |
-
SMTP_HOST: Optional[str] = None
|
| 67 |
-
SMTP_PORT: int = 587
|
| 68 |
-
SMTP_USERNAME: Optional[str] = None
|
| 69 |
-
SMTP_PASSWORD: Optional[str] = None
|
| 70 |
-
SMTP_FROM_EMAIL: Optional[str] = None
|
| 71 |
-
SMTP_USE_TLS: bool = True
|
| 72 |
|
| 73 |
# Logging
|
| 74 |
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
|
|
|
| 57 |
TWILIO_AUTH_TOKEN: Optional[str] = os.getenv("TWILIO_AUTH_TOKEN")
|
| 58 |
TWILIO_PHONE_NUMBER: Optional[str] = os.getenv("TWILIO_PHONE_NUMBER")
|
| 59 |
|
| 60 |
+
# Notification template names (resolved by notification-ms dispatcher)
|
| 61 |
+
NOTIFICATION_TEMPLATE_OTP_VERIFICATION: str = os.getenv("NOTIFICATION_TEMPLATE_OTP_VERIFICATION", "otp")
|
| 62 |
+
NOTIFICATION_TEMPLATE_CREDENTIALS: str = os.getenv("NOTIFICATION_TEMPLATE_CREDENTIALS", "otp")
|
| 63 |
+
NOTIFICATION_TEMPLATE_PASSWORD_RESET: str = os.getenv("NOTIFICATION_TEMPLATE_PASSWORD_RESET", "otp")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# Logging
|
| 66 |
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
app/system_users/services/service.py
CHANGED
|
@@ -932,7 +932,6 @@ class SystemUserService:
|
|
| 932 |
name=user.first_name or user.username,
|
| 933 |
new_password=reset_link,
|
| 934 |
merchant_id=user.merchant_id or "",
|
| 935 |
-
template_name="password_reset_link",
|
| 936 |
)
|
| 937 |
logger.info(f"Password reset notification queued for {user.phone}")
|
| 938 |
except Exception as nq_err:
|
|
@@ -991,7 +990,6 @@ class SystemUserService:
|
|
| 991 |
otp=otp,
|
| 992 |
expiry_minutes=expiry_minutes,
|
| 993 |
merchant_id=user.merchant_id or "",
|
| 994 |
-
template_name="otp",
|
| 995 |
)
|
| 996 |
|
| 997 |
if queued:
|
|
|
|
| 932 |
name=user.first_name or user.username,
|
| 933 |
new_password=reset_link,
|
| 934 |
merchant_id=user.merchant_id or "",
|
|
|
|
| 935 |
)
|
| 936 |
logger.info(f"Password reset notification queued for {user.phone}")
|
| 937 |
except Exception as nq_err:
|
|
|
|
| 990 |
otp=otp,
|
| 991 |
expiry_minutes=expiry_minutes,
|
| 992 |
merchant_id=user.merchant_id or "",
|
|
|
|
| 993 |
)
|
| 994 |
|
| 995 |
if queued:
|
app/utils/notification_queue.py
CHANGED
|
@@ -3,26 +3,33 @@ Notification queue utility for auth-ms.
|
|
| 3 |
Creates notification docs in MongoDB (notifications collection) and pushes
|
| 4 |
notification_id to Redis queue for the notification-ms worker to consume.
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
Templates are company-level. merchant_name is passed as a variable
|
| 10 |
-
in template_data for rendering (if applicable).
|
| 11 |
"""
|
| 12 |
import json
|
| 13 |
import uuid
|
| 14 |
from datetime import datetime
|
| 15 |
from typing import Optional, Dict, Any, List
|
|
|
|
| 16 |
from app.core.logging import get_logger
|
|
|
|
| 17 |
from app.cache import cache_service
|
| 18 |
from app.nosql import get_database
|
| 19 |
|
| 20 |
logger = get_logger(__name__)
|
| 21 |
|
| 22 |
-
|
| 23 |
COLLECTION = "notifications"
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
class NotificationQueue:
|
| 27 |
"""
|
| 28 |
Enqueues notifications via MongoDB + Redis so notification-ms worker
|
|
@@ -36,9 +43,9 @@ class NotificationQueue:
|
|
| 36 |
channels: List[str],
|
| 37 |
template_name: str,
|
| 38 |
template_data: Dict[str, Any],
|
| 39 |
-
merchant_id: str,
|
| 40 |
merchant_name: str = "",
|
| 41 |
-
priority: str = "
|
| 42 |
source: str = "auth-ms",
|
| 43 |
metadata: Optional[Dict[str, Any]] = None,
|
| 44 |
) -> bool:
|
|
@@ -74,15 +81,16 @@ class NotificationQueue:
|
|
| 74 |
await db[COLLECTION].insert_one(doc)
|
| 75 |
|
| 76 |
redis_client = cache_service.get_client()
|
| 77 |
-
redis_client.lpush(
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
logger.info(
|
| 80 |
"Notification queued",
|
| 81 |
extra={
|
| 82 |
-
"event": "enqueued",
|
| 83 |
"notification_id": notification_id,
|
| 84 |
-
"
|
| 85 |
-
"queue": QUEUE_NAME,
|
| 86 |
"channels": channels,
|
| 87 |
"recipient": recipient,
|
| 88 |
},
|
|
@@ -93,7 +101,7 @@ class NotificationQueue:
|
|
| 93 |
logger.error(
|
| 94 |
"Notification queue failed",
|
| 95 |
exc_info=True,
|
| 96 |
-
extra={"
|
| 97 |
)
|
| 98 |
return False
|
| 99 |
|
|
@@ -106,17 +114,17 @@ class NotificationQueue:
|
|
| 106 |
expiry_minutes: int = 5,
|
| 107 |
merchant_id: str = "",
|
| 108 |
merchant_name: str = "",
|
| 109 |
-
|
| 110 |
-
|
| 111 |
) -> bool:
|
| 112 |
-
"""Queue OTP notification
|
| 113 |
if channels is None:
|
| 114 |
-
channels = [
|
| 115 |
|
| 116 |
return await NotificationQueue._enqueue(
|
| 117 |
recipient=recipient,
|
| 118 |
-
channels=channels,
|
| 119 |
-
template_name=template_name,
|
| 120 |
template_data={
|
| 121 |
"otp": otp,
|
| 122 |
"expiry_minutes": str(expiry_minutes),
|
|
@@ -128,28 +136,27 @@ class NotificationQueue:
|
|
| 128 |
)
|
| 129 |
|
| 130 |
@staticmethod
|
| 131 |
-
async def
|
| 132 |
recipient: str,
|
| 133 |
-
|
| 134 |
username: str,
|
| 135 |
password: str,
|
| 136 |
merchant_id: str = "",
|
| 137 |
merchant_name: str = "",
|
| 138 |
-
|
| 139 |
-
|
| 140 |
) -> bool:
|
| 141 |
-
"""Queue welcome credentials notification
|
| 142 |
if channels is None:
|
| 143 |
-
channels = [
|
| 144 |
|
| 145 |
return await NotificationQueue._enqueue(
|
| 146 |
recipient=recipient,
|
| 147 |
-
channels=channels,
|
| 148 |
-
template_name=template_name,
|
| 149 |
template_data={
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"password": password,
|
| 153 |
"merchant_name": merchant_name,
|
| 154 |
},
|
| 155 |
merchant_id=merchant_id,
|
|
@@ -164,25 +171,20 @@ class NotificationQueue:
|
|
| 164 |
new_password: str,
|
| 165 |
merchant_id: str = "",
|
| 166 |
merchant_name: str = "",
|
| 167 |
-
|
| 168 |
-
|
| 169 |
) -> bool:
|
| 170 |
-
"""Queue password reset notification
|
| 171 |
-
|
| 172 |
-
Uses the 'otp' template key (maps to staff_otp_login in WATI and sms_otp_temp
|
| 173 |
-
for SMS via merchant settings) with new_password as the 'otp' parameter.
|
| 174 |
-
The dispatcher resolves the correct per-channel template from merchant settings.
|
| 175 |
-
"""
|
| 176 |
if channels is None:
|
| 177 |
-
channels = [
|
| 178 |
|
| 179 |
return await NotificationQueue._enqueue(
|
| 180 |
recipient=recipient,
|
| 181 |
-
channels=channels,
|
| 182 |
-
template_name=template_name,
|
| 183 |
template_data={
|
| 184 |
"otp": new_password,
|
| 185 |
-
"expiry_minutes": "0",
|
| 186 |
"merchant_name": merchant_name,
|
| 187 |
},
|
| 188 |
merchant_id=merchant_id,
|
|
|
|
| 3 |
Creates notification docs in MongoDB (notifications collection) and pushes
|
| 4 |
notification_id to Redis queue for the notification-ms worker to consume.
|
| 5 |
|
| 6 |
+
auth-ms does NOT handle WATI/SMTP directly — all delivery is delegated to
|
| 7 |
+
notification-ms which resolves per-merchant channel credentials.
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
import json
|
| 10 |
import uuid
|
| 11 |
from datetime import datetime
|
| 12 |
from typing import Optional, Dict, Any, List
|
| 13 |
+
from enum import Enum
|
| 14 |
from app.core.logging import get_logger
|
| 15 |
+
from app.core.config import settings
|
| 16 |
from app.cache import cache_service
|
| 17 |
from app.nosql import get_database
|
| 18 |
|
| 19 |
logger = get_logger(__name__)
|
| 20 |
|
| 21 |
+
NOTIFICATION_QUEUE_NAME = "notifications:queue"
|
| 22 |
COLLECTION = "notifications"
|
| 23 |
|
| 24 |
|
| 25 |
+
class NotificationChannel(str, Enum):
|
| 26 |
+
"""Supported notification channels."""
|
| 27 |
+
WHATSAPP = "whatsapp"
|
| 28 |
+
SMS = "sms"
|
| 29 |
+
EMAIL = "email"
|
| 30 |
+
PUSH = "push"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
class NotificationQueue:
|
| 34 |
"""
|
| 35 |
Enqueues notifications via MongoDB + Redis so notification-ms worker
|
|
|
|
| 43 |
channels: List[str],
|
| 44 |
template_name: str,
|
| 45 |
template_data: Dict[str, Any],
|
| 46 |
+
merchant_id: str = "",
|
| 47 |
merchant_name: str = "",
|
| 48 |
+
priority: str = "normal",
|
| 49 |
source: str = "auth-ms",
|
| 50 |
metadata: Optional[Dict[str, Any]] = None,
|
| 51 |
) -> bool:
|
|
|
|
| 81 |
await db[COLLECTION].insert_one(doc)
|
| 82 |
|
| 83 |
redis_client = cache_service.get_client()
|
| 84 |
+
redis_client.lpush(
|
| 85 |
+
NOTIFICATION_QUEUE_NAME,
|
| 86 |
+
json.dumps({"notification_id": notification_id}),
|
| 87 |
+
)
|
| 88 |
|
| 89 |
logger.info(
|
| 90 |
"Notification queued",
|
| 91 |
extra={
|
|
|
|
| 92 |
"notification_id": notification_id,
|
| 93 |
+
"template": template_name,
|
|
|
|
| 94 |
"channels": channels,
|
| 95 |
"recipient": recipient,
|
| 96 |
},
|
|
|
|
| 101 |
logger.error(
|
| 102 |
"Notification queue failed",
|
| 103 |
exc_info=True,
|
| 104 |
+
extra={"template": template_name, "recipient": recipient},
|
| 105 |
)
|
| 106 |
return False
|
| 107 |
|
|
|
|
| 114 |
expiry_minutes: int = 5,
|
| 115 |
merchant_id: str = "",
|
| 116 |
merchant_name: str = "",
|
| 117 |
+
channels: Optional[List[NotificationChannel]] = None,
|
| 118 |
+
template_name: Optional[str] = None,
|
| 119 |
) -> bool:
|
| 120 |
+
"""Queue OTP notification. Delivery handled by notification-ms."""
|
| 121 |
if channels is None:
|
| 122 |
+
channels = [NotificationChannel.WHATSAPP, NotificationChannel.SMS]
|
| 123 |
|
| 124 |
return await NotificationQueue._enqueue(
|
| 125 |
recipient=recipient,
|
| 126 |
+
channels=[ch.value for ch in channels],
|
| 127 |
+
template_name=template_name or settings.NOTIFICATION_TEMPLATE_OTP_VERIFICATION,
|
| 128 |
template_data={
|
| 129 |
"otp": otp,
|
| 130 |
"expiry_minutes": str(expiry_minutes),
|
|
|
|
| 136 |
)
|
| 137 |
|
| 138 |
@staticmethod
|
| 139 |
+
async def send_credentials(
|
| 140 |
recipient: str,
|
| 141 |
+
name: str,
|
| 142 |
username: str,
|
| 143 |
password: str,
|
| 144 |
merchant_id: str = "",
|
| 145 |
merchant_name: str = "",
|
| 146 |
+
channels: Optional[List[NotificationChannel]] = None,
|
| 147 |
+
template_name: Optional[str] = None,
|
| 148 |
) -> bool:
|
| 149 |
+
"""Queue welcome credentials notification. Delivery handled by notification-ms."""
|
| 150 |
if channels is None:
|
| 151 |
+
channels = [NotificationChannel.WHATSAPP, NotificationChannel.SMS]
|
| 152 |
|
| 153 |
return await NotificationQueue._enqueue(
|
| 154 |
recipient=recipient,
|
| 155 |
+
channels=[ch.value for ch in channels],
|
| 156 |
+
template_name=template_name or settings.NOTIFICATION_TEMPLATE_CREDENTIALS,
|
| 157 |
template_data={
|
| 158 |
+
"otp": password,
|
| 159 |
+
"expiry_minutes": "0",
|
|
|
|
| 160 |
"merchant_name": merchant_name,
|
| 161 |
},
|
| 162 |
merchant_id=merchant_id,
|
|
|
|
| 171 |
new_password: str,
|
| 172 |
merchant_id: str = "",
|
| 173 |
merchant_name: str = "",
|
| 174 |
+
channels: Optional[List[NotificationChannel]] = None,
|
| 175 |
+
template_name: Optional[str] = None,
|
| 176 |
) -> bool:
|
| 177 |
+
"""Queue password reset notification. Delivery handled by notification-ms."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
if channels is None:
|
| 179 |
+
channels = [NotificationChannel.WHATSAPP, NotificationChannel.SMS]
|
| 180 |
|
| 181 |
return await NotificationQueue._enqueue(
|
| 182 |
recipient=recipient,
|
| 183 |
+
channels=[ch.value for ch in channels],
|
| 184 |
+
template_name=template_name or settings.NOTIFICATION_TEMPLATE_PASSWORD_RESET,
|
| 185 |
template_data={
|
| 186 |
"otp": new_password,
|
| 187 |
+
"expiry_minutes": "0",
|
| 188 |
"merchant_name": merchant_name,
|
| 189 |
},
|
| 190 |
merchant_id=merchant_id,
|