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,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authorized Secure Handler with hierarchical routing support.
|
|
3
|
+
|
|
4
|
+
Extends SecureLambdaHandler to add fine-grained authorization using the
|
|
5
|
+
authorization middleware for hierarchical multi-tenant routes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, Callable
|
|
9
|
+
from aws_lambda_powertools import Logger
|
|
10
|
+
|
|
11
|
+
from .secure_handler import SecureLambdaHandler
|
|
12
|
+
from geek_cafe_saas_sdk.middleware.authorization import (
|
|
13
|
+
Operation,
|
|
14
|
+
AuthorizationMiddleware,
|
|
15
|
+
extract_auth_context,
|
|
16
|
+
extract_resource_context,
|
|
17
|
+
AuthorizationResult
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = Logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthorizedSecureLambdaHandler(SecureLambdaHandler):
|
|
24
|
+
"""
|
|
25
|
+
Secure handler with built-in authorization for hierarchical routes.
|
|
26
|
+
|
|
27
|
+
Use this when:
|
|
28
|
+
- Using hierarchical routes: /tenants/{tid}/users/{uid}/resources/{rid}
|
|
29
|
+
- Need fine-grained authorization (global admin, tenant admin, user)
|
|
30
|
+
- Want automatic tenant isolation and permission checks
|
|
31
|
+
|
|
32
|
+
The handler:
|
|
33
|
+
1. Validates JWT via API Gateway (inherited from SecureLambdaHandler)
|
|
34
|
+
2. Extracts actor from JWT (who is making request)
|
|
35
|
+
3. Extracts resource from path (what they want to access)
|
|
36
|
+
4. Checks authorization (can actor access resource?)
|
|
37
|
+
5. Calls business logic only if authorized
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
handler = create_handler(
|
|
41
|
+
service_class=MessageService,
|
|
42
|
+
require_authorization=True, # Enable authorization
|
|
43
|
+
operation=Operation.READ,
|
|
44
|
+
resource_type="message"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def lambda_handler(event, context):
|
|
48
|
+
return handler.execute(event, context, business_logic)
|
|
49
|
+
|
|
50
|
+
def business_logic(event, service, user_context):
|
|
51
|
+
# Authorization already checked - just implement logic
|
|
52
|
+
message_id = event['pathParameters']['message_id']
|
|
53
|
+
return service.get_by_id(message_id)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
operation: Optional[Operation] = None,
|
|
59
|
+
resource_type: Optional[str] = None,
|
|
60
|
+
extract_resource_fn: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
|
61
|
+
skip_authorization: bool = False,
|
|
62
|
+
**kwargs
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize the authorized secure handler.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
operation: Operation to authorize (READ, WRITE, DELETE, CREATE).
|
|
69
|
+
If None, inferred from HTTP method.
|
|
70
|
+
resource_type: Type of resource (e.g., "message", "contact_thread")
|
|
71
|
+
extract_resource_fn: Custom function to extract resource context from event
|
|
72
|
+
skip_authorization: Skip authorization check (for backward compatibility)
|
|
73
|
+
**kwargs: Arguments passed to SecureLambdaHandler
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(**kwargs)
|
|
76
|
+
self.operation = operation
|
|
77
|
+
self.resource_type = resource_type
|
|
78
|
+
self.extract_resource_fn = extract_resource_fn
|
|
79
|
+
self.skip_authorization = skip_authorization
|
|
80
|
+
|
|
81
|
+
def _infer_operation(self, event: Dict[str, Any]) -> Operation:
|
|
82
|
+
"""Infer operation from HTTP method."""
|
|
83
|
+
method = event.get('httpMethod', 'GET').upper()
|
|
84
|
+
|
|
85
|
+
if method == 'GET':
|
|
86
|
+
return Operation.READ
|
|
87
|
+
elif method == 'POST':
|
|
88
|
+
return Operation.CREATE
|
|
89
|
+
elif method in ['PUT', 'PATCH']:
|
|
90
|
+
return Operation.WRITE
|
|
91
|
+
elif method == 'DELETE':
|
|
92
|
+
return Operation.DELETE
|
|
93
|
+
else:
|
|
94
|
+
return Operation.READ # Default
|
|
95
|
+
|
|
96
|
+
def _check_authorization(
|
|
97
|
+
self,
|
|
98
|
+
event: Dict[str, Any]
|
|
99
|
+
) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""
|
|
101
|
+
Check if user is authorized to access the resource.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Error response dict if unauthorized, None if authorized
|
|
105
|
+
"""
|
|
106
|
+
if self.skip_authorization:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Extract actor from JWT
|
|
111
|
+
actor = extract_auth_context(event)
|
|
112
|
+
|
|
113
|
+
# Extract resource from path
|
|
114
|
+
if self.extract_resource_fn:
|
|
115
|
+
resource = self.extract_resource_fn(event)
|
|
116
|
+
else:
|
|
117
|
+
resource = extract_resource_context(event, self.resource_type)
|
|
118
|
+
|
|
119
|
+
# Validate tenant_id is in path
|
|
120
|
+
if not resource.tenant_id:
|
|
121
|
+
logger.warning("Authorization check failed: tenant_id not in path parameters")
|
|
122
|
+
from geek_cafe_saas_sdk.utilities.response import error_response
|
|
123
|
+
return error_response(
|
|
124
|
+
"tenant_id is required in path parameters",
|
|
125
|
+
"AUTHORIZATION_ERROR",
|
|
126
|
+
400
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Determine operation
|
|
130
|
+
operation = self.operation if self.operation else self._infer_operation(event)
|
|
131
|
+
|
|
132
|
+
# Check authorization
|
|
133
|
+
result: AuthorizationResult = AuthorizationMiddleware.can_perform_operation(
|
|
134
|
+
actor, resource, operation
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not result.allowed:
|
|
138
|
+
logger.warning(
|
|
139
|
+
f"Authorization denied: {result.reason}",
|
|
140
|
+
extra={
|
|
141
|
+
'actor_user_id': actor.user_id,
|
|
142
|
+
'actor_tenant_id': actor.tenant_id,
|
|
143
|
+
'resource_tenant_id': resource.tenant_id,
|
|
144
|
+
'resource_user_id': resource.user_id,
|
|
145
|
+
'resource_id': resource.resource_id,
|
|
146
|
+
'operation': operation.value,
|
|
147
|
+
'reason': result.reason
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
from geek_cafe_saas_sdk.utilities.response import error_response
|
|
151
|
+
return error_response(
|
|
152
|
+
"You do not have permission to access this resource",
|
|
153
|
+
"AUTHORIZATION_DENIED",
|
|
154
|
+
403,
|
|
155
|
+
additional_data={'reason': result.reason}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Authorization passed - add context to event for business logic
|
|
159
|
+
event['authorization_context'] = {
|
|
160
|
+
'actor': actor,
|
|
161
|
+
'resource': resource,
|
|
162
|
+
'operation': operation.value,
|
|
163
|
+
'reason': result.reason,
|
|
164
|
+
'context': result.context
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Log successful authorization for audit
|
|
168
|
+
logger.info(
|
|
169
|
+
f"Authorization granted: {result.reason}",
|
|
170
|
+
extra={
|
|
171
|
+
'actor_user_id': actor.user_id,
|
|
172
|
+
'actor_tenant_id': actor.tenant_id,
|
|
173
|
+
'resource_tenant_id': resource.tenant_id,
|
|
174
|
+
'resource_user_id': resource.user_id,
|
|
175
|
+
'resource_id': resource.resource_id,
|
|
176
|
+
'operation': operation.value,
|
|
177
|
+
'reason': result.reason
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return None # Authorized
|
|
182
|
+
|
|
183
|
+
except KeyError as e:
|
|
184
|
+
logger.error(f"Authorization check failed: Missing JWT claim: {e}")
|
|
185
|
+
from geek_cafe_saas_sdk.utilities.response import error_response
|
|
186
|
+
return error_response(
|
|
187
|
+
f"Missing required authentication claim: {str(e)}",
|
|
188
|
+
"AUTHENTICATION_ERROR",
|
|
189
|
+
401
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.exception(f"Authorization check failed with unexpected error: {e}")
|
|
193
|
+
from geek_cafe_saas_sdk.utilities.response import error_response
|
|
194
|
+
return error_response(
|
|
195
|
+
"Authorization check failed",
|
|
196
|
+
"AUTHORIZATION_ERROR",
|
|
197
|
+
500
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def execute(
|
|
201
|
+
self,
|
|
202
|
+
event: Dict[str, Any],
|
|
203
|
+
context: Any,
|
|
204
|
+
business_logic: Callable[[Dict[str, Any], Any, Dict[str, Any]], Any],
|
|
205
|
+
injected_service: Optional[Any] = None
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
Execute handler with authorization check.
|
|
209
|
+
|
|
210
|
+
Overrides parent execute() to add authorization before calling business logic.
|
|
211
|
+
"""
|
|
212
|
+
# Check authorization first
|
|
213
|
+
auth_error = self._check_authorization(event)
|
|
214
|
+
if auth_error:
|
|
215
|
+
return auth_error
|
|
216
|
+
|
|
217
|
+
# Authorization passed - call parent execute (handles everything else)
|
|
218
|
+
return super().execute(event, context, business_logic, injected_service)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Lambda handler with common functionality.
|
|
3
|
+
|
|
4
|
+
Provides a foundation for creating Lambda handlers with standardized
|
|
5
|
+
request/response handling, error management, and service injection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Any, Callable, Optional, Type, TypeVar
|
|
10
|
+
from aws_lambda_powertools import Logger
|
|
11
|
+
|
|
12
|
+
from geek_cafe_saas_sdk.utilities.response import (
|
|
13
|
+
error_response,
|
|
14
|
+
service_result_to_response,
|
|
15
|
+
)
|
|
16
|
+
from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
|
|
17
|
+
from geek_cafe_saas_sdk.middleware.auth import extract_user_context
|
|
18
|
+
from .service_pool import ServicePool
|
|
19
|
+
|
|
20
|
+
logger = Logger()
|
|
21
|
+
|
|
22
|
+
T = TypeVar('T') # Service type
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseLambdaHandler:
|
|
26
|
+
"""
|
|
27
|
+
Base class for Lambda handlers with common functionality.
|
|
28
|
+
|
|
29
|
+
Handles:
|
|
30
|
+
- Request body parsing and case conversion
|
|
31
|
+
- Service initialization and pooling
|
|
32
|
+
- User context extraction
|
|
33
|
+
- Response formatting
|
|
34
|
+
- Event unwrapping (SQS, SNS, etc.)
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
service_class: Optional[Type[T]] = None,
|
|
41
|
+
service_kwargs: Optional[Dict[str, Any]] = None,
|
|
42
|
+
require_body: bool = False,
|
|
43
|
+
convert_case: bool = True,
|
|
44
|
+
unwrap_message: bool = True,
|
|
45
|
+
apply_cors: bool = True,
|
|
46
|
+
apply_error_handling: bool = True,
|
|
47
|
+
require_auth: bool = True,
|
|
48
|
+
):
|
|
49
|
+
self.service_class = service_class
|
|
50
|
+
self.service_kwargs = service_kwargs or {}
|
|
51
|
+
self.require_body = require_body
|
|
52
|
+
self.convert_case = convert_case
|
|
53
|
+
self.unwrap_message = unwrap_message
|
|
54
|
+
self.apply_cors = apply_cors
|
|
55
|
+
self.apply_error_handling = apply_error_handling
|
|
56
|
+
self.require_auth = require_auth
|
|
57
|
+
|
|
58
|
+
# Initialize service pool if a class is provided
|
|
59
|
+
self._service_pool = ServicePool(service_class, **self.service_kwargs) if service_class else None
|
|
60
|
+
|
|
61
|
+
def _get_service(self, injected_service: Optional[T]) -> Optional[T]:
|
|
62
|
+
"""
|
|
63
|
+
Get service instance (injected or from pool).
|
|
64
|
+
"""
|
|
65
|
+
if injected_service:
|
|
66
|
+
return injected_service
|
|
67
|
+
|
|
68
|
+
if self._service_pool:
|
|
69
|
+
return self._service_pool.get()
|
|
70
|
+
|
|
71
|
+
# Fallback for direct instantiation if pooling is not used (rare)
|
|
72
|
+
if self.service_class:
|
|
73
|
+
return self.service_class(**self.service_kwargs)
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def execute(
|
|
78
|
+
self,
|
|
79
|
+
event: Dict[str, Any],
|
|
80
|
+
context: Any,
|
|
81
|
+
business_logic: Callable[[Dict[str, Any], Any, Dict[str, Any]], Any],
|
|
82
|
+
injected_service: Optional[T] = None
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Execute the Lambda handler with the given business logic.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
event: Lambda event dictionary
|
|
89
|
+
context: Lambda context object
|
|
90
|
+
business_logic: Callable that implements the business logic
|
|
91
|
+
injected_service: Optional service instance for testing
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Lambda response dictionary
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Unwrap message if needed (SQS, SNS, etc.)
|
|
98
|
+
if self.unwrap_message and "message" in event:
|
|
99
|
+
event = event["message"]
|
|
100
|
+
|
|
101
|
+
# Validate requestContext presence (Rule #4)
|
|
102
|
+
if "requestContext" not in event:
|
|
103
|
+
return error_response(
|
|
104
|
+
"requestContext missing from event. Ensure API Gateway is properly configured.",
|
|
105
|
+
"CONFIGURATION_ERROR",
|
|
106
|
+
500
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Validate authentication if required
|
|
110
|
+
if self.require_auth:
|
|
111
|
+
authorizer = event.get("requestContext", {}).get("authorizer")
|
|
112
|
+
if not authorizer or not authorizer.get("claims", {}).get("custom:user_id"):
|
|
113
|
+
return error_response(
|
|
114
|
+
"Authentication required but not provided",
|
|
115
|
+
"AUTHENTICATION_REQUIRED",
|
|
116
|
+
401
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Check if body is required
|
|
120
|
+
if self.require_body and not event.get("body"):
|
|
121
|
+
return error_response(
|
|
122
|
+
"Request body is required",
|
|
123
|
+
"VALIDATION_ERROR",
|
|
124
|
+
400
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Parse and validate body
|
|
128
|
+
if event.get("body"):
|
|
129
|
+
try:
|
|
130
|
+
body = LambdaEventUtility.get_body_from_event(event, raise_on_error=self.require_body)
|
|
131
|
+
if body and self.convert_case:
|
|
132
|
+
body = LambdaEventUtility.to_snake_case_for_backend(body)
|
|
133
|
+
if body:
|
|
134
|
+
event["parsed_body"] = body
|
|
135
|
+
except (ValueError, KeyError) as e:
|
|
136
|
+
# If error handling is disabled, let the exception propagate for testing
|
|
137
|
+
if not self.apply_error_handling:
|
|
138
|
+
raise
|
|
139
|
+
return error_response(
|
|
140
|
+
str(e),
|
|
141
|
+
"VALIDATION_ERROR",
|
|
142
|
+
400
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Extract user context from authorizer claims
|
|
146
|
+
user_context = extract_user_context(event)
|
|
147
|
+
|
|
148
|
+
# Get service instance
|
|
149
|
+
service = self._get_service(injected_service)
|
|
150
|
+
|
|
151
|
+
# Execute business logic
|
|
152
|
+
result = business_logic(event, service, user_context)
|
|
153
|
+
|
|
154
|
+
# Determine appropriate HTTP status code based on HTTP method
|
|
155
|
+
http_method = event.get('httpMethod', '').upper()
|
|
156
|
+
if http_method == 'POST':
|
|
157
|
+
success_status = 201 # Created
|
|
158
|
+
elif http_method == 'DELETE':
|
|
159
|
+
success_status = 204 # No Content
|
|
160
|
+
else:
|
|
161
|
+
success_status = 200 # OK (GET, PUT, PATCH, etc.)
|
|
162
|
+
|
|
163
|
+
# Format response - handle both ServiceResult and plain dict
|
|
164
|
+
if hasattr(result, 'success'):
|
|
165
|
+
# It's a ServiceResult object
|
|
166
|
+
response = service_result_to_response(result, success_status=success_status)
|
|
167
|
+
else:
|
|
168
|
+
# It's a plain dict - wrap it in a success response
|
|
169
|
+
from geek_cafe_saas_sdk.utilities.response import success_response
|
|
170
|
+
response = success_response(result, success_status)
|
|
171
|
+
|
|
172
|
+
# Note: CORS headers are already added by success_response/service_result_to_response
|
|
173
|
+
# The apply_cors flag is for decorator usage, not runtime response modification
|
|
174
|
+
return response
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.exception(f"Handler execution error: {e}")
|
|
178
|
+
if self.apply_error_handling:
|
|
179
|
+
# Convert exception to error response (error_response is imported at top)
|
|
180
|
+
return error_response(
|
|
181
|
+
str(e),
|
|
182
|
+
"INTERNAL_ERROR",
|
|
183
|
+
500
|
|
184
|
+
)
|
|
185
|
+
raise
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory for creating Lambda handlers based on configuration.
|
|
3
|
+
|
|
4
|
+
Centralizes handler selection logic and provides a single point
|
|
5
|
+
for configuring authentication strategy across all Lambda functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, Type, TypeVar, Any
|
|
10
|
+
from aws_lambda_powertools import Logger
|
|
11
|
+
|
|
12
|
+
from .base_handler import BaseLambdaHandler
|
|
13
|
+
from .api_key_handler import ApiKeyLambdaHandler
|
|
14
|
+
from .public_handler import PublicLambdaHandler
|
|
15
|
+
from .secure_handler import SecureLambdaHandler
|
|
16
|
+
from .authorized_secure_handler import AuthorizedSecureLambdaHandler
|
|
17
|
+
|
|
18
|
+
logger = Logger()
|
|
19
|
+
|
|
20
|
+
T = TypeVar('T') # Service type
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HandlerFactory:
|
|
24
|
+
"""
|
|
25
|
+
Factory for creating Lambda handlers with appropriate authentication.
|
|
26
|
+
|
|
27
|
+
Configuration via environment variables:
|
|
28
|
+
|
|
29
|
+
- AUTH_TYPE (default: "secure"):
|
|
30
|
+
- "secure": Uses API Gateway authorizer (Cognito/Lambda/IAM)
|
|
31
|
+
- "api_key": Validates x-api-key header against API_KEY env var
|
|
32
|
+
- "public": No authentication required
|
|
33
|
+
- "none": Alias for "public"
|
|
34
|
+
|
|
35
|
+
- AUTH_STRICT (default: "true"):
|
|
36
|
+
- "true": Strict validation, fail if auth is missing
|
|
37
|
+
- "false": Permissive mode for local dev/testing
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
# Simple usage - defaults to secure handler
|
|
41
|
+
handler = HandlerFactory.create(
|
|
42
|
+
service_class=VoteService,
|
|
43
|
+
require_body=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Explicit type
|
|
47
|
+
handler = HandlerFactory.create(
|
|
48
|
+
service_class=VoteService,
|
|
49
|
+
auth_type="api_key", # Override environment
|
|
50
|
+
require_body=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Public endpoint
|
|
54
|
+
handler = HandlerFactory.create_public(
|
|
55
|
+
service_class=ConfigService
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# In lambda function
|
|
59
|
+
def lambda_handler(event, context):
|
|
60
|
+
return handler.execute(event, context, business_logic)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Auth type constants
|
|
64
|
+
AUTH_TYPE_SECURE = "secure"
|
|
65
|
+
AUTH_TYPE_API_KEY = "api_key"
|
|
66
|
+
AUTH_TYPE_PUBLIC = "public"
|
|
67
|
+
AUTH_TYPE_NONE = "none" # Alias for public
|
|
68
|
+
|
|
69
|
+
# Environment variable names
|
|
70
|
+
ENV_AUTH_TYPE = "AUTH_TYPE"
|
|
71
|
+
ENV_AUTH_STRICT = "AUTH_STRICT"
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def create(
|
|
75
|
+
cls,
|
|
76
|
+
service_class: Optional[Type[T]] = None,
|
|
77
|
+
auth_type: Optional[str] = None,
|
|
78
|
+
strict: Optional[bool] = None,
|
|
79
|
+
require_authorization: bool = False,
|
|
80
|
+
operation: Optional[Any] = None,
|
|
81
|
+
resource_type: Optional[str] = None,
|
|
82
|
+
**handler_kwargs
|
|
83
|
+
) -> BaseLambdaHandler:
|
|
84
|
+
"""
|
|
85
|
+
Create a handler with appropriate authentication and optional authorization.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
service_class: Service class to instantiate
|
|
89
|
+
auth_type: Override AUTH_TYPE env var ("secure", "api_key", "public")
|
|
90
|
+
strict: Override AUTH_STRICT env var (True/False)
|
|
91
|
+
require_authorization: Enable fine-grained authorization for hierarchical routes
|
|
92
|
+
operation: Operation to authorize (Operation.READ, etc.). If None, inferred from HTTP method
|
|
93
|
+
resource_type: Type of resource for authorization (e.g., "message", "contact_thread")
|
|
94
|
+
**handler_kwargs: Additional arguments for handler (require_body, etc.)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Configured handler instance
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
# Simple secure handler (existing behavior)
|
|
101
|
+
handler = HandlerFactory.create(service_class=MessageService)
|
|
102
|
+
|
|
103
|
+
# With authorization for hierarchical routes
|
|
104
|
+
handler = HandlerFactory.create(
|
|
105
|
+
service_class=MessageService,
|
|
106
|
+
require_authorization=True,
|
|
107
|
+
operation=Operation.READ,
|
|
108
|
+
resource_type="message"
|
|
109
|
+
)
|
|
110
|
+
"""
|
|
111
|
+
# Get auth type from args or environment
|
|
112
|
+
if auth_type is None:
|
|
113
|
+
auth_type = os.getenv(cls.ENV_AUTH_TYPE, cls.AUTH_TYPE_SECURE).lower()
|
|
114
|
+
else:
|
|
115
|
+
auth_type = auth_type.lower()
|
|
116
|
+
|
|
117
|
+
# Get strict mode
|
|
118
|
+
if strict is None:
|
|
119
|
+
strict_str = os.getenv(cls.ENV_AUTH_STRICT, "true").lower()
|
|
120
|
+
strict = strict_str in ("true", "1", "yes")
|
|
121
|
+
|
|
122
|
+
# Log configuration
|
|
123
|
+
logger.info(
|
|
124
|
+
f"Creating handler with auth_type={auth_type}, strict={strict}, "
|
|
125
|
+
f"service={service_class.__name__ if service_class else 'None'}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Prepare service kwargs, including the system inbox ID if applicable
|
|
129
|
+
service_kwargs = {}
|
|
130
|
+
if service_class and hasattr(service_class, '__init__') and 'system_inbox_id' in service_class.__init__.__code__.co_varnames:
|
|
131
|
+
service_kwargs['system_inbox_id'] = os.getenv('SYSTEM_INBOX_ID', 'support-inbox')
|
|
132
|
+
|
|
133
|
+
# Create appropriate handler
|
|
134
|
+
if auth_type == cls.AUTH_TYPE_API_KEY:
|
|
135
|
+
return ApiKeyLambdaHandler(
|
|
136
|
+
service_class=service_class,
|
|
137
|
+
service_kwargs=service_kwargs,
|
|
138
|
+
**handler_kwargs
|
|
139
|
+
)
|
|
140
|
+
elif auth_type in (cls.AUTH_TYPE_PUBLIC, cls.AUTH_TYPE_NONE):
|
|
141
|
+
return PublicLambdaHandler(
|
|
142
|
+
service_class=service_class,
|
|
143
|
+
service_kwargs=service_kwargs,
|
|
144
|
+
**handler_kwargs
|
|
145
|
+
)
|
|
146
|
+
elif auth_type == cls.AUTH_TYPE_SECURE:
|
|
147
|
+
# Use AuthorizedSecureLambdaHandler if authorization is required
|
|
148
|
+
if require_authorization:
|
|
149
|
+
return AuthorizedSecureLambdaHandler(
|
|
150
|
+
service_class=service_class,
|
|
151
|
+
service_kwargs=service_kwargs,
|
|
152
|
+
require_authorizer_claims=strict,
|
|
153
|
+
operation=operation,
|
|
154
|
+
resource_type=resource_type,
|
|
155
|
+
**handler_kwargs
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
return SecureLambdaHandler(
|
|
159
|
+
service_class=service_class,
|
|
160
|
+
service_kwargs=service_kwargs,
|
|
161
|
+
require_authorizer_claims=strict,
|
|
162
|
+
**handler_kwargs
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"Unknown auth_type '{auth_type}', defaulting to secure handler"
|
|
167
|
+
)
|
|
168
|
+
# Use AuthorizedSecureLambdaHandler if authorization is required
|
|
169
|
+
if require_authorization:
|
|
170
|
+
return AuthorizedSecureLambdaHandler(
|
|
171
|
+
service_class=service_class,
|
|
172
|
+
service_kwargs=service_kwargs,
|
|
173
|
+
require_authorizer_claims=strict,
|
|
174
|
+
operation=operation,
|
|
175
|
+
resource_type=resource_type,
|
|
176
|
+
**handler_kwargs
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
return SecureLambdaHandler(
|
|
180
|
+
service_class=service_class,
|
|
181
|
+
service_kwargs=service_kwargs,
|
|
182
|
+
require_authorizer_claims=strict,
|
|
183
|
+
**handler_kwargs
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def create_secure(
|
|
188
|
+
cls,
|
|
189
|
+
service_class: Optional[Type[T]] = None,
|
|
190
|
+
**handler_kwargs
|
|
191
|
+
) -> SecureLambdaHandler:
|
|
192
|
+
"""
|
|
193
|
+
Create a secure handler (API Gateway authorizer).
|
|
194
|
+
|
|
195
|
+
Convenience method that explicitly creates a secure handler
|
|
196
|
+
regardless of environment configuration.
|
|
197
|
+
"""
|
|
198
|
+
return SecureLambdaHandler(
|
|
199
|
+
service_class=service_class,
|
|
200
|
+
**handler_kwargs
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def create_api_key(
|
|
205
|
+
cls,
|
|
206
|
+
service_class: Optional[Type[T]] = None,
|
|
207
|
+
**handler_kwargs
|
|
208
|
+
) -> ApiKeyLambdaHandler:
|
|
209
|
+
"""
|
|
210
|
+
Create an API key handler.
|
|
211
|
+
|
|
212
|
+
Convenience method that explicitly creates an API key handler
|
|
213
|
+
regardless of environment configuration.
|
|
214
|
+
"""
|
|
215
|
+
return ApiKeyLambdaHandler(
|
|
216
|
+
service_class=service_class,
|
|
217
|
+
**handler_kwargs
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def create_public(
|
|
222
|
+
cls,
|
|
223
|
+
service_class: Optional[Type[T]] = None,
|
|
224
|
+
**handler_kwargs
|
|
225
|
+
) -> PublicLambdaHandler:
|
|
226
|
+
"""
|
|
227
|
+
Create a public handler (no auth).
|
|
228
|
+
|
|
229
|
+
Convenience method that explicitly creates a public handler
|
|
230
|
+
regardless of environment configuration.
|
|
231
|
+
"""
|
|
232
|
+
return PublicLambdaHandler(
|
|
233
|
+
service_class=service_class,
|
|
234
|
+
**handler_kwargs
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Convenience function for quick handler creation
|
|
239
|
+
def create_handler(
|
|
240
|
+
service_class: Optional[Type[T]] = None,
|
|
241
|
+
**kwargs
|
|
242
|
+
) -> BaseLambdaHandler:
|
|
243
|
+
"""
|
|
244
|
+
Convenience function for creating handlers.
|
|
245
|
+
|
|
246
|
+
Equivalent to HandlerFactory.create()
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
from geek_cafe_saas_sdk.lambda_handlers import create_handler
|
|
250
|
+
|
|
251
|
+
handler = create_handler(
|
|
252
|
+
service_class=VoteService,
|
|
253
|
+
require_body=True
|
|
254
|
+
)
|
|
255
|
+
"""
|
|
256
|
+
return HandlerFactory.create(service_class=service_class, **kwargs)
|