This commit is contained in:
Mikan
2026-06-20 19:13:05 +03:00
parent 32575e217e
commit 8514c63ec6
193 changed files with 22105 additions and 11660 deletions

85
app/core/security.py Normal file
View File

@@ -0,0 +1,85 @@
"""Security: JWT creation/verification and password hashing."""
from __future__ import annotations
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.config import get_settings
_pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
"""Hash a password using bcrypt."""
return _pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
"""Verify a password against its bcrypt hash."""
try:
return _pwd_context.verify(plain, hashed)
except (ValueError, TypeError):
return False
def create_access_token(
subject: str | uuid.UUID,
extra_claims: dict[str, Any] | None = None,
expires_in_minutes: int | None = None,
) -> str:
"""Create a signed JWT access token."""
cfg = get_settings()
minutes = expires_in_minutes or cfg.access_token_expire_minutes
now = datetime.now(timezone.utc)
payload: dict[str, Any] = {
"sub": str(subject),
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=minutes)).timestamp()),
"type": "access",
}
if extra_claims:
payload.update(extra_claims)
return jwt.encode(payload, cfg.secret_key, algorithm=cfg.jwt_algorithm)
def create_refresh_token(
subject: str | uuid.UUID, expires_in_minutes: int | None = None
) -> str:
"""Create a signed JWT refresh token."""
cfg = get_settings()
minutes = expires_in_minutes or cfg.refresh_token_expire_minutes
now = datetime.now(timezone.utc)
payload = {
"sub": str(subject),
"iat": int(now.timestamp()),
"exp": int((now + timedelta(minutes=minutes)).timestamp()),
"type": "refresh",
}
return jwt.encode(payload, cfg.secret_key, algorithm=cfg.jwt_algorithm)
def decode_token(token: str) -> dict[str, Any]:
"""Decode and verify a JWT. Raises JWTError on failure."""
cfg = get_settings()
return jwt.decode(token, cfg.secret_key, algorithms=[cfg.jwt_algorithm])
def validate_password_strength(password: str) -> list[str]:
"""Return a list of validation errors (empty list = valid password)."""
errors: list[str] = []
if len(password) < 8:
errors.append("Password must be at least 8 characters long")
if not any(c.isalpha() for c in password):
errors.append("Password must contain at least one letter")
if not any(c.isdigit() for c in password):
errors.append("Password must contain at least one digit")
# Tiny blacklist of trivial passwords
blacklist = {"password", "12345678", "qwerty12", "password1", "abcdefgh"}
if password.lower() in blacklist:
errors.append("Password is too common")
return errors