auth service completed and tested
This commit is contained in:
parent
08b1815156
commit
486fadbfb3
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""
|
||||||
|
FastAPI Application Entry Point
|
||||||
|
|
||||||
|
This module initializes and configures the FastAPI application with:
|
||||||
|
- CORS middleware for cross-origin requests
|
||||||
|
- Request timing middleware for performance monitoring
|
||||||
|
- Custom exception handlers for consistent error responses
|
||||||
|
- Prometheus instrumentation for metrics
|
||||||
|
- API routers for endpoint organization
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
import routers
|
||||||
|
|
||||||
|
from create_file import create_app
|
||||||
|
from prometheus_fastapi_instrumentator import Instrumentator
|
||||||
|
from app_handler import setup_middleware, get_uvicorn_config
|
||||||
|
|
||||||
|
|
||||||
|
print("Loading app.py module...")
|
||||||
|
|
||||||
|
# Initialize FastAPI application
|
||||||
|
app = create_app(routers=routers)
|
||||||
|
|
||||||
|
# Setup Prometheus metrics
|
||||||
|
Instrumentator().instrument(app=app).expose(app=app)
|
||||||
|
|
||||||
|
# Configure middleware and exception handlers
|
||||||
|
setup_middleware(app)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Starting server from __main__...")
|
||||||
|
# Run the application with Uvicorn
|
||||||
|
uvicorn_config = get_uvicorn_config()
|
||||||
|
print(f"Using config: {uvicorn_config}")
|
||||||
|
uvicorn.Server(uvicorn.Config(**uvicorn_config)).run()
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
"""
|
||||||
|
FastAPI Application Handler Module
|
||||||
|
|
||||||
|
This module contains all the handler functions for configuring and setting up the FastAPI application:
|
||||||
|
- CORS middleware configuration
|
||||||
|
- Exception handlers setup
|
||||||
|
- Uvicorn server configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi import FastAPI, Request, HTTPException, status
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from ErrorHandlers.bases import (
|
||||||
|
BaseErrorModelClass,
|
||||||
|
StatusesModelClass,
|
||||||
|
LanguageModelClass,
|
||||||
|
)
|
||||||
|
from ErrorHandlers import statuses
|
||||||
|
from middleware.auth_middleware import MiddlewareModule
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cors_middleware(app: FastAPI) -> None:
|
||||||
|
"""
|
||||||
|
Configure CORS middleware for the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
"""
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
Handle HTTP exceptions and return formatted error responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: FastAPI request object
|
||||||
|
exc: HTTP exception instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSONResponse: Formatted error response
|
||||||
|
"""
|
||||||
|
error_code = getattr(exc, "error_code", None)
|
||||||
|
if error_code:
|
||||||
|
status_code = StatusesModelClass.retrieve_error_by_code(error_code)
|
||||||
|
error_message = LanguageModelClass.retrieve_error_by_code(
|
||||||
|
error_code, request.headers.get("accept-language", "en")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status_code = exc.status_code
|
||||||
|
error_message = str(exc.detail)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status_code,
|
||||||
|
content={"detail": error_message, "error_code": error_code},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
Handle generic exceptions and return formatted error responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: FastAPI request object
|
||||||
|
exc: Exception instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSONResponse: Formatted error response
|
||||||
|
"""
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content={"detail": "Internal server error", "error_code": "INTERNAL_ERROR"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_exception_handlers(app: FastAPI) -> None:
|
||||||
|
"""
|
||||||
|
Configure custom exception handlers for the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
"""
|
||||||
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||||
|
app.add_exception_handler(Exception, generic_exception_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_middleware(app: FastAPI) -> None:
|
||||||
|
"""
|
||||||
|
Configure all middleware for the application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
"""
|
||||||
|
setup_cors_middleware(app)
|
||||||
|
app.add_middleware(MiddlewareModule.RequestTimingMiddleware)
|
||||||
|
setup_exception_handlers(app)
|
||||||
|
|
||||||
|
|
||||||
|
def get_uvicorn_config() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get Uvicorn server configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Uvicorn configuration dictionary
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"app": "app:app",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 41575,
|
||||||
|
"log_level": "info",
|
||||||
|
"reload": True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""
|
||||||
|
Base FastAPI application configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI(title="API Service")
|
||||||
|
|
||||||
|
# Configure CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""
|
||||||
|
FastAPI Application Factory Module
|
||||||
|
|
||||||
|
This module provides functionality to create and configure a FastAPI application with:
|
||||||
|
- Custom OpenAPI schema configuration
|
||||||
|
- Security scheme configuration for Bearer authentication
|
||||||
|
- Automatic router registration
|
||||||
|
- Response class configuration
|
||||||
|
- Security requirements for protected endpoints
|
||||||
|
"""
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from fastapi import FastAPI, APIRouter
|
||||||
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from AllConfigs.main import MainConfig as Config
|
||||||
|
from middleware.auth_middleware import MiddlewareModule
|
||||||
|
|
||||||
|
|
||||||
|
def setup_security_schema() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Configure security schema for the OpenAPI documentation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Security schema configuration
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"Bearer": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
"description": "Enter the token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def configure_route_security(
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
protected_paths: List[str]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Configure security requirements for a specific route.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Route path
|
||||||
|
method: HTTP method
|
||||||
|
schema: OpenAPI schema to modify
|
||||||
|
protected_paths: List of paths that require authentication
|
||||||
|
"""
|
||||||
|
if path in protected_paths:
|
||||||
|
if "paths" not in schema:
|
||||||
|
schema["paths"] = {}
|
||||||
|
|
||||||
|
if path not in schema["paths"]:
|
||||||
|
schema["paths"][path] = {}
|
||||||
|
|
||||||
|
if method not in schema["paths"][path]:
|
||||||
|
schema["paths"][path][method] = {}
|
||||||
|
|
||||||
|
schema["paths"][path][method]["security"] = [{"Bearer": []}]
|
||||||
|
|
||||||
|
|
||||||
|
def get_routers(routers_module: ModuleType) -> List[APIRouter]:
|
||||||
|
"""
|
||||||
|
Extract all routers from the routers module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
routers_module: Module containing router definitions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[APIRouter]: List of router instances
|
||||||
|
"""
|
||||||
|
routers = []
|
||||||
|
for attr_name in dir(routers_module):
|
||||||
|
attr = getattr(routers_module, attr_name)
|
||||||
|
if isinstance(attr, APIRouter):
|
||||||
|
routers.append(attr)
|
||||||
|
return routers
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(routers: ModuleType) -> FastAPI:
|
||||||
|
"""
|
||||||
|
Create and configure a FastAPI application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
routers: Module containing router definitions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FastAPI: Configured FastAPI application instance
|
||||||
|
"""
|
||||||
|
# Initialize FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title=Config.TITLE,
|
||||||
|
description=Config.DESCRIPTION,
|
||||||
|
default_response_class=JSONResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add home route that redirects to API documentation
|
||||||
|
@app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION))
|
||||||
|
async def home() -> RedirectResponse:
|
||||||
|
"""Redirect root path to API documentation."""
|
||||||
|
return RedirectResponse(url="/docs")
|
||||||
|
|
||||||
|
# Get all routers
|
||||||
|
router_instances = get_routers(routers)
|
||||||
|
|
||||||
|
# Find protected paths
|
||||||
|
protected_paths = []
|
||||||
|
for router in router_instances:
|
||||||
|
for route in router.routes:
|
||||||
|
if isinstance(route, APIRoute):
|
||||||
|
# Check if the route has auth_required decorator
|
||||||
|
if any(d.__name__ == 'auth_required' for d in route.dependencies):
|
||||||
|
protected_paths.append(route.path)
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
for router in router_instances:
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
# Configure custom OpenAPI schema
|
||||||
|
def custom_openapi():
|
||||||
|
if app.openapi_schema:
|
||||||
|
return app.openapi_schema
|
||||||
|
|
||||||
|
openapi_schema = get_openapi(
|
||||||
|
title=Config.TITLE,
|
||||||
|
version="1.0.0",
|
||||||
|
description=Config.DESCRIPTION,
|
||||||
|
routes=app.routes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add security schemes
|
||||||
|
openapi_schema["components"] = {"securitySchemes": setup_security_schema()}
|
||||||
|
|
||||||
|
# Configure security for each route
|
||||||
|
for route in app.routes:
|
||||||
|
if isinstance(route, APIRoute):
|
||||||
|
configure_route_security(
|
||||||
|
route.path,
|
||||||
|
route.methods.pop().lower(),
|
||||||
|
openapi_schema,
|
||||||
|
protected_paths
|
||||||
|
)
|
||||||
|
|
||||||
|
app.openapi_schema = openapi_schema
|
||||||
|
return app.openapi_schema
|
||||||
|
|
||||||
|
app.openapi = custom_openapi
|
||||||
|
return app
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
"""
|
||||||
|
Authentication and Authorization middleware for FastAPI applications.
|
||||||
|
|
||||||
|
This module provides authentication decorator for protecting endpoints
|
||||||
|
and a middleware for request timing measurements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from time import perf_counter
|
||||||
|
from typing import Callable, Optional, Dict, Any, Tuple
|
||||||
|
from functools import wraps
|
||||||
|
from fastapi import HTTPException, Request, Response, status
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from AllConfigs.Token.config import Auth
|
||||||
|
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareModule:
|
||||||
|
"""
|
||||||
|
Module containing authentication and middleware functionality.
|
||||||
|
|
||||||
|
This class provides:
|
||||||
|
- Token extraction and validation
|
||||||
|
- Authentication decorator for endpoints
|
||||||
|
- Request timing middleware
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_access_token(request: Request) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Extract access token from request headers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: FastAPI request object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, str]: A tuple containing (scheme, token)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPExceptionApi: If token is missing or malformed
|
||||||
|
"""
|
||||||
|
auth_header = request.headers.get(Auth.ACCESS_TOKEN_TAG)
|
||||||
|
if not auth_header:
|
||||||
|
raise HTTPExceptionApi(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="No authorization header",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scheme, token = auth_header.split()
|
||||||
|
if scheme.lower() != "bearer":
|
||||||
|
raise HTTPExceptionApi(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid authentication scheme",
|
||||||
|
)
|
||||||
|
return scheme, token
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPExceptionApi(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token format"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def validate_token(token: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate the authentication token.
|
||||||
|
|
||||||
|
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(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=f"Token validation failed: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def auth_required(cls, func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
Decorator for protecting FastAPI endpoints with authentication.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@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"}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: The FastAPI route handler function to protect
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable: Wrapped function that checks authentication before execution
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(request: Request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
# Get token from header
|
||||||
|
_, token = cls.get_access_token(request)
|
||||||
|
|
||||||
|
# Validate token and get user data
|
||||||
|
token_data = await cls.validate_token(token)
|
||||||
|
|
||||||
|
# Add user data to request state for use in endpoint
|
||||||
|
request.state.user = token_data
|
||||||
|
|
||||||
|
# Call the original endpoint function
|
||||||
|
return await func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
except HTTPExceptionApi:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPExceptionApi(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=f"Authentication failed: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class RequestTimingMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware for measuring and logging request timing.
|
||||||
|
Only handles timing, no authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||||
|
"""
|
||||||
|
Process each request through the middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: FastAPI request object
|
||||||
|
call_next: Next middleware in the chain
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: Processed response with timing headers
|
||||||
|
"""
|
||||||
|
start_time = perf_counter()
|
||||||
|
|
||||||
|
# Process the request
|
||||||
|
response = await call_next(request)
|
||||||
|
|
||||||
|
# Add timing information to response headers
|
||||||
|
self._add_timing_headers(response, start_time)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_timing_headers(response: Response, start_time: float) -> None:
|
||||||
|
"""
|
||||||
|
Add request timing information to response headers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: FastAPI response object
|
||||||
|
start_time: Time when request processing started
|
||||||
|
"""
|
||||||
|
end_time = perf_counter()
|
||||||
|
elapsed = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||||
|
|
||||||
|
response.headers.update(
|
||||||
|
{
|
||||||
|
"request-start": f"{start_time:.6f}",
|
||||||
|
"request-end": f"{end_time:.6f}",
|
||||||
|
"request-duration": f"{elapsed:.2f}ms",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
"""
|
||||||
|
OpenAPI Schema Creator Module
|
||||||
|
|
||||||
|
This module provides functionality to create and customize OpenAPI documentation:
|
||||||
|
- Custom security schemes (Bearer Auth, API Key)
|
||||||
|
- Response schemas and examples
|
||||||
|
- Tag management and descriptions
|
||||||
|
- Error responses and validation
|
||||||
|
- Custom documentation extensions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Set
|
||||||
|
from fastapi import FastAPI, APIRouter
|
||||||
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
from AllConfigs.main import MainConfig as Config
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAPISchemaCreator:
|
||||||
|
"""
|
||||||
|
OpenAPI schema creator and customizer for FastAPI applications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: FastAPI):
|
||||||
|
"""
|
||||||
|
Initialize the OpenAPI schema creator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
"""
|
||||||
|
self.app = app
|
||||||
|
self.protected_paths: Set[str] = set()
|
||||||
|
self.tags_metadata = self._create_tags_metadata()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_tags_metadata() -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Create metadata for API tags.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, str]]: List of tag metadata
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "Authentication",
|
||||||
|
"description": "Operations related to user authentication and authorization",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Users",
|
||||||
|
"description": "User management and profile operations",
|
||||||
|
},
|
||||||
|
# Add more tags as needed
|
||||||
|
]
|
||||||
|
|
||||||
|
def _create_security_schemes(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create security scheme definitions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Security scheme configurations
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"Bearer Auth": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "evyos-session-key",
|
||||||
|
"description": "Enter: **'Bearer <JWT>'**, where JWT is the access token",
|
||||||
|
},
|
||||||
|
"API Key": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "X-API-Key",
|
||||||
|
"description": "API key for service authentication",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_common_responses(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create common response schemas.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Common response configurations
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Authentication failed or not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"detail": "Invalid authentication credentials",
|
||||||
|
"error_code": "INVALID_CREDENTIALS",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden - Insufficient permissions",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/HTTPValidationError"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/HTTPValidationError"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"detail": "Internal server error occurred",
|
||||||
|
"error_code": "INTERNAL_ERROR",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure_route_security(
|
||||||
|
self, path: str, method: str, schema: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Configure security requirements for a specific route.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Route path
|
||||||
|
method: HTTP method
|
||||||
|
schema: OpenAPI schema to modify
|
||||||
|
"""
|
||||||
|
if path not in Config.INSECURE_PATHS:
|
||||||
|
schema["paths"][path][method]["security"] = [
|
||||||
|
{"Bearer Auth": []},
|
||||||
|
{"API Key": []},
|
||||||
|
]
|
||||||
|
schema["paths"][path][method]["responses"].update(
|
||||||
|
self._create_common_responses()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_schema(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create the complete OpenAPI schema.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Complete OpenAPI schema
|
||||||
|
"""
|
||||||
|
openapi_schema = get_openapi(
|
||||||
|
title=Config.TITLE,
|
||||||
|
description=Config.DESCRIPTION,
|
||||||
|
version="1.0.0",
|
||||||
|
routes=self.app.routes,
|
||||||
|
tags=self.tags_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add security schemes
|
||||||
|
if "components" not in openapi_schema:
|
||||||
|
openapi_schema["components"] = {}
|
||||||
|
|
||||||
|
openapi_schema["components"]["securitySchemes"] = self._create_security_schemes()
|
||||||
|
|
||||||
|
# Configure route security and responses
|
||||||
|
for route in self.app.routes:
|
||||||
|
if isinstance(route, APIRoute) and route.include_in_schema:
|
||||||
|
path = str(route.path)
|
||||||
|
methods = [method.lower() for method in route.methods]
|
||||||
|
|
||||||
|
for method in methods:
|
||||||
|
self.configure_route_security(path, method, openapi_schema)
|
||||||
|
|
||||||
|
# Add custom documentation extensions
|
||||||
|
openapi_schema["x-documentation"] = {
|
||||||
|
"postman_collection": "/docs/postman",
|
||||||
|
"swagger_ui": "/docs",
|
||||||
|
"redoc": "/redoc",
|
||||||
|
}
|
||||||
|
|
||||||
|
return openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def create_openapi_schema(app: FastAPI) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Create OpenAPI schema for a FastAPI application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Complete OpenAPI schema
|
||||||
|
"""
|
||||||
|
creator = OpenAPISchemaCreator(app)
|
||||||
|
return creator.create_schema()
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from .base_router import test_route
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"test_route"
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""
|
||||||
|
Base router configuration and setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from middleware.auth_middleware import MiddlewareModule
|
||||||
|
|
||||||
|
# Create test router
|
||||||
|
test_route = APIRouter(prefix="/test", tags=["Test"])
|
||||||
|
|
||||||
|
@test_route.get("/health")
|
||||||
|
@MiddlewareModule.auth_required
|
||||||
|
async def health_check(request: Request):
|
||||||
|
return {"status": "healthy", "message": "Service is running"}
|
||||||
|
|
||||||
|
@test_route.get("/ping")
|
||||||
|
async def ping_test():
|
||||||
|
return {"ping": "pong", "service": "base-router"}
|
||||||
|
|
||||||
|
# Initialize and include test routes
|
||||||
|
def init_test_routes():
|
||||||
|
return test_route
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy all required directories
|
||||||
|
COPY DockerApiServices/AllApiNeeds /app/
|
||||||
|
COPY ApiLibrary /app/ApiLibrary
|
||||||
|
COPY ApiValidations /app/ApiValidations
|
||||||
|
COPY AllConfigs /app/AllConfigs
|
||||||
|
COPY ErrorHandlers /app/ErrorHandlers
|
||||||
|
COPY Schemas /app/Schemas
|
||||||
|
COPY Services /app/Services
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY DockerApiServices/requirements.txt /app/
|
||||||
|
RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set Python path to include app directory
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
# Run the application using the configured uvicorn server
|
||||||
|
CMD ["python", "app.py"]
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0.post1
|
||||||
|
pydantic==2.10.5
|
||||||
|
sqlalchemy==2.0.37
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
motor==3.3.2
|
||||||
|
redis==5.2.1
|
||||||
|
pytest==7.4.4
|
||||||
|
pytest-asyncio==0.21.2
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
coverage==7.6.10
|
||||||
|
arrow==1.3.0
|
||||||
|
redmail==0.6.0
|
||||||
|
sqlalchemy-mixins==2.0.5
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install poetry && \
|
||||||
|
poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-dev
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "EventServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Docker Services Guide
|
||||||
|
|
||||||
|
This repository contains multiple microservices that can be run using Docker Compose.
|
||||||
|
|
||||||
|
## Quick Start (With Cache)
|
||||||
|
For regular development when dependencies haven't changed:
|
||||||
|
```bash
|
||||||
|
# Build and run Auth Service
|
||||||
|
docker compose -f ../docker-compose-services.yml up auth-service
|
||||||
|
|
||||||
|
# Build and run Event Service
|
||||||
|
docker compose -f ../docker-compose-services.yml up event-service
|
||||||
|
|
||||||
|
# Build and run Validation Service
|
||||||
|
docker compose -f ../docker-compose-services.yml up validation-service
|
||||||
|
|
||||||
|
# Build and run all services
|
||||||
|
docker compose -f ../docker-compose-services.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clean Build (No Cache)
|
||||||
|
Use these commands when changing Dockerfile or dependencies:
|
||||||
|
```bash
|
||||||
|
# Auth Service
|
||||||
|
docker compose -f ../docker-compose-services.yml build --no-cache auth-service && docker compose -f ../docker-compose-services.yml up auth-service
|
||||||
|
|
||||||
|
# Event Service
|
||||||
|
docker compose -f ../docker-compose-services.yml build --no-cache event-service && docker compose -f ../docker-compose-services.yml up event-service
|
||||||
|
|
||||||
|
# Validation Service
|
||||||
|
docker compose -f ../docker-compose-services.yml build --no-cache validation-service && docker compose -f ../docker-compose-services.yml up validation-service
|
||||||
|
|
||||||
|
# All Services
|
||||||
|
docker compose -f ../docker-compose-services.yml build --no-cache && docker compose -f ../docker-compose-services.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Ports
|
||||||
|
- Auth Service: `http://localhost:8000`
|
||||||
|
- `/test/health` - Protected health check endpoint (requires authentication)
|
||||||
|
- `/test/ping` - Public ping endpoint
|
||||||
|
- Event Service: `http://localhost:8001`
|
||||||
|
- Validation Service: `http://localhost:8002`
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
- Use clean build (--no-cache) when:
|
||||||
|
- Changing Dockerfile
|
||||||
|
- Updating dependencies
|
||||||
|
- Experiencing caching issues
|
||||||
|
- Use regular build (with cache) when:
|
||||||
|
- Only changing application code
|
||||||
|
- For faster development iterations
|
||||||
|
- Run in detached mode:
|
||||||
|
```bash
|
||||||
|
docker compose -f ../docker-compose-services.yml up -d auth-service
|
||||||
|
```
|
||||||
|
- Stop services:
|
||||||
|
```bash
|
||||||
|
docker compose -f ../docker-compose-services.yml down
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY pyproject.toml .
|
||||||
|
RUN pip install poetry && \
|
||||||
|
poetry config virtualenvs.create false && \
|
||||||
|
poetry install --no-dev
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "ValidationServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "wag-management-api-services"
|
||||||
|
version = "0.1.1"
|
||||||
|
description = "WAG Management API Service"
|
||||||
|
authors = ["Karatay Berkay <karatay.berkay@evyos.com.tr>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
# FastAPI and Web
|
||||||
|
fastapi = "^0.104.1"
|
||||||
|
uvicorn = "^0.24.0"
|
||||||
|
pydantic = "^2.5.2"
|
||||||
|
|
||||||
|
# MongoDB
|
||||||
|
motor = "3.3.2" # Pinned version
|
||||||
|
pymongo = "4.5.0" # Pinned version to match motor
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
sqlalchemy = "^2.0.23"
|
||||||
|
sqlalchemy-mixins = "^2.0.5"
|
||||||
|
psycopg2-binary = "^2.9.9"
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis = "^5.0.1"
|
||||||
|
arrow = "^1.3.0"
|
||||||
|
|
||||||
|
# Email
|
||||||
|
redmail = "^0.6.0"
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
pytest = "^7.4.3"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
|
pytest-cov = "^4.1.0"
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
python-dateutil = "^2.8.2"
|
||||||
|
typing-extensions = "^4.8.0"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^23.11.0"
|
||||||
|
isort = "^5.12.0"
|
||||||
|
mypy = "^1.7.1"
|
||||||
|
flake8 = "^6.1.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ['py39']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
line_length = 88
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.9"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
addopts = "-ra -q --cov=Services"
|
||||||
|
testpaths = [
|
||||||
|
"Ztest",
|
||||||
|
]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0.post1
|
||||||
|
pydantic==2.10.5
|
||||||
|
sqlalchemy==2.0.37
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
motor==3.3.2
|
||||||
|
redis==5.2.1
|
||||||
|
pytest==7.4.4
|
||||||
|
pytest-asyncio==0.21.2
|
||||||
|
pytest-cov==4.1.0
|
||||||
|
coverage==7.6.10
|
||||||
|
arrow==1.3.0
|
||||||
|
redmail==0.6.0
|
||||||
|
sqlalchemy-mixins==2.0.5
|
||||||
|
prometheus-client==0.19.0
|
||||||
|
prometheus-fastapi-instrumentator==6.1.0
|
||||||
|
|
@ -1,12 +1,29 @@
|
||||||
What to do with services?
|
WAG Management API Microservices Setup
|
||||||
|
|
||||||
*
|
1. Authentication Service (Port 8000)
|
||||||
|
- User authentication and authorization
|
||||||
|
- JWT token management
|
||||||
|
- Role-based access control
|
||||||
|
- Uses PostgreSQL for user data
|
||||||
|
|
||||||
*
|
2. Event Service (Port 8001)
|
||||||
|
- Event processing and handling
|
||||||
|
- Message queue integration
|
||||||
|
- Real-time notifications
|
||||||
|
- Uses MongoDB for event storage
|
||||||
|
|
||||||
*
|
3. Validation Service (Port 8002)
|
||||||
|
- Request validation
|
||||||
|
- Data sanitization
|
||||||
|
- Schema validation
|
||||||
|
- Uses Redis for caching
|
||||||
|
|
||||||
*
|
To run the services:
|
||||||
|
```bash
|
||||||
*
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Access services at:
|
||||||
|
- Auth Service: http://localhost:8000
|
||||||
|
- Event Service: http://localhost:8001
|
||||||
|
- Validation Service: http://localhost:8002
|
||||||
|
|
|
||||||
85
README.md
85
README.md
|
|
@ -1,3 +1,84 @@
|
||||||
# wag-managment-api-service-version-4
|
# WAG Management API Service v4
|
||||||
|
|
||||||
wag managment api service version 4
|
This service provides a comprehensive API for managing WAG (Wide Area Gateway) systems. It handles configuration, monitoring, and control operations for WAG devices in the network infrastructure.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
To run the tests using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.test.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Core Services and Components
|
||||||
|
|
||||||
|
- `Services/` - Core service implementations
|
||||||
|
- `PostgresDb/` - PostgreSQL database operations and models
|
||||||
|
- `MongoDb/` - MongoDB operations and document models
|
||||||
|
- `Redis/` - Redis caching and session management
|
||||||
|
- `Email/` - Email notification service
|
||||||
|
|
||||||
|
- `ApiValidations/` - Request validation and data sanitization
|
||||||
|
- Input validation rules
|
||||||
|
- Data sanitization filters
|
||||||
|
- Schema validation middleware
|
||||||
|
|
||||||
|
- `ApiLibrary/` - Common utilities and helper functions
|
||||||
|
- Shared functions and utilities
|
||||||
|
- Common constants and configurations
|
||||||
|
- Helper classes and decorators
|
||||||
|
|
||||||
|
### Configuration and Settings
|
||||||
|
|
||||||
|
- `AllConfigs/` - Configuration management
|
||||||
|
- Database configurations
|
||||||
|
- Service settings
|
||||||
|
- Environment-specific configs
|
||||||
|
|
||||||
|
- `Schemas/` - Data models and schema definitions
|
||||||
|
- Request/Response models
|
||||||
|
- Database schemas
|
||||||
|
- API contract definitions
|
||||||
|
|
||||||
|
### Docker and Deployment
|
||||||
|
|
||||||
|
- `DockerApiServices/` - API service Docker configurations
|
||||||
|
- API service Dockerfile
|
||||||
|
- Service dependencies
|
||||||
|
|
||||||
|
- `DockerStoreServices/` - Storage service Docker configurations
|
||||||
|
- Database service Dockerfiles
|
||||||
|
- Storage service dependencies
|
||||||
|
|
||||||
|
### Error Handling and Events
|
||||||
|
|
||||||
|
- `ErrorHandlers/` - Error handling and exception management
|
||||||
|
- Custom exceptions
|
||||||
|
- Error handlers
|
||||||
|
- Exception middleware
|
||||||
|
|
||||||
|
- `ApiEvents/` - Event handling and processing
|
||||||
|
- Event listeners
|
||||||
|
- Event dispatchers
|
||||||
|
- Message queue handlers
|
||||||
|
|
||||||
|
### Language and Testing
|
||||||
|
|
||||||
|
- `LanguageModels/` - Localization and language support
|
||||||
|
- Language files
|
||||||
|
- Translation models
|
||||||
|
- i18n configurations
|
||||||
|
|
||||||
|
- `Ztest/` - Test suite
|
||||||
|
- Unit tests
|
||||||
|
- Integration tests
|
||||||
|
- Test fixtures and utilities
|
||||||
|
|
||||||
|
### Additional Components
|
||||||
|
|
||||||
|
- `scripts/` - Utility scripts and tools
|
||||||
|
- Deployment scripts
|
||||||
|
- Database migrations
|
||||||
|
- Maintenance utilities
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,21 @@ class PyObjectId(ObjectId):
|
||||||
"""Define the core schema for PyObjectId."""
|
"""Define the core schema for PyObjectId."""
|
||||||
return core_schema.json_or_python_schema(
|
return core_schema.json_or_python_schema(
|
||||||
json_schema=core_schema.str_schema(),
|
json_schema=core_schema.str_schema(),
|
||||||
python_schema=core_schema.union_schema([
|
python_schema=core_schema.union_schema(
|
||||||
core_schema.is_instance_schema(ObjectId),
|
[
|
||||||
core_schema.chain_schema([
|
core_schema.is_instance_schema(ObjectId),
|
||||||
core_schema.str_schema(),
|
core_schema.chain_schema(
|
||||||
core_schema.no_info_plain_validator_function(cls.validate),
|
[
|
||||||
]),
|
core_schema.str_schema(),
|
||||||
]),
|
core_schema.no_info_plain_validator_function(cls.validate),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||||
lambda x: str(x),
|
lambda x: str(x),
|
||||||
return_schema=core_schema.str_schema(),
|
return_schema=core_schema.str_schema(),
|
||||||
when_used='json',
|
when_used="json",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,7 +65,7 @@ class MongoBaseModel(BaseModel):
|
||||||
populate_by_name=True,
|
populate_by_name=True,
|
||||||
from_attributes=True,
|
from_attributes=True,
|
||||||
validate_assignment=True,
|
validate_assignment=True,
|
||||||
extra='allow'
|
extra="allow",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Optional _id field that will be ignored in create operations
|
# Optional _id field that will be ignored in create operations
|
||||||
|
|
@ -94,18 +98,18 @@ class MongoDocument(MongoBaseModel):
|
||||||
created_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
created_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||||
updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def prevent_protected_fields(cls, data: Any) -> Any:
|
def prevent_protected_fields(cls, data: Any) -> Any:
|
||||||
"""Prevent user from setting protected fields like _id and timestamps."""
|
"""Prevent user from setting protected fields like _id and timestamps."""
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
# Remove protected fields from input
|
# Remove protected fields from input
|
||||||
data.pop('_id', None)
|
data.pop("_id", None)
|
||||||
data.pop('created_at', None)
|
data.pop("created_at", None)
|
||||||
data.pop('updated_at', None)
|
data.pop("updated_at", None)
|
||||||
|
|
||||||
# Set timestamps
|
# Set timestamps
|
||||||
data['created_at'] = system_arrow.now().timestamp()
|
data["created_at"] = system_arrow.now().timestamp()
|
||||||
data['updated_at'] = system_arrow.now().timestamp()
|
data["updated_at"] = system_arrow.now().timestamp()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ including domain history and access details.
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from pydantic import Field, model_validator
|
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||||
|
|
||||||
from ApiLibrary import system_arrow
|
from ApiLibrary import system_arrow
|
||||||
from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument
|
from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument
|
||||||
|
|
@ -26,19 +26,17 @@ class DomainData(MongoBaseModel):
|
||||||
user_uu_id: str = Field(..., description="User's unique identifier")
|
user_uu_id: str = Field(..., description="User's unique identifier")
|
||||||
main_domain: str = Field(..., description="Primary domain")
|
main_domain: str = Field(..., description="Primary domain")
|
||||||
other_domains_list: List[str] = Field(
|
other_domains_list: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list, description="List of additional domains"
|
||||||
description="List of additional domains"
|
|
||||||
)
|
)
|
||||||
extra_data: Optional[Dict[str, Any]] = Field(
|
extra_data: Optional[Dict[str, Any]] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
alias="extraData",
|
alias="extraData",
|
||||||
description="Additional domain-related data"
|
description="Additional domain-related data",
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(
|
||||||
from_attributes = True
|
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||||
populate_by_name = True
|
)
|
||||||
validate_assignment = True
|
|
||||||
|
|
||||||
|
|
||||||
class DomainDocument(MongoDocument):
|
class DomainDocument(MongoDocument):
|
||||||
|
|
@ -60,19 +58,19 @@ class DomainDocument(MongoDocument):
|
||||||
|
|
||||||
class DomainDocumentCreate(MongoDocument):
|
class DomainDocumentCreate(MongoDocument):
|
||||||
"""Model for creating new domain documents."""
|
"""Model for creating new domain documents."""
|
||||||
|
|
||||||
data: DomainData = Field(..., description="Initial domain data")
|
data: DomainData = Field(..., description="Initial domain data")
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(
|
||||||
from_attributes = True
|
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||||
populate_by_name = True
|
)
|
||||||
validate_assignment = True
|
|
||||||
|
|
||||||
|
|
||||||
class DomainDocumentUpdate(MongoDocument):
|
class DomainDocumentUpdate(MongoDocument):
|
||||||
"""Model for updating existing domain documents."""
|
"""Model for updating existing domain documents."""
|
||||||
|
|
||||||
data: DomainData = Field(..., description="Updated domain data")
|
data: DomainData = Field(..., description="Updated domain data")
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(
|
||||||
from_attributes = True
|
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||||
populate_by_name = True
|
)
|
||||||
validate_assignment = True
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,7 @@ class PasswordHistoryData(MongoBaseModel):
|
||||||
|
|
||||||
password_history: List[str] = Field([], alias="passwordHistory")
|
password_history: List[str] = Field([], alias="passwordHistory")
|
||||||
access_history_detail: Dict[str, PasswordHistoryDetail] = Field(
|
access_history_detail: Dict[str, PasswordHistoryDetail] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict, alias="accessHistoryDetail"
|
||||||
alias="accessHistoryDetail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class MongoActions(
|
||||||
MongoInsertMixin,
|
MongoInsertMixin,
|
||||||
MongoFindMixin,
|
MongoFindMixin,
|
||||||
MongoDeleteMixin,
|
MongoDeleteMixin,
|
||||||
MongoAggregateMixin
|
MongoAggregateMixin,
|
||||||
):
|
):
|
||||||
"""Main MongoDB actions class that inherits all CRUD operation mixins.
|
"""Main MongoDB actions class that inherits all CRUD operation mixins.
|
||||||
|
|
||||||
|
|
@ -39,11 +39,7 @@ class MongoActions(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, client: MongoClient, database: str, company_uuid: str, storage_reason: str
|
||||||
client: MongoClient,
|
|
||||||
database: str,
|
|
||||||
company_uuid: str,
|
|
||||||
storage_reason: str
|
|
||||||
):
|
):
|
||||||
"""Initialize MongoDB actions with client and collection info.
|
"""Initialize MongoDB actions with client and collection info.
|
||||||
|
|
||||||
|
|
@ -82,7 +78,9 @@ class MongoActions(
|
||||||
"""Insert multiple documents."""
|
"""Insert multiple documents."""
|
||||||
return super().insert_many(self.collection, documents)
|
return super().insert_many(self.collection, documents)
|
||||||
|
|
||||||
def find_one(self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None):
|
def find_one(
|
||||||
|
self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
"""Find a single document."""
|
"""Find a single document."""
|
||||||
return super().find_one(self.collection, filter_query, projection)
|
return super().find_one(self.collection, filter_query, projection)
|
||||||
|
|
||||||
|
|
@ -96,12 +94,7 @@ class MongoActions(
|
||||||
):
|
):
|
||||||
"""Find multiple documents."""
|
"""Find multiple documents."""
|
||||||
return super().find_many(
|
return super().find_many(
|
||||||
self.collection,
|
self.collection, filter_query, projection, sort, limit, skip
|
||||||
filter_query,
|
|
||||||
projection,
|
|
||||||
sort,
|
|
||||||
limit,
|
|
||||||
skip
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_one(
|
def update_one(
|
||||||
|
|
@ -111,12 +104,7 @@ class MongoActions(
|
||||||
upsert: bool = False,
|
upsert: bool = False,
|
||||||
):
|
):
|
||||||
"""Update a single document."""
|
"""Update a single document."""
|
||||||
return super().update_one(
|
return super().update_one(self.collection, filter_query, update_data, upsert)
|
||||||
self.collection,
|
|
||||||
filter_query,
|
|
||||||
update_data,
|
|
||||||
upsert
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_many(
|
def update_many(
|
||||||
self,
|
self,
|
||||||
|
|
@ -125,12 +113,7 @@ class MongoActions(
|
||||||
upsert: bool = False,
|
upsert: bool = False,
|
||||||
):
|
):
|
||||||
"""Update multiple documents."""
|
"""Update multiple documents."""
|
||||||
return super().update_many(
|
return super().update_many(self.collection, filter_query, update_data, upsert)
|
||||||
self.collection,
|
|
||||||
filter_query,
|
|
||||||
update_data,
|
|
||||||
upsert
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete_one(self, filter_query: Dict[str, Any]):
|
def delete_one(self, filter_query: Dict[str, Any]):
|
||||||
"""Delete a single document."""
|
"""Delete a single document."""
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from Services.MongoDb.Models.exceptions import (
|
||||||
PasswordHistoryError,
|
PasswordHistoryError,
|
||||||
PasswordReuseError,
|
PasswordReuseError,
|
||||||
PasswordHistoryLimitError,
|
PasswordHistoryLimitError,
|
||||||
InvalidPasswordDetailError
|
InvalidPasswordDetailError,
|
||||||
)
|
)
|
||||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||||
|
|
||||||
|
|
@ -33,13 +33,13 @@ def handle_mongo_errors(func: Callable) -> Callable:
|
||||||
Returns:
|
Returns:
|
||||||
Wrapped function with error handling
|
Wrapped function with error handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs) -> Any:
|
async def wrapper(*args, **kwargs) -> Any:
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except ConnectionFailure as e:
|
except ConnectionFailure as e:
|
||||||
raise MongoConnectionError(
|
raise MongoConnectionError(
|
||||||
message=str(e),
|
message=str(e), details={"error_type": "connection_failure"}
|
||||||
details={"error_type": "connection_failure"}
|
|
||||||
).to_http_exception()
|
).to_http_exception()
|
||||||
except DuplicateKeyError as e:
|
except DuplicateKeyError as e:
|
||||||
raise MongoDuplicateKeyError(
|
raise MongoDuplicateKeyError(
|
||||||
|
|
@ -48,20 +48,19 @@ def handle_mongo_errors(func: Callable) -> Callable:
|
||||||
).to_http_exception()
|
).to_http_exception()
|
||||||
except PyMongoError as e:
|
except PyMongoError as e:
|
||||||
raise MongoBaseException(
|
raise MongoBaseException(
|
||||||
message=str(e),
|
message=str(e), details={"error_type": "pymongo_error"}
|
||||||
details={"error_type": "pymongo_error"}
|
|
||||||
).to_http_exception()
|
).to_http_exception()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPExceptionApi(
|
raise HTTPExceptionApi(
|
||||||
lang="en",
|
lang="en",
|
||||||
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
async def mongo_base_exception_handler(
|
async def mongo_base_exception_handler(
|
||||||
request: Request,
|
request: Request, exc: MongoBaseException
|
||||||
exc: MongoBaseException
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle base MongoDB exceptions.
|
"""Handle base MongoDB exceptions.
|
||||||
|
|
||||||
|
|
@ -73,14 +72,12 @@ async def mongo_base_exception_handler(
|
||||||
JSON response with error details
|
JSON response with error details
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||||
content={"error": exc.to_http_exception()}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mongo_connection_error_handler(
|
async def mongo_connection_error_handler(
|
||||||
request: Request,
|
request: Request, exc: MongoConnectionError
|
||||||
exc: MongoConnectionError
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle MongoDB connection errors.
|
"""Handle MongoDB connection errors.
|
||||||
|
|
||||||
|
|
@ -93,13 +90,12 @@ async def mongo_connection_error_handler(
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
content={"error": exc.to_http_exception()}
|
content={"error": exc.to_http_exception()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mongo_document_not_found_handler(
|
async def mongo_document_not_found_handler(
|
||||||
request: Request,
|
request: Request, exc: MongoDocumentNotFoundError
|
||||||
exc: MongoDocumentNotFoundError
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle document not found errors.
|
"""Handle document not found errors.
|
||||||
|
|
||||||
|
|
@ -112,13 +108,12 @@ async def mongo_document_not_found_handler(
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
content={"error": exc.to_http_exception()}
|
content={"error": exc.to_http_exception()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mongo_validation_error_handler(
|
async def mongo_validation_error_handler(
|
||||||
request: Request,
|
request: Request, exc: MongoValidationError
|
||||||
exc: MongoValidationError
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle validation errors.
|
"""Handle validation errors.
|
||||||
|
|
||||||
|
|
@ -131,13 +126,12 @@ async def mongo_validation_error_handler(
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
content={"error": exc.to_http_exception()}
|
content={"error": exc.to_http_exception()},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mongo_duplicate_key_error_handler(
|
async def mongo_duplicate_key_error_handler(
|
||||||
request: Request,
|
request: Request, exc: MongoDuplicateKeyError
|
||||||
exc: MongoDuplicateKeyError
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle duplicate key errors.
|
"""Handle duplicate key errors.
|
||||||
|
|
||||||
|
|
@ -149,14 +143,12 @@ async def mongo_duplicate_key_error_handler(
|
||||||
JSON response with duplicate key error details
|
JSON response with duplicate key error details
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT, content={"error": exc.to_http_exception()}
|
||||||
content={"error": exc.to_http_exception()}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def password_history_error_handler(
|
async def password_history_error_handler(
|
||||||
request: Request,
|
request: Request, exc: PasswordHistoryError
|
||||||
exc: PasswordHistoryError
|
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""Handle password history errors.
|
"""Handle password history errors.
|
||||||
|
|
||||||
|
|
@ -168,8 +160,7 @@ async def password_history_error_handler(
|
||||||
JSON response with password history error details
|
JSON response with password history error details
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||||
content={"error": exc.to_http_exception()}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,10 +172,14 @@ def register_exception_handlers(app: Any) -> None:
|
||||||
"""
|
"""
|
||||||
app.add_exception_handler(MongoBaseException, mongo_base_exception_handler)
|
app.add_exception_handler(MongoBaseException, mongo_base_exception_handler)
|
||||||
app.add_exception_handler(MongoConnectionError, mongo_connection_error_handler)
|
app.add_exception_handler(MongoConnectionError, mongo_connection_error_handler)
|
||||||
app.add_exception_handler(MongoDocumentNotFoundError, mongo_document_not_found_handler)
|
app.add_exception_handler(
|
||||||
|
MongoDocumentNotFoundError, mongo_document_not_found_handler
|
||||||
|
)
|
||||||
app.add_exception_handler(MongoValidationError, mongo_validation_error_handler)
|
app.add_exception_handler(MongoValidationError, mongo_validation_error_handler)
|
||||||
app.add_exception_handler(MongoDuplicateKeyError, mongo_duplicate_key_error_handler)
|
app.add_exception_handler(MongoDuplicateKeyError, mongo_duplicate_key_error_handler)
|
||||||
app.add_exception_handler(PasswordHistoryError, password_history_error_handler)
|
app.add_exception_handler(PasswordHistoryError, password_history_error_handler)
|
||||||
app.add_exception_handler(PasswordReuseError, password_history_error_handler)
|
app.add_exception_handler(PasswordReuseError, password_history_error_handler)
|
||||||
app.add_exception_handler(PasswordHistoryLimitError, password_history_error_handler)
|
app.add_exception_handler(PasswordHistoryLimitError, password_history_error_handler)
|
||||||
app.add_exception_handler(InvalidPasswordDetailError, password_history_error_handler)
|
app.add_exception_handler(
|
||||||
|
InvalidPasswordDetailError, password_history_error_handler
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class MongoBaseException(Exception):
|
||||||
self,
|
self,
|
||||||
message: str,
|
message: str,
|
||||||
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
details: Optional[Dict[str, Any]] = None
|
details: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
self.message = message
|
self.message = message
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
|
@ -38,12 +38,12 @@ class MongoConnectionError(MongoBaseException):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: str = "Failed to connect to MongoDB",
|
message: str = "Failed to connect to MongoDB",
|
||||||
details: Optional[Dict[str, Any]] = None
|
details: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
details=details
|
details=details,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,31 +54,24 @@ class MongoDocumentNotFoundError(MongoBaseException):
|
||||||
self,
|
self,
|
||||||
collection: str,
|
collection: str,
|
||||||
filter_query: Dict[str, Any],
|
filter_query: Dict[str, Any],
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None,
|
||||||
):
|
):
|
||||||
message = message or f"Document not found in collection '{collection}'"
|
message = message or f"Document not found in collection '{collection}'"
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
details={
|
details={"collection": collection, "filter": filter_query},
|
||||||
"collection": collection,
|
|
||||||
"filter": filter_query
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MongoValidationError(MongoBaseException):
|
class MongoValidationError(MongoBaseException):
|
||||||
"""Raised when document validation fails."""
|
"""Raised when document validation fails."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
field_errors: Optional[Dict[str, str]] = None
|
|
||||||
):
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
details={"field_errors": field_errors or {}}
|
details={"field_errors": field_errors or {}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -89,16 +82,13 @@ class MongoDuplicateKeyError(MongoBaseException):
|
||||||
self,
|
self,
|
||||||
collection: str,
|
collection: str,
|
||||||
key_pattern: Dict[str, Any],
|
key_pattern: Dict[str, Any],
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None,
|
||||||
):
|
):
|
||||||
message = message or f"Duplicate key error in collection '{collection}'"
|
message = message or f"Duplicate key error in collection '{collection}'"
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
details={
|
details={"collection": collection, "key_pattern": key_pattern},
|
||||||
"collection": collection,
|
|
||||||
"key_pattern": key_pattern
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,7 +99,7 @@ class PasswordHistoryError(MongoBaseException):
|
||||||
self,
|
self,
|
||||||
message: str,
|
message: str,
|
||||||
status_code: int = status.HTTP_400_BAD_REQUEST,
|
status_code: int = status.HTTP_400_BAD_REQUEST,
|
||||||
details: Optional[Dict[str, Any]] = None
|
details: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
super().__init__(message, status_code, details)
|
super().__init__(message, status_code, details)
|
||||||
|
|
||||||
|
|
@ -120,42 +110,34 @@ class PasswordReuseError(PasswordHistoryError):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: str = "Password was used recently",
|
message: str = "Password was used recently",
|
||||||
history_limit: Optional[int] = None
|
history_limit: Optional[int] = None,
|
||||||
):
|
):
|
||||||
details = {"history_limit": history_limit} if history_limit else None
|
details = {"history_limit": history_limit} if history_limit else None
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
details=details
|
details=details,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PasswordHistoryLimitError(PasswordHistoryError):
|
class PasswordHistoryLimitError(PasswordHistoryError):
|
||||||
"""Raised when password history limit is reached."""
|
"""Raised when password history limit is reached."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, limit: int, message: Optional[str] = None):
|
||||||
self,
|
|
||||||
limit: int,
|
|
||||||
message: Optional[str] = None
|
|
||||||
):
|
|
||||||
message = message or f"Password history limit of {limit} reached"
|
message = message or f"Password history limit of {limit} reached"
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
details={"limit": limit}
|
details={"limit": limit},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidPasswordDetailError(PasswordHistoryError):
|
class InvalidPasswordDetailError(PasswordHistoryError):
|
||||||
"""Raised when password history detail is invalid."""
|
"""Raised when password history detail is invalid."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
field_errors: Optional[Dict[str, str]] = None
|
|
||||||
):
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
message=message,
|
message=message,
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
details={"field_errors": field_errors or {}}
|
details={"field_errors": field_errors or {}},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,25 +34,16 @@ def handle_mongo_errors(func):
|
||||||
try:
|
try:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
except ConnectionFailure:
|
except ConnectionFailure:
|
||||||
raise HTTPExceptionApi(
|
raise HTTPExceptionApi(error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en")
|
||||||
error_code="HTTP_503_SERVICE_UNAVAILABLE",
|
|
||||||
lang="en"
|
|
||||||
)
|
|
||||||
except ServerSelectionTimeoutError:
|
except ServerSelectionTimeoutError:
|
||||||
raise HTTPExceptionApi(
|
raise HTTPExceptionApi(error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en")
|
||||||
error_code="HTTP_504_GATEWAY_TIMEOUT",
|
|
||||||
lang="en"
|
|
||||||
)
|
|
||||||
except OperationFailure as e:
|
except OperationFailure as e:
|
||||||
raise HTTPExceptionApi(
|
raise HTTPExceptionApi(error_code="HTTP_400_BAD_REQUEST", lang="en")
|
||||||
error_code="HTTP_400_BAD_REQUEST",
|
|
||||||
lang="en"
|
|
||||||
)
|
|
||||||
except PyMongoError as e:
|
except PyMongoError as e:
|
||||||
raise HTTPExceptionApi(
|
raise HTTPExceptionApi(
|
||||||
error_code="HTTP_500_INTERNAL_SERVER_ERROR",
|
error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en"
|
||||||
lang="en"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,15 @@ class MongoFindMixin:
|
||||||
class MongoUpdateMixin:
|
class MongoUpdateMixin:
|
||||||
"""Mixin for MongoDB update operations."""
|
"""Mixin for MongoDB update operations."""
|
||||||
|
|
||||||
def update_one(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult:
|
def update_one(
|
||||||
|
self, filter_query: Dict[str, Any], update: Dict[str, Any]
|
||||||
|
) -> UpdateResult:
|
||||||
"""Update a single document."""
|
"""Update a single document."""
|
||||||
return self.collection.update_one(filter_query, update)
|
return self.collection.update_one(filter_query, update)
|
||||||
|
|
||||||
def update_many(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult:
|
def update_many(
|
||||||
|
self, filter_query: Dict[str, Any], update: Dict[str, Any]
|
||||||
|
) -> UpdateResult:
|
||||||
"""Update multiple documents."""
|
"""Update multiple documents."""
|
||||||
return self.collection.update_many(filter_query, update)
|
return self.collection.update_many(filter_query, update)
|
||||||
|
|
||||||
|
|
@ -114,7 +118,7 @@ class MongoDBHandler(
|
||||||
self._client = MongoClient(**connection_kwargs)
|
self._client = MongoClient(**connection_kwargs)
|
||||||
|
|
||||||
# Test connection
|
# Test connection
|
||||||
self._client.admin.command('ping')
|
self._client.admin.command("ping")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close MongoDB connection."""
|
"""Close MongoDB connection."""
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ from functools import lru_cache
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session, Session
|
|
||||||
|
|
||||||
from AllConfigs.SqlDatabase.configs import WagDatabase
|
from AllConfigs.SqlDatabase.configs import WagDatabase
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Test fixtures and models."""
|
||||||
|
|
||||||
|
from sqlalchemy import Column, String
|
||||||
|
|
||||||
|
from Services.PostgresDb.Models.mixins import CrudCollection
|
||||||
|
|
||||||
|
|
||||||
|
class TestUser(CrudCollection):
|
||||||
|
"""Test user model for PostgreSQL tests."""
|
||||||
|
|
||||||
|
__tablename__ = "test_users"
|
||||||
|
username = Column(String, unique=True, index=True)
|
||||||
|
email = Column(String, unique=True, index=True)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Test models."""
|
||||||
|
|
||||||
|
from sqlalchemy import Column, String
|
||||||
|
|
||||||
|
from Services.PostgresDb.Models.mixins import CrudCollection
|
||||||
|
|
||||||
|
|
||||||
|
class UserModel(CrudCollection):
|
||||||
|
"""User model for PostgreSQL tests."""
|
||||||
|
|
||||||
|
__tablename__ = "test_users"
|
||||||
|
username = Column(String, unique=True, index=True)
|
||||||
|
email = Column(String, unique=True, index=True)
|
||||||
|
|
@ -7,7 +7,7 @@ from Services.MongoDb.Models.actions import MongoActions
|
||||||
from Services.MongoDb.Models.action_models.domain import (
|
from Services.MongoDb.Models.action_models.domain import (
|
||||||
DomainData,
|
DomainData,
|
||||||
DomainDocumentCreate,
|
DomainDocumentCreate,
|
||||||
DomainDocumentUpdate
|
DomainDocumentUpdate,
|
||||||
)
|
)
|
||||||
from AllConfigs.NoSqlDatabase.configs import MongoConfig
|
from AllConfigs.NoSqlDatabase.configs import MongoConfig
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ def mongo_client():
|
||||||
"""Create MongoDB test client."""
|
"""Create MongoDB test client."""
|
||||||
# Connect using configured credentials
|
# Connect using configured credentials
|
||||||
client = MongoClient(MongoConfig.URL)
|
client = MongoClient(MongoConfig.URL)
|
||||||
client.admin.command('ping') # Test connection
|
client.admin.command("ping") # Test connection
|
||||||
yield client
|
yield client
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ def mongo_actions(mongo_client):
|
||||||
client=mongo_client,
|
client=mongo_client,
|
||||||
database=MongoConfig.DATABASE_NAME,
|
database=MongoConfig.DATABASE_NAME,
|
||||||
company_uuid="test_company",
|
company_uuid="test_company",
|
||||||
storage_reason="domains"
|
storage_reason="domains",
|
||||||
)
|
)
|
||||||
yield actions
|
yield actions
|
||||||
try:
|
try:
|
||||||
|
|
@ -50,7 +50,7 @@ def test_mongo_crud_operations(mongo_actions: MongoActions):
|
||||||
domain_data = DomainData(
|
domain_data = DomainData(
|
||||||
user_uu_id="test_user",
|
user_uu_id="test_user",
|
||||||
main_domain="example.com",
|
main_domain="example.com",
|
||||||
other_domains_list=["old.com"]
|
other_domains_list=["old.com"],
|
||||||
)
|
)
|
||||||
create_doc = DomainDocumentCreate(data=domain_data)
|
create_doc = DomainDocumentCreate(data=domain_data)
|
||||||
|
|
||||||
|
|
@ -67,12 +67,11 @@ def test_mongo_crud_operations(mongo_actions: MongoActions):
|
||||||
update_data = DomainData(
|
update_data = DomainData(
|
||||||
user_uu_id="test_user",
|
user_uu_id="test_user",
|
||||||
main_domain="new.com",
|
main_domain="new.com",
|
||||||
other_domains_list=["example.com", "old.com"]
|
other_domains_list=["example.com", "old.com"],
|
||||||
)
|
)
|
||||||
update_doc = DomainDocumentUpdate(data=update_data)
|
update_doc = DomainDocumentUpdate(data=update_data)
|
||||||
result = mongo_actions.update_one(
|
result = mongo_actions.update_one(
|
||||||
{"_id": doc["_id"]},
|
{"_id": doc["_id"]}, {"$set": update_doc.model_dump()}
|
||||||
{"$set": update_doc.model_dump()}
|
|
||||||
)
|
)
|
||||||
assert result.modified_count == 1
|
assert result.modified_count == 1
|
||||||
|
|
||||||
|
|
@ -87,19 +86,14 @@ def test_mongo_aggregate(mongo_actions: MongoActions):
|
||||||
# Insert test documents
|
# Insert test documents
|
||||||
docs = [
|
docs = [
|
||||||
DomainDocumentCreate(
|
DomainDocumentCreate(
|
||||||
data=DomainData(
|
data=DomainData(user_uu_id="user1", main_domain=f"domain{i}.com")
|
||||||
user_uu_id="user1",
|
|
||||||
main_domain=f"domain{i}.com"
|
|
||||||
)
|
|
||||||
).model_dump()
|
).model_dump()
|
||||||
for i in range(3)
|
for i in range(3)
|
||||||
]
|
]
|
||||||
mongo_actions.insert_many(docs)
|
mongo_actions.insert_many(docs)
|
||||||
|
|
||||||
# Test aggregation
|
# Test aggregation
|
||||||
pipeline = [
|
pipeline = [{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}]
|
||||||
{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}
|
|
||||||
]
|
|
||||||
result = mongo_actions.aggregate(pipeline)
|
result = mongo_actions.aggregate(pipeline)
|
||||||
result_list = list(result)
|
result_list = list(result)
|
||||||
assert len(result_list) == 1
|
assert len(result_list) == 1
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
"""Test PostgreSQL database operations."""
|
"""Test PostgreSQL database operations."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import Column, String, create_engine, text
|
from sqlalchemy import create_engine, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from Services.PostgresDb.database import Base, get_db
|
from Services.PostgresDb.database import Base, get_db
|
||||||
from Services.PostgresDb.Models.mixins import CrudCollection
|
|
||||||
from AllConfigs.SqlDatabase.configs import WagDatabase
|
from AllConfigs.SqlDatabase.configs import WagDatabase
|
||||||
|
from Ztest.models import UserModel
|
||||||
|
|
||||||
class TestUser(CrudCollection):
|
|
||||||
"""Test user model for PostgreSQL tests."""
|
|
||||||
|
|
||||||
__tablename__ = "test_users"
|
|
||||||
|
|
||||||
username = Column(String, unique=True, index=True)
|
|
||||||
email = Column(String, unique=True, index=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|
@ -52,12 +43,12 @@ def db_session(db_engine):
|
||||||
def test_create_user(db_session):
|
def test_create_user(db_session):
|
||||||
"""Test creating a user in the database."""
|
"""Test creating a user in the database."""
|
||||||
# Create user using CrudMixin methods
|
# Create user using CrudMixin methods
|
||||||
user = TestUser(username="testuser", email="test@example.com")
|
user = UserModel(username="testuser", email="test@example.com")
|
||||||
db_session.add(user)
|
db_session.add(user)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Verify user was created
|
# Verify user was created
|
||||||
db_user = db_session.query(TestUser).filter_by(username="testuser").first()
|
db_user = db_session.query(UserModel).filter_by(username="testuser").first()
|
||||||
assert db_user is not None
|
assert db_user is not None
|
||||||
assert db_user.email == "test@example.com"
|
assert db_user.email == "test@example.com"
|
||||||
assert db_user.created_at is not None
|
assert db_user.created_at is not None
|
||||||
|
|
@ -68,16 +59,16 @@ def test_create_user(db_session):
|
||||||
def test_update_user(db_session):
|
def test_update_user(db_session):
|
||||||
"""Test updating a user in the database."""
|
"""Test updating a user in the database."""
|
||||||
# Create user
|
# Create user
|
||||||
user = TestUser(username="updateuser", email="update@example.com")
|
user = UserModel(username="updateuser", email="update@example.com")
|
||||||
db_session.add(user)
|
db_session.add(user)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Update user using CrudMixin methods
|
# Update user using CrudMixin methods
|
||||||
user.update(session=db_session, email="newemail@example.com")
|
user.update(db=db_session, email="newemail@example.com")
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Verify update
|
# Verify update
|
||||||
updated_user = db_session.query(TestUser).filter_by(username="updateuser").first()
|
updated_user = db_session.query(UserModel).filter_by(username="updateuser").first()
|
||||||
assert updated_user.email == "newemail@example.com"
|
assert updated_user.email == "newemail@example.com"
|
||||||
assert updated_user.updated_at is not None
|
assert updated_user.updated_at is not None
|
||||||
|
|
||||||
|
|
@ -85,16 +76,16 @@ def test_update_user(db_session):
|
||||||
def test_soft_delete_user(db_session):
|
def test_soft_delete_user(db_session):
|
||||||
"""Test soft deleting a user from the database."""
|
"""Test soft deleting a user from the database."""
|
||||||
# Create user
|
# Create user
|
||||||
user = TestUser(username="deleteuser", email="delete@example.com")
|
user = UserModel(username="deleteuser", email="delete@example.com")
|
||||||
db_session.add(user)
|
db_session.add(user)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Soft delete by updating deleted and active flags
|
# Soft delete by updating deleted and active flags
|
||||||
user.update(session=db_session, deleted=True, active=False)
|
user.update(db=db_session, deleted=True, active=False)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Verify soft deletion
|
# Verify soft deletion
|
||||||
deleted_user = db_session.query(TestUser).filter_by(username="deleteuser").first()
|
deleted_user = db_session.query(UserModel).filter_by(username="deleteuser").first()
|
||||||
assert deleted_user is not None
|
assert deleted_user is not None
|
||||||
assert deleted_user.deleted
|
assert deleted_user.deleted
|
||||||
assert not deleted_user.active
|
assert not deleted_user.active
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
auth-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: DockerApiServices/AuthServiceApi/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "41575:41575"
|
||||||
|
|
||||||
|
event-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: DockerApiServices/EventServiceApi/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8001:8000"
|
||||||
|
|
||||||
|
validation-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8002:8000"
|
||||||
|
# and lets try to implement potry again in the dockerfile now we now that it is about copy of files
|
||||||
Loading…
Reference in New Issue