Spaces:
Sleeping
Sleeping
Commit ·
e4d981c
1
Parent(s): f295dd5
feat(service_professionals): Standardize authentication to use partner_id across all endpoints
Browse files- Update JWT token extraction to use partner_id instead of staff_id in all endpoints
- Modify get_my_profile and update_my_profile to extract partner_id from token payload
- Add staff_id, staff_code, and user_type to auth dependency return values for service professional context
- Update error messages to reference partner_id for consistency with system-wide authentication standard
- Clarify in documentation that partner_id contains staff_id value for service professionals
- Update JWT token structure documentation to show partner_id as primary identifier
- Ensure all database queries use partner_id for lookups while maintaining staff_id in token for reference
- SERVICE_PROFESSIONALS_API_QUICK_REF.md +1 -1
- SERVICE_PROFESSIONALS_IMPLEMENTATION_SUMMARY.md +8 -5
- app/dependencies/auth.py +4 -1
- app/service_professionals/controllers/router.py +18 -18
- app/service_professionals/models/model.py +2 -2
- app/service_professionals/schemas/schema.py +1 -1
- app/service_professionals/services/service.py +14 -14
SERVICE_PROFESSIONALS_API_QUICK_REF.md
CHANGED
|
@@ -9,7 +9,7 @@ API for service professionals to view and update their own profile. Authenticati
|
|
| 9 |
```
|
| 10 |
|
| 11 |
## Authentication
|
| 12 |
-
All endpoints require JWT authentication with `
|
| 13 |
|
| 14 |
**Header:**
|
| 15 |
```
|
|
|
|
| 9 |
```
|
| 10 |
|
| 11 |
## Authentication
|
| 12 |
+
All endpoints require JWT authentication with `partner_id` in the token payload (which contains the staff_id for service professionals).
|
| 13 |
|
| 14 |
**Header:**
|
| 15 |
```
|
SERVICE_PROFESSIONALS_IMPLEMENTATION_SUMMARY.md
CHANGED
|
@@ -73,17 +73,18 @@ The following are handled by admin interfaces:
|
|
| 73 |
### Authentication Flow
|
| 74 |
|
| 75 |
1. Service professional logs in via Auth-MS (OTP-based)
|
| 76 |
-
2. Auth-MS returns JWT with
|
| 77 |
3. Professional uses JWT to access SPA-MS endpoints
|
| 78 |
-
4. SPA-MS extracts
|
| 79 |
-
5. Operations performed on professional's own record
|
| 80 |
|
| 81 |
### JWT Token Structure
|
| 82 |
|
| 83 |
```json
|
| 84 |
{
|
| 85 |
-
"sub": "
|
| 86 |
-
"
|
|
|
|
| 87 |
"staff_code": "SP001",
|
| 88 |
"user_type": "service_professional",
|
| 89 |
"exp": 1234567890,
|
|
@@ -91,6 +92,8 @@ The following are handled by admin interfaces:
|
|
| 91 |
}
|
| 92 |
```
|
| 93 |
|
|
|
|
|
|
|
| 94 |
### Example Usage
|
| 95 |
|
| 96 |
#### Get My Profile
|
|
|
|
| 73 |
### Authentication Flow
|
| 74 |
|
| 75 |
1. Service professional logs in via Auth-MS (OTP-based)
|
| 76 |
+
2. Auth-MS returns JWT with `partner_id` in payload (contains staff_id)
|
| 77 |
3. Professional uses JWT to access SPA-MS endpoints
|
| 78 |
+
4. SPA-MS extracts `partner_id` from JWT
|
| 79 |
+
5. Operations performed on professional's own record using partner_id
|
| 80 |
|
| 81 |
### JWT Token Structure
|
| 82 |
|
| 83 |
```json
|
| 84 |
{
|
| 85 |
+
"sub": "staff_id_value",
|
| 86 |
+
"partner_id": "staff_id_value",
|
| 87 |
+
"staff_id": "staff_id_value",
|
| 88 |
"staff_code": "SP001",
|
| 89 |
"user_type": "service_professional",
|
| 90 |
"exp": 1234567890,
|
|
|
|
| 92 |
}
|
| 93 |
```
|
| 94 |
|
| 95 |
+
**Note:** `partner_id` is the standard identifier used across all microservices. For service professionals, it contains the `staff_id` value.
|
| 96 |
+
|
| 97 |
### Example Usage
|
| 98 |
|
| 99 |
#### Get My Profile
|
app/dependencies/auth.py
CHANGED
|
@@ -52,13 +52,16 @@ async def verify_token(authorization: Optional[str] = Header(None)) -> dict:
|
|
| 52 |
if not partner_id:
|
| 53 |
raise HTTPException(
|
| 54 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 55 |
-
detail="Token missing partner_id/user_id",
|
| 56 |
headers={"WWW-Authenticate": "Bearer"}
|
| 57 |
)
|
| 58 |
|
| 59 |
return {
|
| 60 |
"partner_id": partner_id,
|
| 61 |
"user_id": payload.get("user_id"),
|
|
|
|
|
|
|
|
|
|
| 62 |
"merchant_id": payload.get("merchant_id"),
|
| 63 |
"role": payload.get("role"),
|
| 64 |
"email": payload.get("email"),
|
|
|
|
| 52 |
if not partner_id:
|
| 53 |
raise HTTPException(
|
| 54 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 55 |
+
detail="Token missing partner_id/user_id/staff_id",
|
| 56 |
headers={"WWW-Authenticate": "Bearer"}
|
| 57 |
)
|
| 58 |
|
| 59 |
return {
|
| 60 |
"partner_id": partner_id,
|
| 61 |
"user_id": payload.get("user_id"),
|
| 62 |
+
"staff_id": payload.get("staff_id"), # Include staff_id for service professionals
|
| 63 |
+
"staff_code": payload.get("staff_code"), # Include staff_code for reference
|
| 64 |
+
"user_type": payload.get("user_type"), # Include user_type to identify professional
|
| 65 |
"merchant_id": payload.get("merchant_id"),
|
| 66 |
"role": payload.get("role"),
|
| 67 |
"email": payload.get("email"),
|
app/service_professionals/controllers/router.py
CHANGED
|
@@ -28,11 +28,11 @@ async def get_my_profile(
|
|
| 28 |
Get current service professional's profile.
|
| 29 |
|
| 30 |
Retrieves profile information for the authenticated service professional
|
| 31 |
-
based on
|
| 32 |
|
| 33 |
**Authentication:**
|
| 34 |
- Requires valid JWT token in Authorization header
|
| 35 |
-
- Extracts
|
| 36 |
|
| 37 |
**Returns:**
|
| 38 |
- Service professional profile details
|
|
@@ -43,20 +43,20 @@ async def get_my_profile(
|
|
| 43 |
- 500: Server error
|
| 44 |
"""
|
| 45 |
try:
|
| 46 |
-
# Extract
|
| 47 |
-
|
| 48 |
-
if not
|
| 49 |
raise HTTPException(
|
| 50 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 51 |
-
detail="
|
| 52 |
)
|
| 53 |
|
| 54 |
-
professional = await service.get_by_id(
|
| 55 |
|
| 56 |
if not professional:
|
| 57 |
raise HTTPException(
|
| 58 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 59 |
-
detail=f"Service professional
|
| 60 |
)
|
| 61 |
|
| 62 |
return professional
|
|
@@ -81,7 +81,7 @@ async def update_my_profile(
|
|
| 81 |
Update current service professional's profile.
|
| 82 |
|
| 83 |
Allows service professionals to update their own profile information.
|
| 84 |
-
|
| 85 |
|
| 86 |
**Supports updating:**
|
| 87 |
- Basic info (name, designation, role, phone, email)
|
|
@@ -92,7 +92,7 @@ async def update_my_profile(
|
|
| 92 |
|
| 93 |
**Authentication:**
|
| 94 |
- Requires valid JWT token in Authorization header
|
| 95 |
-
- Extracts
|
| 96 |
- Can only update own profile
|
| 97 |
|
| 98 |
**Request Body:**
|
|
@@ -110,16 +110,16 @@ async def update_my_profile(
|
|
| 110 |
- 500: Server error
|
| 111 |
"""
|
| 112 |
try:
|
| 113 |
-
# Extract
|
| 114 |
-
|
| 115 |
-
if not
|
| 116 |
raise HTTPException(
|
| 117 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 118 |
-
detail="
|
| 119 |
)
|
| 120 |
|
| 121 |
-
# Extract updated_by from token (use
|
| 122 |
-
updated_by =
|
| 123 |
|
| 124 |
# Only include fields that were actually provided
|
| 125 |
update_data = request.dict(exclude_unset=True)
|
|
@@ -131,7 +131,7 @@ async def update_my_profile(
|
|
| 131 |
)
|
| 132 |
|
| 133 |
professional = await service.update(
|
| 134 |
-
|
| 135 |
data=update_data,
|
| 136 |
updated_by=updated_by
|
| 137 |
)
|
|
@@ -139,7 +139,7 @@ async def update_my_profile(
|
|
| 139 |
if not professional:
|
| 140 |
raise HTTPException(
|
| 141 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 142 |
-
detail=f"Service professional
|
| 143 |
)
|
| 144 |
|
| 145 |
return professional
|
|
|
|
| 28 |
Get current service professional's profile.
|
| 29 |
|
| 30 |
Retrieves profile information for the authenticated service professional
|
| 31 |
+
based on partner_id from JWT token (which contains staff_id).
|
| 32 |
|
| 33 |
**Authentication:**
|
| 34 |
- Requires valid JWT token in Authorization header
|
| 35 |
+
- Extracts partner_id from token (contains staff_id for service professionals)
|
| 36 |
|
| 37 |
**Returns:**
|
| 38 |
- Service professional profile details
|
|
|
|
| 43 |
- 500: Server error
|
| 44 |
"""
|
| 45 |
try:
|
| 46 |
+
# Extract partner_id from JWT token (contains staff_id for service professionals)
|
| 47 |
+
partner_id = token_data.get("partner_id")
|
| 48 |
+
if not partner_id:
|
| 49 |
raise HTTPException(
|
| 50 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 51 |
+
detail="Partner ID not found in token"
|
| 52 |
)
|
| 53 |
|
| 54 |
+
professional = await service.get_by_id(partner_id)
|
| 55 |
|
| 56 |
if not professional:
|
| 57 |
raise HTTPException(
|
| 58 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 59 |
+
detail=f"Service professional not found"
|
| 60 |
)
|
| 61 |
|
| 62 |
return professional
|
|
|
|
| 81 |
Update current service professional's profile.
|
| 82 |
|
| 83 |
Allows service professionals to update their own profile information.
|
| 84 |
+
Partner ID is extracted from JWT token for security (contains staff_id).
|
| 85 |
|
| 86 |
**Supports updating:**
|
| 87 |
- Basic info (name, designation, role, phone, email)
|
|
|
|
| 92 |
|
| 93 |
**Authentication:**
|
| 94 |
- Requires valid JWT token in Authorization header
|
| 95 |
+
- Extracts partner_id from token (contains staff_id for service professionals)
|
| 96 |
- Can only update own profile
|
| 97 |
|
| 98 |
**Request Body:**
|
|
|
|
| 110 |
- 500: Server error
|
| 111 |
"""
|
| 112 |
try:
|
| 113 |
+
# Extract partner_id from JWT token (contains staff_id for service professionals)
|
| 114 |
+
partner_id = token_data.get("partner_id")
|
| 115 |
+
if not partner_id:
|
| 116 |
raise HTTPException(
|
| 117 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 118 |
+
detail="Partner ID not found in token"
|
| 119 |
)
|
| 120 |
|
| 121 |
+
# Extract updated_by from token (use partner_id as identifier)
|
| 122 |
+
updated_by = partner_id
|
| 123 |
|
| 124 |
# Only include fields that were actually provided
|
| 125 |
update_data = request.dict(exclude_unset=True)
|
|
|
|
| 131 |
)
|
| 132 |
|
| 133 |
professional = await service.update(
|
| 134 |
+
partner_id=partner_id,
|
| 135 |
data=update_data,
|
| 136 |
updated_by=updated_by
|
| 137 |
)
|
|
|
|
| 139 |
if not professional:
|
| 140 |
raise HTTPException(
|
| 141 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 142 |
+
detail=f"Service professional not found"
|
| 143 |
)
|
| 144 |
|
| 145 |
return professional
|
app/service_professionals/models/model.py
CHANGED
|
@@ -22,7 +22,7 @@ class AddressModel(BaseModel):
|
|
| 22 |
|
| 23 |
class ServiceProfessionalModel(BaseModel):
|
| 24 |
"""Service Professional model for MongoDB."""
|
| 25 |
-
|
| 26 |
staff_code: str = Field(..., description="Staff code for identification")
|
| 27 |
name: str = Field(..., description="Full name")
|
| 28 |
designation: str = Field(..., description="Job designation/title")
|
|
@@ -42,7 +42,7 @@ class ServiceProfessionalModel(BaseModel):
|
|
| 42 |
class Config:
|
| 43 |
json_schema_extra = {
|
| 44 |
"example": {
|
| 45 |
-
"
|
| 46 |
"staff_code": "SP001",
|
| 47 |
"name": "Priya Sharma",
|
| 48 |
"designation": "Senior Beautician",
|
|
|
|
| 22 |
|
| 23 |
class ServiceProfessionalModel(BaseModel):
|
| 24 |
"""Service Professional model for MongoDB."""
|
| 25 |
+
partner_id: str = Field(..., description="Unique partner identifier")
|
| 26 |
staff_code: str = Field(..., description="Staff code for identification")
|
| 27 |
name: str = Field(..., description="Full name")
|
| 28 |
designation: str = Field(..., description="Job designation/title")
|
|
|
|
| 42 |
class Config:
|
| 43 |
json_schema_extra = {
|
| 44 |
"example": {
|
| 45 |
+
"partner_id": "550e8400-e29b-41d4-a716-446655440001",
|
| 46 |
"staff_code": "SP001",
|
| 47 |
"name": "Priya Sharma",
|
| 48 |
"designation": "Senior Beautician",
|
app/service_professionals/schemas/schema.py
CHANGED
|
@@ -46,7 +46,7 @@ class ServiceProfessionalUpdateRequest(BaseModel):
|
|
| 46 |
|
| 47 |
class ServiceProfessionalResponse(BaseModel):
|
| 48 |
"""Response schema for service professional."""
|
| 49 |
-
|
| 50 |
staff_code: str
|
| 51 |
name: str
|
| 52 |
designation: str
|
|
|
|
| 46 |
|
| 47 |
class ServiceProfessionalResponse(BaseModel):
|
| 48 |
"""Response schema for service professional."""
|
| 49 |
+
partner_id: str
|
| 50 |
staff_code: str
|
| 51 |
name: str
|
| 52 |
designation: str
|
app/service_professionals/services/service.py
CHANGED
|
@@ -19,23 +19,23 @@ class ServiceProfessionalService:
|
|
| 19 |
self.db: AsyncIOMotorDatabase = get_database()
|
| 20 |
self.collection = self.db[SERVICE_PROFESSIONALS_COLLECTION]
|
| 21 |
|
| 22 |
-
async def get_by_id(self,
|
| 23 |
"""
|
| 24 |
-
Get service professional by ID.
|
| 25 |
|
| 26 |
Args:
|
| 27 |
-
|
| 28 |
|
| 29 |
Returns:
|
| 30 |
Service professional or None
|
| 31 |
"""
|
| 32 |
try:
|
| 33 |
-
doc = await self.collection.find_one({"
|
| 34 |
if doc:
|
| 35 |
return ServiceProfessionalModel(**doc)
|
| 36 |
return None
|
| 37 |
except Exception as e:
|
| 38 |
-
logger.error(f"Error fetching service professional {
|
| 39 |
raise HTTPException(
|
| 40 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 41 |
detail="Failed to fetch service professional"
|
|
@@ -43,7 +43,7 @@ class ServiceProfessionalService:
|
|
| 43 |
|
| 44 |
async def update(
|
| 45 |
self,
|
| 46 |
-
|
| 47 |
data: Dict,
|
| 48 |
updated_by: str
|
| 49 |
) -> Optional[ServiceProfessionalModel]:
|
|
@@ -51,7 +51,7 @@ class ServiceProfessionalService:
|
|
| 51 |
Update service professional.
|
| 52 |
|
| 53 |
Args:
|
| 54 |
-
|
| 55 |
data: Update data
|
| 56 |
updated_by: Username of updater
|
| 57 |
|
|
@@ -63,7 +63,7 @@ class ServiceProfessionalService:
|
|
| 63 |
"""
|
| 64 |
try:
|
| 65 |
# Check if exists
|
| 66 |
-
existing = await self.collection.find_one({"
|
| 67 |
if not existing:
|
| 68 |
return None
|
| 69 |
|
|
@@ -71,7 +71,7 @@ class ServiceProfessionalService:
|
|
| 71 |
if "staff_code" in data and data["staff_code"] != existing.get("staff_code"):
|
| 72 |
code_exists = await self.collection.find_one({
|
| 73 |
"staff_code": data["staff_code"],
|
| 74 |
-
"
|
| 75 |
"is_deleted": False
|
| 76 |
})
|
| 77 |
if code_exists:
|
|
@@ -84,7 +84,7 @@ class ServiceProfessionalService:
|
|
| 84 |
if "phone" in data and data["phone"] != existing.get("phone"):
|
| 85 |
phone_exists = await self.collection.find_one({
|
| 86 |
"phone": data["phone"],
|
| 87 |
-
"
|
| 88 |
"is_deleted": False
|
| 89 |
})
|
| 90 |
if phone_exists:
|
|
@@ -105,21 +105,21 @@ class ServiceProfessionalService:
|
|
| 105 |
|
| 106 |
# Update document
|
| 107 |
await self.collection.update_one(
|
| 108 |
-
{"
|
| 109 |
{"$set": data}
|
| 110 |
)
|
| 111 |
|
| 112 |
# Fetch updated document
|
| 113 |
-
updated_doc = await self.collection.find_one({"
|
| 114 |
|
| 115 |
-
logger.info(f"Updated service professional: {
|
| 116 |
|
| 117 |
return ServiceProfessionalModel(**updated_doc)
|
| 118 |
|
| 119 |
except HTTPException:
|
| 120 |
raise
|
| 121 |
except Exception as e:
|
| 122 |
-
logger.error(f"Error updating service professional {
|
| 123 |
raise HTTPException(
|
| 124 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 125 |
detail="Failed to update service professional"
|
|
|
|
| 19 |
self.db: AsyncIOMotorDatabase = get_database()
|
| 20 |
self.collection = self.db[SERVICE_PROFESSIONALS_COLLECTION]
|
| 21 |
|
| 22 |
+
async def get_by_id(self, partner_id: str) -> Optional[ServiceProfessionalModel]:
|
| 23 |
"""
|
| 24 |
+
Get service professional by partner ID.
|
| 25 |
|
| 26 |
Args:
|
| 27 |
+
partner_id: Partner ID
|
| 28 |
|
| 29 |
Returns:
|
| 30 |
Service professional or None
|
| 31 |
"""
|
| 32 |
try:
|
| 33 |
+
doc = await self.collection.find_one({"partner_id": partner_id, "is_deleted": False})
|
| 34 |
if doc:
|
| 35 |
return ServiceProfessionalModel(**doc)
|
| 36 |
return None
|
| 37 |
except Exception as e:
|
| 38 |
+
logger.error(f"Error fetching service professional {partner_id}: {str(e)}", exc_info=True)
|
| 39 |
raise HTTPException(
|
| 40 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 41 |
detail="Failed to fetch service professional"
|
|
|
|
| 43 |
|
| 44 |
async def update(
|
| 45 |
self,
|
| 46 |
+
partner_id: str,
|
| 47 |
data: Dict,
|
| 48 |
updated_by: str
|
| 49 |
) -> Optional[ServiceProfessionalModel]:
|
|
|
|
| 51 |
Update service professional.
|
| 52 |
|
| 53 |
Args:
|
| 54 |
+
partner_id: Partner ID
|
| 55 |
data: Update data
|
| 56 |
updated_by: Username of updater
|
| 57 |
|
|
|
|
| 63 |
"""
|
| 64 |
try:
|
| 65 |
# Check if exists
|
| 66 |
+
existing = await self.collection.find_one({"partner_id": partner_id, "is_deleted": False})
|
| 67 |
if not existing:
|
| 68 |
return None
|
| 69 |
|
|
|
|
| 71 |
if "staff_code" in data and data["staff_code"] != existing.get("staff_code"):
|
| 72 |
code_exists = await self.collection.find_one({
|
| 73 |
"staff_code": data["staff_code"],
|
| 74 |
+
"partner_id": {"$ne": partner_id},
|
| 75 |
"is_deleted": False
|
| 76 |
})
|
| 77 |
if code_exists:
|
|
|
|
| 84 |
if "phone" in data and data["phone"] != existing.get("phone"):
|
| 85 |
phone_exists = await self.collection.find_one({
|
| 86 |
"phone": data["phone"],
|
| 87 |
+
"partner_id": {"$ne": partner_id},
|
| 88 |
"is_deleted": False
|
| 89 |
})
|
| 90 |
if phone_exists:
|
|
|
|
| 105 |
|
| 106 |
# Update document
|
| 107 |
await self.collection.update_one(
|
| 108 |
+
{"partner_id": partner_id},
|
| 109 |
{"$set": data}
|
| 110 |
)
|
| 111 |
|
| 112 |
# Fetch updated document
|
| 113 |
+
updated_doc = await self.collection.find_one({"partner_id": partner_id})
|
| 114 |
|
| 115 |
+
logger.info(f"Updated service professional: {partner_id}")
|
| 116 |
|
| 117 |
return ServiceProfessionalModel(**updated_doc)
|
| 118 |
|
| 119 |
except HTTPException:
|
| 120 |
raise
|
| 121 |
except Exception as e:
|
| 122 |
+
logger.error(f"Error updating service professional {partner_id}: {str(e)}", exc_info=True)
|
| 123 |
raise HTTPException(
|
| 124 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 125 |
detail="Failed to update service professional"
|