Fix SMTP defaults and upload validation
This commit is contained in:
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.core.settings import get_settings
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.core.tenancy.settings_models import BranchSettings
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.notice_cases.models import (
|
||||
NoticeCase,
|
||||
@@ -381,6 +382,75 @@ def add_order(db: Session, *, case: NoticeCase, user, data: dict) -> NoticeCaseO
|
||||
return row
|
||||
|
||||
|
||||
class UploadValidationError(ValueError):
|
||||
"""Raised when a notice/case document upload violates branch file policy."""
|
||||
|
||||
|
||||
DANGEROUS_DOCUMENT_EXTENSIONS = {
|
||||
".exe", ".bat", ".cmd", ".com", ".scr", ".pif",
|
||||
".sh", ".bash", ".zsh", ".ps1", ".psm1",
|
||||
".js", ".jse", ".vbs", ".vbe", ".wsf",
|
||||
".msi", ".dll", ".jar", ".apk",
|
||||
}
|
||||
|
||||
|
||||
def _branch_document_settings(db: Session, branch_id: int | None) -> BranchSettings | None:
|
||||
if not branch_id:
|
||||
return None
|
||||
return db.execute(
|
||||
select(BranchSettings).where(BranchSettings.branch_id == int(branch_id))
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def _normalise_allowed_extensions(raw: str | None) -> set[str]:
|
||||
values = raw or "pdf,jpg,jpeg,png,xlsx,xls,docx,zip"
|
||||
return {
|
||||
"." + item.strip().lower().lstrip(".")
|
||||
for item in values.split(",")
|
||||
if item.strip()
|
||||
}
|
||||
|
||||
|
||||
def notice_case_upload_policy(db: Session, *, branch_id: int | None) -> dict[str, object]:
|
||||
settings = _branch_document_settings(db, branch_id)
|
||||
allowed_exts = _normalise_allowed_extensions(getattr(settings, "allowed_ext_csv", None))
|
||||
max_file_mb = int(getattr(settings, "max_file_mb", 25) or 25)
|
||||
if max_file_mb <= 0:
|
||||
max_file_mb = 25
|
||||
return {
|
||||
"allowed_exts": sorted(allowed_exts),
|
||||
"allowed_ext_csv": ",".join(ext.lstrip(".") for ext in sorted(allowed_exts)),
|
||||
"accept": ",".join(sorted(allowed_exts)),
|
||||
"max_file_mb": max_file_mb,
|
||||
"max_file_bytes": max_file_mb * 1024 * 1024,
|
||||
}
|
||||
|
||||
|
||||
def _validate_notice_case_upload_policy(db: Session, *, case: NoticeCase, upload_file: UploadFile, original: str) -> bytes:
|
||||
policy = notice_case_upload_policy(db, branch_id=case.branch_id)
|
||||
allowed_exts = set(policy["allowed_exts"] or [])
|
||||
max_file_bytes = int(policy["max_file_bytes"] or (25 * 1024 * 1024))
|
||||
|
||||
if not original or original in {".", ".."}:
|
||||
raise UploadValidationError("Invalid filename.")
|
||||
|
||||
ext = Path(original).suffix.lower()
|
||||
if not ext:
|
||||
raise UploadValidationError("File extension is required.")
|
||||
|
||||
if ext in DANGEROUS_DOCUMENT_EXTENSIONS:
|
||||
raise UploadValidationError("Executable or dangerous file types are not allowed.")
|
||||
|
||||
if allowed_exts and ext not in allowed_exts:
|
||||
raise UploadValidationError("File type not allowed. Allowed types: " + ", ".join(sorted(allowed_exts)))
|
||||
|
||||
data = upload_file.file.read(max_file_bytes + 1)
|
||||
if len(data) > max_file_bytes:
|
||||
raise UploadValidationError(f"File too large. Maximum allowed size is {policy['max_file_mb']} MB.")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _storage_root() -> Path:
|
||||
settings = get_settings()
|
||||
base = getattr(settings, "LOCAL_STORAGE_ROOT", None) or getattr(settings, "DOCUMENT_STORAGE_ROOT", None) or "data/storage"
|
||||
@@ -392,14 +462,14 @@ def case_document_absolute_path(document: NoticeCaseDocument) -> Path:
|
||||
|
||||
|
||||
def save_case_document(db: Session, *, case: NoticeCase, upload_file: UploadFile, user, title: str, document_type: str, description: str | None, event_id: int | None = None) -> NoticeCaseDocument:
|
||||
original = Path(upload_file.filename or "case_document.bin").name
|
||||
original = Path(upload_file.filename or "").name
|
||||
data = _validate_notice_case_upload_policy(db, case=case, upload_file=upload_file, original=original)
|
||||
ext = Path(original).suffix.lower()
|
||||
stored = f"case_{case.id}_{uuid4().hex}{ext}"
|
||||
fy_folder = f"FY{case.financial_year}" if case.financial_year else "FY_UNASSIGNED"
|
||||
rel = Path("notice_cases") / fy_folder / str(case.tenant_id) / str(case.client_id) / case.case_code / stored
|
||||
absolute = _storage_root() / rel
|
||||
absolute.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = upload_file.file.read()
|
||||
absolute.write_bytes(data)
|
||||
latest_version = db.execute(select(func.max(NoticeCaseDocument.version_no)).where(NoticeCaseDocument.case_id == case.id, NoticeCaseDocument.title == (title or original))).scalar_one() or 0
|
||||
row = NoticeCaseDocument(
|
||||
|
||||
Reference in New Issue
Block a user