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,213 @@
1
+ """
2
+ Copyright 2024-2025 Geek Cafe, LLC
3
+ MIT License. See Project Root for the license information.
4
+
5
+ Role model for RBAC (Role-Based Access Control).
6
+ Roles aggregate permissions and can be assigned to users.
7
+ """
8
+
9
+ from typing import List, Dict, Any
10
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
11
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
12
+
13
+
14
+ class Role(BaseModel):
15
+ """
16
+ Role definition model.
17
+
18
+ Groups permissions together for easy assignment to users.
19
+ Supports hierarchical roles (platform vs tenant-level).
20
+
21
+ Examples:
22
+ - platform_admin: Full system access
23
+ - tenant_admin: Full access within tenant
24
+ - tenant_user: Standard user permissions
25
+ - tenant_organizer: Enhanced event management
26
+
27
+ Access Patterns:
28
+ - Get role by code (primary key)
29
+ - List all roles (scan/query all)
30
+ - List roles by scope (GSI1: global vs tenant)
31
+ - List roles by tenant (GSI2: tenant-specific custom roles)
32
+ """
33
+
34
+ def __init__(self):
35
+ super().__init__()
36
+
37
+ # Core fields
38
+ self._code: str | None = None # Unique code: "tenant_admin"
39
+ self._name: str | None = None # Display name: "Tenant Administrator"
40
+ self._description: str | None = None
41
+
42
+ # Permissions
43
+ self._permissions: List[str] = [] # List of permission codes
44
+
45
+ # Scope
46
+ self._scope: str = "tenant" # "global" or "tenant"
47
+ self._tenant_id: str | None = None # If tenant-specific custom role
48
+
49
+ # Hierarchy
50
+ self._inherits_from: List[str] = [] # Role codes to inherit from
51
+ self._level: int = 0 # Hierarchical level (higher = more power)
52
+
53
+ # Metadata
54
+ self._is_system: bool = True # System roles can't be deleted
55
+ self._is_assignable: bool = True # Can be assigned to users
56
+ self._metadata: Dict[str, Any] = {}
57
+
58
+ self._setup_indexes()
59
+
60
+ def _setup_indexes(self):
61
+ """Setup DynamoDB indexes for role queries."""
62
+
63
+ # Primary index: role by code
64
+ primary: DynamoDBIndex = DynamoDBIndex()
65
+ primary.name = "primary"
66
+ primary.partition_key.attribute_name = "pk"
67
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(
68
+ ("role", self.code)
69
+ )
70
+ primary.sort_key.attribute_name = "sk"
71
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(
72
+ ("role", self.code)
73
+ )
74
+ self.indexes.add_primary(primary)
75
+
76
+ # GSI1: Roles by scope (global vs tenant)
77
+ gsi: DynamoDBIndex = DynamoDBIndex()
78
+ gsi.name = "gsi1"
79
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
80
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
81
+ ("role_scope", self.scope)
82
+ )
83
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
84
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
85
+ ("level", str(self.level).zfill(5)),
86
+ ("code", self.code)
87
+ )
88
+ self.indexes.add_secondary(gsi)
89
+
90
+ # GSI2: Custom roles by tenant
91
+ gsi: DynamoDBIndex = DynamoDBIndex()
92
+ gsi.name = "gsi2"
93
+ gsi.partition_key.attribute_name = f"{gsi.name}_pk"
94
+ gsi.partition_key.value = lambda: DynamoDBKey.build_key(
95
+ ("tenant", self.tenant_id or "system")
96
+ )
97
+ gsi.sort_key.attribute_name = f"{gsi.name}_sk"
98
+ gsi.sort_key.value = lambda: DynamoDBKey.build_key(
99
+ ("role", self.code)
100
+ )
101
+ self.indexes.add_secondary(gsi)
102
+
103
+ # Code
104
+ @property
105
+ def code(self) -> str | None:
106
+ """Role code (e.g., 'tenant_admin')."""
107
+ return self._code
108
+
109
+ @code.setter
110
+ def code(self, value: str | None):
111
+ self._code = value
112
+
113
+ # Name
114
+ @property
115
+ def name(self) -> str | None:
116
+ """Display name."""
117
+ return self._name
118
+
119
+ @name.setter
120
+ def name(self, value: str | None):
121
+ self._name = value
122
+
123
+ # Description
124
+ @property
125
+ def description(self) -> str | None:
126
+ """Role description."""
127
+ return self._description
128
+
129
+ @description.setter
130
+ def description(self, value: str | None):
131
+ self._description = value
132
+
133
+ # Permissions
134
+ @property
135
+ def permissions(self) -> List[str]:
136
+ """List of permission codes."""
137
+ return self._permissions
138
+
139
+ @permissions.setter
140
+ def permissions(self, value: List[str]):
141
+ self._permissions = value if value else []
142
+
143
+ # Scope
144
+ @property
145
+ def scope(self) -> str:
146
+ """Role scope: 'global' or 'tenant'."""
147
+ return self._scope
148
+
149
+ @scope.setter
150
+ def scope(self, value: str):
151
+ if value not in ["global", "tenant"]:
152
+ raise ValueError("Scope must be 'global' or 'tenant'")
153
+ self._scope = value
154
+
155
+ # Tenant ID
156
+ @property
157
+ def tenant_id(self) -> str | None:
158
+ """Tenant ID for tenant-specific custom roles."""
159
+ return self._tenant_id
160
+
161
+ @tenant_id.setter
162
+ def tenant_id(self, value: str | None):
163
+ self._tenant_id = value
164
+
165
+ # Inherits From
166
+ @property
167
+ def inherits_from(self) -> List[str]:
168
+ """Role codes this role inherits from."""
169
+ return self._inherits_from
170
+
171
+ @inherits_from.setter
172
+ def inherits_from(self, value: List[str]):
173
+ self._inherits_from = value if value else []
174
+
175
+ # Level
176
+ @property
177
+ def level(self) -> int:
178
+ """Hierarchical level (higher = more power)."""
179
+ return self._level
180
+
181
+ @level.setter
182
+ def level(self, value: int):
183
+ self._level = value
184
+
185
+ # Is System
186
+ @property
187
+ def is_system(self) -> bool:
188
+ """Whether this is a system role (cannot be deleted)."""
189
+ return self._is_system
190
+
191
+ @is_system.setter
192
+ def is_system(self, value: bool):
193
+ self._is_system = value
194
+
195
+ # Is Assignable
196
+ @property
197
+ def is_assignable(self) -> bool:
198
+ """Whether this role can be assigned to users."""
199
+ return self._is_assignable
200
+
201
+ @is_assignable.setter
202
+ def is_assignable(self, value: bool):
203
+ self._is_assignable = value
204
+
205
+ # Metadata
206
+ @property
207
+ def metadata(self) -> Dict[str, Any]:
208
+ """Additional metadata."""
209
+ return self._metadata
210
+
211
+ @metadata.setter
212
+ def metadata(self, value: Dict[str, Any]):
213
+ self._metadata = value if value else {}
@@ -0,0 +1,285 @@
1
+ from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
2
+ from boto3_assist.utilities.string_utility import StringUtility
3
+ import datetime as dt
4
+ from typing import List, Optional, Dict, Any
5
+ from geek_cafe_saas_sdk.models.base_model import BaseModel
6
+
7
+
8
+ class User(BaseModel):
9
+ """
10
+ User model for event scheduling system.
11
+
12
+ Represents users with roles, tenant association, and profile information.
13
+ """
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+ self._email: str | None = None
18
+ self._first_name: str | None = None
19
+ self._last_name: str | None = None
20
+ self._roles: List[str] = ["tenant_user"]
21
+ self._avatar: str | None = None
22
+
23
+ # Cognito integration
24
+ self._cognito_user_name: str | None = None
25
+
26
+ # User status and lifecycle
27
+ self._status: str = "active" # active|invited|disabled
28
+ self._invited_utc_ts: float | None = None
29
+ self._activated_utc_ts: float | None = None
30
+ self._disabled_utc_ts: float | None = None
31
+
32
+ self._setup_indexes()
33
+
34
+ def _setup_indexes(self):
35
+ """Setup DynamoDB indexes for user queries."""
36
+
37
+ # Primary index: users by ID
38
+ primary: DynamoDBIndex = DynamoDBIndex()
39
+ primary.name = "primary"
40
+ primary.partition_key.attribute_name = "pk"
41
+ primary.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
42
+ primary.sort_key.attribute_name = "sk"
43
+ primary.sort_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
44
+ self.indexes.add_primary(primary)
45
+
46
+ ## GSI: 1 - Users by email (for uniqueness and login)
47
+ gsi1: DynamoDBIndex = DynamoDBIndex()
48
+ gsi1.name = "gsi1"
49
+ gsi1.partition_key.attribute_name = f"{gsi1.name}_pk"
50
+ gsi1.partition_key.value = lambda: DynamoDBKey.build_key(("email", self.email))
51
+ gsi1.sort_key.attribute_name = f"{gsi1.name}_sk"
52
+ gsi1.sort_key.value = lambda: DynamoDBKey.build_key(("email", self.email))
53
+ self.indexes.add_secondary(gsi1)
54
+
55
+ ## GSI: 2 - Users by tenant
56
+ gsi2: DynamoDBIndex = DynamoDBIndex()
57
+ gsi2.name = "gsi2"
58
+ gsi2.partition_key.attribute_name = f"{gsi2.name}_pk"
59
+ gsi2.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
60
+ gsi2.sort_key.attribute_name = f"{gsi2.name}_sk"
61
+ gsi2.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.created_utc_ts))
62
+ self.indexes.add_secondary(gsi2)
63
+
64
+ ## GSI: 3 - Users by role (for admin queries)
65
+ gsi3: DynamoDBIndex = DynamoDBIndex()
66
+ gsi3.name = "gsi3"
67
+ gsi3.partition_key.attribute_name = f"{gsi3.name}_pk"
68
+ gsi3.partition_key.value = lambda: DynamoDBKey.build_key(("role", self.primary_role))
69
+ gsi3.sort_key.attribute_name = f"{gsi3.name}_sk"
70
+ gsi3.sort_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("ts", self.created_utc_ts))
71
+ self.indexes.add_secondary(gsi3)
72
+
73
+ ## GSI: 4 - All users (for admin listing)
74
+ gsi4: DynamoDBIndex = DynamoDBIndex()
75
+ gsi4.name = "gsi4"
76
+ gsi4.partition_key.attribute_name = f"{gsi4.name}_pk"
77
+ gsi4.partition_key.value = lambda: DynamoDBKey.build_key(("user", "all"))
78
+ gsi4.sort_key.attribute_name = f"{gsi4.name}_sk"
79
+ gsi4.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.created_utc_ts))
80
+ self.indexes.add_secondary(gsi4)
81
+
82
+ @property
83
+ def email(self) -> str | None:
84
+ """User's email address."""
85
+ return self._email
86
+
87
+ @email.setter
88
+ def email(self, value: str | None):
89
+ """Set email and ensure it's lowercase."""
90
+ if value:
91
+ self._email = value.lower()
92
+ else:
93
+ self._email = value
94
+
95
+ @property
96
+ def first_name(self) -> str | None:
97
+ """User's first name."""
98
+ return self._first_name
99
+
100
+ @first_name.setter
101
+ def first_name(self, value: str | None):
102
+ self._first_name = value
103
+
104
+ @property
105
+ def last_name(self) -> str | None:
106
+ """User's last name."""
107
+ return self._last_name
108
+
109
+ @last_name.setter
110
+ def last_name(self, value: str | None):
111
+ self._last_name = value
112
+
113
+ @property
114
+ def full_name(self) -> str:
115
+ """User's full name."""
116
+ if self.first_name and self.last_name:
117
+ return f"{self.first_name} {self.last_name}"
118
+ elif self.first_name:
119
+ return self.first_name
120
+ elif self.last_name:
121
+ return self.last_name
122
+ return ""
123
+
124
+ @property
125
+ def roles(self) -> List[str]:
126
+ """User's roles."""
127
+ return self._roles
128
+
129
+ @roles.setter
130
+ def roles(self, value: List[str]):
131
+ """Set roles, ensuring it's always a list."""
132
+ if value is None:
133
+ self._roles = ["tenant_user"]
134
+ elif isinstance(value, list):
135
+ self._roles = value if value else ["tenant_user"]
136
+ else:
137
+ self._roles = ["tenant_user"]
138
+
139
+ @property
140
+ def primary_role(self) -> str:
141
+ """Primary role (first in the list, or 'tenant_user' if empty)."""
142
+ return self._roles[0] if self._roles else "tenant_user"
143
+
144
+ @property
145
+ def is_admin(self) -> bool:
146
+ """Check if user has any admin role."""
147
+ admin_roles = {"platform_admin", "tenant_admin"}
148
+ return any(role in admin_roles for role in self._roles)
149
+
150
+ @property
151
+ def is_organizer(self) -> bool:
152
+ """Check if user has organizer role."""
153
+ return "tenant_organizer" in self._roles
154
+
155
+ @property
156
+ def avatar(self) -> str | None:
157
+ """User's avatar URL."""
158
+ return self._avatar
159
+
160
+ @avatar.setter
161
+ def avatar(self, value: str | None):
162
+ self._avatar = value
163
+
164
+ @property
165
+ def cognito_user_name(self) -> str | None:
166
+ """Cognito username (sub/UUID from Cognito User Pool)."""
167
+ return self._cognito_user_name
168
+
169
+ @cognito_user_name.setter
170
+ def cognito_user_name(self, value: str | None):
171
+ self._cognito_user_name = value
172
+
173
+ def has_role(self, role: str) -> bool:
174
+ """Check if user has a specific role."""
175
+ return role in self._roles
176
+
177
+ def add_role(self, role: str):
178
+ """Add a role to the user if not already present."""
179
+ if role not in self._roles:
180
+ self._roles.append(role)
181
+
182
+ def remove_role(self, role: str):
183
+ """Remove a role from the user."""
184
+ if role in self._roles:
185
+ self._roles.remove(role)
186
+ # Ensure at least 'tenant_user' role remains
187
+ if not self._roles:
188
+ self._roles = ["tenant_user"]
189
+
190
+ # Status
191
+ @property
192
+ def status(self) -> str:
193
+ """User status (active|invited|disabled)."""
194
+ return self._status
195
+
196
+ @status.setter
197
+ def status(self, value: str):
198
+ if value not in ["active", "invited", "disabled"]:
199
+ raise ValueError(f"Invalid status: {value}. Must be active, invited, or disabled.")
200
+ self._status = value
201
+
202
+ # Invited timestamp
203
+ @property
204
+ def invited_utc_ts(self) -> float | None:
205
+ """Timestamp when user was invited."""
206
+ return self._invited_utc_ts
207
+
208
+ @invited_utc_ts.setter
209
+ def invited_utc_ts(self, value: float | None):
210
+ self._invited_utc_ts = value
211
+
212
+ # Activated timestamp
213
+ @property
214
+ def activated_utc_ts(self) -> float | None:
215
+ """Timestamp when user activated their account."""
216
+ return self._activated_utc_ts
217
+
218
+ @activated_utc_ts.setter
219
+ def activated_utc_ts(self, value: float | None):
220
+ self._activated_utc_ts = value
221
+
222
+ # Disabled timestamp
223
+ @property
224
+ def disabled_utc_ts(self) -> float | None:
225
+ """Timestamp when user was disabled."""
226
+ return self._disabled_utc_ts
227
+
228
+ @disabled_utc_ts.setter
229
+ def disabled_utc_ts(self, value: float | None):
230
+ self._disabled_utc_ts = value
231
+
232
+ # Status helper methods
233
+ def is_active(self) -> bool:
234
+ """Check if user is active."""
235
+ return self._status == "active"
236
+
237
+ def is_invited(self) -> bool:
238
+ """Check if user is in invited state (pending activation)."""
239
+ return self._status == "invited"
240
+
241
+ def is_disabled(self) -> bool:
242
+ """Check if user is disabled."""
243
+ return self._status == "disabled"
244
+
245
+ def invite(self):
246
+ """
247
+ Mark user as invited (pending activation).
248
+
249
+ Sets status to 'invited' and records invitation timestamp.
250
+ Used for invite workflow where user must accept/activate.
251
+ """
252
+ self._status = "invited"
253
+ self._invited_utc_ts = dt.datetime.now(dt.UTC).timestamp()
254
+
255
+ def activate(self):
256
+ """
257
+ Activate an invited user.
258
+
259
+ Sets status to 'active' and records activation timestamp.
260
+ Called when user accepts invite or completes signup.
261
+ """
262
+ self._status = "active"
263
+ self._activated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
264
+
265
+ def disable(self):
266
+ """
267
+ Disable user account.
268
+
269
+ Sets status to 'disabled' and records disabled timestamp.
270
+ Disabled users cannot log in or access the system.
271
+ """
272
+ self._status = "disabled"
273
+ self._disabled_utc_ts = dt.datetime.now(dt.UTC).timestamp()
274
+
275
+ def enable(self):
276
+ """
277
+ Re-enable a disabled user account.
278
+
279
+ Sets status back to 'active'.
280
+ """
281
+ if self._status == "disabled":
282
+ self._status = "active"
283
+ self._disabled_utc_ts = None
284
+
285
+
@@ -0,0 +1,16 @@
1
+ # Auth Domain Services
2
+
3
+ from .user_service import UserService
4
+ from .authorization_service import AuthorizationService, AuthorizationContext
5
+ from .resource_permission_service import ResourcePermissionService
6
+ from .permission_registry import permission_registry, PermissionDefinition, RoleDefinition
7
+
8
+ __all__ = [
9
+ "UserService",
10
+ "AuthorizationService",
11
+ "AuthorizationContext",
12
+ "ResourcePermissionService",
13
+ "permission_registry",
14
+ "PermissionDefinition",
15
+ "RoleDefinition",
16
+ ]