first commit

This commit is contained in:
Mateusz Gruszczyński
2026-03-04 15:21:03 +01:00
commit 5429f176c9
53 changed files with 3808 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
APP_ENV: str = "dev"
APP_NAME: str = "mt-traffic"
BASE_URL: str = "http://localhost:8000"
DATABASE_URL: str = "sqlite+aiosqlite:///./mt_traffic.db"
DOCKER_DATABASE_URL: str = "sqlite+aiosqlite:////data/mt_traffic.db"
SESSION_SECRET: str = "change_me"
CREDENTIALS_MASTER_KEY: str = "change_me_32bytes_base64_fernet"
PASSWORD_HASH_ALG: str = "argon2"
COOKIE_SECURE: bool = False
COOKIE_SAMESITE: str = "lax"
CORS_ORIGINS: str = "http://localhost:3000"
DEFAULT_POLL_INTERVAL_MS: int = 1000
MAX_WS_SUBSCRIPTIONS_PER_USER: int = 10
ADMIN_BOOTSTRAP_EMAIL: str = "admin@example.com"
ADMIN_BOOTSTRAP_PASSWORD: str = "admin1234"
settings = Settings()

24
backend/app/core/db.py Normal file
View File

@@ -0,0 +1,24 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
class Base(DeclarativeBase):
pass
def _db_url() -> str:
# w dockerze zwykle ustawiamy DATABASE_URL przez env, więc preferujemy settings.DATABASE_URL
return settings.DATABASE_URL
engine = create_async_engine(_db_url(), future=True, echo=False)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
async def init_db() -> None:
from app.models import user, router, dashboard # noqa
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
yield session

View File

@@ -0,0 +1,7 @@
import logging
def setup_logging():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)

View File

@@ -0,0 +1,68 @@
import base64
import secrets
from datetime import datetime, timedelta
from typing import Optional
from cryptography.fernet import Fernet
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from passlib.context import CryptContext
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.models.user import User, UserRole
pwd_context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)
def session_serializer() -> URLSafeTimedSerializer:
return URLSafeTimedSerializer(settings.SESSION_SECRET, salt="session")
SESSION_COOKIE = "mt_session"
CSRF_COOKIE = "mt_csrf"
def create_session_token(user_id: int) -> str:
s = session_serializer()
return s.dumps({"uid": user_id})
def read_session_token(token: str, max_age_seconds: int = 60 * 60 * 24 * 7) -> Optional[int]:
s = session_serializer()
try:
data = s.loads(token, max_age=max_age_seconds)
uid = int(data.get("uid"))
return uid
except (BadSignature, SignatureExpired, Exception):
return None
def new_csrf_token() -> str:
return secrets.token_urlsafe(32)
def fernet() -> Fernet:
# CREDENTIALS_MASTER_KEY powinien być base64 urlsafe 32 bytes
key = settings.CREDENTIALS_MASTER_KEY.encode("utf-8")
return Fernet(key)
def encrypt_secret(plain: str) -> str:
return fernet().encrypt(plain.encode("utf-8")).decode("utf-8")
def decrypt_secret(enc: str) -> str:
return fernet().decrypt(enc.encode("utf-8")).decode("utf-8")
async def bootstrap_admin_if_needed(session: AsyncSession) -> None:
res = await session.execute(select(User).limit(1))
first = res.scalar_one_or_none()
if first:
return
admin = User(
email=settings.ADMIN_BOOTSTRAP_EMAIL,
password_hash=hash_password(settings.ADMIN_BOOTSTRAP_PASSWORD),
role=UserRole.ADMIN,
is_active=True,
)
session.add(admin)
await session.commit()