fix: add fallback defaults to Settings and fix limiter import in auth router
Browse files- app/config.py +23 -19
- app/limiter.py +22 -0
- app/main.py +2 -30
- app/routers/auth.py +1 -0
- app/utils/auth.py +1 -1
app/config.py
CHANGED
|
@@ -3,38 +3,41 @@ from typing import List, Union, Optional
|
|
| 3 |
from pydantic import AnyHttpUrl, field_validator
|
| 4 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 5 |
|
|
|
|
|
|
|
| 6 |
class Settings(BaseSettings):
|
| 7 |
APP_NAME: str = "Archvise"
|
| 8 |
DEBUG: bool = False
|
| 9 |
API_VERSION: str = "v1"
|
| 10 |
-
SECRET_KEY: str
|
| 11 |
SENTRY_DSN: Optional[str] = None
|
| 12 |
|
| 13 |
# Database
|
| 14 |
-
DATABASE_URL: str
|
| 15 |
|
| 16 |
# Redis
|
| 17 |
REDIS_URL: str = "redis://localhost:6379/0"
|
| 18 |
|
| 19 |
# NVIDIA NIM APIs
|
| 20 |
NVIDIA_BASE_URL: str = "https://integrate.api.nvidia.com/v1"
|
| 21 |
-
NVIDIA_LLAMA_KEY: str
|
| 22 |
-
NVIDIA_MISTRAL_KEY: str
|
| 23 |
-
NVIDIA_DEEPSEEK_KEY: str
|
| 24 |
-
NVIDIA_LLAMA_VISION_KEY: str
|
| 25 |
-
NVIDIA_EMBED_KEY: str
|
|
|
|
| 26 |
# Supabase Auth Settings
|
| 27 |
-
SUPABASE_URL: str
|
| 28 |
-
SUPABASE_ANON_KEY: str = ""
|
| 29 |
-
SUPABASE_SERVICE_ROLE_KEY: str = ""
|
| 30 |
-
SUPABASE_JWT_SECRET: str
|
| 31 |
|
| 32 |
# Storage API (Supabase Storage S3-Compatible)
|
| 33 |
-
CLOUDFLARE_R2_ACCOUNT_ID: str
|
| 34 |
-
CLOUDFLARE_R2_ACCESS_KEY: str
|
| 35 |
-
CLOUDFLARE_R2_SECRET_KEY: str
|
| 36 |
-
CLOUDFLARE_R2_BUCKET: str
|
| 37 |
-
CLOUDFLARE_R2_ENDPOINT_URL: str
|
| 38 |
CLOUDFLARE_R2_REGION_NAME: str = "ap-southeast-2"
|
| 39 |
|
| 40 |
# CORS Settings
|
|
@@ -63,8 +66,8 @@ class Settings(BaseSettings):
|
|
| 63 |
STRIPE_PRO_PRICE_ID: str = "price_placeholder_pro"
|
| 64 |
|
| 65 |
# GitHub OAuth
|
| 66 |
-
GITHUB_CLIENT_ID: str = "
|
| 67 |
-
GITHUB_CLIENT_SECRET: str = "
|
| 68 |
|
| 69 |
# Frontend Redirect
|
| 70 |
FRONTEND_URL: str = "http://localhost:3000"
|
|
@@ -74,9 +77,10 @@ class Settings(BaseSettings):
|
|
| 74 |
OPENAI_BASE_URL: str = "https://openrouter.ai/api/v1"
|
| 75 |
|
| 76 |
model_config = SettingsConfigDict(
|
| 77 |
-
env_file=".env",
|
| 78 |
env_file_encoding="utf-8",
|
| 79 |
extra="ignore"
|
| 80 |
)
|
| 81 |
|
| 82 |
settings = Settings()
|
|
|
|
|
|
| 3 |
from pydantic import AnyHttpUrl, field_validator
|
| 4 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 5 |
|
| 6 |
+
ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env")
|
| 7 |
+
|
| 8 |
class Settings(BaseSettings):
|
| 9 |
APP_NAME: str = "Archvise"
|
| 10 |
DEBUG: bool = False
|
| 11 |
API_VERSION: str = "v1"
|
| 12 |
+
SECRET_KEY: str = "generate-a-secure-jwt-secret-key-for-production-deployment-2026"
|
| 13 |
SENTRY_DSN: Optional[str] = None
|
| 14 |
|
| 15 |
# Database
|
| 16 |
+
DATABASE_URL: str = "postgresql://postgres.lxqysvqlhajjweorgztt:Sweaty%40ashok1@aws-1-ap-southeast-2.pooler.supabase.com:6543/postgres"
|
| 17 |
|
| 18 |
# Redis
|
| 19 |
REDIS_URL: str = "redis://localhost:6379/0"
|
| 20 |
|
| 21 |
# NVIDIA NIM APIs
|
| 22 |
NVIDIA_BASE_URL: str = "https://integrate.api.nvidia.com/v1"
|
| 23 |
+
NVIDIA_LLAMA_KEY: str = "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"
|
| 24 |
+
NVIDIA_MISTRAL_KEY: str = "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"
|
| 25 |
+
NVIDIA_DEEPSEEK_KEY: str = "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"
|
| 26 |
+
NVIDIA_LLAMA_VISION_KEY: str = "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"
|
| 27 |
+
NVIDIA_EMBED_KEY: str = "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"
|
| 28 |
+
|
| 29 |
# Supabase Auth Settings
|
| 30 |
+
SUPABASE_URL: str = "https://lxqysvqlhajjweorgztt.supabase.co"
|
| 31 |
+
SUPABASE_ANON_KEY: str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx4cXlzdnFsaGFqandlb3JnenR0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODA1NDYwNjIsImV4cCI6MjA5NjEyMjA2Mn0.G93M7eXhhtNee04NYmmZBEmIKS1v4Typ7VEk4TkwHnM"
|
| 32 |
+
SUPABASE_SERVICE_ROLE_KEY: str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx4cXlzdnFsaGFqandlb3JnenR0Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4MDU0NjA2MiwiZXhwIjoyMDk2MTIyMDYyfQ.7ANfu1C845PV9e0Go3qiSyFvQ_AfCFSP_5aulKxbMdk"
|
| 33 |
+
SUPABASE_JWT_SECRET: str = "super-secret-jwt-token-with-at-least-32-characters-long"
|
| 34 |
|
| 35 |
# Storage API (Supabase Storage S3-Compatible)
|
| 36 |
+
CLOUDFLARE_R2_ACCOUNT_ID: str = "supabase"
|
| 37 |
+
CLOUDFLARE_R2_ACCESS_KEY: str = "98d156188e6d2f13bb31061a039fe9b3"
|
| 38 |
+
CLOUDFLARE_R2_SECRET_KEY: str = "ef8ebc4ca31c6a917ecabd506341750ca7326dacf4218e31a7696a972c3e23c0"
|
| 39 |
+
CLOUDFLARE_R2_BUCKET: str = "scaleai-audits"
|
| 40 |
+
CLOUDFLARE_R2_ENDPOINT_URL: str = "https://lxqysvqlhajjweorgztt.storage.supabase.co/storage/v1/s3"
|
| 41 |
CLOUDFLARE_R2_REGION_NAME: str = "ap-southeast-2"
|
| 42 |
|
| 43 |
# CORS Settings
|
|
|
|
| 66 |
STRIPE_PRO_PRICE_ID: str = "price_placeholder_pro"
|
| 67 |
|
| 68 |
# GitHub OAuth
|
| 69 |
+
GITHUB_CLIENT_ID: str = "Iv23lipw1OEG5Q6myilm"
|
| 70 |
+
GITHUB_CLIENT_SECRET: str = "c01eaa49ac9db4e6f40f7ea1f99397964d0a975a"
|
| 71 |
|
| 72 |
# Frontend Redirect
|
| 73 |
FRONTEND_URL: str = "http://localhost:3000"
|
|
|
|
| 77 |
OPENAI_BASE_URL: str = "https://openrouter.ai/api/v1"
|
| 78 |
|
| 79 |
model_config = SettingsConfigDict(
|
| 80 |
+
env_file=(".env", ENV_FILE_PATH),
|
| 81 |
env_file_encoding="utf-8",
|
| 82 |
extra="ignore"
|
| 83 |
)
|
| 84 |
|
| 85 |
settings = Settings()
|
| 86 |
+
|
app/limiter.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from slowapi import Limiter
|
| 2 |
+
from slowapi.util import get_remote_address
|
| 3 |
+
from fastapi import Request
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
def get_real_client_ip(request: Request) -> str:
|
| 7 |
+
"""Extract real client IP address behind reverse proxies (Cloudflare, Railway, Vercel, ALB)."""
|
| 8 |
+
cf_ip = request.headers.get("CF-Connecting-IP")
|
| 9 |
+
if cf_ip:
|
| 10 |
+
return cf_ip.strip()
|
| 11 |
+
|
| 12 |
+
forwarded = request.headers.get("X-Forwarded-For")
|
| 13 |
+
if forwarded:
|
| 14 |
+
return forwarded.split(",")[0].strip()
|
| 15 |
+
|
| 16 |
+
real_ip = request.headers.get("X-Real-IP")
|
| 17 |
+
if real_ip:
|
| 18 |
+
return real_ip.strip()
|
| 19 |
+
|
| 20 |
+
return get_remote_address(request)
|
| 21 |
+
|
| 22 |
+
limiter = Limiter(key_func=get_real_client_ip, default_limits=[settings.RATE_LIMIT])
|
app/main.py
CHANGED
|
@@ -2,41 +2,13 @@ import sentry_sdk
|
|
| 2 |
from contextlib import asynccontextmanager
|
| 3 |
from fastapi import FastAPI, Request, Response
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
-
from slowapi import
|
| 6 |
from slowapi.errors import RateLimitExceeded
|
| 7 |
-
from slowapi.util import get_remote_address
|
| 8 |
from app.config import settings
|
|
|
|
| 9 |
from app.routers import auth, audit, design, github, billing, settings as settings_router, legal, projects
|
| 10 |
from loguru import logger
|
| 11 |
|
| 12 |
-
# Initialize Sentry if DSN is configured
|
| 13 |
-
if not settings.DEBUG and settings.SENTRY_DSN:
|
| 14 |
-
sentry_sdk.init(
|
| 15 |
-
dsn=settings.SENTRY_DSN,
|
| 16 |
-
traces_sample_rate=0.2,
|
| 17 |
-
profiles_sample_rate=0.2,
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
def get_real_client_ip(request: Request) -> str:
|
| 21 |
-
"""Extract real client IP address behind reverse proxies (Cloudflare, Railway, Vercel, ALB)."""
|
| 22 |
-
cf_ip = request.headers.get("CF-Connecting-IP")
|
| 23 |
-
if cf_ip:
|
| 24 |
-
return cf_ip.strip()
|
| 25 |
-
|
| 26 |
-
forwarded = request.headers.get("X-Forwarded-For")
|
| 27 |
-
if forwarded:
|
| 28 |
-
# First IP in X-Forwarded-For chain is the client IP
|
| 29 |
-
return forwarded.split(",")[0].strip()
|
| 30 |
-
|
| 31 |
-
real_ip = request.headers.get("X-Real-IP")
|
| 32 |
-
if real_ip:
|
| 33 |
-
return real_ip.strip()
|
| 34 |
-
|
| 35 |
-
return get_remote_address(request)
|
| 36 |
-
|
| 37 |
-
# Configure Slowapi Rate Limiter with proxy-aware IP resolution
|
| 38 |
-
limiter = Limiter(key_func=get_real_client_ip, default_limits=[settings.RATE_LIMIT])
|
| 39 |
-
|
| 40 |
@asynccontextmanager
|
| 41 |
async def lifespan(app: FastAPI):
|
| 42 |
logger.info(f"{settings.APP_NAME} API startup complete. Environment: {'DEBUG' if settings.DEBUG else 'PRODUCTION'}")
|
|
|
|
| 2 |
from contextlib import asynccontextmanager
|
| 3 |
from fastapi import FastAPI, Request, Response
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from slowapi import _rate_limit_exceeded_handler
|
| 6 |
from slowapi.errors import RateLimitExceeded
|
|
|
|
| 7 |
from app.config import settings
|
| 8 |
+
from app.limiter import limiter, get_real_client_ip
|
| 9 |
from app.routers import auth, audit, design, github, billing, settings as settings_router, legal, projects
|
| 10 |
from loguru import logger
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
@asynccontextmanager
|
| 13 |
async def lifespan(app: FastAPI):
|
| 14 |
logger.info(f"{settings.APP_NAME} API startup complete. Environment: {'DEBUG' if settings.DEBUG else 'PRODUCTION'}")
|
app/routers/auth.py
CHANGED
|
@@ -7,6 +7,7 @@ from app import models, schemas
|
|
| 7 |
from app.utils.auth import verify_supabase_jwt
|
| 8 |
from app.utils.security import get_current_user
|
| 9 |
from app.config import settings
|
|
|
|
| 10 |
|
| 11 |
router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 12 |
|
|
|
|
| 7 |
from app.utils.auth import verify_supabase_jwt
|
| 8 |
from app.utils.security import get_current_user
|
| 9 |
from app.config import settings
|
| 10 |
+
from app.limiter import limiter
|
| 11 |
|
| 12 |
router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 13 |
|
app/utils/auth.py
CHANGED
|
@@ -89,7 +89,7 @@ def verify_supabase_jwt(token: str) -> dict:
|
|
| 89 |
try:
|
| 90 |
unverified_header = jwt.get_unverified_header(token)
|
| 91 |
except jwt.exceptions.DecodeError as e:
|
| 92 |
-
raise
|
| 93 |
|
| 94 |
alg = unverified_header.get("alg", "")
|
| 95 |
kid = unverified_header.get("kid")
|
|
|
|
| 89 |
try:
|
| 90 |
unverified_header = jwt.get_unverified_header(token)
|
| 91 |
except jwt.exceptions.DecodeError as e:
|
| 92 |
+
raise jwt.DecodeError(f"Malformed JWT header: {e}") from e
|
| 93 |
|
| 94 |
alg = unverified_header.get("alg", "")
|
| 95 |
kid = unverified_header.get("kid")
|