bmc_hub/app/bug_reports/backend/router.py

213 lines
6.9 KiB
Python
Raw Normal View History

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",
)