Init first files
This commit is contained in:
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
53
backend/app/core/config.py
Normal file
53
backend/app/core/config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from functools import lru_cache
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||
|
||||
app_name: str = "NexaPG Monitor"
|
||||
environment: str = "dev"
|
||||
api_v1_prefix: str = "/api/v1"
|
||||
log_level: str = "INFO"
|
||||
|
||||
db_host: str = "db"
|
||||
db_port: int = 5432
|
||||
db_name: str = "nexapg"
|
||||
db_user: str = "nexapg"
|
||||
db_password: str = "nexapg"
|
||||
|
||||
jwt_secret_key: str
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_access_token_minutes: int = 15
|
||||
jwt_refresh_token_minutes: int = 60 * 24 * 7
|
||||
|
||||
encryption_key: str
|
||||
cors_origins: str = "http://localhost:5173"
|
||||
poll_interval_seconds: int = 30
|
||||
init_admin_email: str = "admin@example.com"
|
||||
init_admin_password: str = "ChangeMe123!"
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.db_user}:{self.db_password}"
|
||||
f"@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||
)
|
||||
|
||||
@property
|
||||
def cors_origins_list(self) -> list[str]:
|
||||
return [item.strip() for item in self.cors_origins.split(",") if item.strip()]
|
||||
|
||||
@field_validator("environment")
|
||||
@classmethod
|
||||
def validate_environment(cls, value: str) -> str:
|
||||
allowed = {"dev", "staging", "prod", "test"}
|
||||
if value not in allowed:
|
||||
raise ValueError(f"environment must be one of {allowed}")
|
||||
return value
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
18
backend/app/core/db.py
Normal file
18
backend/app/core/db.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
engine = create_async_engine(settings.database_url, future=True, pool_pre_ping=True)
|
||||
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
42
backend/app/core/deps.py
Normal file
42
backend/app/core/deps.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.config import get_settings
|
||||
from app.core.db import get_db
|
||||
from app.models.models import User
|
||||
|
||||
settings = get_settings()
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(bearer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
if not credentials:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing token")
|
||||
token = credentials.credentials
|
||||
try:
|
||||
payload = jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
|
||||
except JWTError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc
|
||||
|
||||
if payload.get("type") != "access":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user = await db.scalar(select(User).where(User.id == int(user_id)))
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
def require_roles(*roles: str):
|
||||
async def role_dependency(user: User = Depends(get_current_user)) -> User:
|
||||
if user.role not in roles:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
return user
|
||||
|
||||
return role_dependency
|
||||
22
backend/app/core/logging.py
Normal file
22
backend/app/core/logging.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"msg": record.getMessage(),
|
||||
}
|
||||
if record.exc_info:
|
||||
payload["exc_info"] = self.formatException(record.exc_info)
|
||||
return json.dumps(payload, ensure_ascii=True)
|
||||
|
||||
|
||||
def configure_logging(level: str) -> None:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(JsonFormatter())
|
||||
logging.basicConfig(level=level, handlers=[handler], force=True)
|
||||
30
backend/app/core/security.py
Normal file
30
backend/app/core/security.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
pwd_context = CryptContext(schemes=["argon2"], 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 create_token(subject: str, token_type: str, expires_minutes: int) -> str:
|
||||
now = datetime.now(timezone.utc)
|
||||
exp = now + timedelta(minutes=expires_minutes)
|
||||
payload = {"sub": subject, "type": token_type, "iat": int(now.timestamp()), "exp": int(exp.timestamp())}
|
||||
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
def create_access_token(subject: str) -> str:
|
||||
return create_token(subject, "access", settings.jwt_access_token_minutes)
|
||||
|
||||
|
||||
def create_refresh_token(subject: str) -> str:
|
||||
return create_token(subject, "refresh", settings.jwt_refresh_token_minutes)
|
||||
Reference in New Issue
Block a user