Initial commit

This commit is contained in:
belviskhoremk
2026-03-06 22:57:58 +00:00
commit c4d836a0f9
60 changed files with 5423 additions and 0 deletions

0
app/core/__init__.py Normal file
View File

71
app/core/config.py Normal file
View File

@@ -0,0 +1,71 @@
"""Application configuration loaded from environment variables."""
from __future__ import annotations
from functools import lru_cache
from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── App ───────────────────────────────────────────────
APP_NAME: str = "Deals24Togo"
APP_ENV: str = "development"
DEBUG: bool = False
ALLOWED_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
@property
def allowed_origins_list(self) -> List[str]:
return [o.strip() for o in self.ALLOWED_ORIGINS.split(",") if o.strip()]
# ── Supabase ──────────────────────────────────────────
SUPABASE_URL: str
SUPABASE_KEY: str
SUPABASE_SERVICE_ROLE_KEY: str
# ── Auth / JWT ────────────────────────────────────────
SUPABASE_JWT_SECRET: str = "change-me"
# ── Storage ───────────────────────────────────────────
SUPABASE_STORAGE_BUCKET: str = "listings"
MAX_UPLOAD_SIZE_MB: int = 10
@property
def max_upload_size_bytes(self) -> int:
return self.MAX_UPLOAD_SIZE_MB * 1024 * 1024
# ── Rate limiting ─────────────────────────────────────
RATE_LIMIT_PER_MINUTE: int = 60
# ── Sentry ────────────────────────────────────────────
SENTRY_DSN: str = ""
# ── Email ─────────────────────────────────────────────
SMTP_HOST: str = ""
SMTP_PORT: int = 587
SMTP_USER: str = ""
SMTP_PASSWORD: str = ""
EMAIL_FROM: str = "noreply@deals24togo.com"
# ── Frontend ──────────────────────────────────────────
FRONTEND_URL: str = "http://localhost:5173"
# ── CinetPay ──────────────────────────────────────────
CINETPAY_API_KEY: str = ""
CINETPAY_SITE_ID: str = ""
BACKEND_PUBLIC_URL: str = "http://localhost:8000"
SUBSCRIPTION_MONTHLY_AMOUNT: int = 1000
SUBSCRIPTION_YEARLY_AMOUNT: int = 10000
@lru_cache
def get_settings() -> Settings:
return Settings() # type: ignore[call-arg]

35
app/core/exceptions.py Normal file
View File

@@ -0,0 +1,35 @@
"""Application-level exceptions with HTTP status codes."""
from __future__ import annotations
class AppException(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code = status_code
self.detail = detail
super().__init__(detail)
class NotFoundException(AppException):
def __init__(self, detail: str = "Resource not found"):
super().__init__(status_code=404, detail=detail)
class UnauthorizedException(AppException):
def __init__(self, detail: str = "Not authenticated"):
super().__init__(status_code=401, detail=detail)
class ForbiddenException(AppException):
def __init__(self, detail: str = "Not enough permissions"):
super().__init__(status_code=403, detail=detail)
class BadRequestException(AppException):
def __init__(self, detail: str = "Bad request"):
super().__init__(status_code=400, detail=detail)
class ConflictException(AppException):
def __init__(self, detail: str = "Resource already exists"):
super().__init__(status_code=409, detail=detail)

58
app/core/logging.py Normal file
View File

@@ -0,0 +1,58 @@
"""Structured logging with structlog."""
from __future__ import annotations
import logging
import sys
import structlog
from app.core.config import get_settings
def setup_logging() -> None:
settings = get_settings()
log_level = logging.DEBUG if settings.DEBUG else logging.INFO
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
formatter = structlog.stdlib.ProcessorFormatter(
processor=(
structlog.dev.ConsoleRenderer()
if settings.DEBUG
else structlog.processors.JSONRenderer()
),
)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
root = logging.getLogger()
root.handlers.clear()
root.addHandler(handler)
root.setLevel(log_level)
# Silence noisy libs
for name in ("httpx", "httpcore", "uvicorn.access"):
logging.getLogger(name).setLevel(logging.WARNING)
def get_logger(name: str = __name__) -> structlog.stdlib.BoundLogger:
return structlog.get_logger(name)

23
app/core/supabase.py Normal file
View File

@@ -0,0 +1,23 @@
"""Supabase client helpers — one anon client, one service-role client."""
from __future__ import annotations
from functools import lru_cache
from supabase import Client, create_client
from app.core.config import get_settings
@lru_cache
def get_supabase_client() -> Client:
"""Public / anon client — respects RLS policies."""
s = get_settings()
return create_client(s.SUPABASE_URL, s.SUPABASE_KEY)
@lru_cache
def get_supabase_admin() -> Client:
"""Service-role client — bypasses RLS. Use with care."""
s = get_settings()
return create_client(s.SUPABASE_URL, s.SUPABASE_SERVICE_ROLE_KEY)