- Added a new modal for reporting bugs, including fields for describing the issue and attaching files. - Implemented backend API for creating bug reports, including rate limiting and metadata logging. - Introduced a new database table to track bug report submissions for auditing purposes. - Enhanced the frontend to capture screenshots automatically and allow manual file uploads. - Added error handling and user feedback for the bug reporting process. - Updated existing templates and scripts to integrate the new bug reporting functionality.
213 lines
6.9 KiB
Python
213 lines
6.9 KiB
Python
import base64
|
|
import json
|
|
import logging
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
|
|
from app.bug_reports.backend.models import BugReportPayload, BugReportResult
|
|
from app.core.config import settings
|
|
from app.core.database import execute_query, execute_query_single
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
UPLOAD_BASE_PATH = Path(settings.UPLOAD_DIR).resolve()
|
|
SAG_FILE_SUBDIR = "sag_files"
|
|
(UPLOAD_BASE_PATH / SAG_FILE_SUBDIR).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def _table_exists(table_name: str) -> bool:
|
|
row = execute_query_single(
|
|
"""
|
|
SELECT 1
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = %s
|
|
LIMIT 1
|
|
""",
|
|
(table_name,),
|
|
)
|
|
return bool(row)
|
|
|
|
|
|
def _decode_data_url(data_url: str) -> tuple[bytes, str]:
|
|
# Expected format: data:image/png;base64,....
|
|
match = re.match(r"^data:([\w/+.-]+);base64,(.+)$", data_url or "", flags=re.DOTALL)
|
|
if not match:
|
|
raise HTTPException(status_code=400, detail="Invalid base64 data URL")
|
|
|
|
content_type = match.group(1)
|
|
b64_data = match.group(2)
|
|
try:
|
|
raw = base64.b64decode(b64_data, validate=True)
|
|
except Exception:
|
|
raise HTTPException(status_code=400, detail="Invalid base64 encoding")
|
|
|
|
return raw, content_type
|
|
|
|
|
|
def _store_raw_file(raw: bytes, filename: str) -> tuple[str, int]:
|
|
safe_name = Path(filename).name
|
|
stored_name = f"{SAG_FILE_SUBDIR}/{uuid4().hex}_{safe_name}"
|
|
destination = UPLOAD_BASE_PATH / stored_name
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with destination.open("wb") as f:
|
|
f.write(raw)
|
|
|
|
return stored_name, len(raw)
|
|
|
|
|
|
def _create_sag_file_record(sag_id: int, filename: str, content_type: str, size_bytes: int, stored_name: str) -> None:
|
|
execute_query(
|
|
"""
|
|
INSERT INTO sag_files (sag_id, filename, content_type, size_bytes, stored_name)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
""",
|
|
(sag_id, filename, content_type, size_bytes, stored_name),
|
|
)
|
|
|
|
|
|
def _rate_limit(user_id: int) -> None:
|
|
if not _table_exists("bug_report_submissions"):
|
|
# If migration is not yet applied, fail-open to avoid blocking support workflows.
|
|
return
|
|
|
|
max_per_hour = max(1, int(settings.BUG_REPORT_MAX_PER_HOUR))
|
|
row = execute_query_single(
|
|
"""
|
|
SELECT COUNT(*)::int AS count
|
|
FROM bug_report_submissions
|
|
WHERE user_id = %s
|
|
AND created_at >= %s
|
|
""",
|
|
(user_id, datetime.utcnow() - timedelta(hours=1)),
|
|
)
|
|
count = int((row or {}).get("count") or 0)
|
|
if count >= max_per_hour:
|
|
raise HTTPException(status_code=429, detail="Rate limit exceeded for bug reports")
|
|
|
|
|
|
def _resolve_customer_id() -> int:
|
|
configured_id = int(settings.BUG_REPORT_DEFAULT_CUSTOMER_ID)
|
|
configured_row = execute_query_single("SELECT id FROM customers WHERE id = %s", (configured_id,))
|
|
if configured_row:
|
|
return int(configured_row["id"])
|
|
|
|
named_row = execute_query_single(
|
|
"""
|
|
SELECT id
|
|
FROM customers
|
|
WHERE LOWER(name) = LOWER(%s)
|
|
ORDER BY id ASC
|
|
LIMIT 1
|
|
""",
|
|
("BMC Networks",),
|
|
)
|
|
if named_row:
|
|
return int(named_row["id"])
|
|
|
|
fallback = execute_query_single("SELECT id FROM customers ORDER BY id ASC LIMIT 1")
|
|
if fallback:
|
|
return int(fallback["id"])
|
|
|
|
raise HTTPException(status_code=400, detail="No customers available for bug report case creation")
|
|
|
|
|
|
@router.post("/bug-reports", response_model=BugReportResult)
|
|
async def create_bug_report(payload: BugReportPayload, request: Request):
|
|
user_id = getattr(request.state, "user_id", None) or 1
|
|
|
|
_rate_limit(int(user_id))
|
|
|
|
title_seed = (payload.actual or "").strip().splitlines()[0][:80]
|
|
title = f"Bug: {title_seed or 'Ukendt fejl'}"
|
|
|
|
metadata_json = json.dumps(payload.metadata or {}, ensure_ascii=False, indent=2)
|
|
logs_preview = (payload.logs or [])[:50]
|
|
logs_json = json.dumps(logs_preview, ensure_ascii=False, indent=2)
|
|
|
|
description = (
|
|
"## Hvad gik galt\n"
|
|
f"{payload.actual.strip()}\n\n"
|
|
"## Hvad burde være sket\n"
|
|
f"{payload.expected.strip()}\n\n"
|
|
"## Metadata\n"
|
|
f"```json\n{metadata_json}\n```\n\n"
|
|
"## Log preview (seneste 50)\n"
|
|
f"```json\n{logs_json}\n```\n"
|
|
)
|
|
|
|
customer_id = _resolve_customer_id()
|
|
assigned_user_id: Optional[int] = settings.BUG_REPORT_AUTO_ASSIGN_USER_ID
|
|
|
|
created = execute_query(
|
|
"""
|
|
INSERT INTO sag_sager
|
|
(titel, beskrivelse, template_key, status, customer_id, ansvarlig_bruger_id, created_by_user_id)
|
|
VALUES
|
|
(%s, %s, %s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""",
|
|
(
|
|
title,
|
|
description,
|
|
"bug_report",
|
|
"åben",
|
|
customer_id,
|
|
assigned_user_id,
|
|
user_id,
|
|
),
|
|
)
|
|
|
|
if not created:
|
|
raise HTTPException(status_code=500, detail="Failed to create bug case")
|
|
|
|
sag_id = int(created[0]["id"])
|
|
|
|
# Attach screenshot if provided
|
|
if payload.screenshot_base64:
|
|
raw, content_type = _decode_data_url(payload.screenshot_base64)
|
|
if len(raw) > settings.BUG_REPORT_MAX_SCREENSHOT_BYTES:
|
|
raise HTTPException(status_code=400, detail="Screenshot too large")
|
|
|
|
stored_name, size = _store_raw_file(raw, f"bugreport_{sag_id}.png")
|
|
_create_sag_file_record(sag_id, "screenshot.png", content_type, size, stored_name)
|
|
|
|
# Attach logs as json file
|
|
logs_raw = json.dumps(payload.logs or [], ensure_ascii=False, indent=2).encode("utf-8")
|
|
stored_name, size = _store_raw_file(logs_raw, f"bugreport_{sag_id}_logs.json")
|
|
_create_sag_file_record(sag_id, "logs.json", "application/json", size, stored_name)
|
|
|
|
# Optional extra file
|
|
if payload.extra_file_base64 and payload.extra_file_name:
|
|
raw, content_type = _decode_data_url(payload.extra_file_base64)
|
|
if len(raw) > settings.BUG_REPORT_MAX_ATTACHMENT_BYTES:
|
|
raise HTTPException(status_code=400, detail="Extra file too large")
|
|
stored_name, size = _store_raw_file(raw, payload.extra_file_name)
|
|
_create_sag_file_record(sag_id, payload.extra_file_name, content_type, size, stored_name)
|
|
|
|
# Track submission for rate-limiting/audit
|
|
if _table_exists("bug_report_submissions"):
|
|
execute_query(
|
|
"""
|
|
INSERT INTO bug_report_submissions (sag_id, user_id, screenshot_attached)
|
|
VALUES (%s, %s, %s)
|
|
""",
|
|
(sag_id, user_id, bool(payload.screenshot_base64)),
|
|
)
|
|
|
|
logger.info("✅ Bug report case created: SAG-%s by user_id=%s", sag_id, user_id)
|
|
|
|
return BugReportResult(
|
|
success=True,
|
|
sag_id=sag_id,
|
|
case_url=f"/sag/{sag_id}/v3",
|
|
message="Fejl rapporteret og sag oprettet",
|
|
)
|