99from api .models .membership import Membership
1010from api .models .pagination_options import PaginationOptions
1111from api .models .staff_user import StaffUser as StaffUserModel
12+ from api .models .tenant import Tenant as TenantModel
1213from api .models .user_group_membership import UserGroupMembership
1314from api .schemas .staff_user import StaffUserSchema
1415from api .services .user_group_membership_service import UserGroupMembershipService
@@ -44,25 +45,32 @@ def create_or_update_user(self, user: dict):
4445 self .validate_fields (user )
4546
4647 external_id = user .get ('external_id' )
48+ tenant_id = getattr (g , 'tenant_id' , None )
49+ has_no_tenant_roles = len (user .get ('roles' , [])) == 0
4750 db_user = StaffUserModel .get_user_by_external_id (external_id , include_inactive = True )
4851
4952 if db_user is None :
5053 new_user = StaffUserModel .create_user (user )
51- if len (user .get ('roles' , [])) == 0 :
54+ if has_no_tenant_roles and tenant_id :
55+ UserGroupMembershipService .ensure_access_request_membership (external_id , tenant_id )
56+ if has_no_tenant_roles :
5257 self ._send_access_request_email (new_user , getattr (g , 'tenant_id' , None ))
5358 return new_user
5459
60+ if has_no_tenant_roles and tenant_id :
61+ UserGroupMembershipService .ensure_access_request_membership (external_id , tenant_id )
62+
5563 return StaffUserModel .update_user (db_user .id , user )
5664
5765 @classmethod
5866 def sync_super_admin_membership (cls , token_info : dict , token_roles , tenant_id = None ):
5967 """Sync SUPER_ADMIN tenant membership from keycloak roles at login/request time."""
60- if getattr (g , '_syncing_super_admin_membership ' , False ):
68+ if getattr (g , 'syncing_super_admin_membership ' , False ):
6169 return
6270
63- g ._syncing_super_admin_membership = True
71+ g .syncing_super_admin_membership = True
6472 if not token_info :
65- g ._syncing_super_admin_membership = False
73+ g .syncing_super_admin_membership = False
6674 return
6775
6876 try :
@@ -94,23 +102,41 @@ def sync_super_admin_membership(cls, token_info: dict, token_roles, tenant_id=No
94102 if not has_super_admin_role :
95103 UserGroupMembershipService .remove_group_memberships_by_group_name (external_id , 'SUPER_ADMIN' )
96104 finally :
97- g ._syncing_super_admin_membership = False
105+ g .syncing_super_admin_membership = False
98106
99107 @staticmethod
100108 def _send_access_request_email (user : StaffUserModel , tenant_id : Optional [int ] = None ) -> None :
101109 """Send a new user email.Throws error if fails."""
102110 templates = current_app .config ['EMAIL_TEMPLATES' ]
103- to_email_address = templates ['ACCESS_REQUEST' ]['DEST_EMAIL_ADDRESS' ]
104- if to_email_address is None :
105- return
106111 template_id = templates ['ACCESS_REQUEST' ]['ID' ]
107112 subject , body , args = StaffUserService ._render_email_template (user , tenant_id )
113+
114+ resolved_tenant_id = tenant_id if tenant_id is not None else getattr (g , 'tenant_id' , None )
115+ recipient_emails = []
116+
117+ global_contact_email = templates ['ACCESS_REQUEST' ]['DEST_EMAIL_ADDRESS' ]
118+ if global_contact_email :
119+ recipient_emails .append (global_contact_email )
120+
121+ if resolved_tenant_id :
122+ tenant = TenantModel .find_by_id (resolved_tenant_id )
123+ if tenant and tenant .contact_email :
124+ recipient_emails .append (tenant .contact_email )
125+
126+ # Preserve order and avoid duplicate sends when contacts are the same.
127+ recipient_emails = list (dict .fromkeys (recipient_emails ))
128+ if not recipient_emails :
129+ return
130+
108131 try :
109- notification .send_email (subject = subject ,
110- email = to_email_address ,
111- html_body = body ,
112- args = args ,
113- template_id = template_id )
132+ for email in recipient_emails :
133+ notification .send_email (
134+ subject = subject ,
135+ email = email ,
136+ html_body = body ,
137+ args = args ,
138+ template_id = template_id ,
139+ )
114140 except Exception as exc : # noqa: B902
115141 current_app .logger .error ('<Notification for new user registration failed' , exc )
116142 raise BusinessException (
@@ -153,9 +179,11 @@ def attach_roles(user_collection):
153179 composite_role = UserGroupMembershipService .get_user_group_within_tenant (user .get ('external_id' ),
154180 g .tenant_id )
155181 user ['composite_roles' ] = ''
182+ user ['main_role' ] = ''
156183 if composite_role :
157184 user ['composite_roles' ] = composite_role
158- user ['main_role' ] = CompositeRoles [composite_role ].value
185+ if composite_role in CompositeRoles .__members__ :
186+ user ['main_role' ] = CompositeRoles [composite_role ].value
159187
160188 @classmethod
161189 def find_users (
@@ -221,7 +249,12 @@ def validate_user(db_user: StaffUserModel):
221249
222250 @classmethod
223251 def delete_deactivated_user (cls , user_id , actor_external_id : Optional [str ] = None ):
224- """Permanently delete a user after required safeguards are satisfied."""
252+ """Delete a deactivated user or only remove current tenant membership.
253+
254+ Behavior is tenant-aware:
255+ - If the user has memberships in other tenants, only remove membership in the current tenant.
256+ - If the user only belongs to the current tenant, fully delete the user.
257+ """
225258 db_user = StaffUserModel .get_by_id (user_id , include_inactive = True )
226259 if db_user is None :
227260 raise BusinessException (error = 'User not found.' , status_code = HTTPStatus .NOT_FOUND )
@@ -238,9 +271,48 @@ def delete_deactivated_user(cls, user_id, actor_external_id: Optional[str] = Non
238271 status_code = HTTPStatus .CONFLICT ,
239272 )
240273
274+ current_tenant_id = getattr (g , 'tenant_id' , None )
275+ if not current_tenant_id :
276+ raise BusinessException (
277+ error = 'Tenant context is required for this operation.' ,
278+ status_code = HTTPStatus .BAD_REQUEST ,
279+ )
280+
281+ user_group_memberships = UserGroupMembership .query .filter (
282+ UserGroupMembership .staff_user_external_id == db_user .external_id ,
283+ ).all ()
284+
285+ has_current_tenant_membership = any (
286+ membership .tenant_id == current_tenant_id for membership in user_group_memberships
287+ )
288+ if not has_current_tenant_membership :
289+ raise BusinessException (
290+ error = 'User does not have membership in the current tenant.' ,
291+ status_code = HTTPStatus .BAD_REQUEST ,
292+ )
293+
294+ has_other_tenant_memberships = any (
295+ membership .tenant_id != current_tenant_id for membership in user_group_memberships
296+ )
297+
298+ if has_other_tenant_memberships :
299+ UserGroupMembership .query .filter (
300+ UserGroupMembership .staff_user_external_id == db_user .external_id ,
301+ UserGroupMembership .tenant_id == current_tenant_id ,
302+ ).delete (synchronize_session = False )
303+ db .session .commit ()
304+ return {
305+ 'message' : 'User removed from current tenant successfully.' ,
306+ 'action' : 'removed_current_tenant_membership' ,
307+ }
308+
241309 Membership .query .filter (Membership .user_id == db_user .id ).delete (synchronize_session = False )
242310 UserGroupMembership .query .filter (
243311 UserGroupMembership .staff_user_external_id == db_user .external_id ,
244312 ).delete (synchronize_session = False )
245313 db .session .delete (db_user )
246314 db .session .commit ()
315+ return {
316+ 'message' : 'User deleted successfully.' ,
317+ 'action' : 'deleted_user' ,
318+ }
0 commit comments