@router.get("/list") async def list_phones(active_only: bool = Query(False), search: str = Query(""), page: int = Query(1, ge=1), page_size: int = Query(200, ge=1, le=500), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): q = select(CompanyPhoneNumber).where(CompanyPhoneNumber.user_id == user.id) if active_only: q = q.where(CompanyPhoneNumber.is_active == True) # noqa: E712 if search: q = q.where(CompanyPhoneNumber.phone.ilike(f"%{search}%") | CompanyPhoneNumber.label.ilike(f"%{search}%")) total = (await db.execute( select(func.count()).select_from(q.subquery()))).scalar() or 0 rows = (await db.execute( q.order_by(CompanyPhoneNumber.id.desc()).offset((page - 1) * page_size).limit(page_size) )).scalars().all() return {"phones": [_serialize(p) for p in rows], "total": total, "page": page, "page_size": page_size} @router.post("/add") async def add_phone(payload: PhoneIn, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), _: None = Depends(require_csrf)): """Add one phone number manually.""" dup = await db.execute( select(CompanyPhoneNumber).where(CompanyPhoneNumber.user_id == user.id, CompanyPhoneNumber.phone == payload.phone)) if dup.first(): raise HTTPException(status_code=409, detail="Phone number already exists") p = CompanyPhoneNumber(user_id=user.id, phone=payload.phone, label=payload.label.strip()) db.add(p) await db.commit() await log_audit(db, user.id, "phone.add", "company_phone", p.id, {"label": p.label}) return _serialize(p) @router.post("/import") ===ATT-DL=== @router.get("/{att_id}") async def get_attachment(att_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): att = await _get_own(db, user, att_id) out = _serialize(att) if att.is_html_attachment: out["html_body"] = att.html_attachment_body return out @router.get("/{att_id}/download") async def download_attachment(att_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): att = await _get_own(db, user, att_id) if att.is_html_attachment or not att.stored_path: # inline HTML attachment: return as a file download from fastapi.responses import Response content = (att.html_attachment_body or "").encode("utf-8") return Response(content, media_type="text/html", headers={"Content-Disposition": f'attachment; filename="{att.filename}"'}) loop = asyncio.get_event_loop() path = await loop.run_in_executor(None, att_svc.safe_join, att.stored_path) if not path: raise HTTPException(status_code=404, detail="Stored file missing") return FileResponse(path, filename=att.filename, media_type=att.mime_type or "application/octet-stream") @router.get("/{att_id}/preview") async def preview_attachment(att_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): """Preview: images and PDFs return base64 data URI; text/html return content.""" att = await _get_own(db, user, att_id) if att.is_html_attachment: body = att.html_attachment_body or "" return {"previewable": True, "kind": "html", "content": sanitize_preview(body)[:50000]} content = await asyncio.get_event_loop().run_in_executor( None, lambda: att_svc.read_attachment_bytes(att) or b"") if not content: return {"previewable": False, "reason": "no_content"} if len(content) > 2 * 1024 * 1024: return {"previewable": False, "reason": "too_large", "size": len(content)} if att.kind in ("image", "pdf"): mime = att.mime_type or ("application/pdf" if att.kind == "pdf" else "image/png") return {"previewable": True, "kind": att.kind, "data_uri": f"data:{mime};base64,{base64.b64encode(content).decode()}"} if att.kind in ("text", "html", "file"): try: text = content.decode("utf-8") except UnicodeDecodeError: return {"previewable": False, "reason": "binary"} if att.mime_type == "text/html": text = sanitize_preview(text) return {"previewable": True, "kind": "text", "content": text[:20000]} return {"previewable": False, "reason": "unsupported_kind"} @router.post("/{att_id}/delete")