Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import secrets
|
||||
from fastapi import Request
|
||||
|
||||
CSRF_KEY = "csrf_token"
|
||||
|
||||
def get_or_create_csrf_token(request: Request) -> str:
|
||||
token = request.session.get(CSRF_KEY)
|
||||
if not token:
|
||||
token = secrets.token_urlsafe(32)
|
||||
request.session[CSRF_KEY] = token
|
||||
return token
|
||||
|
||||
def validate_csrf(request: Request, form_token: str | None) -> None:
|
||||
token = request.session.get(CSRF_KEY)
|
||||
if not token or not form_token or token != form_token:
|
||||
raise PermissionError("CSRF validation failed")
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.deps import get_common_db
|
||||
from app.core.security.jwt_tokens import decode_token
|
||||
from app.modules.core.iam.models import User
|
||||
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
|
||||
def get_current_user_jwt(
|
||||
creds: HTTPAuthorizationCredentials | None = Depends(bearer),
|
||||
db: Session = Depends(get_common_db),
|
||||
) -> User | None:
|
||||
if not creds or not creds.credentials:
|
||||
return None
|
||||
data = decode_token(creds.credentials)
|
||||
if data.get("typ") != "access":
|
||||
return None
|
||||
|
||||
user_id = int(data.get("sub", 0) or 0)
|
||||
if not user_id:
|
||||
return None
|
||||
user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none()
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
return user
|
||||
|
||||
def require_jwt_user(user: User | None = Depends(get_current_user_jwt)) -> User:
|
||||
if not user:
|
||||
raise PermissionError("Not authenticated (JWT)")
|
||||
return user
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
from jwt import PyJWTError
|
||||
|
||||
from app.core.settings import get_settings
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def encode_access_token(payload: dict[str, Any], expires_minutes: int) -> str:
|
||||
s = get_settings()
|
||||
now = utcnow()
|
||||
exp = now + timedelta(minutes=expires_minutes)
|
||||
token_payload = {
|
||||
**payload,
|
||||
"iss": s.JWT_ISSUER,
|
||||
"aud": s.JWT_AUDIENCE,
|
||||
"iat": int(now.timestamp()),
|
||||
"exp": int(exp.timestamp()),
|
||||
"typ": "access",
|
||||
}
|
||||
return jwt.encode(token_payload, s.SECRET_KEY, algorithm="HS256")
|
||||
|
||||
def decode_token(token: str) -> dict[str, Any]:
|
||||
s = get_settings()
|
||||
try:
|
||||
data = jwt.decode(
|
||||
token,
|
||||
s.SECRET_KEY,
|
||||
algorithms=["HS256"],
|
||||
audience=s.JWT_AUDIENCE,
|
||||
issuer=s.JWT_ISSUER,
|
||||
options={"require": ["exp", "iat", "iss", "aud"]},
|
||||
)
|
||||
return data
|
||||
except PyJWTError as e:
|
||||
raise PermissionError("Invalid token") from e
|
||||
@@ -0,0 +1,23 @@
|
||||
import secrets
|
||||
from fastapi import Request
|
||||
|
||||
OTP_CODE_KEY = "otp_code"
|
||||
OTP_VERIFIED_KEY = "otp_verified"
|
||||
|
||||
def start_otp(request: Request) -> str:
|
||||
# 6-digit numeric code
|
||||
code = str(secrets.randbelow(900000) + 100000)
|
||||
request.session[OTP_CODE_KEY] = code
|
||||
request.session[OTP_VERIFIED_KEY] = False
|
||||
return code
|
||||
|
||||
def verify_otp(request: Request, code: str) -> bool:
|
||||
expected = request.session.get(OTP_CODE_KEY)
|
||||
if expected and code and code.strip() == expected:
|
||||
request.session[OTP_VERIFIED_KEY] = True
|
||||
request.session.pop(OTP_CODE_KEY, None)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_otp_verified(request: Request) -> bool:
|
||||
return bool(request.session.get(OTP_VERIFIED_KEY))
|
||||
@@ -0,0 +1,8 @@
|
||||
from passlib.context import CryptContext
|
||||
_pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def hash_password(p: str) -> str:
|
||||
return _pwd.hash(p)
|
||||
|
||||
def verify_password(p: str, h: str) -> bool:
|
||||
return _pwd.verify(p, h)
|
||||
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.deps import get_common_db
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.core.tenancy.models import Branch
|
||||
from app.modules.core.tenancy.settings_models import BranchSettings
|
||||
|
||||
SESSION_USER_ID_KEY = "user_id"
|
||||
SESSION_LOGIN_AT_KEY = "login_at"
|
||||
|
||||
def _now_utc() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def get_current_user(request: Request, db: Session = Depends(get_common_db)) -> User | None:
|
||||
user_id = request.session.get(SESSION_USER_ID_KEY)
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
user = db.execute(select(User).where(User.id == int(user_id))).scalar_one_or_none()
|
||||
if not user or not user.is_active or not getattr(user, "allow_login", True) or getattr(user, "is_locked", False) or getattr(user, "deleted_at", None) is not None:
|
||||
return None
|
||||
|
||||
# Enforce session duration from BranchSettings
|
||||
login_at = request.session.get(SESSION_LOGIN_AT_KEY)
|
||||
if login_at:
|
||||
try:
|
||||
login_at_dt = datetime.fromisoformat(login_at)
|
||||
except Exception:
|
||||
login_at_dt = None
|
||||
else:
|
||||
login_at_dt = None
|
||||
|
||||
bs = db.execute(select(BranchSettings).where(BranchSettings.branch_id == user.branch_id)).scalar_one_or_none()
|
||||
max_minutes = bs.session_duration_minutes if bs else 480
|
||||
|
||||
if login_at_dt:
|
||||
if _now_utc() - login_at_dt > timedelta(minutes=max_minutes):
|
||||
# expire session
|
||||
request.session.pop(SESSION_USER_ID_KEY, None)
|
||||
request.session.pop(SESSION_LOGIN_AT_KEY, None)
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
def require_login(user: User | None = Depends(get_current_user)) -> User:
|
||||
if not user:
|
||||
raise PermissionError("Not authenticated")
|
||||
return user
|
||||
Reference in New Issue
Block a user