Initial commit: backend scaffold, auth, frontend login
- FastAPI + SQLAlchemy async + Alembic + Postgres backend - Auth: JWT access + rotating refresh tokens, argon2, roles, audit log - React 19 + Vite frontend: login page, protected route, auth context - Docker Compose: postgres, redis, migrate, api, worker (placeholder), frontend/nginx Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""Конфигурация приложения. Единственное место, читающее окружение."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
from urllib.parse import quote
|
||||
|
||||
from pydantic import Field, PostgresDsn
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
Environment = Literal["local", "staging", "production"]
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=(".env", "../.env"),
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
# --- Общее ---
|
||||
project_name: str = "lux_fiscal"
|
||||
environment: Environment = "local"
|
||||
debug: bool = False
|
||||
log_level: str = "INFO"
|
||||
base_url: str = "http://localhost:8000"
|
||||
|
||||
# Хранится строкой, а не list[str]: для сложных типов pydantic-settings
|
||||
# пытается разобрать значение как JSON до валидаторов, и привычная запись
|
||||
# "a,b" из .env приводит к ошибке запуска. Разбор — в свойстве ниже.
|
||||
cors_origins_raw: str = Field(default="", alias="CORS_ORIGINS")
|
||||
|
||||
# --- Postgres ---
|
||||
postgres_host: str = "postgres"
|
||||
postgres_port: int = 5432
|
||||
postgres_db: str = "lux_fiscal"
|
||||
postgres_user: str = "lux_fiscal"
|
||||
postgres_password: str = "postgres"
|
||||
|
||||
# --- Redis ---
|
||||
redis_host: str = "redis"
|
||||
redis_port: int = 6379
|
||||
redis_db: int = 0
|
||||
|
||||
# --- Криптография ---
|
||||
secret_key: str
|
||||
encryption_key: str
|
||||
access_token_expire_minutes: int = 15
|
||||
refresh_token_expire_days: int = 7
|
||||
|
||||
# --- Первый администратор (только для команды bootstrap) ---
|
||||
first_admin_email: str = "admin@example.com"
|
||||
first_admin_password: str = ""
|
||||
first_admin_name: str = "Администратор"
|
||||
|
||||
@property
|
||||
def cors_origins(self) -> list[str]:
|
||||
"""Список разрешённых origin'ов из строки через запятую."""
|
||||
return [item.strip() for item in self.cors_origins_raw.split(",") if item.strip()]
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""DSN для асинхронного драйвера (приложение и воркер).
|
||||
|
||||
Логин и пароль экранируются вручную: `PostgresDsn.build` не делает
|
||||
percent-encoding сам, и любой спецсимвол в пароле (`,`, `@`, `:`, `/`)
|
||||
ломает разбор URL — в т.ч. молча указывая неверный порт.
|
||||
"""
|
||||
return str(
|
||||
PostgresDsn.build(
|
||||
scheme="postgresql+asyncpg",
|
||||
username=quote(self.postgres_user, safe=""),
|
||||
password=quote(self.postgres_password, safe=""),
|
||||
host=self.postgres_host,
|
||||
port=self.postgres_port,
|
||||
path=self.postgres_db,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def redis_url(self) -> str:
|
||||
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.environment == "production"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings() # type: ignore[call-arg]
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
Reference in New Issue
Block a user