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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
TenantService for managing tenant organizations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
10
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
11
|
+
from geek_cafe_saas_sdk.domains.auth.services import UserService
|
|
12
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
13
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
14
|
+
from geek_cafe_saas_sdk.domains.tenancy.models import Tenant
|
|
15
|
+
from geek_cafe_saas_sdk.domains.auth.models import User
|
|
16
|
+
from geek_cafe_saas_sdk.domains.tenancy.models import Subscription
|
|
17
|
+
from geek_cafe_saas_sdk.utilities.cognito_utility import CognitoUtility
|
|
18
|
+
from geek_cafe_saas_sdk.utilities.environment_variables import EnvironmentVariables
|
|
19
|
+
import datetime as dt
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TenantService(DatabaseService[Tenant]):
|
|
23
|
+
"""
|
|
24
|
+
Service for tenant management operations.
|
|
25
|
+
|
|
26
|
+
Handles CRUD operations for tenants, including creating tenants with
|
|
27
|
+
primary users, managing tenant status, and coordinating with subscriptions.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None,
|
|
31
|
+
user_service: UserService = None, cognito_utility: CognitoUtility = None,
|
|
32
|
+
user_pool_id: str = None, enable_cognito: bool = True):
|
|
33
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
34
|
+
# User service for creating primary users
|
|
35
|
+
self.user_service = user_service or UserService(
|
|
36
|
+
dynamodb=dynamodb, table_name=table_name
|
|
37
|
+
)
|
|
38
|
+
# Cognito integration (optional for testing)
|
|
39
|
+
self.cognito_utility = cognito_utility
|
|
40
|
+
self.user_pool_id = user_pool_id or EnvironmentVariables.get_cognito_user_pool()
|
|
41
|
+
self.enable_cognito = enable_cognito
|
|
42
|
+
|
|
43
|
+
def create(self, tenant_id: str, user_id: str, payload: Dict[str, Any]) -> ServiceResult[Tenant]:
|
|
44
|
+
"""
|
|
45
|
+
Create a new tenant.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
tenant_id: Tenant ID (for multi-tenant context, can be same as new tenant)
|
|
49
|
+
user_id: User ID creating the tenant
|
|
50
|
+
payload: Tenant data (name, status, plan_tier, etc.)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
ServiceResult with Tenant
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
# Validate required fields
|
|
57
|
+
required_fields = ['name']
|
|
58
|
+
self._validate_required_fields(payload, required_fields)
|
|
59
|
+
|
|
60
|
+
# Create tenant instance
|
|
61
|
+
tenant = Tenant().map(payload)
|
|
62
|
+
tenant.tenant_id = tenant.id or tenant_id # Self-referential
|
|
63
|
+
tenant.user_id = user_id
|
|
64
|
+
tenant.created_by_id = user_id
|
|
65
|
+
|
|
66
|
+
# Set defaults
|
|
67
|
+
if not tenant.status:
|
|
68
|
+
tenant.status = "active"
|
|
69
|
+
if not tenant.plan_tier:
|
|
70
|
+
tenant.plan_tier = "free"
|
|
71
|
+
|
|
72
|
+
# Prepare for save
|
|
73
|
+
tenant.prep_for_save()
|
|
74
|
+
|
|
75
|
+
# Save to database
|
|
76
|
+
return self._save_model(tenant)
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return self._handle_service_exception(e, 'create_tenant',
|
|
80
|
+
tenant_id=tenant_id, user_id=user_id)
|
|
81
|
+
|
|
82
|
+
def create_with_user(self, user_payload: Dict[str, Any],
|
|
83
|
+
tenant_payload: Optional[Dict[str, Any]] = None,
|
|
84
|
+
temp_password: Optional[str] = None,
|
|
85
|
+
send_invitation: bool = False) -> ServiceResult[Dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
Create tenant with primary user atomically (signup flow).
|
|
88
|
+
|
|
89
|
+
This is the main signup flow - creates a new tenant and primary admin user
|
|
90
|
+
together. The user becomes the tenant's primary contact.
|
|
91
|
+
|
|
92
|
+
If Cognito is enabled, also creates the Cognito user with custom attributes.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
user_payload: User data (email, first_name, last_name, etc.)
|
|
96
|
+
tenant_payload: Optional tenant data (name, etc.). If not provided,
|
|
97
|
+
tenant name is derived from user info.
|
|
98
|
+
temp_password: Optional temporary password for Cognito user
|
|
99
|
+
send_invitation: If True, sends Cognito invitation email
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ServiceResult with dict containing:
|
|
103
|
+
{
|
|
104
|
+
"tenant": Tenant,
|
|
105
|
+
"user": User,
|
|
106
|
+
"cognito_user": dict (if Cognito enabled),
|
|
107
|
+
"temp_password": str (if auto-generated and Cognito enabled)
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
# Validate user fields
|
|
112
|
+
required_user_fields = ['email', 'first_name', 'last_name']
|
|
113
|
+
self._validate_required_fields(user_payload, required_user_fields)
|
|
114
|
+
|
|
115
|
+
# Prepare tenant data
|
|
116
|
+
if tenant_payload is None:
|
|
117
|
+
tenant_payload = {}
|
|
118
|
+
|
|
119
|
+
# Generate tenant name from user if not provided
|
|
120
|
+
if 'name' not in tenant_payload:
|
|
121
|
+
first_name = user_payload.get('first_name', 'User')
|
|
122
|
+
last_name = user_payload.get('last_name', 'Organization')
|
|
123
|
+
tenant_payload['name'] = f"{first_name} {last_name}'s Organization"
|
|
124
|
+
|
|
125
|
+
# Create tenant first
|
|
126
|
+
tenant = Tenant().map(tenant_payload)
|
|
127
|
+
tenant.prep_for_save()
|
|
128
|
+
|
|
129
|
+
# Set self-referential tenant_id
|
|
130
|
+
tenant.tenant_id = tenant.id
|
|
131
|
+
|
|
132
|
+
# Set defaults
|
|
133
|
+
if not tenant.status:
|
|
134
|
+
tenant.status = "active"
|
|
135
|
+
if not tenant.plan_tier:
|
|
136
|
+
tenant.plan_tier = "free"
|
|
137
|
+
|
|
138
|
+
# Create default features for free tier
|
|
139
|
+
if not tenant.features:
|
|
140
|
+
tenant.features = {
|
|
141
|
+
"chat": True,
|
|
142
|
+
"events": True,
|
|
143
|
+
"groups": True,
|
|
144
|
+
"analytics": False,
|
|
145
|
+
"api_access": False
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Create primary user
|
|
149
|
+
user = User().map(user_payload)
|
|
150
|
+
user.tenant_id = tenant.id
|
|
151
|
+
user.prep_for_save()
|
|
152
|
+
|
|
153
|
+
# Set user as creator of themselves
|
|
154
|
+
user.user_id = user.id
|
|
155
|
+
user.created_by_id = user.id
|
|
156
|
+
|
|
157
|
+
# Grant tenant admin role
|
|
158
|
+
if 'tenant_admin' not in user.roles:
|
|
159
|
+
user.roles = ['tenant_admin']
|
|
160
|
+
|
|
161
|
+
# Set user status as active (they're signing up, not invited)
|
|
162
|
+
user.status = "active"
|
|
163
|
+
user.activated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
164
|
+
|
|
165
|
+
# Link tenant to primary user
|
|
166
|
+
tenant.primary_contact_user_id = user.id
|
|
167
|
+
tenant.created_by_id = user.id
|
|
168
|
+
tenant.user_id = user.id
|
|
169
|
+
|
|
170
|
+
# Save both (in future, could use TransactWrite for atomicity)
|
|
171
|
+
tenant_result = self._save_model(tenant)
|
|
172
|
+
if not tenant_result.success:
|
|
173
|
+
return ServiceResult(
|
|
174
|
+
success=False,
|
|
175
|
+
message=f"Failed to create tenant: {tenant_result.message}",
|
|
176
|
+
error_code="TENANT_CREATION_FAILED"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Save user
|
|
180
|
+
user_result = self.user_service._save_model(user)
|
|
181
|
+
if not user_result.success:
|
|
182
|
+
# TODO: Rollback tenant creation (or use TransactWrite)
|
|
183
|
+
return ServiceResult(
|
|
184
|
+
success=False,
|
|
185
|
+
message=f"Failed to create user: {user_result.message}",
|
|
186
|
+
error_code="USER_CREATION_FAILED"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Create Cognito user if enabled
|
|
190
|
+
cognito_response = None
|
|
191
|
+
returned_temp_password = None
|
|
192
|
+
|
|
193
|
+
if self.enable_cognito and self.user_pool_id:
|
|
194
|
+
try:
|
|
195
|
+
# Initialize Cognito utility if not provided
|
|
196
|
+
cognito = self.cognito_utility or CognitoUtility()
|
|
197
|
+
|
|
198
|
+
# Store password for return (before it's used)
|
|
199
|
+
if not send_invitation:
|
|
200
|
+
returned_temp_password = temp_password
|
|
201
|
+
|
|
202
|
+
# Create Cognito user with custom attributes
|
|
203
|
+
cognito_response = cognito.admin_create_user(
|
|
204
|
+
user_pool_id=self.user_pool_id,
|
|
205
|
+
temp_password=temp_password,
|
|
206
|
+
user=user,
|
|
207
|
+
send_invitation=send_invitation
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Extract cognito username (sub) from response
|
|
211
|
+
if cognito_response and 'User' in cognito_response:
|
|
212
|
+
cognito_user = cognito_response['User']
|
|
213
|
+
user.cognito_user_name = cognito_user.get('Username')
|
|
214
|
+
|
|
215
|
+
# Update user in DynamoDB with Cognito username
|
|
216
|
+
self.user_service._save_model(user)
|
|
217
|
+
|
|
218
|
+
except Exception as cognito_error:
|
|
219
|
+
# Cognito creation failed - should we rollback?
|
|
220
|
+
# For now, log error and return partial success
|
|
221
|
+
return ServiceResult(
|
|
222
|
+
success=False,
|
|
223
|
+
message=f"User created in DynamoDB but Cognito creation failed: {str(cognito_error)}",
|
|
224
|
+
error_code="COGNITO_CREATION_FAILED",
|
|
225
|
+
data={
|
|
226
|
+
"tenant": tenant,
|
|
227
|
+
"user": user,
|
|
228
|
+
"cognito_error": str(cognito_error)
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Return successful result
|
|
233
|
+
result_data = {
|
|
234
|
+
"tenant": tenant,
|
|
235
|
+
"user": user
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if cognito_response:
|
|
239
|
+
result_data["cognito_user"] = cognito_response
|
|
240
|
+
|
|
241
|
+
if returned_temp_password:
|
|
242
|
+
result_data["temp_password"] = returned_temp_password
|
|
243
|
+
|
|
244
|
+
return ServiceResult(
|
|
245
|
+
success=True,
|
|
246
|
+
data=result_data
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
return self._handle_service_exception(e, 'create_tenant_with_user')
|
|
251
|
+
|
|
252
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
|
|
253
|
+
"""
|
|
254
|
+
Get tenant by ID with access control.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
resource_id: Tenant ID to retrieve
|
|
258
|
+
tenant_id: Requesting tenant ID (for access control)
|
|
259
|
+
user_id: Requesting user ID (for access control)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
ServiceResult with Tenant
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
tenant = self._get_model_by_id(resource_id, Tenant)
|
|
266
|
+
|
|
267
|
+
if not tenant:
|
|
268
|
+
raise NotFoundError(f"Tenant with ID {resource_id} not found")
|
|
269
|
+
|
|
270
|
+
# Check if deleted
|
|
271
|
+
if tenant.is_deleted():
|
|
272
|
+
raise NotFoundError(f"Tenant with ID {resource_id} not found")
|
|
273
|
+
|
|
274
|
+
# Validate tenant access (only same tenant can view, or admin)
|
|
275
|
+
# For now, allow if requesting tenant matches
|
|
276
|
+
if tenant.id != tenant_id:
|
|
277
|
+
# TODO: Add admin check here
|
|
278
|
+
raise AccessDeniedError("You don't have access to this tenant")
|
|
279
|
+
|
|
280
|
+
return ServiceResult.success_result(tenant)
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return self._handle_service_exception(e, 'get_tenant',
|
|
284
|
+
resource_id=resource_id, tenant_id=tenant_id)
|
|
285
|
+
|
|
286
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
287
|
+
payload: Dict[str, Any]) -> ServiceResult[Tenant]:
|
|
288
|
+
"""
|
|
289
|
+
Update tenant information.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
resource_id: Tenant ID to update
|
|
293
|
+
tenant_id: Requesting tenant ID (for access control)
|
|
294
|
+
user_id: User ID making the update
|
|
295
|
+
payload: Fields to update
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
ServiceResult with updated Tenant
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
# Get existing tenant
|
|
302
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
303
|
+
if not get_result.success:
|
|
304
|
+
return get_result
|
|
305
|
+
|
|
306
|
+
tenant = get_result.data
|
|
307
|
+
|
|
308
|
+
# Update fields from payload
|
|
309
|
+
tenant.map(payload)
|
|
310
|
+
tenant.updated_by_id = user_id
|
|
311
|
+
tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
312
|
+
tenant.version += 1
|
|
313
|
+
|
|
314
|
+
# Save updated tenant
|
|
315
|
+
return self._save_model(tenant)
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return self._handle_service_exception(e, 'update_tenant',
|
|
319
|
+
resource_id=resource_id, tenant_id=tenant_id)
|
|
320
|
+
|
|
321
|
+
def list_by_status(self, status: str, tenant_id: str, user_id: str,
|
|
322
|
+
limit: int = 50) -> ServiceResult[List[Tenant]]:
|
|
323
|
+
"""
|
|
324
|
+
List tenants by status (for admin queries).
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
status: Tenant status (active|inactive|archived)
|
|
328
|
+
tenant_id: Requesting tenant ID
|
|
329
|
+
user_id: Requesting user ID
|
|
330
|
+
limit: Maximum number of results
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
ServiceResult with list of Tenants
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
# TODO: Add admin check - only admins should list all tenants
|
|
337
|
+
|
|
338
|
+
# Create temp tenant for GSI1 query
|
|
339
|
+
temp_tenant = Tenant()
|
|
340
|
+
temp_tenant.status = status
|
|
341
|
+
|
|
342
|
+
result = self._query_by_index(
|
|
343
|
+
temp_tenant,
|
|
344
|
+
"gsi1",
|
|
345
|
+
ascending=False,
|
|
346
|
+
limit=limit
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if not result.success:
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
# Filter out deleted tenants
|
|
353
|
+
active_tenants = [t for t in result.data if not t.is_deleted()]
|
|
354
|
+
|
|
355
|
+
return ServiceResult.success_result(active_tenants)
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
return self._handle_service_exception(e, 'list_tenants_by_status',
|
|
359
|
+
status=status, tenant_id=tenant_id)
|
|
360
|
+
|
|
361
|
+
def list_all(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[Tenant]]:
|
|
362
|
+
"""
|
|
363
|
+
List all tenants sorted by name (for admin queries).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
tenant_id: Requesting tenant ID
|
|
367
|
+
user_id: Requesting user ID
|
|
368
|
+
limit: Maximum number of results
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
ServiceResult with list of Tenants
|
|
372
|
+
"""
|
|
373
|
+
try:
|
|
374
|
+
# TODO: Add admin check - only admins should list all tenants
|
|
375
|
+
|
|
376
|
+
# Create temp tenant for GSI2 query
|
|
377
|
+
temp_tenant = Tenant()
|
|
378
|
+
temp_tenant.name = "" # This will be overridden by GSI2 PK
|
|
379
|
+
|
|
380
|
+
# Need to manually set GSI2 PK for "all tenants" query
|
|
381
|
+
# This is a workaround for the query pattern
|
|
382
|
+
|
|
383
|
+
# For now, query by active status as a proxy
|
|
384
|
+
return self.list_by_status("active", tenant_id, user_id, limit)
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
return self._handle_service_exception(e, 'list_all_tenants',
|
|
388
|
+
tenant_id=tenant_id)
|
|
389
|
+
|
|
390
|
+
def deactivate(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
|
|
391
|
+
"""
|
|
392
|
+
Deactivate a tenant (soft disable).
|
|
393
|
+
|
|
394
|
+
Sets tenant status to 'inactive'. Optionally can cascade to disable users.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
resource_id: Tenant ID to deactivate
|
|
398
|
+
tenant_id: Requesting tenant ID
|
|
399
|
+
user_id: User ID performing action
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
ServiceResult with updated Tenant
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
# Get tenant
|
|
406
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
407
|
+
if not get_result.success:
|
|
408
|
+
return get_result
|
|
409
|
+
|
|
410
|
+
tenant = get_result.data
|
|
411
|
+
|
|
412
|
+
# Deactivate
|
|
413
|
+
tenant.deactivate()
|
|
414
|
+
tenant.updated_by_id = user_id
|
|
415
|
+
tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
416
|
+
tenant.version += 1
|
|
417
|
+
|
|
418
|
+
# Save
|
|
419
|
+
save_result = self._save_model(tenant)
|
|
420
|
+
|
|
421
|
+
# TODO: Cascade to users - disable all users in tenant
|
|
422
|
+
# This would be done in a separate method or background job
|
|
423
|
+
|
|
424
|
+
return save_result
|
|
425
|
+
|
|
426
|
+
except Exception as e:
|
|
427
|
+
return self._handle_service_exception(e, 'deactivate_tenant',
|
|
428
|
+
resource_id=resource_id, tenant_id=tenant_id)
|
|
429
|
+
|
|
430
|
+
def activate(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[Tenant]:
|
|
431
|
+
"""
|
|
432
|
+
Activate a tenant.
|
|
433
|
+
|
|
434
|
+
Sets tenant status to 'active'.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
resource_id: Tenant ID to activate
|
|
438
|
+
tenant_id: Requesting tenant ID
|
|
439
|
+
user_id: User ID performing action
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
ServiceResult with updated Tenant
|
|
443
|
+
"""
|
|
444
|
+
try:
|
|
445
|
+
# Get tenant
|
|
446
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
447
|
+
if not get_result.success:
|
|
448
|
+
return get_result
|
|
449
|
+
|
|
450
|
+
tenant = get_result.data
|
|
451
|
+
|
|
452
|
+
# Activate
|
|
453
|
+
tenant.activate()
|
|
454
|
+
tenant.updated_by_id = user_id
|
|
455
|
+
tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
456
|
+
tenant.version += 1
|
|
457
|
+
|
|
458
|
+
# Save
|
|
459
|
+
return self._save_model(tenant)
|
|
460
|
+
|
|
461
|
+
except Exception as e:
|
|
462
|
+
return self._handle_service_exception(e, 'activate_tenant',
|
|
463
|
+
resource_id=resource_id, tenant_id=tenant_id)
|
|
464
|
+
|
|
465
|
+
def get_user_count(self, tenant_id: str, user_id: str) -> ServiceResult[int]:
|
|
466
|
+
"""
|
|
467
|
+
Get count of users in tenant.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
tenant_id: Tenant ID
|
|
471
|
+
user_id: Requesting user ID
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
ServiceResult with user count
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
# Query users by tenant
|
|
478
|
+
users_result = self.user_service.get_users_by_tenant(
|
|
479
|
+
tenant_id, user_id, limit=1000 # TODO: Handle pagination for large counts
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if not users_result.success:
|
|
483
|
+
return ServiceResult(
|
|
484
|
+
success=False,
|
|
485
|
+
error="Failed to count users",
|
|
486
|
+
error_code="USER_COUNT_FAILED"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Count active users
|
|
490
|
+
active_users = [u for u in users_result.data if not u.is_deleted() and u.is_active()]
|
|
491
|
+
count = len(active_users)
|
|
492
|
+
|
|
493
|
+
return ServiceResult.success_result(count)
|
|
494
|
+
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return self._handle_service_exception(e, 'get_user_count',
|
|
497
|
+
tenant_id=tenant_id)
|
|
498
|
+
|
|
499
|
+
def can_add_user(self, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
500
|
+
"""
|
|
501
|
+
Check if tenant can add another user (based on subscription limits).
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
tenant_id: Tenant ID
|
|
505
|
+
user_id: Requesting user ID
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
ServiceResult with boolean (True if can add, False if at limit)
|
|
509
|
+
"""
|
|
510
|
+
try:
|
|
511
|
+
# Get tenant
|
|
512
|
+
tenant_result = self.get_by_id(tenant_id, tenant_id, user_id)
|
|
513
|
+
if not tenant_result.success:
|
|
514
|
+
return tenant_result
|
|
515
|
+
|
|
516
|
+
tenant = tenant_result.data
|
|
517
|
+
|
|
518
|
+
# If no user limit, can always add
|
|
519
|
+
if tenant.max_users is None:
|
|
520
|
+
return ServiceResult.success_result(True)
|
|
521
|
+
|
|
522
|
+
# Get current user count
|
|
523
|
+
count_result = self.get_user_count(tenant_id, user_id)
|
|
524
|
+
if not count_result.success:
|
|
525
|
+
return count_result
|
|
526
|
+
|
|
527
|
+
current_count = count_result.data
|
|
528
|
+
|
|
529
|
+
# Check if at limit
|
|
530
|
+
can_add = current_count < tenant.max_users
|
|
531
|
+
|
|
532
|
+
return ServiceResult.success_result(can_add)
|
|
533
|
+
|
|
534
|
+
except Exception as e:
|
|
535
|
+
return self._handle_service_exception(e, 'can_add_user',
|
|
536
|
+
tenant_id=tenant_id)
|
|
537
|
+
|
|
538
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
539
|
+
"""
|
|
540
|
+
Soft delete tenant.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
resource_id: Tenant ID to delete
|
|
544
|
+
tenant_id: Requesting tenant ID
|
|
545
|
+
user_id: User ID performing delete
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
ServiceResult with boolean (True if deleted)
|
|
549
|
+
"""
|
|
550
|
+
try:
|
|
551
|
+
# Get tenant
|
|
552
|
+
get_result = self.get_by_id(resource_id, tenant_id, user_id)
|
|
553
|
+
if not get_result.success:
|
|
554
|
+
return ServiceResult(success=False, message=get_result.message,
|
|
555
|
+
error_code=get_result.error_code)
|
|
556
|
+
|
|
557
|
+
tenant = get_result.data
|
|
558
|
+
|
|
559
|
+
# Soft delete
|
|
560
|
+
tenant.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
561
|
+
tenant.deleted_by_id = user_id
|
|
562
|
+
tenant.updated_by_id = user_id
|
|
563
|
+
tenant.updated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
564
|
+
|
|
565
|
+
# Save
|
|
566
|
+
save_result = self._save_model(tenant)
|
|
567
|
+
if not save_result.success:
|
|
568
|
+
return ServiceResult(success=False, message=save_result.message,
|
|
569
|
+
error_code=save_result.error_code)
|
|
570
|
+
|
|
571
|
+
return ServiceResult.success_result(True)
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
return self._handle_service_exception(e, 'delete_tenant',
|
|
575
|
+
resource_id=resource_id, tenant_id=tenant_id)
|
|
File without changes
|
|
File without changes
|