update events via wrapper routers

This commit is contained in:
2025-01-16 19:32:59 +03:00
parent 049a7c1e11
commit 426b69b33c
42 changed files with 2344 additions and 460 deletions

View File

@@ -20,15 +20,44 @@ from fastapi.openapi.utils import get_openapi
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
app = FastAPI()
app = FastAPI(
responses={
422: {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"detail": {
"type": "array",
"items": {
"type": "object",
"properties": {
"loc": {
"type": "array",
"items": {"type": "string"},
},
"msg": {"type": "string"},
"type": {"type": "string"},
},
},
}
},
}
}
},
}
}
)
# Get all routers and protected routes from the new configuration
routers, protected_routes = get_all_routers()
# Include all routers
for router in routers:
app.include_router(router)
# Configure OpenAPI schema with security
def custom_openapi():
if app.openapi_schema:
@@ -47,7 +76,9 @@ def create_app() -> FastAPI:
# Configure security for protected routes
for path, methods in protected_routes.items():
for method in methods:
configure_route_security(path, method, openapi_schema, list(protected_routes.keys()))
configure_route_security(
path, method, openapi_schema, list(protected_routes.keys())
)
app.openapi_schema = openapi_schema
return app.openapi_schema

View File

@@ -18,6 +18,7 @@ from AllConfigs.main import MainConfig as Config
from create_routes import get_all_routers
def setup_security_schema() -> Dict[str, Any]:
"""
Configure security schema for the OpenAPI documentation.
@@ -32,12 +33,13 @@ def setup_security_schema() -> Dict[str, Any]:
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Enter the token"
"description": "Enter the token",
}
}
}
}
def configure_route_security(
path: str, method: str, schema: Dict[str, Any], protected_paths: List[str]
) -> None:
@@ -55,6 +57,7 @@ def configure_route_security(
if method.lower() in schema["paths"][path]:
schema["paths"][path][method.lower()]["security"] = [{"Bearer": []}]
def create_app() -> FastAPI:
"""
Create and configure a FastAPI application with dynamic route creation.
@@ -68,7 +71,7 @@ def create_app() -> FastAPI:
description=Config.DESCRIPTION,
default_response_class=JSONResponse,
)
@app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION))
async def home() -> RedirectResponse:
"""Redirect root path to API documentation."""
@@ -76,11 +79,11 @@ def create_app() -> FastAPI:
# Get all routers and protected routes using the dynamic route creation
routers, protected_routes = get_all_routers()
# Include all routers
for router in routers:
app.include_router(router)
# Configure OpenAPI schema with security
def custom_openapi():
if app.openapi_schema:
@@ -100,7 +103,9 @@ def create_app() -> FastAPI:
# Configure security for protected routes
for path, methods in protected_routes.items():
for method in methods:
configure_route_security(path, method, openapi_schema, list(protected_routes.keys()))
configure_route_security(
path, method, openapi_schema, list(protected_routes.keys())
)
app.openapi_schema = openapi_schema
return app.openapi_schema

View File

@@ -5,8 +5,8 @@ Handles dynamic route creation based on configurations.
from typing import Optional, Dict, Any, List, Callable, TypeVar, ParamSpec
P = ParamSpec('P') # For function parameters
R = TypeVar('R') # For return type
P = ParamSpec("P") # For function parameters
R = TypeVar("R") # For return type
from dataclasses import dataclass
from functools import wraps
@@ -17,7 +17,6 @@ from pydantic import BaseModel
from AllConfigs.main import MainConfig as Config
@dataclass
class EndpointFactoryConfig:
endpoint: str
@@ -39,17 +38,17 @@ class EndpointFactoryConfig:
class EnhancedEndpointFactory:
def __init__(self, router_config: dict):
self.router = APIRouter(
prefix=router_config['prefix'],
tags=router_config['tags'],
include_in_schema=router_config.get('include_in_schema', True)
prefix=router_config["prefix"],
tags=router_config["tags"],
include_in_schema=router_config.get("include_in_schema", True),
)
self.endpoints = router_config['endpoints']
self.endpoints = router_config["endpoints"]
self.protected_routes: Dict[str, List[str]] = {}
def create_endpoint(self, config: EndpointFactoryConfig):
"""
Create an endpoint directly from the configuration.
Args:
config: EndpointFactoryConfig instance containing endpoint configuration
"""
@@ -70,7 +69,7 @@ class EnhancedEndpointFactory:
response_model=config.response_model,
summary=config.summary,
description=config.description,
**config.extra_options
**config.extra_options,
)(endpoint_function)
def get_router(self) -> APIRouter:
@@ -94,13 +93,13 @@ async def ping_test(request: Request, service_name: str = "base-router"):
def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]:
"""
Get all configured routers and their protected routes.
Get all routers and protected routes from route configurations.
Returns:
tuple: (routers, protected_routes)
"""
from events.route_configs import get_route_configs
from ApiEvents.route_configs import get_route_configs
routers = []
all_protected_routes = {}
@@ -109,25 +108,23 @@ def get_all_routers() -> tuple[List[APIRouter], Dict[str, List[str]]]:
factory_all = []
for config in route_configs:
factory = EnhancedEndpointFactory(config)
# Create endpoints from configuration
for endpoint_dict in config['endpoints']:
for endpoint_dict in config["endpoints"]:
endpoint_config = EndpointFactoryConfig(
endpoint=endpoint_dict['endpoint'],
method=endpoint_dict['method'],
summary=endpoint_dict['summary'],
description=endpoint_dict['description'],
endpoint_function=endpoint_dict['endpoint_function'],
is_auth_required=endpoint_dict['is_auth_required'],
is_event_required=endpoint_dict['is_event_required'],
extra_options=endpoint_dict.get('extra_options', {})
endpoint=endpoint_dict["endpoint"],
method=endpoint_dict["method"],
summary=endpoint_dict["summary"],
description=endpoint_dict["description"],
endpoint_function=endpoint_dict["endpoint_function"],
is_auth_required=endpoint_dict["is_auth_required"],
is_event_required=endpoint_dict["is_event_required"],
extra_options=endpoint_dict.get("extra_options", {}),
)
factory.create_endpoint(endpoint_config)
factory_all.append(
endpoint_config.__dict__
)
factory_all.append(endpoint_config.__dict__)
# Add router and protected routes
routers.append(factory.get_router())
all_protected_routes.update(factory.get_protected_routes())
return routers, all_protected_routes
return routers, all_protected_routes

View File

@@ -1 +1,5 @@
from .token_event_middleware import TokenEventMiddleware
from .auth_middleware import RequestTimingMiddleware, MiddlewareModule
__all__ = ["TokenEventMiddleware", "RequestTimingMiddleware", "MiddlewareModule"]

View File

@@ -6,70 +6,83 @@ and a middleware for request timing measurements.
"""
from time import perf_counter
from typing import Callable, Optional, Dict, Any, Tuple
from typing import Callable, Optional, Dict, Any, Tuple, Union
from functools import wraps
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request, Response
from AllConfigs.Token.config import Auth
from ApiLibrary.common.line_number import get_line_number_for_error
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
from .base_context import BaseContext
from ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
class AuthContext(BaseContext):
"""
Context class for authentication middleware.
Extends BaseContext to provide authentication-specific functionality.
"""
def __init__(
self, token_context: Union[OccupantTokenObject, EmployeeTokenObject]
) -> None:
super().__init__()
self.token_context = token_context
@property
def is_employee(self) -> bool:
"""Check if authenticated token is for an employee."""
return isinstance(self.token_context, EmployeeTokenObject)
@property
def is_occupant(self) -> bool:
"""Check if authenticated token is for an occupant."""
return isinstance(self.token_context, OccupantTokenObject)
@property
def user_id(self) -> str:
"""Get the user's UUID from token context."""
return self.token_context.user_uu_id if self.token_context else ""
def __repr__(self) -> str:
user_type = "Employee" if self.is_employee else "Occupant"
return f"AuthContext({user_type}Token: {self.user_id})"
class MiddlewareModule:
"""
Module containing authentication and middleware functionality.
This class provides:
- Token extraction and validation
- Authentication decorator for endpoints
Middleware module for handling authentication and request timing.
"""
@staticmethod
def get_access_token(request: Request) -> Tuple[str, str]:
def get_user_from_request(
request: Request,
) -> AuthContext:
"""
Extract access token from request headers.
Get authenticated token context from request.
Args:
request: FastAPI request object
Returns:
Tuple[str, str]: A tuple containing (scheme, token)
AuthContext: Context containing the authenticated token data
Raises:
HTTPExceptionApi: If token is missing or malformed
HTTPExceptionApi: If token is missing, invalid, or user not found
"""
auth_header = request.headers.get(Auth.ACCESS_TOKEN_TAG)
if not auth_header:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
from ApiServices.Token.token_handler import TokenService
try:
scheme, token = auth_header.split()
if scheme.lower() != "bearer":
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
return scheme, token
except ValueError:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
# Get token and validate - will raise HTTPExceptionApi if invalid
redis_token = TokenService.get_access_token_from_request(request=request)
@staticmethod
async def validate_token(token: str) -> Dict[str, Any]:
"""
Validate the authentication token.
# Get token context - will validate token and raise appropriate errors
token_context = TokenService.get_object_via_access_key(access_token=redis_token)
if not token_context:
raise HTTPExceptionApi(
error_code="USER_NOT_FOUND", lang="tr", loc=get_line_number_for_error()
)
Args:
token: JWT token to validate
Returns:
Dict[str, Any]: User data extracted from token
Raises:
HTTPExceptionApi: If token is invalid
"""
try:
# TODO: Implement your token validation logic
# Example:
# return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
return {"user_id": "test", "role": "user"} # Placeholder
except Exception as e:
raise HTTPExceptionApi(error_code="HTTP_401_UNAUTHORIZED", lang="tr")
return AuthContext(token_context=token_context)
@classmethod
def auth_required(cls, func: Callable) -> Callable:
@@ -80,30 +93,36 @@ class MiddlewareModule:
@router.get("/protected")
@MiddlewareModule.auth_required
async def protected_endpoint(request: Request):
user = request.state.user # Access authenticated user data
return {"message": "Protected content"}
@router.get("/public") # No decorator = public endpoint
async def public_endpoint():
return {"message": "Public content"}
auth = protected_endpoint.auth # Access auth context
if auth.is_employee:
# Handle employee logic
employee_id = auth.token_context.employee_id
else:
# Handle occupant logic
occupant_id = auth.token_context.occupant_id
return {"user_id": auth.user_id}
Args:
func: The FastAPI route handler function to protect
Returns:
Callable: Wrapped function that checks authentication before execution
Raises:
HTTPExceptionApi: If authentication fails
"""
@wraps(func)
def wrapper(request: Request, *args, **kwargs):
from ApiServices import TokenService
# Get token from header
# token = TokenService.get_access_token_from_request(request=request)
# print(token)
# if not token:
# raise HTTPExceptionApi(error_code="NOT_AUTHORIZED", lang="tr")
# Get and validate token context from request
auth_context = cls.get_user_from_request(request)
# Attach auth context to function
func.auth = auth_context
# Call the original endpoint function
return func(request, *args, **kwargs)
return wrapper

View File

@@ -0,0 +1,47 @@
"""Base context for middleware."""
from typing import Optional, Dict, Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from ApiServices.Token.token_handler import OccupantTokenObject, EmployeeTokenObject
class BaseContext:
"""Base context class for middleware."""
def __init__(self) -> None:
self._token_context: Optional[
Union["OccupantTokenObject", "EmployeeTokenObject"]
] = None
self._function_code: Optional[str] = None
@property
def token_context(
self,
) -> Optional[Union["OccupantTokenObject", "EmployeeTokenObject"]]:
"""Get token context if available."""
return self._token_context
@token_context.setter
def token_context(
self, value: Union["OccupantTokenObject", "EmployeeTokenObject"]
) -> None:
"""Set token context."""
self._token_context = value
@property
def function_code(self) -> Optional[str]:
"""Get function code if available."""
return self._function_code
@function_code.setter
def function_code(self, value: str) -> None:
"""Set function code."""
self._function_code = value
def to_dict(self) -> Dict[str, Any]:
"""Convert context to dictionary."""
return {
"token_context": self._token_context,
"function_code": self._function_code,
}

View File

@@ -0,0 +1,76 @@
"""
Token event middleware for handling authentication and event tracking.
"""
from functools import wraps
from typing import Callable, Dict, Any
from .auth_middleware import MiddlewareModule
from .base_context import BaseContext
class TokenEventHandler(BaseContext):
"""Handler for token events with authentication context."""
def __init__(self, func: Callable, url_of_endpoint: str):
"""Initialize the handler with function and URL."""
super().__init__()
self.func = func
self.url_of_endpoint = url_of_endpoint
def update_context(self, function_code: str):
"""Update the event context with function code."""
self.function_code = function_code
class TokenEventMiddleware:
"""
Module containing token and event handling functionality.
This class provides:
- Token and event context management
- Event validation decorator for endpoints
"""
@staticmethod
def event_required(
func: Callable[..., Dict[str, Any]]
) -> Callable[..., Dict[str, Any]]:
"""
Decorator for endpoints with token and event requirements.
This decorator:
1. First validates authentication using MiddlewareModule.auth_required
2. Then adds event tracking context
Args:
func: The function to be decorated
Returns:
Callable: The wrapped function with both auth and event handling
"""
# First apply authentication
authenticated_func = MiddlewareModule.auth_required(func)
@wraps(authenticated_func)
def wrapper(*args, **kwargs) -> Dict[str, Any]:
# Create handler with context
handler = TokenEventHandler(
func=authenticated_func,
url_of_endpoint=authenticated_func.url_of_endpoint,
)
# Update event-specific context
handler.update_context(
function_code="7192c2aa-5352-4e36-98b3-dafb7d036a3d" # Keep function_code as URL
)
# Copy auth context from authenticated function
if hasattr(authenticated_func, "auth"):
handler.token_context = authenticated_func.auth.token_context
# Make handler available to the function
authenticated_func.handler = handler
# Call the authenticated function
return authenticated_func(*args, **kwargs)
return wrapper