auth service completed and tested

This commit is contained in:
berkay 2025-01-14 19:16:24 +03:00
parent 08b1815156
commit 486fadbfb3
33 changed files with 1325 additions and 248 deletions

View File

@ -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()

View File

@ -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,
}

View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@

View File

@ -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",
}
)

View File

@ -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()

View File

@ -0,0 +1,5 @@
from .base_router import test_route
__all__ = [
"test_route"
]

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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"]

View File

@ -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
```

View File

@ -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"]

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -21,17 +21,21 @@ class PyObjectId(ObjectId):
"""Define the core schema for PyObjectId."""
return core_schema.json_or_python_schema(
json_schema=core_schema.str_schema(),
python_schema=core_schema.union_schema([
core_schema.is_instance_schema(ObjectId),
core_schema.chain_schema([
core_schema.str_schema(),
core_schema.no_info_plain_validator_function(cls.validate),
]),
]),
python_schema=core_schema.union_schema(
[
core_schema.is_instance_schema(ObjectId),
core_schema.chain_schema(
[
core_schema.str_schema(),
core_schema.no_info_plain_validator_function(cls.validate),
]
),
]
),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda x: str(x),
return_schema=core_schema.str_schema(),
when_used='json',
when_used="json",
),
)
@ -61,7 +65,7 @@ class MongoBaseModel(BaseModel):
populate_by_name=True,
from_attributes=True,
validate_assignment=True,
extra='allow'
extra="allow",
)
# 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())
updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
@model_validator(mode='before')
@model_validator(mode="before")
@classmethod
def prevent_protected_fields(cls, data: Any) -> Any:
"""Prevent user from setting protected fields like _id and timestamps."""
if isinstance(data, dict):
# Remove protected fields from input
data.pop('_id', None)
data.pop('created_at', None)
data.pop('updated_at', None)
data.pop("_id", None)
data.pop("created_at", None)
data.pop("updated_at", None)
# Set timestamps
data['created_at'] = system_arrow.now().timestamp()
data['updated_at'] = system_arrow.now().timestamp()
data["created_at"] = system_arrow.now().timestamp()
data["updated_at"] = system_arrow.now().timestamp()
return data

View File

@ -7,7 +7,7 @@ including domain history and access details.
from datetime import datetime
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 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")
main_domain: str = Field(..., description="Primary domain")
other_domains_list: List[str] = Field(
default_factory=list,
description="List of additional domains"
default_factory=list, description="List of additional domains"
)
extra_data: Optional[Dict[str, Any]] = Field(
default_factory=dict,
alias="extraData",
description="Additional domain-related data"
description="Additional domain-related data",
)
class Config:
from_attributes = True
populate_by_name = True
validate_assignment = True
model_config = ConfigDict(
from_attributes=True, populate_by_name=True, validate_assignment=True
)
class DomainDocument(MongoDocument):
@ -60,19 +58,19 @@ class DomainDocument(MongoDocument):
class DomainDocumentCreate(MongoDocument):
"""Model for creating new domain documents."""
data: DomainData = Field(..., description="Initial domain data")
class Config:
from_attributes = True
populate_by_name = True
validate_assignment = True
model_config = ConfigDict(
from_attributes=True, populate_by_name=True, validate_assignment=True
)
class DomainDocumentUpdate(MongoDocument):
"""Model for updating existing domain documents."""
data: DomainData = Field(..., description="Updated domain data")
class Config:
from_attributes = True
populate_by_name = True
validate_assignment = True
model_config = ConfigDict(
from_attributes=True, populate_by_name=True, validate_assignment=True
)

View File

@ -27,8 +27,7 @@ class PasswordHistoryData(MongoBaseModel):
password_history: List[str] = Field([], alias="passwordHistory")
access_history_detail: Dict[str, PasswordHistoryDetail] = Field(
default_factory=dict,
alias="accessHistoryDetail"
default_factory=dict, alias="accessHistoryDetail"
)

View File

@ -30,7 +30,7 @@ class MongoActions(
MongoInsertMixin,
MongoFindMixin,
MongoDeleteMixin,
MongoAggregateMixin
MongoAggregateMixin,
):
"""Main MongoDB actions class that inherits all CRUD operation mixins.
@ -39,11 +39,7 @@ class MongoActions(
"""
def __init__(
self,
client: MongoClient,
database: str,
company_uuid: str,
storage_reason: str
self, client: MongoClient, database: str, company_uuid: str, storage_reason: str
):
"""Initialize MongoDB actions with client and collection info.
@ -82,7 +78,9 @@ class MongoActions(
"""Insert multiple 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."""
return super().find_one(self.collection, filter_query, projection)
@ -96,12 +94,7 @@ class MongoActions(
):
"""Find multiple documents."""
return super().find_many(
self.collection,
filter_query,
projection,
sort,
limit,
skip
self.collection, filter_query, projection, sort, limit, skip
)
def update_one(
@ -111,12 +104,7 @@ class MongoActions(
upsert: bool = False,
):
"""Update a single document."""
return super().update_one(
self.collection,
filter_query,
update_data,
upsert
)
return super().update_one(self.collection, filter_query, update_data, upsert)
def update_many(
self,
@ -125,12 +113,7 @@ class MongoActions(
upsert: bool = False,
):
"""Update multiple documents."""
return super().update_many(
self.collection,
filter_query,
update_data,
upsert
)
return super().update_many(self.collection, filter_query, update_data, upsert)
def delete_one(self, filter_query: Dict[str, Any]):
"""Delete a single document."""

View File

@ -19,7 +19,7 @@ from Services.MongoDb.Models.exceptions import (
PasswordHistoryError,
PasswordReuseError,
PasswordHistoryLimitError,
InvalidPasswordDetailError
InvalidPasswordDetailError,
)
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
@ -33,13 +33,13 @@ def handle_mongo_errors(func: Callable) -> Callable:
Returns:
Wrapped function with error handling
"""
async def wrapper(*args, **kwargs) -> Any:
try:
return await func(*args, **kwargs)
except ConnectionFailure as e:
raise MongoConnectionError(
message=str(e),
details={"error_type": "connection_failure"}
message=str(e), details={"error_type": "connection_failure"}
).to_http_exception()
except DuplicateKeyError as e:
raise MongoDuplicateKeyError(
@ -48,20 +48,19 @@ def handle_mongo_errors(func: Callable) -> Callable:
).to_http_exception()
except PyMongoError as e:
raise MongoBaseException(
message=str(e),
details={"error_type": "pymongo_error"}
message=str(e), details={"error_type": "pymongo_error"}
).to_http_exception()
except Exception as e:
raise HTTPExceptionApi(
lang="en",
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return wrapper
async def mongo_base_exception_handler(
request: Request,
exc: MongoBaseException
request: Request, exc: MongoBaseException
) -> JSONResponse:
"""Handle base MongoDB exceptions.
@ -73,14 +72,12 @@ async def mongo_base_exception_handler(
JSON response with error details
"""
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.to_http_exception()}
status_code=exc.status_code, content={"error": exc.to_http_exception()}
)
async def mongo_connection_error_handler(
request: Request,
exc: MongoConnectionError
request: Request, exc: MongoConnectionError
) -> JSONResponse:
"""Handle MongoDB connection errors.
@ -93,13 +90,12 @@ async def mongo_connection_error_handler(
"""
return JSONResponse(
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(
request: Request,
exc: MongoDocumentNotFoundError
request: Request, exc: MongoDocumentNotFoundError
) -> JSONResponse:
"""Handle document not found errors.
@ -112,13 +108,12 @@ async def mongo_document_not_found_handler(
"""
return JSONResponse(
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(
request: Request,
exc: MongoValidationError
request: Request, exc: MongoValidationError
) -> JSONResponse:
"""Handle validation errors.
@ -131,13 +126,12 @@ async def mongo_validation_error_handler(
"""
return JSONResponse(
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(
request: Request,
exc: MongoDuplicateKeyError
request: Request, exc: MongoDuplicateKeyError
) -> JSONResponse:
"""Handle duplicate key errors.
@ -149,14 +143,12 @@ async def mongo_duplicate_key_error_handler(
JSON response with duplicate key error details
"""
return JSONResponse(
status_code=status.HTTP_409_CONFLICT,
content={"error": exc.to_http_exception()}
status_code=status.HTTP_409_CONFLICT, content={"error": exc.to_http_exception()}
)
async def password_history_error_handler(
request: Request,
exc: PasswordHistoryError
request: Request, exc: PasswordHistoryError
) -> JSONResponse:
"""Handle password history errors.
@ -168,8 +160,7 @@ async def password_history_error_handler(
JSON response with password history error details
"""
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.to_http_exception()}
status_code=exc.status_code, 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(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(MongoDuplicateKeyError, mongo_duplicate_key_error_handler)
app.add_exception_handler(PasswordHistoryError, 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(InvalidPasswordDetailError, password_history_error_handler)
app.add_exception_handler(
InvalidPasswordDetailError, password_history_error_handler
)

View File

@ -17,7 +17,7 @@ class MongoBaseException(Exception):
self,
message: str,
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.status_code = status_code
@ -38,12 +38,12 @@ class MongoConnectionError(MongoBaseException):
def __init__(
self,
message: str = "Failed to connect to MongoDB",
details: Optional[Dict[str, Any]] = None
details: Optional[Dict[str, Any]] = None,
):
super().__init__(
message=message,
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
details=details
details=details,
)
@ -54,31 +54,24 @@ class MongoDocumentNotFoundError(MongoBaseException):
self,
collection: str,
filter_query: Dict[str, Any],
message: Optional[str] = None
message: Optional[str] = None,
):
message = message or f"Document not found in collection '{collection}'"
super().__init__(
message=message,
status_code=status.HTTP_404_NOT_FOUND,
details={
"collection": collection,
"filter": filter_query
}
details={"collection": collection, "filter": filter_query},
)
class MongoValidationError(MongoBaseException):
"""Raised when document validation fails."""
def __init__(
self,
message: str,
field_errors: Optional[Dict[str, str]] = None
):
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
super().__init__(
message=message,
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,
collection: str,
key_pattern: Dict[str, Any],
message: Optional[str] = None
message: Optional[str] = None,
):
message = message or f"Duplicate key error in collection '{collection}'"
super().__init__(
message=message,
status_code=status.HTTP_409_CONFLICT,
details={
"collection": collection,
"key_pattern": key_pattern
}
details={"collection": collection, "key_pattern": key_pattern},
)
@ -109,7 +99,7 @@ class PasswordHistoryError(MongoBaseException):
self,
message: str,
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)
@ -120,42 +110,34 @@ class PasswordReuseError(PasswordHistoryError):
def __init__(
self,
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
super().__init__(
message=message,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
details=details
details=details,
)
class PasswordHistoryLimitError(PasswordHistoryError):
"""Raised when password history limit is reached."""
def __init__(
self,
limit: int,
message: Optional[str] = None
):
def __init__(self, limit: int, message: Optional[str] = None):
message = message or f"Password history limit of {limit} reached"
super().__init__(
message=message,
status_code=status.HTTP_409_CONFLICT,
details={"limit": limit}
details={"limit": limit},
)
class InvalidPasswordDetailError(PasswordHistoryError):
"""Raised when password history detail is invalid."""
def __init__(
self,
message: str,
field_errors: Optional[Dict[str, str]] = None
):
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
super().__init__(
message=message,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
details={"field_errors": field_errors or {}}
details={"field_errors": field_errors or {}},
)

View File

@ -34,25 +34,16 @@ def handle_mongo_errors(func):
try:
return func(*args, **kwargs)
except ConnectionFailure:
raise HTTPExceptionApi(
error_code="HTTP_503_SERVICE_UNAVAILABLE",
lang="en"
)
raise HTTPExceptionApi(error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en")
except ServerSelectionTimeoutError:
raise HTTPExceptionApi(
error_code="HTTP_504_GATEWAY_TIMEOUT",
lang="en"
)
raise HTTPExceptionApi(error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en")
except OperationFailure as e:
raise HTTPExceptionApi(
error_code="HTTP_400_BAD_REQUEST",
lang="en"
)
raise HTTPExceptionApi(error_code="HTTP_400_BAD_REQUEST", lang="en")
except PyMongoError as e:
raise HTTPExceptionApi(
error_code="HTTP_500_INTERNAL_SERVER_ERROR",
lang="en"
error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en"
)
return wrapper

View File

@ -42,11 +42,15 @@ class MongoFindMixin:
class MongoUpdateMixin:
"""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."""
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."""
return self.collection.update_many(filter_query, update)
@ -114,7 +118,7 @@ class MongoDBHandler(
self._client = MongoClient(**connection_kwargs)
# Test connection
self._client.admin.command('ping')
self._client.admin.command("ping")
def close(self):
"""Close MongoDB connection."""

View File

@ -3,8 +3,7 @@ from functools import lru_cache
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session, Session
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
from AllConfigs.SqlDatabase.configs import WagDatabase

13
Ztest/fixtures.py Normal file
View File

@ -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)

13
Ztest/models.py Normal file
View File

@ -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)

View File

@ -7,7 +7,7 @@ from Services.MongoDb.Models.actions import MongoActions
from Services.MongoDb.Models.action_models.domain import (
DomainData,
DomainDocumentCreate,
DomainDocumentUpdate
DomainDocumentUpdate,
)
from AllConfigs.NoSqlDatabase.configs import MongoConfig
@ -17,7 +17,7 @@ def mongo_client():
"""Create MongoDB test client."""
# Connect using configured credentials
client = MongoClient(MongoConfig.URL)
client.admin.command('ping') # Test connection
client.admin.command("ping") # Test connection
yield client
client.close()
@ -32,7 +32,7 @@ def mongo_actions(mongo_client):
client=mongo_client,
database=MongoConfig.DATABASE_NAME,
company_uuid="test_company",
storage_reason="domains"
storage_reason="domains",
)
yield actions
try:
@ -50,7 +50,7 @@ def test_mongo_crud_operations(mongo_actions: MongoActions):
domain_data = DomainData(
user_uu_id="test_user",
main_domain="example.com",
other_domains_list=["old.com"]
other_domains_list=["old.com"],
)
create_doc = DomainDocumentCreate(data=domain_data)
@ -67,12 +67,11 @@ def test_mongo_crud_operations(mongo_actions: MongoActions):
update_data = DomainData(
user_uu_id="test_user",
main_domain="new.com",
other_domains_list=["example.com", "old.com"]
other_domains_list=["example.com", "old.com"],
)
update_doc = DomainDocumentUpdate(data=update_data)
result = mongo_actions.update_one(
{"_id": doc["_id"]},
{"$set": update_doc.model_dump()}
{"_id": doc["_id"]}, {"$set": update_doc.model_dump()}
)
assert result.modified_count == 1
@ -87,19 +86,14 @@ def test_mongo_aggregate(mongo_actions: MongoActions):
# Insert test documents
docs = [
DomainDocumentCreate(
data=DomainData(
user_uu_id="user1",
main_domain=f"domain{i}.com"
)
data=DomainData(user_uu_id="user1", main_domain=f"domain{i}.com")
).model_dump()
for i in range(3)
]
mongo_actions.insert_many(docs)
# Test aggregation
pipeline = [
{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}
]
pipeline = [{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}]
result = mongo_actions.aggregate(pipeline)
result_list = list(result)
assert len(result_list) == 1

View File

@ -1,21 +1,12 @@
"""Test PostgreSQL database operations."""
import pytest
from sqlalchemy import Column, String, create_engine, text
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
from Services.PostgresDb.database import Base, get_db
from Services.PostgresDb.Models.mixins import CrudCollection
from AllConfigs.SqlDatabase.configs import WagDatabase
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)
from Ztest.models import UserModel
@pytest.fixture(scope="session")
@ -52,12 +43,12 @@ def db_session(db_engine):
def test_create_user(db_session):
"""Test creating a user in the database."""
# 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.commit()
# 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.email == "test@example.com"
assert db_user.created_at is not None
@ -68,16 +59,16 @@ def test_create_user(db_session):
def test_update_user(db_session):
"""Test updating a user in the database."""
# Create user
user = TestUser(username="updateuser", email="update@example.com")
user = UserModel(username="updateuser", email="update@example.com")
db_session.add(user)
db_session.commit()
# 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()
# 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.updated_at is not None
@ -85,16 +76,16 @@ def test_update_user(db_session):
def test_soft_delete_user(db_session):
"""Test soft deleting a user from the database."""
# Create user
user = TestUser(username="deleteuser", email="delete@example.com")
user = UserModel(username="deleteuser", email="delete@example.com")
db_session.add(user)
db_session.commit()
# 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()
# 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.deleted
assert not deleted_user.active

View File

@ -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