From 83e39824ce0dc544350acbaba1455cba4caeff57 Mon Sep 17 00:00:00 2001 From: Jannik Reinhard Date: Sun, 26 Jul 2026 16:28:33 +0200 Subject: [PATCH 1/2] fix runtime setup and proxy handling --- .github/workflows/ci.yml | 38 +++++++++++++++++ Dockerfile | 5 ++- app/__init__.py | 2 +- app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1086 bytes app/__pycache__/config.cpython-314.pyc | Bin 0 -> 11710 bytes app/config.py | 39 +++++++++-------- app/database.py | 7 +-- app/main.py | 10 ++--- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 225 bytes .../rate_limit_middleware.cpython-314.pyc | Bin 0 -> 11744 bytes app/middleware/rate_limit_middleware.py | 36 +++++++++------- frontend/login.html | 7 ++- setup.ps1 | 37 +++++++++------- supervisord.conf | 4 +- ...configuration.cpython-314-pytest-9.1.1.pyc | Bin 0 -> 6774 bytes tests/test_runtime_configuration.py | 40 ++++++++++++++++++ 16 files changed, 162 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 app/__pycache__/__init__.cpython-314.pyc create mode 100644 app/__pycache__/config.cpython-314.pyc create mode 100644 app/middleware/__pycache__/__init__.cpython-314.pyc create mode 100644 app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc create mode 100644 tests/__pycache__/test_runtime_configuration.cpython-314-pytest-9.1.1.pyc create mode 100644 tests/test_runtime_configuration.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cbf75af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + cache: pip + - name: Install test dependencies + run: pip install fastapi pydantic-settings sqlalchemy loguru pytest + - name: Run regression tests + run: python -m pytest tests -q + - name: Parse PowerShell scripts + shell: pwsh + run: | + $errors = @() + Get-ChildItem -Recurse -Include *.ps1,*.psm1 | ForEach-Object { + $tokens = $null + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$parseErrors) > $null + $errors += $parseErrors + } + if ($errors.Count -gt 0) { + $errors | Format-List + exit 1 + } diff --git a/Dockerfile b/Dockerfile index 7cdb018..4a99ae2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,7 +90,8 @@ ENV PYTHONUNBUFFERED=1 \ # Application settings (should be overridden in production) DATABASE_URL=sqlite:///./data/documents.db \ SECRET_KEY=MUST-BE-SET-IN-PRODUCTION \ - AI_PROVIDER=openai + AI_PROVIDER=openai \ + TRUSTED_PROXY_IPS=127.0.0.1 # Add metadata labels LABEL maintainer="Document Manager Team" \ @@ -106,4 +107,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8000/api/health || exit 1 # Use entrypoint script for initialization -ENTRYPOINT ["/app/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/app/__init__.py b/app/__init__.py index 2ddc165..37b1d2d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,7 +4,7 @@ # Configure loguru to write logs to the logs directory log_dir = Path(__file__).parent.parent / "data" / "logs" -log_dir.mkdir(exist_ok=True) +log_dir.mkdir(parents=True, exist_ok=True) # Remove default logger logger.remove() diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94873b988acf3c23598d4d4d15d13711ac59d219 GIT binary patch literal 1086 zcmZ`%%}*0S6rb&Gx9!%Jf`FKqv;+gGx?My<4Ivy56%HU-z{CXH4Bde)>_;=ZB~T=( zXrhT14jw#m)SLebiG(_diHY&x4MU7q-)t#z(6`w)zxOro?fdP`bV@-WR?q7C*9b!2 zxY3ft892QzAXGnO19z% z>C#0jkeB7Tw!hI9(xdRmMVONdbQb3_O?*-JqTw?$%;>lnYvNv{$F3kft|xS*C^min z&s~=FA+gn>Blu0HIFq6RY7xEDn zt6)mpz-Mu?W%+^OZIsT|7hV?y&Br>u2C9yh&SQ^}4kWY|01YFIcN2+g!V#@d%MG~g z_-j5d>Sw;T=o9K|>)3UzjRGZ>TgKGXCcRSC;Uj4GJdR?eF8qwBBnW&KG zU|U@wRK>PUR0kYHUB}zBjL(Z_u#4gAG+e1@hGDr@U>JqUHj9th*jX|0IOQa!;q^J! zb@X=e-JOGpqppEdFj4K%PGC7?^a+SMGowyU=BB1b9cR?{3-j~yySZn%9h+#+uky-Gb#?VAS+_0%VH<3vURc=l2++nP|>Q$8mK8p)-GlKB;rgvd#n gXMnqr$n1|DC9+|xy^-o{q`MoLuHUJc5DAC;19~L>=Kufz literal 0 HcmV?d00001 diff --git a/app/__pycache__/config.cpython-314.pyc b/app/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0de06ef9bbd6235e41f269bda2c4b3d4189f2239 GIT binary patch literal 11710 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 QvPV29;zO^Qt79+q-_u6x9{>OV literal 0 HcmV?d00001 diff --git a/app/config.py b/app/config.py index 772dbf0..78edfed 100644 --- a/app/config.py +++ b/app/config.py @@ -24,13 +24,6 @@ class Settings(BaseSettings): azure_openai_chat_deployment: str = "" azure_openai_embeddings_deployment: str = "" - def __init__(self, **kwargs): - # Don't use any values from kwargs that might come from env vars - # Only use explicitly passed values (which should be none for base Settings) - # Filter out any kwargs that might come from env vars - filtered_kwargs = {k: v for k, v in kwargs.items() if not k.startswith('NEVER_MATCH_THIS_PREFIX_')} - super().__init__(**filtered_kwargs) - # ChromaDB chroma_host: str = "localhost" chroma_port: int = 8001 @@ -61,7 +54,10 @@ def __init__(self, **kwargs): jwt_secret_key: Optional[str] = None # JWT secret key for authentication algorithm: str = "HS256" access_token_expire_minutes: int = 30 + environment: str = "development" production_mode: bool = False # Set to True in production for secure cookies + cors_origins: str = "http://localhost:3000,http://localhost:8000,http://127.0.0.1:8000" + trusted_proxy_ips: str = "127.0.0.1,::1" # Logging log_level: str = "INFO" @@ -73,21 +69,31 @@ def __init__(self, **kwargs): ai_max_retries: int = 2 # Maximum number of retries for failed AI requests model_config = SettingsConfigDict( - # No env_file - all settings come from database or defaults + # Runtime values may be supplied by Docker/Kubernetes environment + # variables. Database-backed settings still override these defaults. case_sensitive=False, - # Explicitly disable reading from environment variables env_file=None, - # Don't read from environment variables at all - # This ensures settings only come from database or defaults env_ignore_empty=True, - # This is the key setting to disable env vars completely - # By setting a prefix that will never match, we prevent env var loading - env_prefix="NEVER_MATCH_THIS_PREFIX_" + env_prefix="" ) + def model_post_init(self, __context: Any) -> None: + # Keep the existing ENVIRONMENT=production setup contract while still + # allowing an explicit PRODUCTION_MODE value to override it. + if "production_mode" not in self.model_fields_set: + self.production_mode = self.environment.lower() == "production" + @property def allowed_extensions_list(self) -> list: return [ext.strip().lower() for ext in self.allowed_extensions.split(",")] + + @property + def cors_origins_list(self) -> list[str]: + return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] + + @property + def trusted_proxy_ips_list(self) -> list[str]: + return [ip.strip() for ip in self.trusted_proxy_ips.split(",") if ip.strip()] @property def max_file_size_bytes(self) -> int: @@ -107,8 +113,8 @@ class DatabaseSettings(Settings): """Settings that loads configuration from database""" def __init__(self, db: Session = None, **kwargs): - # First, load defaults WITHOUT environment variables - # We pass _env_file=None to ensure no env vars are loaded + # Load defaults and runtime environment variables first. Persisted + # application settings take precedence when a database is available. super().__init__(_env_file=None, **kwargs) # Then override with database values if available @@ -188,4 +194,3 @@ def reset_settings(): """Reset the global settings instance""" global _settings _settings = None - diff --git a/app/database.py b/app/database.py index a573d3f..0652cae 100644 --- a/app/database.py +++ b/app/database.py @@ -3,9 +3,10 @@ from sqlalchemy.orm import sessionmaker, Session from loguru import logger import os +from .config import get_settings -# Use simple SQLite database with default path -DATABASE_URL = "sqlite:///./data/documents.db" +# Honour the documented DATABASE_URL runtime setting. +DATABASE_URL = get_settings().database_url # Ensure data directory exists os.makedirs("data", exist_ok=True) @@ -13,7 +14,7 @@ # Create engine with minimal configuration engine = create_engine( DATABASE_URL, - connect_args={"check_same_thread": False} + connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {} ) # Create session factory diff --git a/app/main.py b/app/main.py index 6d72a18..84f5768 100644 --- a/app/main.py +++ b/app/main.py @@ -29,10 +29,12 @@ version="1.0.0" ) +app_settings = get_settings() + # CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3000", "http://localhost:8000", "http://127.0.0.1:8000"], # Restrict origins + allow_origins=app_settings.cors_origins_list, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Content-Type", "Authorization", "X-CSRF-Token"], # Restrict headers @@ -44,9 +46,6 @@ app.add_exception_handler(StarletteHTTPException, ErrorHandler.starlette_exception_handler) app.add_exception_handler(Exception, ErrorHandler.general_exception_handler) -# Get settings to determine production mode -app_settings = get_settings() - # Enable CSRF protection csrf_protect = CSRFProtect( secure=app_settings.production_mode, # Use secure cookies in production @@ -71,7 +70,8 @@ default_limit=100, # 100 requests per minute for general endpoints window_seconds=60, login_limit=5, # 5 login attempts per 5 minutes - login_window_seconds=300 + login_window_seconds=300, + trusted_proxy_ips=app_settings.trusted_proxy_ips_list, ) rate_limit.init_app(app) diff --git a/app/middleware/__pycache__/__init__.cpython-314.pyc b/app/middleware/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b6db50eba5853fa28727897aace694f114058d4 GIT binary patch literal 225 zcmdPqJehWTtCiu4`nbpPrhLstZ(? zo0y}Ulv<>ln3I#Ln+p_3)y+z*EXqiPYxYgdOH5BK(oZZX(9g|GNy$kqPb^B+kB`sH o%PfhH*DI*J#bJ}1pHiBWYFESxbPmYD#UREfW=2NFB4!{90CE{StpET3 literal 0 HcmV?d00001 diff --git a/app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc b/app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7afc78f243c83acd179ed3d5e9f28636b19cd782 GIT binary patch literal 11744 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 str: """Extract client IP address from request.""" - # Check for proxy headers - forwarded_for = request.headers.get("X-Forwarded-For") - if forwarded_for: - # Take the first IP in the chain - return forwarded_for.split(",")[0].strip() - - # Check for other proxy headers - real_ip = request.headers.get("X-Real-IP") - if real_ip: - return real_ip - - # Fallback to direct connection - return request.client.host if request.client else "unknown" + direct_ip = request.client.host if request.client else "unknown" + + # Forwarding headers are attacker-controlled unless the direct peer is + # a configured reverse proxy. Never let an arbitrary client choose the + # rate-limit bucket used for its request. + if direct_ip in self.trusted_proxy_ips: + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + return forwarded_for.split(",")[0].strip() + + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip.strip() + + return direct_ip def get_rate_limit(self, path: str) -> Tuple[int, int]: """Get rate limit for a specific path.""" @@ -212,4 +216,4 @@ def __init__(self, app=None, **kwargs): def init_app(self, app): """Initialize rate limiting for the FastAPI app.""" # Add rate limit middleware - app.add_middleware(RateLimitMiddleware, **self.config) \ No newline at end of file + app.add_middleware(RateLimitMiddleware, **self.config) diff --git a/frontend/login.html b/frontend/login.html index df61331..02fb414 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -378,7 +378,10 @@

Admin User Setup

- \ No newline at end of file + diff --git a/setup.ps1 b/setup.ps1 index 897cbfa..c5ea698 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -32,11 +32,18 @@ function Require-Docker() { function New-EnvFile() { if (-not (Test-Path .env)) { Info 'Creating .env file...' - $secret = try { python - <<'PY' -import secrets -print(secrets.token_urlsafe(32)) -PY - } catch { 'change-me-in-production' } + $secret = 'change-me-in-production' + $pythonCommand = Get-Command python3, python -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($pythonCommand) { + try { + $generatedSecret = & $pythonCommand.Source -c 'import secrets; print(secrets.token_urlsafe(32))' + if ($LASTEXITCODE -eq 0 -and $generatedSecret) { + $secret = $generatedSecret.Trim() + } + } catch { + Warn 'Could not generate a random secret with Python; update SECRET_KEY manually.' + } + } @" # Security - CHANGE THIS IN PRODUCTION! @@ -94,22 +101,23 @@ function Invoke-Prod() { exit 1 } - $env = Get-Content .env | Where-Object { $_ -notmatch '^#' -and $_.Trim() } - foreach ($line in $env) { + $envValues = @{} + $envLines = Get-Content .env | Where-Object { $_ -notmatch '^#' -and $_.Trim() } + foreach ($line in $envLines) { $kv = $line.Split('=',2) - if ($kv.Length -eq 2) { $env:$($kv[0]) = $kv[1] } + if ($kv.Length -eq 2) { $envValues[$kv[0].Trim()] = $kv[1] } } $pwdPath = (Get-Location).Path docker run -d ` --name "$ContainerName" ` -p 8000:8000 ` - -e SECRET_KEY="$env:SECRET_KEY" ` - -e DATABASE_URL="$env:DATABASE_URL" ` - -e AI_PROVIDER="$env:AI_PROVIDER" ` - -e OPENAI_API_KEY="$env:OPENAI_API_KEY" ` - -e ENVIRONMENT="$env:ENVIRONMENT" ` - -e LOG_LEVEL="$env:LOG_LEVEL" ` + -e "SECRET_KEY=$($envValues['SECRET_KEY'])" ` + -e "DATABASE_URL=$($envValues['DATABASE_URL'])" ` + -e "AI_PROVIDER=$($envValues['AI_PROVIDER'])" ` + -e "OPENAI_API_KEY=$($envValues['OPENAI_API_KEY'])" ` + -e "ENVIRONMENT=$($envValues['ENVIRONMENT'])" ` + -e "LOG_LEVEL=$($envValues['LOG_LEVEL'])" ` -v "$pwdPath/data:/app/data" ` -v "$pwdPath/staging:/app/staging" ` -v "$pwdPath/storage:/app/storage" ` @@ -176,4 +184,3 @@ Examples: "@ } } - diff --git a/supervisord.conf b/supervisord.conf index 40d821d..df41bd1 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -14,7 +14,7 @@ stderr_logfile=/app/data/logs/chromadb.stderr.log environment=IS_PERSISTENT="TRUE",ANONYMIZED_TELEMETRY="FALSE",ALLOW_RESET="FALSE" [program:documentmanager] -command=python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*' +command=python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips=%(ENV_TRUSTED_PROXY_IPS)s directory=/app autostart=true autorestart=true @@ -35,4 +35,4 @@ chmod=0700 supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] -serverurl=unix:///tmp/supervisor.sock \ No newline at end of file +serverurl=unix:///tmp/supervisor.sock diff --git a/tests/__pycache__/test_runtime_configuration.cpython-314-pytest-9.1.1.pyc b/tests/__pycache__/test_runtime_configuration.cpython-314-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c5ef2bd3af6e77f4aeeb8eeefc13a43cb5351f3 GIT binary patch literal 6774 zcmeHM+i%z5~H)U^elSoo<3%g(|vt- z$Ft;L4Cn!>dzXTX9eM}Vxuwt|uk(G3;Ok}du;2&TDFi@91a^>#2mgtKk0l}(SZjt@bimgO-B-`T5xKWIpPOA0 zXIGcRkLQ2U2;z;zr}DNDzI#tt6YmL23ro4R#`R5IS2gIMSS!>ka#h!o@)HS@k7S`% zG5Gmheo45OTba+@H$p0@73&3EsZ|ZHx}7$Hnw67abri*TBO+;)$rRh@M(pJYevZS`{`~mGr_U!Su=Cah|5rGw#ZmyOK34<}@bUl_?EUROa0n zy^PA|hOFfdRRprwpk(KJEM2e|7``4Me3!D5THJTosNTeqdqZ6r;yeTIDSCPtz z4VffhALT>^407U;v`sc8r|}9by(AO*jA&Gfq+ZpPiYyjt)rU%{PUzMo)omk7J!AjA zVYj8CXg?xyQ3RJNq*lcTM*K`7inLLdHG24&TW)dLzeRh0-`H9FE8l&@$M*SHlbdM{&6skZpE=^J z&mq|Pm{WC>&!-&qfSa*v&s^vh?a@KG6=?As2Oikx2bx@Z_YoEQeENv9K8IYo$qzVH zN5O$C$N`tOYtLNh7WU_W+zK?Hqu$u(Z#22(=6g%c_%bT-<$Zqnh_gP2VBv2#RY$?W zEXV=3Y}cN-&@I}RgK{g-f{s17&kr`a;a%Mn{O}QHeGa+dCO_y@9R&xnAP3yAU3=z2 zx3E74dCA0!SHi7PZp+jq%hmH4 zbOXE=uzR*kAe&K>Y^q)$ocDz&Bu1ByJ2In_$u#_B8sX7&7L`mknMv=l2De$$^yBEy z6Cc$GyecWmMbwVNncGvz@eE+n$;myB;V+aGIM0p1CLHz=1VphaKNdx!ODA&satGSt{I!38*kN9D4IOoj9CVGe z5+5FR&F(BeA4=^`?<}{t*o(;Z7ttT@=33G0^R9v2T=P=)JD)e|-&y!3#Dsf}c=MEp zz&W_PaLA89RQ)nSg*&oheB?*mVbRn-f8ViTWVLEEvO;)hM`qB$xVzxk0BzSO+Ugs| z`7+{w+zOz`Em&*;zRvZg!UMb`bixC?!-4R?eedw#y9f`q-{3&I2oLsVFzOo%59B=< zlt4zpkOYWEulwvDi8--@q!5xsfv3ND{^7GmvMA_Dh(|sdM=^n75`=lmlPRp@YhyAE z;*2xTBpqj+d3DV?@a*G`p5SB-`iWs*_^*NgsE89sem1!hL0U`SF`c&mX-&*bAnXlTdx zB+x$@UIW1YRm8THhFD7NXzZab*<%km6a0ViQDtzn$}#>=wo*9`Mp e<}XbD- Date: Sun, 26 Jul 2026 16:28:52 +0200 Subject: [PATCH 2/2] ignore Python test caches --- .gitignore | 4 ++++ app/__pycache__/__init__.cpython-314.pyc | Bin 1086 -> 0 bytes app/__pycache__/config.cpython-314.pyc | Bin 11710 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 225 -> 0 bytes .../rate_limit_middleware.cpython-314.pyc | Bin 11744 -> 0 bytes ...e_configuration.cpython-314-pytest-9.1.1.pyc | Bin 6774 -> 0 bytes 6 files changed, 4 insertions(+) delete mode 100644 app/__pycache__/__init__.cpython-314.pyc delete mode 100644 app/__pycache__/config.cpython-314.pyc delete mode 100644 app/middleware/__pycache__/__init__.cpython-314.pyc delete mode 100644 app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc delete mode 100644 tests/__pycache__/test_runtime_configuration.cpython-314-pytest-9.1.1.pyc diff --git a/.gitignore b/.gitignore index 752d5af..f04fadd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ data/storage/LegalAssociatesLLP/2024-09-08/4f380a04-02af-4a46-8b3d-f39bae42e9da_ data/storage/PropertyManagementInc/2025-01-27/53b571aa-a79f-4be7-b867-7f0c258f39c8_2025-01-27_Property_Management_Inc_Medical Record.pdf data/storage/TechStartGmbH/2025-06-09/75d86917-31e1-4a2c-b5c4-3433f9841d1c_2025-06-09_TechStart_GmbH_Contract.pdf .DS_Store +# Python bytecode and test caches +__pycache__/ +*.py[cod] +.pytest_cache/ diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 94873b988acf3c23598d4d4d15d13711ac59d219..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1086 zcmZ`%%}*0S6rb&Gx9!%Jf`FKqv;+gGx?My<4Ivy56%HU-z{CXH4Bde)>_;=ZB~T=( zXrhT14jw#m)SLebiG(_diHY&x4MU7q-)t#z(6`w)zxOro?fdP`bV@-WR?q7C*9b!2 zxY3ft892QzAXGnO19z% z>C#0jkeB7Tw!hI9(xdRmMVONdbQb3_O?*-JqTw?$%;>lnYvNv{$F3kft|xS*C^min z&s~=FA+gn>Blu0HIFq6RY7xEDn zt6)mpz-Mu?W%+^OZIsT|7hV?y&Br>u2C9yh&SQ^}4kWY|01YFIcN2+g!V#@d%MG~g z_-j5d>Sw;T=o9K|>)3UzjRGZ>TgKGXCcRSC;Uj4GJdR?eF8qwBBnW&KG zU|U@wRK>PUR0kYHUB}zBjL(Z_u#4gAG+e1@hGDr@U>JqUHj9th*jX|0IOQa!;q^J! zb@X=e-JOGpqppEdFj4K%PGC7?^a+SMGowyU=BB1b9cR?{3-j~yySZn%9h+#+uky-Gb#?VAS+_0%VH<3vURc=l2++nP|>Q$8mK8p)-GlKB;rgvd#n gXMnqr$n1|DC9+|xy^-o{q`MoLuHUJc5DAC;19~L>=Kufz diff --git a/app/__pycache__/config.cpython-314.pyc b/app/__pycache__/config.cpython-314.pyc deleted file mode 100644 index 0de06ef9bbd6235e41f269bda2c4b3d4189f2239..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11710 zcmdryZFC#QaeKhwg9P{mkl+_cfCNdA5NOm_3?^$-rYjn~@XgR5YoY52qGxbE<7Zlq8_UG$Ax-(~=vAItxzG?36-)(sFLf2YPmtEksAe<+%(lZzOPgmt=>k?`-eapEOHBs(<-=S z-}zSAFVxCyLY>?$c;tXkFLwwHa;MNJcL`1M4ruQ_Z<2eacEWtu31*>LunFE_ULR*y zm<)143(TrrXvMjed#8eo=3_KJ(uOoO;pTPJJl+SE3n zx9wnU-J9BWGf8@|MIID(%0poy?-9(f=DSq8vAT;b(xqfV7Dqy%P+uq>m7}3}I+mRm zQ?k?-pH{8ujF^fh#uh!YcvTLH^V4EH4hj++3`+CSWOC6ln~{S<>EL`Kl~{D`?%zEW z?B5sc9|&fscsU_nQHffXOvj?hxwIrF4*Lk;#V0IQ>d%zq=q!x7Xw~o%dYVRO#YKnC zmrTz}i>^>sqM_+TDkOvQi8LCM7wra3wG8z4pBY_j&%|eXGpX6$3mN9dYrpm`rEzrH?Hho|EMaOh-}Zk-`4{{@%x^`%0(-yNCPw z;cs9Bsf!gwymw?|K;=)29Xp%I9U>%Ay@QbFdw8`n274_ci76=|CoYSsT})k$%p{Vc zT8*{DY${E~h&Z2-7u0g3z--PWuEtPUswKqMmJ2<2>m%|&TEbktCu(2;apycz<9*c2$2)O;p}n-f&NyyZ_mYl=1rWG>#|iXH;i|DBz^dSXk6EqC&~!W-L+cuu zhpAJ`Hwa^(G1W*+f{uvExCELbs}9hM1Wl(($jo zTcEC1X=7wyCq`hq%4$P#61TLlc^rg3IkILY&X%R4`HuZc$Nqbc{i}f;`M{tO7|aLu zD}nthfdfB$NeP^~cJ#i}UoaEr2=zn1O`59DEm_BwCuwR3Jko6wZ2&gf1fYjg`D8)@ z&qKkB66yt@Yan|g+KN`w2$(H28tIgzTBHnEZupUDB15}iT$m=XFGMv1m$vYJZQ<)5?;gQ< zRsBT;_!y2bj3|~$Oc>XT+MpLy61ocz(3D>I zQOz&|pR6G-0;_~tq-)3&(~v3d=Fn~PpYKO4nR=3zya_#jV=dYFNm_!-hh-LK#!F7Y zTZg$PX%%P=Ou=@VL;GMDuq)13no5y0O@M!u=l~GuZfwz2#j3a97VXltrKYrtrYB8B zJ$bC{Gtg5y001;3IFJt>Qi6x_!J|s>=vx!_f+tq0CY9icrM>S={&e!4^IxgFZhhVP zn)7RwKvb$Gm+h0FB9rSFEq}BAa1H<0wE+H?^@I+?=%1`05qyyn1qm|+33Ipk+Y^6P zh6)n?|EM4}GR;adm=^FiR~I6Q%r;tL0&1y-cpmGcsfn+sCfhTd$0^ozrL!mBc|hqr zkncRKbRK>yaIf>&O4T`~^VkjgPWMl{-`V+<^6RG8?XTItRt`j^>fEyZ9O%k93USnL z;9i*J0KZ~Rq<#gr5<`J&0WhOsv$PcmfSVKS=LtTg3&Mn7$66SkAj56H-#2`YVIkt+ z(Y<7{=vBfH>aeljG8rggi%&!J-&O%#JJ(@B*8Z z)V*nh$n*l77F2IhL$_(2#?^iq0GKj&ecs)vxI6RiU5b0xiaU7CzUpx29bU!Zz43hB z(SFy_{=n_Ies#&b*E%|5e!DZ**YDHbXqE)GAy&>l-I__3rqc z_|M!U%g&MaaI7yswe0k7sFe}v}Q*emkI5aK(bxOhk^k~%1n&Szo))W`T zASIEvl0DU9&yRZ&8()a%>HUsb|aT~eVcAa2iC#Lbh1N*>O=*b#pCRIl?fnM)&0u)rO)G{5BXeJT4C@!c~#astFk0sUe z*j!ZB>y>aqO)f|YNylrWi&;3ZZyBO)6BSHXCJkOkZP-M{=Us-QG{p3(cT)>{UR=y1 z(+lj3*S@K>D4rB4;c>*~XnH;x!EB3aAel6kRgc~hOT)QaJ7=b%^P=jY>9nlxQngY~ z#OOGE9%c`TEDK{WIWP-jAeF@&Oe7PP=Tv7VoyjCc$}oBg`cn+>)pALUL5_h>$a%nb_ z!~~970n3TW42VVM;b2q^sGi=?DK4N;@lsZV^RJwk7t>i;tppmn0+5X+M5%{ohLfW2 zWIjgar_<@=CmT|9#8L$qp%4Cmyh48Ns#Eykf`xEgp`0M%D>x9TCUq@uOnmi>!gm*H zkl-SoMui_NxDlx(HMI&KDAXb1A$1K3zoSr(h?g`6ZVrAUsPKJ-79_M1Z^z9e-*`&l zLj@la{KVI;@cRmFh_sVHm%{HX1Q6*UJ-rIwU+6?+CzGbVunUo1hBOy~i1d*bzrqI# zAw-6VtLcrvS6dXmv#UWqsjqN^L-Z&| z+#ZE*FFeE0lN_n7S9pKn6hlvQq|vMJqlGgJo#fbDgbGs(Jr6?_CoaNBpXErkTj9Nh zC_|??(%z}?Jq0*Dz{BF4E{Vv{8IE|G6uzx6%g{MZlotyLhF;)EL$kv77A`V0qmA5L zxWv%QnxL(PD-6BLk$N`N0z(%yx&4LTX6Q@WJ2(IGWki|70^f&9+p|}-MhXGDr_nPSL5QwY)s zw%L?4gAJDuPy`YJ83DR$)u|<$w9FpuMv@)OFvNHA0>wy}9z<{i!C3_70H`%3dA^x! zDuy{g$VWmtFcFJHW!SybXe59PoW+b~OiU&t5m*5)jKQLhr?t!xo;WE2wsB!8apOYV z*D#zr4Joe%f9@3gboS)NR+|TNXIDLe+{x9Z9l0@}?g}mQ4XX$nS3CAC^WN3&oy)v` zwXc7f@50W3+^N<2W&rKMWxfe0?OlL0?S#SVeZ^V_dqi^xs15DxsZDLUGpntm%e-f; zn$)-EJ-hCDcHJ6X@r2;v9o@iK=gpm1-O;nm`&R3r|EjkQWNUaacY3v@cbTt)M|pq) zewutRl(%h}uU&2R!+@(z&AG9kyE{?5mercN)y9@pciqDZTdg&B`jHD$rCN$~%}K14 zk4%^}TQ@^>ja8wWMxENAPH{zQc!L@?sBloy2Y}j|OYiS54(fk&#td8JcM01hY%Ne{ ziv@KB%lb0#?cFzR|c4*-d$J;WvpRqEE zl~LfP3@A26u`>$XkpaaHH%(<>PA(6V1Dx!D;K8X@AJyXqBT`)q88yEQVSzuHj>aXw zrqWpoI+;%SXTTHtjeuiQP6OP;uO86f))JzsDLze6S)b^O)RwK1Zpx42PTUChZI&K9 z$o9N3%zoH9Y)Xow7j$5_j}`@^YqUFBnJ|aRXJLupV$3AKgWOk3XG;ll#u?BDy&(+_|B$XKix@W{W495vNLhC7i?D<>Y;Jc1r#gaj8!_e!UblTZUw+bpeA*7_2#sl|A)xcxXm7*bI6Ibv`_696d{=4{h({Cr$MS0}H2oa(_~a#~ zzYlDdHaMz*kmlvlM>e% zbJz??a6W4aVGD7phI)#TW_ zgj@mX>Fi_`BLR59I)pO}#ZWmtaSAsGlQmjPDObkcOeWn1J#3!@1=Y&dapHcK_`n+l zR$j1%ZR2>MvZWaE3H*L&F>c*` zveaf@+4F z5!H+*Ld)f764)(pT|O_Vwz;T;VTWp)6(I3dM$wzTujc)ot{ zZvEhc2Hz5YYtMHN+&=J~r}b;YE66Y)P3jR{f2#D!@X^{%-?n1cHa3+zWeFB-A_O829}QA%HH?x0c+lK zm|Jfq&HW$#(ncD`xep$8kjB8b#+K^`9}*7as%^e;bSay!4KCLP*UUiqPiYTKdHTaCx^tVI10RFfQ;onu-5PCcagHC`yv9>(ZZTZO#4&bKPzzU<_##!$Luzg0$ zxYoy;F~JPb6vEFscI9OzH_~uX-d)4@E_}Gc*DkR=V`!mZ0Ao*c#~f22O-MV zV%X)3C2TQllx9&Ti(V#NOm~MZDVs6EnT{~uM#84;_yp6i?r&gRZoKGWjJ{w$ErwL= zeT%+kgTs&v!XCD0i)|Cp%c4K(2jelD6bY*Ng2KXKpwC%c!Jc{oEZUgG#*#2vGhD7MtZCpwq;{T`YjyqF91lmgk4bl`h8%v z1Oh@vrWkLjZI9WDtt?E3W?&%0Sh(w6?|!ZO##1Y>r)^xhHVT{DZ+e+3%0s^BlVN?5eZ!i^pC) zc76EsXWzA#f3fVi9R_>W$lvY4iGj4 z=jH7C6_x~19a>%UOlU$jG_IDZ6FA;YTGzR~)@@;9>+>lX4nAG6lkVPczkKuM{EiW2 z$H@I12Nh@F+R0USOWxhBxVvw;^X}k1cW|}3HecPTRCg{--?@0ddUV}Fx}Rd}NMD74 z=mfrolU~O96$DoiEC5h#@bYQMMNm9dP;{mgUz+|1!S5i*0VsY%rKclI^b6SJ1;DPX zp4O8RuOJlxCQzh(06xT|#6R-;AMtQb`wu*)*O27ufM0%=2R*CHh+M zusF|f;50%o%`E>75KHg7oDe5&u)GbeY+mrSU|!0Jz5GFyZ)x|vs;*^w7o0rpHFs?d z4{E(%K6$O|Jx48Ma_Jdmb^bq}f zXvJxeo(2Gs7{~n+sr&^w^e@E!&!qh!Kg)6cYcD+{`1?>WAL5R1*RMV#_`5c0A{8~c QvPV29;zO^Qt79+q-_u6x9{>OV diff --git a/app/middleware/__pycache__/__init__.cpython-314.pyc b/app/middleware/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 5b6db50eba5853fa28727897aace694f114058d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 225 zcmdPqJehWTtCiu4`nbpPrhLstZ(? zo0y}Ulv<>ln3I#Ln+p_3)y+z*EXqiPYxYgdOH5BK(oZZX(9g|GNy$kqPb^B+kB`sH o%PfhH*DI*J#bJ}1pHiBWYFESxbPmYD#UREfW=2NFB4!{90CE{StpET3 diff --git a/app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc b/app/middleware/__pycache__/rate_limit_middleware.cpython-314.pyc deleted file mode 100644 index 7afc78f243c83acd179ed3d5e9f28636b19cd782..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11744 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 e<}XbD-