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,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for updating chat messages.
|
|
3
|
+
|
|
4
|
+
Supports multiple operations:
|
|
5
|
+
- Edit message content
|
|
6
|
+
- Add/remove reactions
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
11
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ChatMessageService
|
|
12
|
+
|
|
13
|
+
# Factory creates handler (defaults to secure auth)
|
|
14
|
+
handler_wrapper = create_handler(
|
|
15
|
+
service_class=ChatMessageService,
|
|
16
|
+
require_body=True,
|
|
17
|
+
convert_case=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Update a chat message.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event: Lambda event from API Gateway
|
|
27
|
+
context: Lambda context
|
|
28
|
+
injected_service: Optional ChatMessageService for testing
|
|
29
|
+
|
|
30
|
+
Path parameters:
|
|
31
|
+
id: Chat message ID
|
|
32
|
+
|
|
33
|
+
Expected body (camelCase from frontend):
|
|
34
|
+
{
|
|
35
|
+
"action": "update" | "add_reaction" | "remove_reaction",
|
|
36
|
+
|
|
37
|
+
// For action="update":
|
|
38
|
+
"content": "Updated message content",
|
|
39
|
+
|
|
40
|
+
// For action="add_reaction":
|
|
41
|
+
"emoji": "👍",
|
|
42
|
+
|
|
43
|
+
// For action="remove_reaction":
|
|
44
|
+
"emoji": "👍"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Returns 200 with updated chat message
|
|
48
|
+
"""
|
|
49
|
+
return handler_wrapper.execute(event, context, update_chat_message, injected_service)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def update_chat_message(
|
|
53
|
+
event: Dict[str, Any],
|
|
54
|
+
service: ChatMessageService,
|
|
55
|
+
user_context: Dict[str, str]
|
|
56
|
+
) -> Any:
|
|
57
|
+
"""
|
|
58
|
+
Business logic for updating chat messages.
|
|
59
|
+
|
|
60
|
+
Routes to different service methods based on action parameter.
|
|
61
|
+
"""
|
|
62
|
+
# Extract path parameter
|
|
63
|
+
path_params = event.get("pathParameters") or {}
|
|
64
|
+
message_id = path_params.get("id")
|
|
65
|
+
|
|
66
|
+
if not message_id:
|
|
67
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
68
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
69
|
+
return ServiceResult.exception_result(
|
|
70
|
+
ValidationError("Message ID is required in path")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
payload = event["parsed_body"]
|
|
74
|
+
action = payload.get("action", "update")
|
|
75
|
+
|
|
76
|
+
user_id = user_context.get("user_id")
|
|
77
|
+
tenant_id = user_context.get("tenant_id")
|
|
78
|
+
|
|
79
|
+
# Route to appropriate service method based on action
|
|
80
|
+
|
|
81
|
+
if action == "add_reaction":
|
|
82
|
+
# Add a reaction to the message
|
|
83
|
+
emoji = payload.get("emoji")
|
|
84
|
+
if not emoji:
|
|
85
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
86
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
87
|
+
return ServiceResult.exception_result(
|
|
88
|
+
ValidationError("emoji is required for add_reaction action")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return service.add_reaction(
|
|
92
|
+
message_id=message_id,
|
|
93
|
+
tenant_id=tenant_id,
|
|
94
|
+
user_id=user_id,
|
|
95
|
+
emoji=emoji
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
elif action == "remove_reaction":
|
|
99
|
+
# Remove a reaction from the message
|
|
100
|
+
emoji = payload.get("emoji")
|
|
101
|
+
if not emoji:
|
|
102
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
103
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
104
|
+
return ServiceResult.exception_result(
|
|
105
|
+
ValidationError("emoji is required for remove_reaction action")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return service.remove_reaction(
|
|
109
|
+
message_id=message_id,
|
|
110
|
+
tenant_id=tenant_id,
|
|
111
|
+
user_id=user_id,
|
|
112
|
+
emoji=emoji
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
# Edit message content (only sender can edit)
|
|
117
|
+
updates = {}
|
|
118
|
+
|
|
119
|
+
if "content" in payload:
|
|
120
|
+
updates["content"] = payload["content"]
|
|
121
|
+
|
|
122
|
+
return service.update(
|
|
123
|
+
resource_id=message_id,
|
|
124
|
+
tenant_id=tenant_id,
|
|
125
|
+
user_id=user_id,
|
|
126
|
+
updates=updates
|
|
127
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for creating contact threads.
|
|
3
|
+
|
|
4
|
+
Supports both guest contact forms (API key auth) and authenticated users.
|
|
5
|
+
Auth strategy is controlled by AUTH_TYPE environment variable:
|
|
6
|
+
- AUTH_TYPE=api_key - For public contact forms (validates x-api-key)
|
|
7
|
+
- AUTH_TYPE=secure - For authenticated app users (API Gateway authorizer)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict, Any
|
|
11
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
12
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ContactThreadService
|
|
13
|
+
|
|
14
|
+
# Factory automatically selects handler based on AUTH_TYPE env var
|
|
15
|
+
handler_wrapper = create_handler(
|
|
16
|
+
service_class=ContactThreadService,
|
|
17
|
+
require_body=True,
|
|
18
|
+
convert_case=True
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Create a new contact thread.
|
|
25
|
+
|
|
26
|
+
Two deployment modes:
|
|
27
|
+
|
|
28
|
+
1. Public Contact Form (AUTH_TYPE=api_key):
|
|
29
|
+
- Validates x-api-key header
|
|
30
|
+
- Creates contact from guest/anonymous user
|
|
31
|
+
- Used for website contact forms
|
|
32
|
+
|
|
33
|
+
2. Authenticated App (AUTH_TYPE=secure):
|
|
34
|
+
- Trusts API Gateway Cognito authorizer
|
|
35
|
+
- Creates contact from logged-in user
|
|
36
|
+
- Used for in-app support tickets
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
event: Lambda event from API Gateway
|
|
40
|
+
context: Lambda context
|
|
41
|
+
injected_service: Optional ContactThreadService for testing
|
|
42
|
+
|
|
43
|
+
Expected body (camelCase from frontend):
|
|
44
|
+
{
|
|
45
|
+
"subject": "Contact inquiry",
|
|
46
|
+
"sender": {
|
|
47
|
+
"id": "guest-session-xyz",
|
|
48
|
+
"name": "John Doe",
|
|
49
|
+
"email": "john@example.com"
|
|
50
|
+
},
|
|
51
|
+
"initialMessage": "Message content",
|
|
52
|
+
"inboxId": "support" | "sales" | "billing",
|
|
53
|
+
"priority": "low" | "medium" | "high" | "urgent",
|
|
54
|
+
"source": "web" | "mobile" | "api"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Returns 201 with created contact thread
|
|
58
|
+
"""
|
|
59
|
+
return handler_wrapper.execute(event, context, create_contact_thread, injected_service)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_contact_thread(
|
|
63
|
+
event: Dict[str, Any],
|
|
64
|
+
service: ContactThreadService,
|
|
65
|
+
user_context: Dict[str, str]
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""
|
|
68
|
+
Business logic for creating contact threads.
|
|
69
|
+
|
|
70
|
+
Handles both guest users (API key) and authenticated users (Cognito).
|
|
71
|
+
"""
|
|
72
|
+
payload = event["parsed_body"]
|
|
73
|
+
|
|
74
|
+
# Determine tenant and user ID
|
|
75
|
+
# For authenticated users, use token data
|
|
76
|
+
# For guest contact forms, use payload data
|
|
77
|
+
tenant_id = user_context.get("tenant_id") or payload.get("tenant_id", "default")
|
|
78
|
+
|
|
79
|
+
authenticated_user_id = user_context.get("user_id")
|
|
80
|
+
if authenticated_user_id:
|
|
81
|
+
user_id = authenticated_user_id
|
|
82
|
+
else:
|
|
83
|
+
# For API key requests, allow userId from sender
|
|
84
|
+
sender = payload.get("sender", {})
|
|
85
|
+
user_id = sender.get("id", "anonymous")
|
|
86
|
+
|
|
87
|
+
# Create the contact thread
|
|
88
|
+
result = service.create(
|
|
89
|
+
tenant_id=tenant_id,
|
|
90
|
+
user_id=user_id,
|
|
91
|
+
payload=payload
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return result
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for deleting (soft delete) contact threads.
|
|
3
|
+
|
|
4
|
+
Requires authentication and appropriate access permissions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ContactThreadService
|
|
10
|
+
|
|
11
|
+
# Factory creates handler (defaults to secure auth)
|
|
12
|
+
handler_wrapper = create_handler(
|
|
13
|
+
service_class=ContactThreadService,
|
|
14
|
+
require_body=False
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Delete (soft delete) a contact thread.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
event: Lambda event from API Gateway
|
|
24
|
+
context: Lambda context
|
|
25
|
+
injected_service: Optional ContactThreadService for testing
|
|
26
|
+
|
|
27
|
+
Path parameters:
|
|
28
|
+
id: Contact thread ID
|
|
29
|
+
|
|
30
|
+
Returns 200 with success boolean
|
|
31
|
+
"""
|
|
32
|
+
return handler_wrapper.execute(event, context, delete_contact_thread, injected_service)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def delete_contact_thread(
|
|
36
|
+
event: Dict[str, Any],
|
|
37
|
+
service: ContactThreadService,
|
|
38
|
+
user_context: Dict[str, str]
|
|
39
|
+
) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Business logic for deleting a contact thread.
|
|
42
|
+
|
|
43
|
+
Performs soft delete (sets deleted timestamp).
|
|
44
|
+
"""
|
|
45
|
+
# Extract path parameter
|
|
46
|
+
path_params = event.get("pathParameters") or {}
|
|
47
|
+
thread_id = path_params.get("id")
|
|
48
|
+
|
|
49
|
+
if not thread_id:
|
|
50
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
51
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
52
|
+
return ServiceResult.exception_result(
|
|
53
|
+
ValidationError("Thread ID is required in path")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
user_id = user_context.get("user_id")
|
|
57
|
+
tenant_id = user_context.get("tenant_id")
|
|
58
|
+
user_inboxes = user_context.get("inboxes", [])
|
|
59
|
+
|
|
60
|
+
# Delete the contact thread
|
|
61
|
+
return service.delete(
|
|
62
|
+
resource_id=thread_id,
|
|
63
|
+
tenant_id=tenant_id,
|
|
64
|
+
user_id=user_id,
|
|
65
|
+
user_inboxes=user_inboxes
|
|
66
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for getting a contact thread by ID.
|
|
3
|
+
|
|
4
|
+
Requires authentication (defaults to secure mode).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
9
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ContactThreadService
|
|
10
|
+
|
|
11
|
+
# Factory creates handler (defaults to secure auth)
|
|
12
|
+
handler_wrapper = create_handler(
|
|
13
|
+
service_class=ContactThreadService,
|
|
14
|
+
require_body=False
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Get a contact thread by ID.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
event: Lambda event from API Gateway
|
|
24
|
+
context: Lambda context
|
|
25
|
+
injected_service: Optional ContactThreadService for testing
|
|
26
|
+
|
|
27
|
+
Path parameters:
|
|
28
|
+
id: Contact thread ID
|
|
29
|
+
|
|
30
|
+
Returns 200 with contact thread details
|
|
31
|
+
"""
|
|
32
|
+
return handler_wrapper.execute(event, context, get_contact_thread, injected_service)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_contact_thread(
|
|
36
|
+
event: Dict[str, Any],
|
|
37
|
+
service: ContactThreadService,
|
|
38
|
+
user_context: Dict[str, str]
|
|
39
|
+
) -> Any:
|
|
40
|
+
"""
|
|
41
|
+
Business logic for getting a contact thread.
|
|
42
|
+
|
|
43
|
+
Access control is enforced via inbox access or sender/assignee checks.
|
|
44
|
+
"""
|
|
45
|
+
# Extract path parameter
|
|
46
|
+
path_params = event.get("pathParameters") or {}
|
|
47
|
+
thread_id = path_params.get("id")
|
|
48
|
+
|
|
49
|
+
if not thread_id:
|
|
50
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
51
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
52
|
+
return ServiceResult.exception_result(
|
|
53
|
+
ValidationError("Thread ID is required in path")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Get user's inbox access
|
|
57
|
+
user_id = user_context.get("user_id")
|
|
58
|
+
tenant_id = user_context.get("tenant_id")
|
|
59
|
+
user_inboxes = user_context.get("inboxes", [])
|
|
60
|
+
|
|
61
|
+
# Get the contact thread with access control
|
|
62
|
+
return service.get_by_id(
|
|
63
|
+
resource_id=thread_id,
|
|
64
|
+
tenant_id=tenant_id,
|
|
65
|
+
user_id=user_id,
|
|
66
|
+
user_inboxes=user_inboxes
|
|
67
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for listing contact threads.
|
|
3
|
+
|
|
4
|
+
Supports multiple query patterns via query parameters:
|
|
5
|
+
- inbox + status (default): List threads in an inbox by status
|
|
6
|
+
- assigned: List threads assigned to current user
|
|
7
|
+
- sender_email: List all threads from a specific email
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict, Any
|
|
11
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
12
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ContactThreadService
|
|
13
|
+
|
|
14
|
+
# Factory creates handler (defaults to secure auth)
|
|
15
|
+
handler_wrapper = create_handler(
|
|
16
|
+
service_class=ContactThreadService,
|
|
17
|
+
require_body=False
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
List contact threads based on query parameters.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
event: Lambda event from API Gateway
|
|
27
|
+
context: Lambda context
|
|
28
|
+
injected_service: Optional ContactThreadService for testing
|
|
29
|
+
|
|
30
|
+
Query parameters:
|
|
31
|
+
inbox_id: Inbox ID (support, sales, billing)
|
|
32
|
+
status: Status filter (open, in_progress, resolved, closed)
|
|
33
|
+
assigned: "me" to get threads assigned to current user
|
|
34
|
+
sender_email: Email address to find all threads from sender
|
|
35
|
+
limit: Maximum number of results (default 50)
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
GET /contact-threads?inbox_id=support&status=open
|
|
39
|
+
GET /contact-threads?assigned=me
|
|
40
|
+
GET /contact-threads?sender_email=guest@example.com
|
|
41
|
+
|
|
42
|
+
Returns 200 with list of contact threads
|
|
43
|
+
"""
|
|
44
|
+
return handler_wrapper.execute(event, context, list_contact_threads, injected_service)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def list_contact_threads(
|
|
48
|
+
event: Dict[str, Any],
|
|
49
|
+
service: ContactThreadService,
|
|
50
|
+
user_context: Dict[str, str]
|
|
51
|
+
) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Business logic for listing contact threads.
|
|
54
|
+
|
|
55
|
+
Routes to different service methods based on query parameters.
|
|
56
|
+
"""
|
|
57
|
+
query_params = event.get("queryStringParameters") or {}
|
|
58
|
+
|
|
59
|
+
user_id = user_context.get("user_id")
|
|
60
|
+
tenant_id = user_context.get("tenant_id")
|
|
61
|
+
limit = int(query_params.get("limit", "50"))
|
|
62
|
+
|
|
63
|
+
# Route to appropriate service method
|
|
64
|
+
|
|
65
|
+
# Pattern 1: List by assigned user
|
|
66
|
+
if query_params.get("assigned") == "me":
|
|
67
|
+
status = query_params.get("status")
|
|
68
|
+
return service.list_by_assigned_user(
|
|
69
|
+
assigned_to=user_id,
|
|
70
|
+
tenant_id=tenant_id,
|
|
71
|
+
status=status,
|
|
72
|
+
limit=limit
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Pattern 2: List by sender email
|
|
76
|
+
if "sender_email" in query_params:
|
|
77
|
+
sender_email = query_params.get("sender_email")
|
|
78
|
+
return service.list_by_sender_email(
|
|
79
|
+
sender_email=sender_email,
|
|
80
|
+
tenant_id=tenant_id,
|
|
81
|
+
limit=limit
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Pattern 3: List by inbox and status (default)
|
|
85
|
+
inbox_id = query_params.get("inbox_id", "support")
|
|
86
|
+
status = query_params.get("status", "open")
|
|
87
|
+
priority = query_params.get("priority")
|
|
88
|
+
|
|
89
|
+
return service.list_by_inbox_and_status(
|
|
90
|
+
inbox_id=inbox_id,
|
|
91
|
+
status=status,
|
|
92
|
+
tenant_id=tenant_id,
|
|
93
|
+
priority=priority,
|
|
94
|
+
limit=limit
|
|
95
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for updating contact threads.
|
|
3
|
+
|
|
4
|
+
Supports multiple update operations:
|
|
5
|
+
- Update general fields (subject, priority, tags, etc.)
|
|
6
|
+
- Add messages to thread
|
|
7
|
+
- Assign to staff member
|
|
8
|
+
- Update status
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Any
|
|
12
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
13
|
+
from geek_cafe_saas_sdk.domains.messaging.services import ContactThreadService
|
|
14
|
+
|
|
15
|
+
# Factory creates handler (defaults to secure auth)
|
|
16
|
+
handler_wrapper = create_handler(
|
|
17
|
+
service_class=ContactThreadService,
|
|
18
|
+
require_body=True,
|
|
19
|
+
convert_case=True
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handler(event: Dict[str, Any], context: Any, injected_service=None) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Update a contact thread.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
event: Lambda event from API Gateway
|
|
29
|
+
context: Lambda context
|
|
30
|
+
injected_service: Optional ContactThreadService for testing
|
|
31
|
+
|
|
32
|
+
Path parameters:
|
|
33
|
+
id: Contact thread ID
|
|
34
|
+
|
|
35
|
+
Expected body (camelCase from frontend):
|
|
36
|
+
{
|
|
37
|
+
"action": "update" | "add_message" | "assign" | "update_status",
|
|
38
|
+
|
|
39
|
+
// For action="update":
|
|
40
|
+
"subject": "Updated subject",
|
|
41
|
+
"priority": "urgent",
|
|
42
|
+
"tags": ["bug", "urgent"],
|
|
43
|
+
|
|
44
|
+
// For action="add_message":
|
|
45
|
+
"message": {
|
|
46
|
+
"content": "Reply message",
|
|
47
|
+
"senderName": "Support Staff",
|
|
48
|
+
"isStaffReply": true
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// For action="assign":
|
|
52
|
+
"assignedTo": "staff_user_id",
|
|
53
|
+
|
|
54
|
+
// For action="update_status":
|
|
55
|
+
"status": "resolved"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Returns 200 with updated contact thread
|
|
59
|
+
"""
|
|
60
|
+
return handler_wrapper.execute(event, context, update_contact_thread, injected_service)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def update_contact_thread(
|
|
64
|
+
event: Dict[str, Any],
|
|
65
|
+
service: ContactThreadService,
|
|
66
|
+
user_context: Dict[str, str]
|
|
67
|
+
) -> Any:
|
|
68
|
+
"""
|
|
69
|
+
Business logic for updating contact threads.
|
|
70
|
+
|
|
71
|
+
Routes to different service methods based on action parameter.
|
|
72
|
+
"""
|
|
73
|
+
# Extract path parameter
|
|
74
|
+
path_params = event.get("pathParameters") or {}
|
|
75
|
+
thread_id = path_params.get("id")
|
|
76
|
+
|
|
77
|
+
if not thread_id:
|
|
78
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
79
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
80
|
+
return ServiceResult.exception_result(
|
|
81
|
+
ValidationError("Thread ID is required in path")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
payload = event["parsed_body"]
|
|
85
|
+
action = payload.get("action", "update")
|
|
86
|
+
|
|
87
|
+
user_id = user_context.get("user_id")
|
|
88
|
+
tenant_id = user_context.get("tenant_id")
|
|
89
|
+
user_inboxes = user_context.get("inboxes", [])
|
|
90
|
+
|
|
91
|
+
# Route to appropriate service method based on action
|
|
92
|
+
|
|
93
|
+
if action == "add_message":
|
|
94
|
+
# Add a message to the thread
|
|
95
|
+
message_data = payload.get("message", {})
|
|
96
|
+
return service.add_message(
|
|
97
|
+
thread_id=thread_id,
|
|
98
|
+
tenant_id=tenant_id,
|
|
99
|
+
user_id=user_id,
|
|
100
|
+
message_data=message_data,
|
|
101
|
+
user_inboxes=user_inboxes
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
elif action == "assign":
|
|
105
|
+
# Assign thread to a staff member
|
|
106
|
+
assigned_to = payload.get("assigned_to")
|
|
107
|
+
if not assigned_to:
|
|
108
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
109
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
110
|
+
return ServiceResult.exception_result(
|
|
111
|
+
ValidationError("assigned_to is required for assign action")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return service.assign_thread(
|
|
115
|
+
thread_id=thread_id,
|
|
116
|
+
tenant_id=tenant_id,
|
|
117
|
+
user_id=user_id,
|
|
118
|
+
assigned_to=assigned_to,
|
|
119
|
+
user_inboxes=user_inboxes
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
elif action == "update_status":
|
|
123
|
+
# Update thread status
|
|
124
|
+
status = payload.get("status")
|
|
125
|
+
if not status:
|
|
126
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
127
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError
|
|
128
|
+
return ServiceResult.exception_result(
|
|
129
|
+
ValidationError("status is required for update_status action")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return service.update_status(
|
|
133
|
+
thread_id=thread_id,
|
|
134
|
+
tenant_id=tenant_id,
|
|
135
|
+
user_id=user_id,
|
|
136
|
+
status=status,
|
|
137
|
+
user_inboxes=user_inboxes
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
# General update (update multiple fields)
|
|
142
|
+
# Extract allowed fields
|
|
143
|
+
updates = {}
|
|
144
|
+
allowed_fields = ['subject', 'status', 'priority', 'assigned_to', 'tags', 'inbox_id']
|
|
145
|
+
|
|
146
|
+
for field in allowed_fields:
|
|
147
|
+
if field in payload:
|
|
148
|
+
updates[field] = payload[field]
|
|
149
|
+
|
|
150
|
+
return service.update(
|
|
151
|
+
resource_id=thread_id,
|
|
152
|
+
tenant_id=tenant_id,
|
|
153
|
+
user_id=user_id,
|
|
154
|
+
updates=updates,
|
|
155
|
+
user_inboxes=user_inboxes
|
|
156
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Messaging Domain Models
|
|
2
|
+
|
|
3
|
+
from .chat_channel import ChatChannel
|
|
4
|
+
from .chat_channel_member import ChatChannelMember
|
|
5
|
+
from .chat_message import ChatMessage
|
|
6
|
+
from .contact_thread import ContactThread
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ChatChannel",
|
|
10
|
+
"ChatChannelMember",
|
|
11
|
+
"ChatMessage",
|
|
12
|
+
"ContactThread",
|
|
13
|
+
]
|