MukeshKapoor25 commited on
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 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 `staff_id` in the token payload.
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 staff_id in payload
77
  3. Professional uses JWT to access SPA-MS endpoints
78
- 4. SPA-MS extracts staff_id from JWT
79
- 5. Operations performed on professional's own record
80
 
81
  ### JWT Token Structure
82
 
83
  ```json
84
  {
85
- "sub": "staff_id",
86
- "staff_id": "550e8400-e29b-41d4-a716-446655440001",
 
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 staff_id from JWT token.
32
 
33
  **Authentication:**
34
  - Requires valid JWT token in Authorization header
35
- - Extracts staff_id from token
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 staff_id from JWT token
47
- staff_id = token_data.get("staff_id")
48
- if not staff_id:
49
  raise HTTPException(
50
  status_code=status.HTTP_401_UNAUTHORIZED,
51
- detail="Staff ID not found in token"
52
  )
53
 
54
- professional = await service.get_by_id(staff_id)
55
 
56
  if not professional:
57
  raise HTTPException(
58
  status_code=status.HTTP_404_NOT_FOUND,
59
- detail=f"Service professional with staff ID {staff_id} not found"
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
- Staff ID is extracted from JWT token for security.
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 staff_id from token
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 staff_id from JWT token
114
- staff_id = token_data.get("staff_id")
115
- if not staff_id:
116
  raise HTTPException(
117
  status_code=status.HTTP_401_UNAUTHORIZED,
118
- detail="Staff ID not found in token"
119
  )
120
 
121
- # Extract updated_by from token (use staff_id as identifier)
122
- updated_by = staff_id
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
- staff_id=staff_id,
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 with staff ID {staff_id} not found"
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
- staff_id: str = Field(..., description="Unique staff 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,7 +42,7 @@ class ServiceProfessionalModel(BaseModel):
42
  class Config:
43
  json_schema_extra = {
44
  "example": {
45
- "staff_id": "550e8400-e29b-41d4-a716-446655440001",
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
- staff_id: str
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, staff_id: str) -> Optional[ServiceProfessionalModel]:
23
  """
24
- Get service professional by ID.
25
 
26
  Args:
27
- staff_id: Staff ID
28
 
29
  Returns:
30
  Service professional or None
31
  """
32
  try:
33
- doc = await self.collection.find_one({"staff_id": staff_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 {staff_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,7 +43,7 @@ class ServiceProfessionalService:
43
 
44
  async def update(
45
  self,
46
- staff_id: str,
47
  data: Dict,
48
  updated_by: str
49
  ) -> Optional[ServiceProfessionalModel]:
@@ -51,7 +51,7 @@ class ServiceProfessionalService:
51
  Update service professional.
52
 
53
  Args:
54
- staff_id: Staff ID
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({"staff_id": staff_id, "is_deleted": False})
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
- "staff_id": {"$ne": staff_id},
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
- "staff_id": {"$ne": staff_id},
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
- {"staff_id": staff_id},
109
  {"$set": data}
110
  )
111
 
112
  # Fetch updated document
113
- updated_doc = await self.collection.find_one({"staff_id": staff_id})
114
 
115
- logger.info(f"Updated service professional: {staff_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 {staff_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"
 
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"