MukeshKapoor25 commited on
Commit
42e40ce
Β·
1 Parent(s): 7426dce

feat(customer_auth): Implement OTP-based customer authentication system

Browse files

- Add customer authentication schemas with OTP request/response models
- Implement customer auth service with OTP generation, verification, and JWT token creation
- Create customer auth dependency for JWT validation and customer extraction
- Add customer database initialization with scm_customers and customer_otps collections
- Integrate customer auth router into main application
- Add comprehensive documentation for customer auth implementation and API endpoints
- Include mobile app integration example with JavaScript client code
- Add setup and test scripts for customer authentication workflow
- Implement security features including 6-digit OTP, 5-minute expiration, and rate limiting
- Support new customer registration and existing customer login flows

CUSTOMER_AUTH_IMPLEMENTATION.md ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Customer Authentication Implementation
2
+
3
+ ## Overview
4
+
5
+ This document describes the implementation of OTP-based customer authentication in the Auth microservice. The system provides secure mobile-based authentication for customers using SMS OTP verification.
6
+
7
+ ## Architecture
8
+
9
+ ### Components
10
+
11
+ 1. **Customer Auth Schemas** (`app/auth/schemas/customer_auth.py`)
12
+ - Request/response models for OTP operations
13
+ - Input validation for mobile numbers and OTP codes
14
+
15
+ 2. **Customer Auth Service** (`app/auth/services/customer_auth_service.py`)
16
+ - Core business logic for OTP generation and verification
17
+ - Customer creation and management
18
+ - JWT token generation for customers
19
+
20
+ 3. **Customer Auth Dependencies** (`app/dependencies/customer_auth.py`)
21
+ - Authentication middleware for customer endpoints
22
+ - JWT token validation and customer extraction
23
+
24
+ 4. **Database Collections**
25
+ - `scm_customers`: Customer profile data
26
+ - `customer_otps`: OTP storage with TTL expiration
27
+
28
+ ## API Endpoints
29
+
30
+ ### 1. Send OTP
31
+ ```http
32
+ POST /auth/customer/send-otp
33
+ Content-Type: application/json
34
+
35
+ {
36
+ "mobile": "+919999999999"
37
+ }
38
+ ```
39
+
40
+ **Response:**
41
+ ```json
42
+ {
43
+ "success": true,
44
+ "message": "OTP sent successfully",
45
+ "expires_in": 300
46
+ }
47
+ ```
48
+
49
+ ### 2. Verify OTP
50
+ ```http
51
+ POST /auth/customer/verify-otp
52
+ Content-Type: application/json
53
+
54
+ {
55
+ "mobile": "+919999999999",
56
+ "otp": "123456"
57
+ }
58
+ ```
59
+
60
+ **Response:**
61
+ ```json
62
+ {
63
+ "access_token": "jwt_token_here",
64
+ "customer_id": "uuid",
65
+ "is_new_customer": false,
66
+ "token_type": "bearer",
67
+ "expires_in": 86400
68
+ }
69
+ ```
70
+
71
+ ### 3. Get Customer Profile
72
+ ```http
73
+ GET /auth/customer/me
74
+ Authorization: Bearer <access_token>
75
+ ```
76
+
77
+ **Response:**
78
+ ```json
79
+ {
80
+ "customer_id": "uuid",
81
+ "mobile": "+919999999999",
82
+ "merchant_id": null,
83
+ "type": "customer"
84
+ }
85
+ ```
86
+
87
+ ### 4. Customer Logout
88
+ ```http
89
+ POST /auth/customer/logout
90
+ Authorization: Bearer <access_token>
91
+ ```
92
+
93
+ **Response:**
94
+ ```json
95
+ {
96
+ "success": true,
97
+ "message": "Customer logged out successfully"
98
+ }
99
+ ```
100
+
101
+ ## Database Schema
102
+
103
+ ### scm_customers Collection
104
+ ```javascript
105
+ {
106
+ "_id": ObjectId,
107
+ "customer_id": "uuid", // Unique customer identifier
108
+ "phone": "+919999999999", // Mobile number (unique)
109
+ "name": "Customer Name", // Full name (optional)
110
+ "email": "email@example.com", // Email address (optional)
111
+ "status": "active", // active | inactive
112
+ "merchant_id": "uuid", // Associated merchant (optional)
113
+ "notes": "Registration notes", // Free-form notes
114
+ "created_at": ISODate,
115
+ "updated_at": ISODate,
116
+ "last_login_at": ISODate
117
+ }
118
+ ```
119
+
120
+ **Indexes:**
121
+ - `phone` (unique, sparse)
122
+ - `customer_id` (unique)
123
+ - `merchant_id` (sparse)
124
+ - `status`
125
+ - `{merchant_id: 1, status: 1}` (compound)
126
+
127
+ ### customer_otps Collection
128
+ ```javascript
129
+ {
130
+ "_id": ObjectId,
131
+ "mobile": "+919999999999", // Mobile number (unique)
132
+ "otp": "123456", // 6-digit OTP code
133
+ "created_at": ISODate,
134
+ "expires_at": ISODate, // TTL expiration
135
+ "attempts": 0, // Verification attempts
136
+ "verified": false // Whether OTP was used
137
+ }
138
+ ```
139
+
140
+ **Indexes:**
141
+ - `mobile` (unique)
142
+ - `expires_at` (TTL, expireAfterSeconds: 0)
143
+ - `created_at`
144
+
145
+ ## Security Features
146
+
147
+ ### OTP Security
148
+ - **6-digit random OTP** generated using `secrets.randbelow()`
149
+ - **5-minute expiration** with automatic cleanup
150
+ - **Maximum 3 verification attempts** per OTP
151
+ - **One-time use** - OTP marked as verified after successful use
152
+ - **Rate limiting** - New OTP request replaces previous one
153
+
154
+ ### JWT Token Security
155
+ - **Customer-specific tokens** with `type: "customer"` claim
156
+ - **Standard expiration** based on system configuration
157
+ - **Stateless authentication** - no server-side session storage
158
+ - **Secure payload** includes customer_id, mobile, and merchant_id
159
+
160
+ ### Input Validation
161
+ - **Mobile number format** validation using regex
162
+ - **International format** support (+country_code)
163
+ - **OTP format** validation (digits only, 4-6 characters)
164
+ - **Pydantic validation** for all request/response models
165
+
166
+ ## Customer Lifecycle
167
+
168
+ ### New Customer Registration
169
+ 1. Customer enters mobile number
170
+ 2. System sends OTP via SMS
171
+ 3. Customer verifies OTP
172
+ 4. New customer record created in `scm_customers`
173
+ 5. JWT token issued with `is_new_customer: true`
174
+ 6. Customer can complete profile later
175
+
176
+ ### Existing Customer Login
177
+ 1. Customer enters mobile number
178
+ 2. System sends OTP via SMS
179
+ 3. Customer verifies OTP
180
+ 4. Existing customer record updated with `last_login_at`
181
+ 5. JWT token issued with `is_new_customer: false`
182
+
183
+ ### Customer-Merchant Association
184
+ - Initially, customers have `merchant_id: null`
185
+ - Association happens when customer makes first purchase
186
+ - Allows customers to shop across multiple merchants
187
+ - Merchant-specific data isolation maintained
188
+
189
+ ## Error Handling
190
+
191
+ ### OTP Errors
192
+ - **Invalid mobile format**: 422 Unprocessable Entity
193
+ - **OTP not found**: 401 Unauthorized
194
+ - **Expired OTP**: 401 Unauthorized
195
+ - **Already used OTP**: 401 Unauthorized
196
+ - **Too many attempts**: 429 Too Many Requests
197
+ - **Invalid OTP**: 401 Unauthorized
198
+
199
+ ### Authentication Errors
200
+ - **Missing token**: 401 Unauthorized
201
+ - **Invalid token**: 401 Unauthorized
202
+ - **Expired token**: 401 Unauthorized
203
+ - **Non-customer token**: 403 Forbidden
204
+
205
+ ## Testing
206
+
207
+ ### Manual Testing
208
+ Use the provided test script:
209
+ ```bash
210
+ cd cuatrolabs-auth-ms
211
+ python test_customer_auth.py
212
+ ```
213
+
214
+ ### Test Scenarios
215
+ 1. **Happy Path**: Send OTP β†’ Verify OTP β†’ Access protected endpoints
216
+ 2. **Error Cases**: Invalid mobile, wrong OTP, expired OTP, missing token
217
+ 3. **Security**: Multiple attempts, token validation, logout
218
+
219
+ ### Test Mobile Numbers
220
+ - Use `+919999999999` for testing
221
+ - OTP is logged in application logs for development
222
+ - In production, integrate with SMS service provider
223
+
224
+ ## Integration Points
225
+
226
+ ### SMS Service Integration
227
+ Currently, OTPs are logged for testing. To integrate with SMS service:
228
+
229
+ 1. Add SMS service configuration to `settings.py`
230
+ 2. Update `CustomerAuthService.send_otp()` method
231
+ 3. Replace logging with actual SMS API call
232
+ 4. Handle SMS service errors appropriately
233
+
234
+ ### Frontend Integration
235
+ ```javascript
236
+ // Send OTP
237
+ const sendOTP = async (mobile) => {
238
+ const response = await fetch('/auth/customer/send-otp', {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify({ mobile })
242
+ });
243
+ return response.json();
244
+ };
245
+
246
+ // Verify OTP
247
+ const verifyOTP = async (mobile, otp) => {
248
+ const response = await fetch('/auth/customer/verify-otp', {
249
+ method: 'POST',
250
+ headers: { 'Content-Type': 'application/json' },
251
+ body: JSON.stringify({ mobile, otp })
252
+ });
253
+ return response.json();
254
+ };
255
+
256
+ // Store token and customer data
257
+ const { access_token, customer_id, is_new_customer } = await verifyOTP(mobile, otp);
258
+ localStorage.setItem('access_token', access_token);
259
+ localStorage.setItem('customer_id', customer_id);
260
+
261
+ // Use token for API calls
262
+ const headers = {
263
+ 'Authorization': `Bearer ${access_token}`,
264
+ 'Content-Type': 'application/json'
265
+ };
266
+ ```
267
+
268
+ ### Session Management
269
+ ```javascript
270
+ // Check if user is logged in
271
+ const isLoggedIn = () => {
272
+ const token = localStorage.getItem('access_token');
273
+ return token && !isTokenExpired(token);
274
+ };
275
+
276
+ // Auto-restore session
277
+ const restoreSession = async () => {
278
+ if (isLoggedIn()) {
279
+ // Redirect to main app
280
+ window.location.href = '/(tabs)/index';
281
+ } else {
282
+ // Stay on auth screen
283
+ clearSession();
284
+ }
285
+ };
286
+
287
+ // Logout
288
+ const logout = async () => {
289
+ const token = localStorage.getItem('access_token');
290
+ if (token) {
291
+ await fetch('/auth/customer/logout', {
292
+ method: 'POST',
293
+ headers: { 'Authorization': `Bearer ${token}` }
294
+ });
295
+ }
296
+ clearSession();
297
+ };
298
+
299
+ const clearSession = () => {
300
+ localStorage.removeItem('access_token');
301
+ localStorage.removeItem('customer_id');
302
+ // Redirect to auth screen
303
+ };
304
+ ```
305
+
306
+ ## Deployment Considerations
307
+
308
+ ### Environment Variables
309
+ ```bash
310
+ # MongoDB connection
311
+ MONGODB_URL=mongodb://localhost:27017
312
+ MONGODB_DB_NAME=cuatrolabs_auth
313
+
314
+ # JWT configuration
315
+ JWT_SECRET_KEY=your-secret-key
316
+ TOKEN_EXPIRATION_HOURS=24
317
+
318
+ # SMS service (when integrated)
319
+ SMS_API_KEY=your-sms-api-key
320
+ SMS_API_URL=https://api.sms-provider.com
321
+ ```
322
+
323
+ ### Production Checklist
324
+ - [ ] Configure SMS service integration
325
+ - [ ] Set up proper JWT secret key
326
+ - [ ] Configure CORS origins
327
+ - [ ] Set up monitoring and logging
328
+ - [ ] Configure rate limiting
329
+ - [ ] Set up database backups
330
+ - [ ] Test OTP delivery in production
331
+ - [ ] Verify token expiration handling
332
+
333
+ ## Monitoring and Logging
334
+
335
+ ### Key Metrics
336
+ - OTP send success/failure rates
337
+ - OTP verification success/failure rates
338
+ - Customer registration rates
339
+ - Authentication token usage
340
+ - Failed authentication attempts
341
+
342
+ ### Log Events
343
+ - `customer_otp_sent`: OTP generation and sending
344
+ - `customer_otp_verified`: Successful OTP verification
345
+ - `customer_login_success`: Customer authentication success
346
+ - `customer_logout`: Customer logout events
347
+ - `customer_auth_failed`: Authentication failures
348
+
349
+ ### Alerts
350
+ - High OTP failure rates
351
+ - Unusual authentication patterns
352
+ - SMS service failures
353
+ - Database connection issues
354
+
355
+ ## Future Enhancements
356
+
357
+ ### Planned Features
358
+ 1. **Email OTP**: Alternative to SMS for customers with email
359
+ 2. **Social Login**: Google, Facebook, Apple authentication
360
+ 3. **Biometric Auth**: Fingerprint, Face ID support
361
+ 4. **Multi-factor Auth**: Additional security layer
362
+ 5. **Customer Profiles**: Extended profile management
363
+ 6. **Merchant Association**: Customer-merchant relationship management
364
+
365
+ ### Performance Optimizations
366
+ 1. **OTP Caching**: Redis for OTP storage
367
+ 2. **Token Blacklisting**: Revoked token management
368
+ 3. **Rate Limiting**: Advanced rate limiting per customer
369
+ 4. **SMS Queuing**: Async SMS delivery
370
+ 5. **Database Sharding**: Scale customer data storage
371
+
372
+ ## Support and Troubleshooting
373
+
374
+ ### Common Issues
375
+ 1. **OTP not received**: Check SMS service logs, verify mobile format
376
+ 2. **Token expired**: Implement token refresh mechanism
377
+ 3. **Customer not found**: Check database connectivity and indexes
378
+ 4. **Invalid mobile format**: Verify regex pattern and validation
379
+
380
+ ### Debug Endpoints
381
+ - `GET /health`: Service health check
382
+ - `GET /debug/db-status`: Database connection status
383
+
384
+ ### Contact
385
+ For technical support or questions about this implementation, contact the development team.
app/auth/controllers/router.py CHANGED
@@ -12,6 +12,14 @@ from app.dependencies.auth import get_system_user_service, get_current_user
12
  from app.system_users.models.model import SystemUserModel
13
  from app.core.config import settings
14
  from app.core.logging import get_logger
 
 
 
 
 
 
 
 
15
 
16
  logger = get_logger(__name__)
17
 
@@ -613,6 +621,230 @@ async def get_password_rotation_status(
613
  )
614
 
615
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  @router.post("/password-rotation-policy")
617
  async def get_password_rotation_policy(
618
  user_service: SystemUserService = Depends(get_system_user_service)
 
12
  from app.system_users.models.model import SystemUserModel
13
  from app.core.config import settings
14
  from app.core.logging import get_logger
15
+ from app.auth.schemas.customer_auth import (
16
+ SendOTPRequest,
17
+ VerifyOTPRequest,
18
+ CustomerAuthResponse,
19
+ SendOTPResponse
20
+ )
21
+ from app.auth.services.customer_auth_service import CustomerAuthService
22
+ from app.dependencies.customer_auth import get_current_customer, CustomerUser
23
 
24
  logger = get_logger(__name__)
25
 
 
621
  )
622
 
623
 
624
+ @router.post("/customer/send-otp", response_model=SendOTPResponse)
625
+ async def send_customer_otp(request: SendOTPRequest):
626
+ """
627
+ Send OTP to customer mobile number for authentication.
628
+
629
+ - **mobile**: Customer mobile number in international format (e.g., +919999999999)
630
+
631
+ **Process:**
632
+ 1. Validates mobile number format
633
+ 2. Generates 6-digit OTP
634
+ 3. Stores OTP with 5-minute expiration
635
+ 4. Sends OTP via SMS (currently logged for testing)
636
+
637
+ **Rate Limiting:**
638
+ - Maximum 3 verification attempts per OTP
639
+ - OTP expires after 5 minutes
640
+ - New OTP request replaces previous one
641
+
642
+ Raises:
643
+ HTTPException: 400 - Invalid mobile number format
644
+ HTTPException: 500 - Failed to send OTP
645
+ """
646
+ try:
647
+ customer_auth_service = CustomerAuthService()
648
+ success, message, expires_in = await customer_auth_service.send_otp(request.mobile)
649
+
650
+ if not success:
651
+ raise HTTPException(
652
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
653
+ detail=message
654
+ )
655
+
656
+ logger.info(f"OTP sent to customer mobile: {request.mobile}")
657
+
658
+ return SendOTPResponse(
659
+ success=True,
660
+ message=message,
661
+ expires_in=expires_in
662
+ )
663
+
664
+ except HTTPException:
665
+ raise
666
+ except Exception as e:
667
+ logger.error(f"Unexpected error sending OTP to {request.mobile}: {str(e)}", exc_info=True)
668
+ raise HTTPException(
669
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
670
+ detail="An unexpected error occurred while sending OTP"
671
+ )
672
+
673
+
674
+ @router.post("/customer/verify-otp", response_model=CustomerAuthResponse)
675
+ async def verify_customer_otp(request: VerifyOTPRequest):
676
+ """
677
+ Verify OTP and authenticate customer.
678
+
679
+ - **mobile**: Customer mobile number used for OTP
680
+ - **otp**: 6-digit OTP code received via SMS
681
+
682
+ **Process:**
683
+ 1. Validates OTP against stored record
684
+ 2. Checks expiration and attempt limits
685
+ 3. Finds existing customer or creates new one
686
+ 4. Generates JWT access token
687
+ 5. Returns customer authentication data
688
+
689
+ **Customer Creation:**
690
+ - New customers are automatically created on first successful OTP verification
691
+ - Customer profile can be completed later via separate endpoints
692
+ - Initial customer record contains only mobile number
693
+
694
+ **Session Handling:**
695
+ - Returns JWT access token for API authentication
696
+ - Token includes customer_id and mobile number
697
+ - Token expires based on system configuration (default: 24 hours)
698
+
699
+ Raises:
700
+ HTTPException: 400 - Invalid OTP format or mobile number
701
+ HTTPException: 401 - Invalid, expired, or already used OTP
702
+ HTTPException: 429 - Too many attempts
703
+ HTTPException: 500 - Server error
704
+ """
705
+ try:
706
+ customer_auth_service = CustomerAuthService()
707
+ customer_data, message = await customer_auth_service.verify_otp(
708
+ request.mobile,
709
+ request.otp
710
+ )
711
+
712
+ if not customer_data:
713
+ # Determine appropriate status code based on message
714
+ if "expired" in message.lower():
715
+ status_code = status.HTTP_401_UNAUTHORIZED
716
+ elif "too many attempts" in message.lower():
717
+ status_code = status.HTTP_429_TOO_MANY_REQUESTS
718
+ else:
719
+ status_code = status.HTTP_401_UNAUTHORIZED
720
+
721
+ raise HTTPException(
722
+ status_code=status_code,
723
+ detail=message
724
+ )
725
+
726
+ # Create JWT token for customer
727
+ access_token = customer_auth_service.create_customer_token(customer_data)
728
+
729
+ logger.info(
730
+ f"Customer authenticated successfully: {customer_data['customer_id']}",
731
+ extra={
732
+ "event": "customer_login_success",
733
+ "customer_id": customer_data["customer_id"],
734
+ "mobile": request.mobile,
735
+ "is_new_customer": customer_data["is_new_customer"]
736
+ }
737
+ )
738
+
739
+ return CustomerAuthResponse(
740
+ access_token=access_token,
741
+ customer_id=customer_data["customer_id"],
742
+ is_new_customer=customer_data["is_new_customer"],
743
+ expires_in=settings.TOKEN_EXPIRATION_HOURS * 3600
744
+ )
745
+
746
+ except HTTPException:
747
+ raise
748
+ except Exception as e:
749
+ logger.error(f"Unexpected error verifying OTP for {request.mobile}: {str(e)}", exc_info=True)
750
+ raise HTTPException(
751
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
752
+ detail="An unexpected error occurred during OTP verification"
753
+ )
754
+
755
+
756
+ @router.get("/customer/me")
757
+ async def get_customer_profile(
758
+ current_customer: CustomerUser = Depends(get_current_customer)
759
+ ):
760
+ """
761
+ Get current customer profile information.
762
+
763
+ Requires customer JWT token in Authorization header (Bearer token).
764
+
765
+ **Returns:**
766
+ - **customer_id**: Unique customer identifier
767
+ - **mobile**: Customer mobile number
768
+ - **merchant_id**: Associated merchant (if any)
769
+ - **type**: Always "customer"
770
+
771
+ **Usage:**
772
+ - Use this endpoint to verify customer authentication
773
+ - Get basic customer information for app initialization
774
+ - Check if customer is associated with a merchant
775
+
776
+ Raises:
777
+ HTTPException: 401 - Invalid or expired token
778
+ HTTPException: 403 - Not a customer token
779
+ """
780
+ try:
781
+ logger.info(f"Customer profile accessed: {current_customer.customer_id}")
782
+
783
+ return {
784
+ "customer_id": current_customer.customer_id,
785
+ "mobile": current_customer.mobile,
786
+ "merchant_id": current_customer.merchant_id,
787
+ "type": current_customer.type
788
+ }
789
+
790
+ except Exception as e:
791
+ logger.error(f"Error getting customer profile: {str(e)}", exc_info=True)
792
+ raise HTTPException(
793
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
794
+ detail="Failed to get customer profile"
795
+ )
796
+
797
+
798
+ @router.post("/customer/logout")
799
+ async def logout_customer(
800
+ current_customer: CustomerUser = Depends(get_current_customer)
801
+ ):
802
+ """
803
+ Logout current customer.
804
+
805
+ Requires customer JWT token in Authorization header (Bearer token).
806
+
807
+ **Process:**
808
+ - Validates customer JWT token
809
+ - Records logout event for audit purposes
810
+ - Returns success confirmation
811
+
812
+ **Note:** Since we're using stateless JWT tokens, the client is responsible for:
813
+ - Removing the token from local storage
814
+ - Clearing any cached customer data
815
+ - Redirecting to login screen
816
+
817
+ **Security:**
818
+ - Logs logout event with customer information
819
+ - Provides audit trail for customer sessions
820
+
821
+ Raises:
822
+ HTTPException: 401 - Invalid or expired token
823
+ HTTPException: 403 - Not a customer token
824
+ """
825
+ try:
826
+ logger.info(
827
+ f"Customer logged out: {current_customer.customer_id}",
828
+ extra={
829
+ "event": "customer_logout",
830
+ "customer_id": current_customer.customer_id,
831
+ "mobile": current_customer.mobile
832
+ }
833
+ )
834
+
835
+ return {
836
+ "success": True,
837
+ "message": "Customer logged out successfully"
838
+ }
839
+
840
+ except Exception as e:
841
+ logger.error(f"Error during customer logout: {str(e)}", exc_info=True)
842
+ raise HTTPException(
843
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
844
+ detail="An unexpected error occurred during logout"
845
+ )
846
+
847
+
848
  @router.post("/password-rotation-policy")
849
  async def get_password_rotation_policy(
850
  user_service: SystemUserService = Depends(get_system_user_service)
app/auth/db_init_customer.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database initialization for customer authentication collections.
3
+ """
4
+ from motor.motor_asyncio import AsyncIOMotorDatabase
5
+ from app.nosql import get_database
6
+ from app.core.logging import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ async def init_customer_auth_collections():
12
+ """Initialize collections and indexes for customer authentication."""
13
+ try:
14
+ db: AsyncIOMotorDatabase = get_database()
15
+
16
+ # Create indexes for scm_customers collection
17
+ customers_collection = db.scm_customers
18
+
19
+ # Index on phone for fast lookup
20
+ await customers_collection.create_index("phone", unique=True, sparse=True)
21
+
22
+ # Index on customer_id for fast lookup
23
+ await customers_collection.create_index("customer_id", unique=True)
24
+
25
+ # Index on merchant_id for merchant-specific queries
26
+ await customers_collection.create_index("merchant_id", sparse=True)
27
+
28
+ # Index on status for filtering
29
+ await customers_collection.create_index("status")
30
+
31
+ # Compound index for merchant + status queries
32
+ await customers_collection.create_index([("merchant_id", 1), ("status", 1)])
33
+
34
+ logger.info("βœ… scm_customers collection indexes created")
35
+
36
+ # Create indexes for customer_otps collection
37
+ otps_collection = db.customer_otps
38
+
39
+ # Index on mobile for fast lookup
40
+ await otps_collection.create_index("mobile", unique=True)
41
+
42
+ # TTL index on expires_at for automatic cleanup
43
+ await otps_collection.create_index("expires_at", expireAfterSeconds=0)
44
+
45
+ # Index on created_at for cleanup queries
46
+ await otps_collection.create_index("created_at")
47
+
48
+ logger.info("βœ… customer_otps collection indexes created")
49
+
50
+ logger.info("πŸŽ‰ Customer authentication database initialization completed")
51
+
52
+ except Exception as e:
53
+ logger.error(f"❌ Error initializing customer auth collections: {str(e)}", exc_info=True)
54
+ raise
55
+
56
+
57
+ if __name__ == "__main__":
58
+ import asyncio
59
+ asyncio.run(init_customer_auth_collections())
app/auth/schemas/customer_auth.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Customer authentication schemas for OTP-based login.
3
+ """
4
+ from typing import Optional
5
+ from pydantic import BaseModel, Field, field_validator
6
+ import re
7
+
8
+ PHONE_REGEX = re.compile(r"^\+?[0-9\-\s]{8,20}$")
9
+
10
+
11
+ class SendOTPRequest(BaseModel):
12
+ """Request schema for sending OTP to customer."""
13
+ mobile: str = Field(..., min_length=8, max_length=20, description="Mobile number with country code")
14
+
15
+ @field_validator("mobile")
16
+ @classmethod
17
+ def validate_mobile(cls, v: str) -> str:
18
+ if not PHONE_REGEX.match(v):
19
+ raise ValueError("Invalid mobile number format; use international format like +919999999999")
20
+ return v
21
+
22
+
23
+ class VerifyOTPRequest(BaseModel):
24
+ """Request schema for verifying OTP."""
25
+ mobile: str = Field(..., min_length=8, max_length=20, description="Mobile number with country code")
26
+ otp: str = Field(..., min_length=4, max_length=6, description="OTP code")
27
+
28
+ @field_validator("mobile")
29
+ @classmethod
30
+ def validate_mobile(cls, v: str) -> str:
31
+ if not PHONE_REGEX.match(v):
32
+ raise ValueError("Invalid mobile number format; use international format like +919999999999")
33
+ return v
34
+
35
+ @field_validator("otp")
36
+ @classmethod
37
+ def validate_otp(cls, v: str) -> str:
38
+ if not v.isdigit():
39
+ raise ValueError("OTP must contain only digits")
40
+ return v
41
+
42
+
43
+ class CustomerAuthResponse(BaseModel):
44
+ """Response schema for successful customer authentication."""
45
+ access_token: str = Field(..., description="JWT access token")
46
+ customer_id: str = Field(..., description="Customer UUID")
47
+ is_new_customer: bool = Field(..., description="Whether this is a new customer registration")
48
+ token_type: str = Field(default="bearer", description="Token type")
49
+ expires_in: int = Field(..., description="Token expiration time in seconds")
50
+
51
+
52
+ class SendOTPResponse(BaseModel):
53
+ """Response schema for OTP send request."""
54
+ success: bool = Field(..., description="Whether OTP was sent successfully")
55
+ message: str = Field(..., description="Response message")
56
+ expires_in: int = Field(..., description="OTP expiration time in seconds")
app/auth/services/customer_auth_service.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Customer authentication service for OTP-based login.
3
+ """
4
+ import secrets
5
+ import uuid
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional, Tuple, Dict, Any
8
+ from motor.motor_asyncio import AsyncIOMotorDatabase
9
+
10
+ from app.core.config import settings
11
+ from app.core.logging import get_logger
12
+ from app.nosql import get_database
13
+ from app.system_users.services.service import SystemUserService
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class CustomerAuthService:
19
+ """Service for customer OTP authentication."""
20
+
21
+ def __init__(self):
22
+ self.db: AsyncIOMotorDatabase = get_database()
23
+ self.customers_collection = self.db.scm_customers
24
+ self.otp_collection = self.db.customer_otps
25
+ self.system_user_service = SystemUserService()
26
+
27
+ async def send_otp(self, mobile: str) -> Tuple[bool, str, int]:
28
+ """
29
+ Send OTP to customer mobile number.
30
+
31
+ Args:
32
+ mobile: Customer mobile number
33
+
34
+ Returns:
35
+ Tuple of (success, message, expires_in_seconds)
36
+ """
37
+ try:
38
+ # Generate 6-digit OTP
39
+ otp = str(secrets.randbelow(900000) + 100000)
40
+
41
+ # Set expiration (5 minutes)
42
+ expires_at = datetime.utcnow() + timedelta(minutes=5)
43
+ expires_in = 300 # 5 minutes in seconds
44
+
45
+ # Store OTP in database
46
+ otp_doc = {
47
+ "mobile": mobile,
48
+ "otp": otp,
49
+ "created_at": datetime.utcnow(),
50
+ "expires_at": expires_at,
51
+ "attempts": 0,
52
+ "verified": False
53
+ }
54
+
55
+ # Upsert OTP (replace existing if any)
56
+ await self.otp_collection.replace_one(
57
+ {"mobile": mobile},
58
+ otp_doc,
59
+ upsert=True
60
+ )
61
+
62
+ # TODO: Integrate with SMS service to send actual OTP
63
+ # For now, log the OTP for testing
64
+ logger.info(f"OTP generated for {mobile}: {otp} (expires in {expires_in}s)")
65
+
66
+ return True, "OTP sent successfully", expires_in
67
+
68
+ except Exception as e:
69
+ logger.error(f"Error sending OTP to {mobile}: {str(e)}", exc_info=True)
70
+ return False, "Failed to send OTP", 0
71
+
72
+ async def verify_otp(self, mobile: str, otp: str) -> Tuple[Optional[Dict[str, Any]], str]:
73
+ """
74
+ Verify OTP and authenticate customer.
75
+
76
+ Args:
77
+ mobile: Customer mobile number
78
+ otp: OTP code to verify
79
+
80
+ Returns:
81
+ Tuple of (customer_data, message)
82
+ """
83
+ try:
84
+ # Find OTP record
85
+ otp_doc = await self.otp_collection.find_one({"mobile": mobile})
86
+
87
+ if not otp_doc:
88
+ logger.warning(f"OTP verification failed - no OTP found for {mobile}")
89
+ return None, "Invalid OTP"
90
+
91
+ # Check if OTP is expired
92
+ if datetime.utcnow() > otp_doc["expires_at"]:
93
+ logger.warning(f"OTP verification failed - expired OTP for {mobile}")
94
+ await self.otp_collection.delete_one({"mobile": mobile})
95
+ return None, "OTP has expired"
96
+
97
+ # Check if already verified
98
+ if otp_doc.get("verified", False):
99
+ logger.warning(f"OTP verification failed - already used OTP for {mobile}")
100
+ return None, "OTP has already been used"
101
+
102
+ # Increment attempts
103
+ attempts = otp_doc.get("attempts", 0) + 1
104
+
105
+ # Check max attempts (3 attempts allowed)
106
+ if attempts > 3:
107
+ logger.warning(f"OTP verification failed - too many attempts for {mobile}")
108
+ await self.otp_collection.delete_one({"mobile": mobile})
109
+ return None, "Too many attempts. Please request a new OTP"
110
+
111
+ # Update attempts
112
+ await self.otp_collection.update_one(
113
+ {"mobile": mobile},
114
+ {"$set": {"attempts": attempts}}
115
+ )
116
+
117
+ # Verify OTP
118
+ if otp_doc["otp"] != otp:
119
+ logger.warning(f"OTP verification failed - incorrect OTP for {mobile}")
120
+ return None, "Invalid OTP"
121
+
122
+ # Mark OTP as verified
123
+ await self.otp_collection.update_one(
124
+ {"mobile": mobile},
125
+ {"$set": {"verified": True}}
126
+ )
127
+
128
+ # Find or create customer
129
+ customer = await self._find_or_create_customer(mobile)
130
+
131
+ logger.info(f"OTP verified successfully for {mobile}")
132
+ return customer, "OTP verified successfully"
133
+
134
+ except Exception as e:
135
+ logger.error(f"Error verifying OTP for {mobile}: {str(e)}", exc_info=True)
136
+ return None, "Failed to verify OTP"
137
+
138
+ async def _find_or_create_customer(self, mobile: str) -> Dict[str, Any]:
139
+ """
140
+ Find existing customer or create new one.
141
+
142
+ Args:
143
+ mobile: Customer mobile number
144
+
145
+ Returns:
146
+ Customer data dictionary
147
+ """
148
+ try:
149
+ # Try to find existing customer
150
+ customer = await self.customers_collection.find_one({"phone": mobile})
151
+
152
+ if customer:
153
+ # Update last login
154
+ await self.customers_collection.update_one(
155
+ {"_id": customer["_id"]},
156
+ {"$set": {"last_login_at": datetime.utcnow()}}
157
+ )
158
+
159
+ return {
160
+ "customer_id": str(customer.get("customer_id", customer["_id"])),
161
+ "mobile": customer["phone"],
162
+ "name": customer.get("name", ""),
163
+ "email": customer.get("email"),
164
+ "is_new_customer": False,
165
+ "status": customer.get("status", "active"),
166
+ "merchant_id": customer.get("merchant_id"),
167
+ "created_at": customer.get("created_at"),
168
+ "last_login_at": datetime.utcnow()
169
+ }
170
+ else:
171
+ # Create new customer
172
+ customer_id = str(uuid.uuid4())
173
+ now = datetime.utcnow()
174
+
175
+ new_customer = {
176
+ "customer_id": customer_id,
177
+ "phone": mobile,
178
+ "name": "", # Will be updated later
179
+ "email": None,
180
+ "status": "active",
181
+ "merchant_id": None, # Will be set when customer makes first purchase
182
+ "notes": "Customer registered via mobile app",
183
+ "created_at": now,
184
+ "updated_at": now,
185
+ "last_login_at": now
186
+ }
187
+
188
+ await self.customers_collection.insert_one(new_customer)
189
+
190
+ logger.info(f"New customer created: {customer_id} for mobile {mobile}")
191
+
192
+ return {
193
+ "customer_id": customer_id,
194
+ "mobile": mobile,
195
+ "name": "",
196
+ "email": None,
197
+ "is_new_customer": True,
198
+ "status": "active",
199
+ "merchant_id": None,
200
+ "created_at": now,
201
+ "last_login_at": now
202
+ }
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error finding/creating customer for {mobile}: {str(e)}", exc_info=True)
206
+ raise
207
+
208
+ def create_customer_token(self, customer_data: Dict[str, Any]) -> str:
209
+ """
210
+ Create JWT token for customer.
211
+
212
+ Args:
213
+ customer_data: Customer information
214
+
215
+ Returns:
216
+ JWT access token
217
+ """
218
+ try:
219
+ token_data = {
220
+ "sub": customer_data["customer_id"],
221
+ "mobile": customer_data["mobile"],
222
+ "customer_id": customer_data["customer_id"],
223
+ "type": "customer",
224
+ "merchant_id": customer_data.get("merchant_id")
225
+ }
226
+
227
+ expires_delta = timedelta(hours=settings.TOKEN_EXPIRATION_HOURS)
228
+
229
+ return self.system_user_service.create_access_token(
230
+ data=token_data,
231
+ expires_delta=expires_delta
232
+ )
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error creating customer token: {str(e)}", exc_info=True)
236
+ raise
237
+
238
+ async def cleanup_expired_otps(self):
239
+ """Clean up expired OTP records."""
240
+ try:
241
+ result = await self.otp_collection.delete_many({
242
+ "expires_at": {"$lt": datetime.utcnow()}
243
+ })
244
+
245
+ if result.deleted_count > 0:
246
+ logger.info(f"Cleaned up {result.deleted_count} expired OTP records")
247
+
248
+ except Exception as e:
249
+ logger.error(f"Error cleaning up expired OTPs: {str(e)}", exc_info=True)
app/dependencies/customer_auth.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Customer authentication dependencies.
3
+ """
4
+ from typing import Optional
5
+ from fastapi import Depends, HTTPException, status
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from pydantic import BaseModel
8
+
9
+ from app.system_users.services.service import SystemUserService
10
+ from app.core.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ security = HTTPBearer()
15
+
16
+
17
+ class CustomerUser(BaseModel):
18
+ """Customer user model for dependency injection."""
19
+ customer_id: str
20
+ mobile: str
21
+ merchant_id: Optional[str] = None
22
+ type: str = "customer"
23
+
24
+
25
+ async def get_current_customer(
26
+ credentials: HTTPAuthorizationCredentials = Depends(security)
27
+ ) -> CustomerUser:
28
+ """
29
+ Get current authenticated customer from JWT token.
30
+
31
+ Args:
32
+ credentials: HTTP Bearer token credentials
33
+
34
+ Returns:
35
+ CustomerUser: Authenticated customer information
36
+
37
+ Raises:
38
+ HTTPException: 401 - Invalid or expired token
39
+ HTTPException: 403 - Not a customer token
40
+ """
41
+ try:
42
+ if not credentials:
43
+ raise HTTPException(
44
+ status_code=status.HTTP_401_UNAUTHORIZED,
45
+ detail="Authentication credentials required",
46
+ headers={"WWW-Authenticate": "Bearer"}
47
+ )
48
+
49
+ # Verify token
50
+ user_service = SystemUserService()
51
+ payload = user_service.verify_token(credentials.credentials, "access")
52
+
53
+ if not payload:
54
+ raise HTTPException(
55
+ status_code=status.HTTP_401_UNAUTHORIZED,
56
+ detail="Invalid or expired token",
57
+ headers={"WWW-Authenticate": "Bearer"}
58
+ )
59
+
60
+ # Check if it's a customer token
61
+ token_type = payload.get("type")
62
+ if token_type != "customer":
63
+ raise HTTPException(
64
+ status_code=status.HTTP_403_FORBIDDEN,
65
+ detail="Customer authentication required"
66
+ )
67
+
68
+ customer_id = payload.get("sub")
69
+ mobile = payload.get("mobile")
70
+ merchant_id = payload.get("merchant_id")
71
+
72
+ if not customer_id or not mobile:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_401_UNAUTHORIZED,
75
+ detail="Invalid token payload"
76
+ )
77
+
78
+ return CustomerUser(
79
+ customer_id=customer_id,
80
+ mobile=mobile,
81
+ merchant_id=merchant_id,
82
+ type="customer"
83
+ )
84
+
85
+ except HTTPException:
86
+ raise
87
+ except Exception as e:
88
+ logger.error(f"Error authenticating customer: {str(e)}", exc_info=True)
89
+ raise HTTPException(
90
+ status_code=status.HTTP_401_UNAUTHORIZED,
91
+ detail="Authentication failed"
92
+ )
93
+
94
+
95
+ async def get_customer_service():
96
+ """Dependency to get customer auth service."""
97
+ from app.auth.services.customer_auth_service import CustomerAuthService
98
+ return CustomerAuthService()
app/main.py CHANGED
@@ -52,6 +52,14 @@ async def lifespan(app: FastAPI):
52
  logger.info("Starting AUTH Microservice")
53
  await connect_to_mongo()
54
 
 
 
 
 
 
 
 
 
55
  logger.info("AUTH Microservice started successfully")
56
 
57
  yield
 
52
  logger.info("Starting AUTH Microservice")
53
  await connect_to_mongo()
54
 
55
+ # Initialize customer authentication collections
56
+ try:
57
+ from app.auth.db_init_customer import init_customer_auth_collections
58
+ await init_customer_auth_collections()
59
+ logger.info("Customer authentication collections initialized")
60
+ except Exception as e:
61
+ logger.error(f"Failed to initialize customer auth collections: {e}")
62
+
63
  logger.info("AUTH Microservice started successfully")
64
 
65
  yield
mobile_app_example.js ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Mobile App Customer Authentication Example
3
+ *
4
+ * This example shows how to integrate the customer authentication APIs
5
+ * in a React Native or similar mobile application.
6
+ */
7
+
8
+ // Configuration
9
+ const AUTH_BASE_URL = 'http://localhost:8001'; // Auth service URL
10
+
11
+ // Storage utilities (use AsyncStorage in React Native)
12
+ const storage = {
13
+ async setItem(key, value) {
14
+ // In React Native: await AsyncStorage.setItem(key, value);
15
+ localStorage.setItem(key, value);
16
+ },
17
+
18
+ async getItem(key) {
19
+ // In React Native: return await AsyncStorage.getItem(key);
20
+ return localStorage.getItem(key);
21
+ },
22
+
23
+ async removeItem(key) {
24
+ // In React Native: await AsyncStorage.removeItem(key);
25
+ localStorage.removeItem(key);
26
+ }
27
+ };
28
+
29
+ // Customer Authentication Service
30
+ class CustomerAuthService {
31
+
32
+ /**
33
+ * Send OTP to customer mobile number
34
+ */
35
+ static async sendOTP(mobile) {
36
+ try {
37
+ const response = await fetch(`${AUTH_BASE_URL}/auth/customer/send-otp`, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify({ mobile }),
43
+ });
44
+
45
+ const data = await response.json();
46
+
47
+ if (!response.ok) {
48
+ throw new Error(data.detail || 'Failed to send OTP');
49
+ }
50
+
51
+ return {
52
+ success: true,
53
+ message: data.message,
54
+ expiresIn: data.expires_in,
55
+ };
56
+ } catch (error) {
57
+ console.error('Send OTP error:', error);
58
+ return {
59
+ success: false,
60
+ error: error.message,
61
+ };
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Verify OTP and authenticate customer
67
+ */
68
+ static async verifyOTP(mobile, otp) {
69
+ try {
70
+ const response = await fetch(`${AUTH_BASE_URL}/auth/customer/verify-otp`, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ },
75
+ body: JSON.stringify({ mobile, otp }),
76
+ });
77
+
78
+ const data = await response.json();
79
+
80
+ if (!response.ok) {
81
+ throw new Error(data.detail || 'Failed to verify OTP');
82
+ }
83
+
84
+ // Store authentication data
85
+ await storage.setItem('access_token', data.access_token);
86
+ await storage.setItem('customer_id', data.customer_id);
87
+ await storage.setItem('mobile', mobile);
88
+
89
+ return {
90
+ success: true,
91
+ accessToken: data.access_token,
92
+ customerId: data.customer_id,
93
+ isNewCustomer: data.is_new_customer,
94
+ expiresIn: data.expires_in,
95
+ };
96
+ } catch (error) {
97
+ console.error('Verify OTP error:', error);
98
+ return {
99
+ success: false,
100
+ error: error.message,
101
+ };
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get customer profile
107
+ */
108
+ static async getProfile() {
109
+ try {
110
+ const token = await storage.getItem('access_token');
111
+
112
+ if (!token) {
113
+ throw new Error('No access token found');
114
+ }
115
+
116
+ const response = await fetch(`${AUTH_BASE_URL}/auth/customer/me`, {
117
+ method: 'GET',
118
+ headers: {
119
+ 'Authorization': `Bearer ${token}`,
120
+ 'Content-Type': 'application/json',
121
+ },
122
+ });
123
+
124
+ const data = await response.json();
125
+
126
+ if (!response.ok) {
127
+ throw new Error(data.detail || 'Failed to get profile');
128
+ }
129
+
130
+ return {
131
+ success: true,
132
+ profile: data,
133
+ };
134
+ } catch (error) {
135
+ console.error('Get profile error:', error);
136
+ return {
137
+ success: false,
138
+ error: error.message,
139
+ };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Logout customer
145
+ */
146
+ static async logout() {
147
+ try {
148
+ const token = await storage.getItem('access_token');
149
+
150
+ if (token) {
151
+ // Call logout endpoint
152
+ await fetch(`${AUTH_BASE_URL}/auth/customer/logout`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Authorization': `Bearer ${token}`,
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ });
159
+ }
160
+
161
+ // Clear stored data
162
+ await storage.removeItem('access_token');
163
+ await storage.removeItem('customer_id');
164
+ await storage.removeItem('mobile');
165
+
166
+ return { success: true };
167
+ } catch (error) {
168
+ console.error('Logout error:', error);
169
+ // Still clear local data even if API call fails
170
+ await storage.removeItem('access_token');
171
+ await storage.removeItem('customer_id');
172
+ await storage.removeItem('mobile');
173
+
174
+ return { success: true };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Check if customer is authenticated
180
+ */
181
+ static async isAuthenticated() {
182
+ try {
183
+ const token = await storage.getItem('access_token');
184
+
185
+ if (!token) {
186
+ return false;
187
+ }
188
+
189
+ // Check if token is expired (basic check)
190
+ const payload = JSON.parse(atob(token.split('.')[1]));
191
+ const currentTime = Math.floor(Date.now() / 1000);
192
+
193
+ if (payload.exp && payload.exp < currentTime) {
194
+ // Token expired, clear storage
195
+ await this.logout();
196
+ return false;
197
+ }
198
+
199
+ return true;
200
+ } catch (error) {
201
+ console.error('Auth check error:', error);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Get stored customer data
208
+ */
209
+ static async getStoredCustomerData() {
210
+ try {
211
+ const [token, customerId, mobile] = await Promise.all([
212
+ storage.getItem('access_token'),
213
+ storage.getItem('customer_id'),
214
+ storage.getItem('mobile'),
215
+ ]);
216
+
217
+ return {
218
+ accessToken: token,
219
+ customerId,
220
+ mobile,
221
+ };
222
+ } catch (error) {
223
+ console.error('Get stored data error:', error);
224
+ return {};
225
+ }
226
+ }
227
+ }
228
+
229
+ // Navigation utilities
230
+ class NavigationService {
231
+
232
+ /**
233
+ * Handle navigation based on authentication state
234
+ */
235
+ static async handleAuthNavigation() {
236
+ const isAuth = await CustomerAuthService.isAuthenticated();
237
+
238
+ if (isAuth) {
239
+ // Redirect to main app
240
+ this.navigateToMainApp();
241
+ } else {
242
+ // Stay on or redirect to auth screen
243
+ this.navigateToAuth();
244
+ }
245
+ }
246
+
247
+ static navigateToMainApp() {
248
+ // In React Native: navigation.navigate('MainTabs');
249
+ // In web: window.location.href = '/(tabs)/index';
250
+ console.log('Navigate to main app');
251
+ }
252
+
253
+ static navigateToAuth() {
254
+ // In React Native: navigation.navigate('Auth');
255
+ // In web: window.location.href = '/auth';
256
+ console.log('Navigate to auth screen');
257
+ }
258
+ }
259
+
260
+ // Example usage in a React component
261
+ class AuthScreen {
262
+
263
+ constructor() {
264
+ this.state = {
265
+ mobile: '',
266
+ otp: '',
267
+ step: 'mobile', // 'mobile' | 'otp'
268
+ loading: false,
269
+ error: null,
270
+ otpTimer: 0,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Handle mobile number submission
276
+ */
277
+ async handleSendOTP() {
278
+ if (!this.state.mobile) {
279
+ this.setState({ error: 'Please enter mobile number' });
280
+ return;
281
+ }
282
+
283
+ this.setState({ loading: true, error: null });
284
+
285
+ const result = await CustomerAuthService.sendOTP(this.state.mobile);
286
+
287
+ if (result.success) {
288
+ this.setState({
289
+ step: 'otp',
290
+ loading: false,
291
+ otpTimer: result.expiresIn,
292
+ });
293
+ this.startOTPTimer();
294
+ } else {
295
+ this.setState({
296
+ loading: false,
297
+ error: result.error,
298
+ });
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Handle OTP verification
304
+ */
305
+ async handleVerifyOTP() {
306
+ if (!this.state.otp) {
307
+ this.setState({ error: 'Please enter OTP' });
308
+ return;
309
+ }
310
+
311
+ this.setState({ loading: true, error: null });
312
+
313
+ const result = await CustomerAuthService.verifyOTP(
314
+ this.state.mobile,
315
+ this.state.otp
316
+ );
317
+
318
+ if (result.success) {
319
+ // Authentication successful
320
+ console.log('Customer authenticated:', result.customerId);
321
+ console.log('Is new customer:', result.isNewCustomer);
322
+
323
+ // Navigate to main app
324
+ NavigationService.navigateToMainApp();
325
+ } else {
326
+ this.setState({
327
+ loading: false,
328
+ error: result.error,
329
+ });
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Start OTP countdown timer
335
+ */
336
+ startOTPTimer() {
337
+ const timer = setInterval(() => {
338
+ this.setState(prevState => {
339
+ if (prevState.otpTimer <= 1) {
340
+ clearInterval(timer);
341
+ return { otpTimer: 0 };
342
+ }
343
+ return { otpTimer: prevState.otpTimer - 1 };
344
+ });
345
+ }, 1000);
346
+ }
347
+
348
+ /**
349
+ * Resend OTP
350
+ */
351
+ async handleResendOTP() {
352
+ await this.handleSendOTP();
353
+ }
354
+
355
+ setState(newState) {
356
+ this.state = { ...this.state, ...newState };
357
+ // In React: this.setState(newState);
358
+ }
359
+ }
360
+
361
+ // App initialization
362
+ class App {
363
+
364
+ async componentDidMount() {
365
+ // Auto-restore session on app launch
366
+ await NavigationService.handleAuthNavigation();
367
+ }
368
+
369
+ /**
370
+ * Make authenticated API calls
371
+ */
372
+ static async makeAuthenticatedRequest(url, options = {}) {
373
+ const { accessToken } = await CustomerAuthService.getStoredCustomerData();
374
+
375
+ if (!accessToken) {
376
+ throw new Error('No access token available');
377
+ }
378
+
379
+ const headers = {
380
+ 'Authorization': `Bearer ${accessToken}`,
381
+ 'Content-Type': 'application/json',
382
+ ...options.headers,
383
+ };
384
+
385
+ const response = await fetch(url, {
386
+ ...options,
387
+ headers,
388
+ });
389
+
390
+ if (response.status === 401) {
391
+ // Token expired, logout and redirect to auth
392
+ await CustomerAuthService.logout();
393
+ NavigationService.navigateToAuth();
394
+ throw new Error('Authentication expired');
395
+ }
396
+
397
+ return response;
398
+ }
399
+ }
400
+
401
+ // Error handling utilities
402
+ class ErrorHandler {
403
+
404
+ static handleAuthError(error) {
405
+ const errorMessages = {
406
+ 'Invalid mobile number format': 'Please enter a valid mobile number with country code (e.g., +919999999999)',
407
+ 'Invalid OTP': 'The OTP you entered is incorrect. Please try again.',
408
+ 'OTP has expired': 'The OTP has expired. Please request a new one.',
409
+ 'Too many attempts': 'Too many failed attempts. Please request a new OTP.',
410
+ 'OTP has already been used': 'This OTP has already been used. Please request a new one.',
411
+ };
412
+
413
+ return errorMessages[error] || error || 'An unexpected error occurred';
414
+ }
415
+ }
416
+
417
+ // Export for use in React Native or other frameworks
418
+ if (typeof module !== 'undefined' && module.exports) {
419
+ module.exports = {
420
+ CustomerAuthService,
421
+ NavigationService,
422
+ ErrorHandler,
423
+ };
424
+ }
425
+
426
+ // Example usage:
427
+ /*
428
+ // In your React Native component:
429
+ import { CustomerAuthService, NavigationService } from './mobile_app_example';
430
+
431
+ const LoginScreen = () => {
432
+ const [mobile, setMobile] = useState('');
433
+ const [otp, setOtp] = useState('');
434
+ const [step, setStep] = useState('mobile');
435
+
436
+ const handleSendOTP = async () => {
437
+ const result = await CustomerAuthService.sendOTP(mobile);
438
+ if (result.success) {
439
+ setStep('otp');
440
+ } else {
441
+ Alert.alert('Error', result.error);
442
+ }
443
+ };
444
+
445
+ const handleVerifyOTP = async () => {
446
+ const result = await CustomerAuthService.verifyOTP(mobile, otp);
447
+ if (result.success) {
448
+ navigation.navigate('MainTabs');
449
+ } else {
450
+ Alert.alert('Error', result.error);
451
+ }
452
+ };
453
+
454
+ // ... render UI
455
+ };
456
+ */
setup_customer_auth.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Setup script for customer authentication system.
4
+ """
5
+ import asyncio
6
+ import sys
7
+ import os
8
+
9
+ # Add the app directory to Python path
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
11
+
12
+ from app.core.logging import setup_logging, get_logger
13
+ from app.nosql import connect_to_mongo, close_mongo_connection
14
+ from app.auth.db_init_customer import init_customer_auth_collections
15
+
16
+ # Setup logging
17
+ setup_logging(log_level="INFO", log_dir="logs")
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ async def setup_customer_auth():
22
+ """Setup customer authentication system."""
23
+ try:
24
+ print("πŸš€ Setting up Customer Authentication System")
25
+ print("=" * 50)
26
+
27
+ # Connect to MongoDB
28
+ print("\n1️⃣ Connecting to MongoDB...")
29
+ await connect_to_mongo()
30
+ logger.info("βœ… Connected to MongoDB")
31
+
32
+ # Initialize collections and indexes
33
+ print("\n2️⃣ Initializing database collections...")
34
+ await init_customer_auth_collections()
35
+ logger.info("βœ… Database collections initialized")
36
+
37
+ # Verify setup
38
+ print("\n3️⃣ Verifying setup...")
39
+ from app.nosql import get_database
40
+
41
+ db = get_database()
42
+
43
+ # Check collections exist
44
+ collections = await db.list_collection_names()
45
+ required_collections = ['scm_customers', 'customer_otps']
46
+
47
+ for collection in required_collections:
48
+ if collection in collections:
49
+ print(f" βœ… Collection '{collection}' exists")
50
+ else:
51
+ print(f" ❌ Collection '{collection}' missing")
52
+
53
+ # Check indexes
54
+ customers_indexes = await db.scm_customers.list_indexes().to_list(length=None)
55
+ otps_indexes = await db.customer_otps.list_indexes().to_list(length=None)
56
+
57
+ print(f" πŸ“Š scm_customers indexes: {len(customers_indexes)}")
58
+ print(f" πŸ“Š customer_otps indexes: {len(otps_indexes)}")
59
+
60
+ print("\nπŸŽ‰ Customer Authentication System setup completed!")
61
+ print("\nπŸ“‹ Next Steps:")
62
+ print(" 1. Start the auth service: python -m app.main")
63
+ print(" 2. Test the APIs: python test_customer_auth.py")
64
+ print(" 3. Check the documentation: CUSTOMER_AUTH_IMPLEMENTATION.md")
65
+
66
+ except Exception as e:
67
+ logger.error(f"❌ Setup failed: {str(e)}", exc_info=True)
68
+ print(f"\n❌ Setup failed: {str(e)}")
69
+ return False
70
+
71
+ finally:
72
+ # Close MongoDB connection
73
+ await close_mongo_connection()
74
+ logger.info("πŸ”Œ MongoDB connection closed")
75
+
76
+ return True
77
+
78
+
79
+ async def cleanup_customer_auth():
80
+ """Cleanup customer authentication collections (for testing)."""
81
+ try:
82
+ print("🧹 Cleaning up Customer Authentication Collections")
83
+ print("=" * 50)
84
+
85
+ # Connect to MongoDB
86
+ await connect_to_mongo()
87
+
88
+ from app.nosql import get_database
89
+ db = get_database()
90
+
91
+ # Drop collections
92
+ collections_to_drop = ['scm_customers', 'customer_otps']
93
+
94
+ for collection in collections_to_drop:
95
+ try:
96
+ await db.drop_collection(collection)
97
+ print(f" βœ… Dropped collection '{collection}'")
98
+ except Exception as e:
99
+ print(f" ⚠️ Collection '{collection}' not found or error: {e}")
100
+
101
+ print("\nπŸŽ‰ Cleanup completed!")
102
+
103
+ except Exception as e:
104
+ logger.error(f"❌ Cleanup failed: {str(e)}", exc_info=True)
105
+ print(f"\n❌ Cleanup failed: {str(e)}")
106
+ return False
107
+
108
+ finally:
109
+ await close_mongo_connection()
110
+
111
+ return True
112
+
113
+
114
+ async def main():
115
+ """Main function."""
116
+ if len(sys.argv) > 1 and sys.argv[1] == "cleanup":
117
+ await cleanup_customer_auth()
118
+ else:
119
+ await setup_customer_auth()
120
+
121
+
122
+ if __name__ == "__main__":
123
+ asyncio.run(main())
test_customer_auth.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for customer authentication APIs.
4
+ """
5
+ import asyncio
6
+ import aiohttp
7
+ import json
8
+ from typing import Dict, Any
9
+
10
+ # Configuration
11
+ BASE_URL = "http://localhost:8001" # Auth service URL
12
+ TEST_MOBILE = "+919999999999"
13
+
14
+
15
+ async def test_customer_auth_flow():
16
+ """Test the complete customer authentication flow."""
17
+ async with aiohttp.ClientSession() as session:
18
+ print("πŸ§ͺ Testing Customer Authentication Flow")
19
+ print("=" * 50)
20
+
21
+ # Test 1: Send OTP
22
+ print("\n1️⃣ Testing Send OTP")
23
+ send_otp_data = {
24
+ "mobile": TEST_MOBILE
25
+ }
26
+
27
+ async with session.post(
28
+ f"{BASE_URL}/auth/customer/send-otp",
29
+ json=send_otp_data,
30
+ headers={"Content-Type": "application/json"}
31
+ ) as response:
32
+ if response.status == 200:
33
+ result = await response.json()
34
+ print(f"βœ… OTP sent successfully")
35
+ print(f" Message: {result['message']}")
36
+ print(f" Expires in: {result['expires_in']} seconds")
37
+ else:
38
+ error = await response.text()
39
+ print(f"❌ Failed to send OTP: {response.status}")
40
+ print(f" Error: {error}")
41
+ return
42
+
43
+ # Get OTP from user input (in production, this would come from SMS)
44
+ print(f"\nπŸ“± Check the logs for OTP sent to {TEST_MOBILE}")
45
+ otp = input("Enter the OTP: ").strip()
46
+
47
+ if not otp:
48
+ print("❌ No OTP provided, skipping verification test")
49
+ return
50
+
51
+ # Test 2: Verify OTP
52
+ print(f"\n2️⃣ Testing Verify OTP")
53
+ verify_otp_data = {
54
+ "mobile": TEST_MOBILE,
55
+ "otp": otp
56
+ }
57
+
58
+ async with session.post(
59
+ f"{BASE_URL}/auth/customer/verify-otp",
60
+ json=verify_otp_data,
61
+ headers={"Content-Type": "application/json"}
62
+ ) as response:
63
+ if response.status == 200:
64
+ result = await response.json()
65
+ access_token = result["access_token"]
66
+ print(f"βœ… OTP verified successfully")
67
+ print(f" Customer ID: {result['customer_id']}")
68
+ print(f" Is new customer: {result['is_new_customer']}")
69
+ print(f" Token expires in: {result['expires_in']} seconds")
70
+ print(f" Access token: {access_token[:50]}...")
71
+ else:
72
+ error = await response.text()
73
+ print(f"❌ Failed to verify OTP: {response.status}")
74
+ print(f" Error: {error}")
75
+ return
76
+
77
+ # Test 3: Get Customer Profile
78
+ print(f"\n3️⃣ Testing Get Customer Profile")
79
+ headers = {
80
+ "Authorization": f"Bearer {access_token}",
81
+ "Content-Type": "application/json"
82
+ }
83
+
84
+ async with session.get(
85
+ f"{BASE_URL}/auth/customer/me",
86
+ headers=headers
87
+ ) as response:
88
+ if response.status == 200:
89
+ result = await response.json()
90
+ print(f"βœ… Customer profile retrieved")
91
+ print(f" Customer ID: {result['customer_id']}")
92
+ print(f" Mobile: {result['mobile']}")
93
+ print(f" Merchant ID: {result['merchant_id']}")
94
+ print(f" Type: {result['type']}")
95
+ else:
96
+ error = await response.text()
97
+ print(f"❌ Failed to get customer profile: {response.status}")
98
+ print(f" Error: {error}")
99
+
100
+ # Test 4: Customer Logout
101
+ print(f"\n4️⃣ Testing Customer Logout")
102
+ async with session.post(
103
+ f"{BASE_URL}/auth/customer/logout",
104
+ headers=headers
105
+ ) as response:
106
+ if response.status == 200:
107
+ result = await response.json()
108
+ print(f"βœ… Customer logged out successfully")
109
+ print(f" Message: {result['message']}")
110
+ else:
111
+ error = await response.text()
112
+ print(f"❌ Failed to logout customer: {response.status}")
113
+ print(f" Error: {error}")
114
+
115
+ print(f"\nπŸŽ‰ Customer authentication flow test completed!")
116
+
117
+
118
+ async def test_error_scenarios():
119
+ """Test error scenarios for customer authentication."""
120
+ async with aiohttp.ClientSession() as session:
121
+ print("\nπŸ§ͺ Testing Error Scenarios")
122
+ print("=" * 50)
123
+
124
+ # Test 1: Invalid mobile number format
125
+ print("\n1️⃣ Testing Invalid Mobile Format")
126
+ invalid_mobile_data = {
127
+ "mobile": "123456" # Invalid format
128
+ }
129
+
130
+ async with session.post(
131
+ f"{BASE_URL}/auth/customer/send-otp",
132
+ json=invalid_mobile_data,
133
+ headers={"Content-Type": "application/json"}
134
+ ) as response:
135
+ if response.status == 422:
136
+ print("βœ… Invalid mobile format correctly rejected")
137
+ else:
138
+ print(f"❌ Expected 422, got {response.status}")
139
+
140
+ # Test 2: Invalid OTP
141
+ print("\n2️⃣ Testing Invalid OTP")
142
+ invalid_otp_data = {
143
+ "mobile": TEST_MOBILE,
144
+ "otp": "000000" # Invalid OTP
145
+ }
146
+
147
+ async with session.post(
148
+ f"{BASE_URL}/auth/customer/verify-otp",
149
+ json=invalid_otp_data,
150
+ headers={"Content-Type": "application/json"}
151
+ ) as response:
152
+ if response.status == 401:
153
+ print("βœ… Invalid OTP correctly rejected")
154
+ else:
155
+ print(f"❌ Expected 401, got {response.status}")
156
+
157
+ # Test 3: Access protected endpoint without token
158
+ print("\n3️⃣ Testing Protected Endpoint Without Token")
159
+ async with session.get(
160
+ f"{BASE_URL}/auth/customer/me"
161
+ ) as response:
162
+ if response.status == 401:
163
+ print("βœ… Protected endpoint correctly requires authentication")
164
+ else:
165
+ print(f"❌ Expected 401, got {response.status}")
166
+
167
+ print(f"\nπŸŽ‰ Error scenarios test completed!")
168
+
169
+
170
+ async def main():
171
+ """Main test function."""
172
+ print("πŸš€ Starting Customer Authentication API Tests")
173
+ print(f"πŸ“ Base URL: {BASE_URL}")
174
+ print(f"πŸ“± Test Mobile: {TEST_MOBILE}")
175
+
176
+ try:
177
+ # Test normal flow
178
+ await test_customer_auth_flow()
179
+
180
+ # Test error scenarios
181
+ await test_error_scenarios()
182
+
183
+ except Exception as e:
184
+ print(f"❌ Test failed with error: {str(e)}")
185
+ import traceback
186
+ traceback.print_exc()
187
+
188
+
189
+ if __name__ == "__main__":
190
+ asyncio.run(main())