Fix SMTP defaults and upload validation
This commit is contained in:
+90
-13
@@ -6,6 +6,7 @@ from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, File, Form, Request, UploadFile
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
@@ -13,6 +14,7 @@ from app.core.security.session_auth import get_current_user
|
||||
from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.core.tenancy.settings_models import BranchSettings
|
||||
from app.modules.core.iam.profile_service import save_user_profile_photo, update_user_public_profile
|
||||
from app.modules.employees.import_service import (
|
||||
build_template_workbook,
|
||||
@@ -173,6 +175,7 @@ def _base_ctx(request: Request, db, current_user, **ctx):
|
||||
"offboarding_task_statuses": OFFBOARDING_TASK_STATUS,
|
||||
"payroll_run_statuses": PAYROLL_RUN_STATUS,
|
||||
"payslip_statuses": PAYSLIP_STATUS,
|
||||
**_upload_policy_context(),
|
||||
}
|
||||
base.update(ctx)
|
||||
return base
|
||||
@@ -254,6 +257,56 @@ def _form_options(db, current_user, scope, *, include_user_id: int | None = None
|
||||
|
||||
UPLOAD_ROOT = Path("data/uploads/employee_documents")
|
||||
ALLOWED_DOCUMENT_EXTENSIONS = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".doc", ".docx", ".xls", ".xlsx"}
|
||||
DANGEROUS_DOCUMENT_EXTENSIONS = {
|
||||
".exe", ".bat", ".cmd", ".com", ".scr", ".pif",
|
||||
".sh", ".bash", ".zsh", ".ps1", ".psm1",
|
||||
".js", ".jse", ".vbs", ".vbe", ".wsf",
|
||||
".msi", ".dll", ".jar", ".apk",
|
||||
}
|
||||
DEFAULT_EMPLOYEE_UPLOAD_MAX_MB = 10
|
||||
|
||||
|
||||
def _normalise_extension(ext: str) -> str:
|
||||
ext = (ext or "").strip().lower()
|
||||
if not ext:
|
||||
return ""
|
||||
return ext if ext.startswith(".") else f".{ext}"
|
||||
|
||||
|
||||
def _extensions_from_csv(raw: str | None, *, fallback: set[str] | None = None) -> set[str]:
|
||||
values = {_normalise_extension(item) for item in (raw or "").split(",") if item.strip()}
|
||||
values = {item for item in values if item}
|
||||
return values or set(fallback or ALLOWED_DOCUMENT_EXTENSIONS)
|
||||
|
||||
|
||||
def _employee_upload_accept(allowed_exts: set[str] | None = None) -> str:
|
||||
exts = allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS
|
||||
return ",".join(sorted(exts))
|
||||
|
||||
|
||||
def _employee_upload_help(max_file_mb: int | None = None, allowed_exts: set[str] | None = None) -> str:
|
||||
exts = ", ".join(ext.lstrip(".").upper() for ext in sorted(allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS))
|
||||
max_mb = max_file_mb or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
|
||||
return f"Allowed file types: {exts}. Maximum size: {max_mb} MB."
|
||||
|
||||
|
||||
def _branch_upload_settings(db, branch_id: int | None) -> tuple[set[str], int]:
|
||||
if branch_id:
|
||||
settings = db.execute(select(BranchSettings).where(BranchSettings.branch_id == int(branch_id))).scalar_one_or_none()
|
||||
if settings:
|
||||
allowed = _extensions_from_csv(getattr(settings, "allowed_ext_csv", None), fallback=ALLOWED_DOCUMENT_EXTENSIONS)
|
||||
max_mb = int(getattr(settings, "max_file_mb", DEFAULT_EMPLOYEE_UPLOAD_MAX_MB) or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB)
|
||||
return allowed, max(1, max_mb)
|
||||
return set(ALLOWED_DOCUMENT_EXTENSIONS), DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
|
||||
|
||||
|
||||
def _upload_policy_context(db=None, branch_id: int | None = None) -> dict:
|
||||
allowed, max_mb = _branch_upload_settings(db, branch_id) if db is not None else (set(ALLOWED_DOCUMENT_EXTENSIONS), DEFAULT_EMPLOYEE_UPLOAD_MAX_MB)
|
||||
return {
|
||||
"employee_upload_accept": _employee_upload_accept(allowed),
|
||||
"employee_upload_help": _employee_upload_help(max_mb, allowed),
|
||||
"employee_upload_max_file_mb": max_mb,
|
||||
}
|
||||
|
||||
|
||||
def _document_type_payload(form):
|
||||
@@ -301,17 +354,39 @@ def _offboarding_payload(form):
|
||||
}
|
||||
|
||||
|
||||
async def _save_employee_upload(upload: UploadFile, *, tenant_id: int, employee_id: int) -> tuple[str, str, str, int | None, str | None]:
|
||||
original = _safe_upload_filename(upload.filename or "document")
|
||||
async def _save_employee_upload(
|
||||
upload: UploadFile,
|
||||
*,
|
||||
tenant_id: int,
|
||||
employee_id: int,
|
||||
allowed_exts: set[str] | None = None,
|
||||
max_file_mb: int | None = None,
|
||||
) -> tuple[str, str, str, int | None, str | None]:
|
||||
original = _safe_upload_filename(upload.filename or "")
|
||||
if not original:
|
||||
raise ValueError("Invalid file name. Please choose a valid document to upload.")
|
||||
|
||||
suffix = Path(original).suffix.lower()
|
||||
if suffix not in ALLOWED_DOCUMENT_EXTENSIONS:
|
||||
raise ValueError("Unsupported file type. Allowed: PDF, images, Word and Excel files.")
|
||||
if not suffix:
|
||||
raise ValueError("Invalid file type. File extension is required.")
|
||||
|
||||
if suffix in DANGEROUS_DOCUMENT_EXTENSIONS:
|
||||
raise ValueError(f"File type not allowed. Extension {suffix} is blocked for employee document uploads.")
|
||||
|
||||
allowed = allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS
|
||||
if suffix not in allowed:
|
||||
allowed_label = ", ".join(ext.lstrip(".").upper() for ext in sorted(allowed))
|
||||
raise ValueError(f"File type not allowed. Extension {suffix} is not permitted. Allowed: {allowed_label}.")
|
||||
|
||||
data = await upload.read()
|
||||
if not data:
|
||||
raise ValueError("Please choose a non-empty file to upload.")
|
||||
max_size = 10 * 1024 * 1024
|
||||
raise ValueError("Invalid upload. Please choose a non-empty file to upload.")
|
||||
|
||||
max_mb = max_file_mb or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
|
||||
max_size = int(max_mb) * 1024 * 1024
|
||||
if len(data) > max_size:
|
||||
raise ValueError("File size should not exceed 10 MB.")
|
||||
raise ValueError(f"Upload failed. File size should not exceed {max_mb} MB.")
|
||||
|
||||
folder = UPLOAD_ROOT / str(tenant_id) / str(employee_id)
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
stored = f"{uuid4().hex}{suffix}"
|
||||
@@ -1295,7 +1370,7 @@ def employee_documents(request: Request, employee_id: int | None = None, status:
|
||||
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee_id, status=status), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee_id, selected_status=status, errors=[])
|
||||
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee_id, status=status), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee_id, selected_status=status, errors=[], **_upload_policy_context(db, branch_id))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -1320,8 +1395,9 @@ async def employee_document_upload(request: Request, employee_id: int, document_
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
employee = get_employee_or_404(db, employee_id, scope)
|
||||
allowed_exts, max_file_mb = _branch_upload_settings(db, employee.branch_id)
|
||||
try:
|
||||
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
|
||||
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id, allowed_exts=allowed_exts, max_file_mb=max_file_mb)
|
||||
create_employee_document_record(
|
||||
db,
|
||||
current_user,
|
||||
@@ -1341,7 +1417,7 @@ async def employee_document_upload(request: Request, employee_id: int, document_
|
||||
)
|
||||
return RedirectResponse(url=f"/employees/documents?employee_id={employee.id}", status_code=303)
|
||||
except Exception as exc:
|
||||
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee.id), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee.id, selected_status=None, errors=[getattr(exc, "detail", str(exc))])
|
||||
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee.id), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee.id, selected_status=None, errors=[getattr(exc, "detail", str(exc))], **_upload_policy_context(db, employee.branch_id))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -1412,7 +1488,7 @@ def employee_self_documents(request: Request):
|
||||
if employee:
|
||||
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
||||
document_types = [dt for dt in list_document_types(db, scope) if dt.allow_employee_upload]
|
||||
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=document_types, errors=[])
|
||||
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=document_types, errors=[], **_upload_policy_context(db, employee.branch_id if employee else None))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -1436,8 +1512,9 @@ async def employee_self_document_upload(request: Request, document_file: UploadF
|
||||
employee = get_employee_for_user(db, current_user)
|
||||
if not employee:
|
||||
return RedirectResponse(url="/employee/register", status_code=303)
|
||||
allowed_exts, max_file_mb = _branch_upload_settings(db, employee.branch_id)
|
||||
try:
|
||||
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
|
||||
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id, allowed_exts=allowed_exts, max_file_mb=max_file_mb)
|
||||
create_employee_document_record(
|
||||
db,
|
||||
current_user,
|
||||
@@ -1458,7 +1535,7 @@ async def employee_self_document_upload(request: Request, document_file: UploadF
|
||||
return RedirectResponse(url="/employee/documents", status_code=303)
|
||||
except Exception as exc:
|
||||
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
||||
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=[dt for dt in list_document_types(db, scope) if dt.allow_employee_upload], errors=[getattr(exc, "detail", str(exc))])
|
||||
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=[dt for dt in list_document_types(db, scope) if dt.allow_employee_upload], errors=[getattr(exc, "detail", str(exc))], **_upload_policy_context(db, employee.branch_id))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user