Initial certificate system
This commit is contained in:
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
33
backend/app/core/config.py
Normal file
33
backend/app/core/config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
def _csv_env(name: str, default: list[str]) -> list[str]:
|
||||
raw = os.getenv(name)
|
||||
if not raw:
|
||||
return default
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
app_name: str = os.getenv("APP_NAME", "Certificate Management System")
|
||||
app_env: str = os.getenv("APP_ENV", "development")
|
||||
database_url: str = os.getenv("DATABASE_URL", "sqlite:///./dev.db")
|
||||
secret_key: str = os.getenv("SECRET_KEY", "change-me")
|
||||
public_base_url: str = os.getenv("PUBLIC_BASE_URL", "http://localhost:8080")
|
||||
certificate_no_secret: str = os.getenv("CERTIFICATE_NO_SECRET", "change-me")
|
||||
pdf_cache_days: int = int(os.getenv("PDF_CACHE_DAYS", "30"))
|
||||
data_root: str = os.getenv("DATA_ROOT", "/data/certificate-system")
|
||||
cors_origins: list[str] = field(
|
||||
default_factory=lambda: _csv_env("CORS_ORIGINS", ["http://localhost:5173", "http://localhost:8080"])
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
15
backend/app/core/paths.py
Normal file
15
backend/app/core/paths.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
DATA_ROOT = Path(settings.data_root)
|
||||
|
||||
|
||||
def ensure_data_dirs() -> None:
|
||||
for name in ["uploads", "error-reports", "exports", "pdf-cache"]:
|
||||
(DATA_ROOT / name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def data_path(name: str) -> Path:
|
||||
ensure_data_dirs()
|
||||
return DATA_ROOT / name
|
||||
71
backend/app/core/security.py
Normal file
71
backend/app/core/security.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def hash_password(password: str, salt: str | None = None) -> str:
|
||||
password_salt = salt or secrets.token_hex(16)
|
||||
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), password_salt.encode("utf-8"), 120_000)
|
||||
return f"pbkdf2_sha256${password_salt}${digest.hex()}"
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
try:
|
||||
scheme, salt, expected = password_hash.split("$", 2)
|
||||
except ValueError:
|
||||
return False
|
||||
if scheme != "pbkdf2_sha256":
|
||||
return False
|
||||
actual = hash_password(password, salt).split("$", 2)[2]
|
||||
return hmac.compare_digest(actual, expected)
|
||||
|
||||
|
||||
def _b64encode(data: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def _b64decode(data: str) -> bytes:
|
||||
padding = "=" * (-len(data) % 4)
|
||||
return base64.urlsafe_b64decode((data + padding).encode("ascii"))
|
||||
|
||||
|
||||
def create_access_token(subject: str, role_code: str, expires_in: int = 60 * 60 * 8) -> str:
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
payload = {"sub": subject, "role": role_code, "exp": int(time.time()) + expires_in}
|
||||
signing_input = ".".join(
|
||||
[
|
||||
_b64encode(json.dumps(header, separators=(",", ":")).encode("utf-8")),
|
||||
_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")),
|
||||
]
|
||||
)
|
||||
signature = hmac.new(settings.secret_key.encode("utf-8"), signing_input.encode("ascii"), hashlib.sha256).digest()
|
||||
return f"{signing_input}.{_b64encode(signature)}"
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
header_b64, payload_b64, signature_b64 = token.split(".", 2)
|
||||
signing_input = f"{header_b64}.{payload_b64}"
|
||||
expected = hmac.new(settings.secret_key.encode("utf-8"), signing_input.encode("ascii"), hashlib.sha256).digest()
|
||||
if not hmac.compare_digest(_b64encode(expected), signature_b64):
|
||||
return None
|
||||
payload = json.loads(_b64decode(payload_b64))
|
||||
if int(payload.get("exp", 0)) < int(time.time()):
|
||||
return None
|
||||
return payload
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def hash_token(token: str) -> str:
|
||||
return hashlib.sha256(f"{settings.secret_key}:{token}".encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def generate_public_token() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
Reference in New Issue
Block a user