geek-cafe-saas-sdk 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geek-cafe-saas-sdk might be problematic. Click here for more details.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
@@ -0,0 +1,408 @@
1
+ # Resource Permission Service
2
+
3
+ from typing import Dict, Any, List, Optional
4
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
6
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
7
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
8
+ from geek_cafe_saas_sdk.domains.auth.models import ResourcePermission
9
+ import datetime as dt
10
+ import time
11
+
12
+
13
+ class ResourcePermissionService(DatabaseService[ResourcePermission]):
14
+ """
15
+ Service for ResourcePermission operations (ABAC).
16
+
17
+ Manages resource-level permission grants for fine-grained sharing.
18
+ """
19
+
20
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
21
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
22
+
23
+ # Required abstract methods from DatabaseService
24
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[ResourcePermission]:
25
+ """Create method - delegates to grant_permission()."""
26
+ required = ['resource_type', 'resource_id', 'permissions', 'grantee_user_id']
27
+ for field in required:
28
+ if field not in kwargs:
29
+ return ServiceResult.error_result(f"{field} is required", "VALIDATION_ERROR")
30
+
31
+ return self.grant_permission(
32
+ grantee_user_id=kwargs['grantee_user_id'],
33
+ tenant_id=tenant_id,
34
+ resource_type=kwargs['resource_type'],
35
+ resource_id=kwargs['resource_id'],
36
+ permissions=kwargs['permissions'],
37
+ granted_by=user_id,
38
+ reason=kwargs.get('reason'),
39
+ expires_at=kwargs.get('expires_at')
40
+ )
41
+
42
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[ResourcePermission]:
43
+ """Get a specific grant by ID."""
44
+ try:
45
+ grant = self._get_model_by_id(resource_id, ResourcePermission)
46
+
47
+ if not grant:
48
+ raise NotFoundError(f"Grant with ID {resource_id} not found")
49
+
50
+ # Validate tenant access
51
+ if grant.tenant_id != tenant_id:
52
+ raise AccessDeniedError("Access denied: different tenant")
53
+
54
+ return ServiceResult.success_result(grant)
55
+
56
+ except Exception as e:
57
+ return self._handle_service_exception(e, 'get_by_id', resource_id=resource_id)
58
+
59
+ def update(self, resource_id: str, tenant_id: str, user_id: str, updates: Dict[str, Any]) -> ServiceResult[ResourcePermission]:
60
+ """Update a grant (e.g., change permissions or expiration)."""
61
+ try:
62
+ grant = self._get_model_by_id(resource_id, ResourcePermission)
63
+
64
+ if not grant:
65
+ raise NotFoundError(f"Grant with ID {resource_id} not found")
66
+
67
+ # Validate tenant access
68
+ if grant.tenant_id != tenant_id:
69
+ raise AccessDeniedError("Access denied: different tenant")
70
+
71
+ # Apply updates
72
+ if 'permissions' in updates:
73
+ grant.permissions = updates['permissions']
74
+ if 'expires_at' in updates:
75
+ grant.expires_at = updates['expires_at']
76
+ if 'reason' in updates:
77
+ grant.reason = updates['reason']
78
+ if 'metadata' in updates:
79
+ grant.metadata = updates['metadata']
80
+
81
+ grant.prep_for_save()
82
+ return self._save_model(grant)
83
+
84
+ except Exception as e:
85
+ return self._handle_service_exception(e, 'update', resource_id=resource_id)
86
+
87
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
88
+ """Revoke a grant."""
89
+ try:
90
+ grant = self._get_model_by_id(resource_id, ResourcePermission)
91
+
92
+ if not grant:
93
+ return ServiceResult.success_result(True) # Already revoked
94
+
95
+ # Validate tenant access
96
+ if grant.tenant_id != tenant_id:
97
+ raise AccessDeniedError("Access denied: different tenant")
98
+
99
+ # Hard delete (or could soft delete with deleted_at timestamp)
100
+ result = self._delete_model(grant)
101
+
102
+ if result.success:
103
+ return ServiceResult.success_result(True)
104
+ return result
105
+
106
+ except Exception as e:
107
+ return self._handle_service_exception(e, 'delete', resource_id=resource_id)
108
+
109
+ # Grant Management Methods
110
+
111
+ def grant_permission(
112
+ self,
113
+ grantee_user_id: str,
114
+ tenant_id: str,
115
+ resource_type: str,
116
+ resource_id: str,
117
+ permissions: List[str],
118
+ granted_by: str,
119
+ reason: Optional[str] = None,
120
+ expires_at: Optional[int] = None,
121
+ metadata: Optional[Dict[str, Any]] = None
122
+ ) -> ServiceResult[ResourcePermission]:
123
+ """
124
+ Grant resource-level permissions to a user.
125
+
126
+ Args:
127
+ grantee_user_id: User to grant permissions to
128
+ tenant_id: Tenant context
129
+ resource_type: Type of resource (event, community, chat_channel, etc.)
130
+ resource_id: ID of specific resource
131
+ permissions: List of permissions to grant (e.g., ["read", "write"])
132
+ granted_by: User ID granting the permission
133
+ reason: Optional reason for the grant
134
+ expires_at: Optional expiration timestamp
135
+ metadata: Optional additional metadata
136
+ """
137
+ try:
138
+ # Validate inputs
139
+ if not permissions or len(permissions) == 0:
140
+ raise ValidationError("At least one permission is required")
141
+
142
+ # Check if grant already exists
143
+ existing = self.get_grant(grantee_user_id, resource_type, resource_id, tenant_id)
144
+
145
+ if existing.success and existing.data:
146
+ # Update existing grant
147
+ grant = existing.data
148
+ grant.permissions = permissions
149
+ grant.granted_by = granted_by
150
+ grant.granted_at = int(time.time())
151
+ grant.expires_at = expires_at
152
+ grant.reason = reason
153
+ if metadata:
154
+ grant.metadata = metadata
155
+ else:
156
+ # Create new grant
157
+ grant = ResourcePermission()
158
+ grant.user_id = grantee_user_id
159
+ grant.tenant_id = tenant_id
160
+ grant.resource_type = resource_type
161
+ grant.resource_id = resource_id
162
+ grant.permissions = permissions
163
+ grant.granted_by = granted_by
164
+ grant.granted_at = int(time.time())
165
+ grant.expires_at = expires_at
166
+ grant.reason = reason
167
+ if metadata:
168
+ grant.metadata = metadata
169
+
170
+ grant.prep_for_save()
171
+ return self._save_model(grant)
172
+
173
+ except Exception as e:
174
+ return self._handle_service_exception(
175
+ e, 'grant_permission',
176
+ user_id=grantee_user_id,
177
+ resource_type=resource_type,
178
+ resource_id=resource_id
179
+ )
180
+
181
+ def revoke_permission(
182
+ self,
183
+ grantee_user_id: str,
184
+ resource_type: str,
185
+ resource_id: str,
186
+ tenant_id: str
187
+ ) -> ServiceResult[bool]:
188
+ """
189
+ Revoke all permissions for a user on a resource.
190
+
191
+ Args:
192
+ grantee_user_id: User to revoke from
193
+ resource_type: Type of resource
194
+ resource_id: ID of resource
195
+ tenant_id: Tenant context
196
+ """
197
+ try:
198
+ grant_result = self.get_grant(grantee_user_id, resource_type, resource_id, tenant_id)
199
+
200
+ if not grant_result.success or not grant_result.data:
201
+ return ServiceResult.success_result(True) # Already revoked
202
+
203
+ grant = grant_result.data
204
+ result = self._delete_model(grant)
205
+
206
+ if result.success:
207
+ return ServiceResult.success_result(True)
208
+ return result
209
+
210
+ except Exception as e:
211
+ return self._handle_service_exception(
212
+ e, 'revoke_permission',
213
+ user_id=grantee_user_id,
214
+ resource_type=resource_type,
215
+ resource_id=resource_id
216
+ )
217
+
218
+ def get_grant(
219
+ self,
220
+ user_id: str,
221
+ resource_type: str,
222
+ resource_id: str,
223
+ tenant_id: str
224
+ ) -> ServiceResult[ResourcePermission]:
225
+ """
226
+ Get a specific grant for user on resource.
227
+
228
+ Args:
229
+ user_id: User ID
230
+ resource_type: Type of resource
231
+ resource_id: ID of resource
232
+ tenant_id: Tenant context
233
+ """
234
+ try:
235
+ # Query using GSI1 (user + resource)
236
+ temp = ResourcePermission()
237
+ temp.user_id = user_id
238
+ temp.resource_type = resource_type
239
+ temp.resource_id = resource_id
240
+ temp.tenant_id = tenant_id
241
+
242
+ result = self._query_by_index(temp, "gsi1", limit=1)
243
+
244
+ if not result.success:
245
+ return result
246
+
247
+ if not result.data or len(result.data) == 0:
248
+ return ServiceResult.error_result("Grant not found", "NOT_FOUND")
249
+
250
+ grant = result.data[0]
251
+
252
+ # Check if expired
253
+ if grant.expires_at and grant.expires_at < int(time.time()):
254
+ return ServiceResult.error_result("Grant has expired", "EXPIRED")
255
+
256
+ return ServiceResult.success_result(grant)
257
+
258
+ except Exception as e:
259
+ return self._handle_service_exception(
260
+ e, 'get_grant',
261
+ user_id=user_id,
262
+ resource_type=resource_type,
263
+ resource_id=resource_id
264
+ )
265
+
266
+ def list_user_grants(
267
+ self,
268
+ user_id: str,
269
+ tenant_id: str,
270
+ limit: int = 50
271
+ ) -> ServiceResult[List[ResourcePermission]]:
272
+ """
273
+ List all grants for a user.
274
+
275
+ Args:
276
+ user_id: User ID
277
+ tenant_id: Tenant context
278
+ limit: Max results
279
+ """
280
+ try:
281
+ temp = ResourcePermission()
282
+ temp.user_id = user_id
283
+ temp.tenant_id = tenant_id
284
+
285
+ result = self._query_by_index(temp, "gsi2", limit=limit)
286
+
287
+ if not result.success:
288
+ return result
289
+
290
+ # Filter out expired grants
291
+ current_time = int(time.time())
292
+ active_grants = [
293
+ grant for grant in result.data
294
+ if not grant.expires_at or grant.expires_at > current_time
295
+ ]
296
+
297
+ return ServiceResult.success_result(active_grants)
298
+
299
+ except Exception as e:
300
+ return self._handle_service_exception(
301
+ e, 'list_user_grants',
302
+ user_id=user_id
303
+ )
304
+
305
+ def list_resource_grants(
306
+ self,
307
+ resource_type: str,
308
+ resource_id: str,
309
+ tenant_id: str,
310
+ limit: int = 50
311
+ ) -> ServiceResult[List[ResourcePermission]]:
312
+ """
313
+ List all grants on a resource (who has access).
314
+
315
+ Args:
316
+ resource_type: Type of resource
317
+ resource_id: ID of resource
318
+ tenant_id: Tenant context
319
+ limit: Max results
320
+ """
321
+ try:
322
+ temp = ResourcePermission()
323
+ temp.resource_type = resource_type
324
+ temp.resource_id = resource_id
325
+ temp.tenant_id = tenant_id
326
+
327
+ result = self._query_by_index(temp, "gsi3", limit=limit)
328
+
329
+ if not result.success:
330
+ return result
331
+
332
+ # Filter out expired grants
333
+ current_time = int(time.time())
334
+ active_grants = [
335
+ grant for grant in result.data
336
+ if not grant.expires_at or grant.expires_at > current_time
337
+ ]
338
+
339
+ return ServiceResult.success_result(active_grants)
340
+
341
+ except Exception as e:
342
+ return self._handle_service_exception(
343
+ e, 'list_resource_grants',
344
+ resource_type=resource_type,
345
+ resource_id=resource_id
346
+ )
347
+
348
+ def get_user_permissions_on_resource(
349
+ self,
350
+ user_id: str,
351
+ resource_type: str,
352
+ resource_id: str,
353
+ tenant_id: str
354
+ ) -> List[str]:
355
+ """
356
+ Get list of permissions user has on a resource.
357
+
358
+ Args:
359
+ user_id: User ID
360
+ resource_type: Type of resource
361
+ resource_id: ID of resource
362
+ tenant_id: Tenant context
363
+
364
+ Returns:
365
+ List of permission strings (e.g., ["read", "write"])
366
+ """
367
+ try:
368
+ grant_result = self.get_grant(user_id, resource_type, resource_id, tenant_id)
369
+
370
+ if grant_result.success and grant_result.data:
371
+ return grant_result.data.permissions
372
+
373
+ return []
374
+
375
+ except:
376
+ return []
377
+
378
+ def has_permission_on_resource(
379
+ self,
380
+ user_id: str,
381
+ resource_type: str,
382
+ resource_id: str,
383
+ permission: str,
384
+ tenant_id: str
385
+ ) -> bool:
386
+ """
387
+ Check if user has a specific permission on a resource.
388
+
389
+ Args:
390
+ user_id: User ID
391
+ resource_type: Type of resource
392
+ resource_id: ID of resource
393
+ permission: Permission to check (e.g., "write")
394
+ tenant_id: Tenant context
395
+
396
+ Returns:
397
+ True if user has permission, False otherwise
398
+ """
399
+ try:
400
+ perms = self.get_user_permissions_on_resource(
401
+ user_id, resource_type, resource_id, tenant_id
402
+ )
403
+
404
+ # Check for wildcard or specific permission
405
+ return "*" in perms or permission in perms
406
+
407
+ except:
408
+ return False
@@ -0,0 +1,274 @@
1
+ # User Service
2
+
3
+ from typing import Dict, Any, Optional, List
4
+ from boto3_assist.dynamodb.dynamodb import DynamoDB
5
+ from geek_cafe_saas_sdk.services.database_service import DatabaseService
6
+ from geek_cafe_saas_sdk.core.service_result import ServiceResult
7
+ from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
8
+ from geek_cafe_saas_sdk.utilities.dynamodb_utils import build_projection_with_reserved_keywords
9
+ from geek_cafe_saas_sdk.domains.auth.models import User
10
+ import datetime as dt
11
+
12
+
13
+ class UserService(DatabaseService[User]):
14
+
15
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
16
+ super().__init__(dynamodb=dynamodb, table_name=table_name)
17
+
18
+ def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[User]:
19
+ """Create a new user."""
20
+ try:
21
+ # Validate required fields
22
+ required_fields = ['email', 'first_name', 'last_name']
23
+ self._validate_required_fields(kwargs, required_fields)
24
+
25
+ # Create user instance using map() approach
26
+ user = User().map(kwargs)
27
+ user.tenant_id = tenant_id
28
+ user.user_id = user_id # Set creator
29
+ user.created_by_id = user_id
30
+
31
+ # Prepare for save (sets ID and timestamps)
32
+ user.prep_for_save()
33
+
34
+ # Save to database
35
+ return self._save_model(user)
36
+
37
+ except Exception as e:
38
+ return self._handle_service_exception(e, 'create_user', tenant_id=tenant_id, user_id=user_id)
39
+
40
+ def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
41
+ """Get user by ID with access control."""
42
+ try:
43
+ user = self._get_model_by_id(resource_id, User)
44
+
45
+ if not user:
46
+ raise NotFoundError(f"User with ID {resource_id} not found")
47
+
48
+ # Check if deleted
49
+ if user.is_deleted():
50
+ raise NotFoundError(f"User with ID {resource_id} not found")
51
+
52
+ # Validate tenant access
53
+ if hasattr(user, 'tenant_id'):
54
+ self._validate_tenant_access(user.tenant_id, tenant_id)
55
+
56
+ return ServiceResult.success_result(user)
57
+
58
+ except Exception as e:
59
+ return self._handle_service_exception(e, 'get_user', resource_id=resource_id, tenant_id=tenant_id)
60
+
61
+ def get_by_email(self, email: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
62
+ """Get user by email using GSI1."""
63
+ try:
64
+ # Create a temporary user instance to get the GSI key
65
+ temp_user = User()
66
+ temp_user.email = email
67
+
68
+ result = self._query_by_index(
69
+ temp_user,
70
+ "gsi1",
71
+ ascending=False
72
+ )
73
+
74
+ if not result.success or not result.data:
75
+ raise NotFoundError(f"User with email {email} not found")
76
+
77
+ # Get the first (most recent) result
78
+ user = result.data[0]
79
+
80
+ # Check if deleted
81
+ if user.is_deleted():
82
+ raise NotFoundError(f"User with email {email} not found")
83
+
84
+ # Validate tenant access
85
+ self._validate_tenant_access(user.tenant_id, tenant_id)
86
+
87
+ return ServiceResult.success_result(user)
88
+
89
+ except Exception as e:
90
+ return self._handle_service_exception(e, 'get_user_by_email', email=email, tenant_id=tenant_id)
91
+
92
+ def get_users_by_tenant(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[User]]:
93
+ """Get all users for a tenant using GSI2."""
94
+ try:
95
+ # Create a temporary user instance to get the GSI key
96
+ temp_user = User()
97
+ temp_user.tenant_id = tenant_id
98
+
99
+ result = self._query_by_index(
100
+ temp_user,
101
+ "gsi2",
102
+ ascending=False, # Most recent first
103
+ limit=limit
104
+ )
105
+
106
+ if not result.success:
107
+ return result
108
+
109
+ # Filter out deleted users and validate tenant access
110
+ active_users = []
111
+ for user in result.data:
112
+ if not user.is_deleted() and user.tenant_id == tenant_id:
113
+ active_users.append(user)
114
+
115
+ return ServiceResult.success_result(active_users)
116
+
117
+ except Exception as e:
118
+ return self._handle_service_exception(e, 'get_users_by_tenant', tenant_id=tenant_id)
119
+
120
+ def get_users_by_role(self, role: str, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[User]]:
121
+ """Get users by role within a tenant using GSI3."""
122
+ try:
123
+ # Create a temporary user instance to get the GSI key
124
+ temp_user = User()
125
+ temp_user._roles = [role] # Set the primary role
126
+
127
+ result = self._query_by_index(
128
+ temp_user,
129
+ "gsi3",
130
+ ascending=False, # Most recent first
131
+ limit=limit
132
+ )
133
+
134
+ if not result.success:
135
+ return result
136
+
137
+ # Filter out deleted users and validate tenant access
138
+ active_users = []
139
+ for user in result.data:
140
+ if not user.is_deleted() and user.tenant_id == tenant_id and user.has_role(role):
141
+ active_users.append(user)
142
+
143
+ return ServiceResult.success_result(active_users)
144
+
145
+ except Exception as e:
146
+ return self._handle_service_exception(e, 'get_users_by_role', role=role, tenant_id=tenant_id)
147
+
148
+ def restore_user(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
149
+ """Restore a soft-deleted user (admin only)."""
150
+ try:
151
+ # Check permissions (admin only)
152
+ if not self._is_admin_user(user_id, tenant_id):
153
+ raise AccessDeniedError("Access denied: insufficient permissions")
154
+
155
+ # Get existing user (even if deleted)
156
+ user = self._get_model_by_id(resource_id, User)
157
+
158
+ if not user:
159
+ raise NotFoundError(f"User with ID {resource_id} not found")
160
+
161
+ # Validate tenant access
162
+ if hasattr(user, 'tenant_id'):
163
+ self._validate_tenant_access(user.tenant_id, tenant_id)
164
+
165
+ # Check if actually deleted
166
+ if not user.is_deleted():
167
+ return ServiceResult.success_result(user) # Already active
168
+
169
+ # Restore: clear deleted timestamp and metadata
170
+ user.deleted_utc_ts = None
171
+ user.deleted_by_id = None
172
+ user.updated_by_id = user_id
173
+ user.prep_for_save() # Updates timestamp
174
+
175
+ # Save the restored user
176
+ save_result = self._save_model(user)
177
+ if save_result.success:
178
+ return ServiceResult.success_result(user)
179
+ else:
180
+ return save_result
181
+
182
+ except Exception as e:
183
+ return self._handle_service_exception(e, 'restore_user', resource_id=resource_id, tenant_id=tenant_id)
184
+
185
+ def update(self, resource_id: str, tenant_id: str, user_id: str,
186
+ updates: Dict[str, Any]) -> ServiceResult[User]:
187
+ try:
188
+ # Get existing user
189
+ user = self._get_model_by_id(resource_id, User)
190
+
191
+ if not user:
192
+ raise NotFoundError(f"User with ID {resource_id} not found")
193
+
194
+ # Validate tenant access
195
+ if hasattr(user, 'tenant_id'):
196
+ self._validate_tenant_access(user.tenant_id, tenant_id)
197
+
198
+ # Check if user can update (admin or self)
199
+ if not (user_id == resource_id or self._is_admin_user(user_id, tenant_id)):
200
+ raise AccessDeniedError("Access denied: insufficient permissions")
201
+
202
+ # Prevent non-admins from updating roles
203
+ if 'roles' in updates and not self._is_admin_user(user_id, tenant_id):
204
+ raise AccessDeniedError("Access denied: only admins can update roles")
205
+
206
+ # Apply updates
207
+ for field, value in updates.items():
208
+ if hasattr(user, field) and field not in ['id', 'created_utc_ts', 'tenant_id', 'organizer_id']:
209
+ if field == 'email':
210
+ user.email = value
211
+ elif field == 'first_name':
212
+ user.first_name = value
213
+ elif field == 'last_name':
214
+ user.last_name = value
215
+ elif field == 'roles':
216
+ user.roles = value
217
+ elif field == 'avatar':
218
+ user.avatar = value
219
+
220
+ # Update metadata
221
+ user.updated_by_id = user_id
222
+ user.prep_for_save() # Updates timestamp
223
+
224
+ # Save updated user
225
+ return self._save_model(user)
226
+
227
+ except Exception as e:
228
+ return self._handle_service_exception(e, 'update_user', resource_id=resource_id, tenant_id=tenant_id)
229
+
230
+ def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
231
+ """Soft delete user with access control."""
232
+ try:
233
+ # Get existing user
234
+ user = self._get_model_by_id(resource_id, User)
235
+
236
+ if not user:
237
+ raise NotFoundError(f"User with ID {resource_id} not found")
238
+
239
+ # Check if already deleted
240
+ if user.is_deleted():
241
+ return ServiceResult.success_result(True)
242
+
243
+ # Validate tenant access
244
+ if hasattr(user, 'tenant_id'):
245
+ self._validate_tenant_access(user.tenant_id, tenant_id)
246
+
247
+ # Check permissions (admin or self)
248
+ if not (user_id == resource_id or self._is_admin_user(user_id, tenant_id)):
249
+ raise AccessDeniedError("Access denied: insufficient permissions")
250
+
251
+ # Prevent deleting self
252
+ if user_id == resource_id:
253
+ raise ValidationError("Cannot delete your own account")
254
+
255
+ # Soft delete: set deleted timestamp and metadata
256
+ user.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
257
+ user.deleted_by_id = user_id
258
+ user.prep_for_save() # Updates timestamp
259
+
260
+ # Save the updated user
261
+ save_result = self._save_model(user)
262
+ if save_result.success:
263
+ return ServiceResult.success_result(True)
264
+ else:
265
+ return save_result
266
+
267
+ except Exception as e:
268
+ return self._handle_service_exception(e, 'delete_user', resource_id=resource_id, tenant_id=tenant_id)
269
+
270
+ def _is_admin_user(self, user_id: str, tenant_id: str) -> bool:
271
+ """Check if user has admin role (placeholder - will be implemented when UserService is available)."""
272
+ # For now, assume no admin privileges
273
+ # This will be enhanced when we have user service integration
274
+ return False
File without changes