hesabixArc/hesabixAPI/build/lib/adapters/api/v1/auth.py
2025-10-02 03:21:43 +03:30

937 lines
27 KiB
Python

# Removed __future__ annotations to fix OpenAPI schema generation
import datetime
from fastapi import APIRouter, Depends, Request, Query
from fastapi.responses import Response
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.responses import success_response, format_datetime_fields
from app.services.captcha_service import create_captcha
from app.services.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats
from app.services.pdf import PDFService
from .schemas import (
RegisterRequest, LoginRequest, ForgotPasswordRequest, ResetPasswordRequest,
ChangePasswordRequest, CreateApiKeyRequest, QueryInfo, FilterItem,
SuccessResponse, CaptchaResponse, LoginResponse, ApiKeyResponse,
ReferralStatsResponse, UserResponse
)
from app.core.auth_dependency import get_current_user, AuthContext
from app.services.api_key_service import list_personal_keys, create_personal_key, revoke_key
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/captcha",
summary="تولید کپچای عددی",
description="تولید کپچای عددی برای تأیید هویت در عملیات حساس",
response_model=SuccessResponse,
responses={
200: {
"description": "کپچا با موفقیت تولید شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کپچا تولید شد",
"data": {
"captcha_id": "abc123def456",
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"ttl_seconds": 180
}
}
}
}
}
}
)
def generate_captcha(db: Session = Depends(get_db)) -> dict:
captcha_id, image_base64, ttl = create_captcha(db)
return success_response({
"captcha_id": captcha_id,
"image_base64": image_base64,
"ttl_seconds": ttl,
})
@router.get("/me",
summary="دریافت اطلاعات کاربر کنونی",
description="دریافت اطلاعات کامل کاربری که در حال حاضر وارد سیستم شده است",
response_model=SuccessResponse,
responses={
200: {
"description": "اطلاعات کاربر با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "اطلاعات کاربر دریافت شد",
"data": {
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"referred_by_user_id": None,
"app_permissions": {"admin": True},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "احراز هویت مورد نیاز است",
"error_code": "UNAUTHORIZED"
}
}
}
}
}
)
def get_current_user_info(
request: Request,
ctx: AuthContext = Depends(get_current_user)
) -> dict:
"""دریافت اطلاعات کاربر کنونی"""
return success_response(ctx.to_dict(), request)
@router.post("/register",
summary="ثبت‌نام کاربر جدید",
description="ثبت‌نام کاربر جدید در سیستم با تأیید کپچا",
response_model=SuccessResponse,
responses={
200: {
"description": "کاربر با موفقیت ثبت‌نام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "ثبت‌نام با موفقیت انجام شد",
"data": {
"api_key": "sk_1234567890abcdef",
"expires_at": None,
"user": {
"id": 1,
"first_name": "احمد",
"last_name": "احمدی",
"email": "ahmad@example.com",
"mobile": "09123456789",
"referral_code": "ABC123",
"app_permissions": None
}
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
409: {
"description": "کاربر با این ایمیل یا موبایل قبلاً ثبت‌نام کرده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر با این ایمیل قبلاً ثبت‌نام کرده است",
"error_code": "USER_EXISTS"
}
}
}
}
}
)
def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)) -> dict:
user_id = register_user(
db=db,
first_name=payload.first_name,
last_name=payload.last_name,
email=payload.email,
mobile=payload.mobile,
password=payload.password,
captcha_id=payload.captcha_id,
captcha_code=payload.captcha_code,
referrer_code=payload.referrer_code,
)
# Create a session api key similar to login
user_agent = request.headers.get("User-Agent")
ip = request.client.host if request.client else None
from app.core.security import generate_api_key
from adapters.db.repositories.api_key_repo import ApiKeyRepository
api_key, key_hash = generate_api_key()
api_repo = ApiKeyRepository(db)
api_repo.create_session_key(user_id=user_id, key_hash=key_hash, device_id=payload.device_id, user_agent=user_agent, ip=ip, expires_at=None)
from adapters.db.models.user import User
user_obj = db.get(User, user_id)
user = {"id": user_id, "first_name": payload.first_name, "last_name": payload.last_name, "email": payload.email, "mobile": payload.mobile, "referral_code": getattr(user_obj, "referral_code", None), "app_permissions": getattr(user_obj, "app_permissions", None)}
response_data = {"api_key": api_key, "expires_at": None, "user": user}
formatted_data = format_datetime_fields(response_data, request)
return success_response(formatted_data, request)
@router.post("/login",
summary="ورود با ایمیل یا موبایل",
description="ورود کاربر به سیستم با استفاده از ایمیل یا شماره موبایل و رمز عبور",
response_model=SuccessResponse,
responses={
200: {
"description": "ورود با موفقیت انجام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "ورود با موفقیت انجام شد",
"data": {
"api_key": "sk_1234567890abcdef",
"expires_at": "2024-01-02T00:00:00Z",
"user": {
"id": 1,
"first_name": "احمد",
"last_name": "احمدی",
"email": "ahmad@example.com",
"mobile": "09123456789",
"referral_code": "ABC123",
"app_permissions": {"admin": True}
}
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
401: {
"description": "اطلاعات ورود نامعتبر است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "ایمیل یا رمز عبور اشتباه است",
"error_code": "INVALID_CREDENTIALS"
}
}
}
}
}
)
def login(request: Request, payload: LoginRequest, db: Session = Depends(get_db)) -> dict:
user_agent = request.headers.get("User-Agent")
ip = request.client.host if request.client else None
api_key, expires_at, user = login_user(
db=db,
identifier=payload.identifier,
password=payload.password,
captcha_id=payload.captcha_id,
captcha_code=payload.captcha_code,
device_id=payload.device_id,
user_agent=user_agent,
ip=ip,
)
# Ensure referral_code is included
from adapters.db.repositories.user_repo import UserRepository
repo = UserRepository(db)
from adapters.db.models.user import User
user_obj = None
if 'id' in user and user['id']:
user_obj = repo.db.get(User, user['id'])
if user_obj is not None:
user["referral_code"] = getattr(user_obj, "referral_code", None)
response_data = {"api_key": api_key, "expires_at": expires_at, "user": user}
formatted_data = format_datetime_fields(response_data, request)
return success_response(formatted_data, request)
@router.post("/forgot-password",
summary="ایجاد توکن بازنشانی رمز عبور",
description="ایجاد توکن برای بازنشانی رمز عبور کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "توکن بازنشانی با موفقیت ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "توکن بازنشانی ارسال شد",
"data": {
"ok": True,
"token": "reset_token_1234567890abcdef"
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر با این ایمیل یا موبایل یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
}
}
)
def forgot_password(payload: ForgotPasswordRequest, db: Session = Depends(get_db)) -> dict:
# In production do not return token; send via email/SMS. Here we return for dev/testing.
token = create_password_reset(db=db, identifier=payload.identifier, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code)
return success_response({"ok": True, "token": token if token else None})
@router.post("/reset-password",
summary="بازنشانی رمز عبور با توکن",
description="بازنشانی رمز عبور کاربر با استفاده از توکن دریافتی",
response_model=SuccessResponse,
responses={
200: {
"description": "رمز عبور با موفقیت بازنشانی شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "رمز عبور با موفقیت تغییر کرد",
"data": {
"ok": True
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کپچا نامعتبر است",
"error_code": "INVALID_CAPTCHA"
}
}
}
},
404: {
"description": "توکن نامعتبر یا منقضی شده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "توکن نامعتبر یا منقضی شده است",
"error_code": "INVALID_TOKEN"
}
}
}
}
}
)
def reset_password_endpoint(payload: ResetPasswordRequest, db: Session = Depends(get_db)) -> dict:
reset_password(db=db, token=payload.token, new_password=payload.new_password, captcha_id=payload.captcha_id, captcha_code=payload.captcha_code)
return success_response({"ok": True})
@router.get("/api-keys",
summary="لیست کلیدهای API شخصی",
description="دریافت لیست کلیدهای API شخصی کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کلیدهای API با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کلیدهای API دریافت شد",
"data": [
{
"id": 1,
"name": "کلید اصلی",
"scopes": "read,write",
"device_id": "device123",
"user_agent": "Mozilla/5.0...",
"ip": "192.168.1.1",
"expires_at": None,
"last_used_at": "2024-01-01T12:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def list_keys(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
items = list_personal_keys(db, ctx.user.id)
return success_response(items)
@router.post("/api-keys",
summary="ایجاد کلید API شخصی",
description="ایجاد کلید API جدید برای کاربر",
response_model=SuccessResponse,
responses={
200: {
"description": "کلید API با موفقیت ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کلید API ایجاد شد",
"data": {
"id": 1,
"api_key": "sk_1234567890abcdef"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def create_key(request: Request, payload: CreateApiKeyRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
id_, api_key = create_personal_key(db, ctx.user.id, payload.name, payload.scopes, None)
return success_response({"id": id_, "api_key": api_key})
@router.post("/change-password",
summary="تغییر رمز عبور",
description="تغییر رمز عبور کاربر با تأیید رمز عبور فعلی",
response_model=SuccessResponse,
responses={
200: {
"description": "رمز عبور با موفقیت تغییر کرد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "رمز عبور با موفقیت تغییر کرد",
"data": {
"ok": True
}
}
}
}
},
400: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "رمز عبور فعلی اشتباه است",
"error_code": "INVALID_CURRENT_PASSWORD"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def change_password_endpoint(request: Request, payload: ChangePasswordRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
# دریافت translator از request state
translator = getattr(request.state, "translator", None)
change_password(
db=db,
user_id=ctx.user.id,
current_password=payload.current_password,
new_password=payload.new_password,
confirm_password=payload.confirm_password,
translator=translator
)
return success_response({"ok": True})
@router.delete("/api-keys/{key_id}",
summary="حذف کلید API",
description="حذف کلید API مشخص شده",
response_model=SuccessResponse,
responses={
200: {
"description": "کلید API با موفقیت حذف شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کلید API حذف شد",
"data": {
"ok": True
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کلید API یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کلید API یافت نشد",
"error_code": "API_KEY_NOT_FOUND"
}
}
}
}
}
)
def delete_key(request: Request, key_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
revoke_key(db, ctx.user.id, key_id)
return success_response({"ok": True})
@router.get("/referrals/stats",
summary="آمار معرفی‌ها",
description="دریافت آمار معرفی‌های کاربر فعلی",
response_model=SuccessResponse,
responses={
200: {
"description": "آمار معرفی‌ها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "آمار معرفی‌ها دریافت شد",
"data": {
"total_referrals": 25,
"active_referrals": 20,
"recent_referrals": 5,
"referral_rate": 0.8
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def get_referral_stats(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), start: str = Query(None, description="تاریخ شروع (ISO format)"), end: str = Query(None, description="تاریخ پایان (ISO format)")):
from datetime import datetime
start_dt = datetime.fromisoformat(start) if start else None
end_dt = datetime.fromisoformat(end) if end else None
stats = referral_stats(db=db, user_id=ctx.user.id, start=start_dt, end=end_dt)
return success_response(stats)
@router.post("/referrals/list",
summary="لیست معرفی‌ها با فیلتر پیشرفته",
description="دریافت لیست معرفی‌ها با قابلیت فیلتر، جستجو، مرتب‌سازی و صفحه‌بندی",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست معرفی‌ها با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست معرفی‌ها دریافت شد",
"data": {
"items": [
{
"id": 1,
"first_name": "علی",
"last_name": "احمدی",
"email": "ali@example.com",
"mobile": "09123456789",
"created_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 10,
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def get_referral_list_advanced(
request: Request,
query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> dict:
"""
دریافت لیست معرفی‌ها با قابلیت فیلتر پیشرفته
پارامترهای QueryInfo:
- sort_by: فیلد مرتب‌سازی (مثال: created_at, first_name, last_name, email)
- sort_desc: ترتیب نزولی (true/false)
- take: تعداد رکورد در هر صفحه (پیش‌فرض: 10)
- skip: تعداد رکورد صرف‌نظر شده (پیش‌فرض: 0)
- search: عبارت جستجو
- search_fields: فیلدهای جستجو (مثال: ["first_name", "last_name", "email"])
- filters: آرایه فیلترها با ساختار:
[
{
"property": "created_at",
"operator": ">=",
"value": "2024-01-01T00:00:00"
},
{
"property": "first_name",
"operator": "*",
"value": "احمد"
}
]
"""
from adapters.db.repositories.user_repo import UserRepository
from adapters.db.models.user import User
from datetime import datetime
# Create a custom query for referrals
repo = UserRepository(db)
# Add filter for referrals only (users with referred_by_user_id = current user)
referral_filter = FilterItem(
property="referred_by_user_id",
operator="=",
value=ctx.user.id
)
# Add referral filter to existing filters
if query_info.filters is None:
query_info.filters = [referral_filter]
else:
query_info.filters.append(referral_filter)
# Set default search fields for referrals
if query_info.search_fields is None:
query_info.search_fields = ["first_name", "last_name", "email"]
# Execute query with filters
referrals, total = repo.query_with_filters(query_info)
# Convert to dictionary format
referral_dicts = [repo.to_dict(referral) for referral in referrals]
# Format datetime fields
formatted_referrals = format_datetime_fields(referral_dicts, request)
# Calculate pagination info
page = (query_info.skip // query_info.take) + 1
total_pages = (total + query_info.take - 1) // query_info.take
return success_response({
"items": formatted_referrals,
"total": total,
"page": page,
"limit": query_info.take,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
}, request)
@router.post("/referrals/export/pdf",
summary="خروجی PDF لیست معرفی‌ها",
description="خروجی PDF لیست معرفی‌ها با قابلیت فیلتر و انتخاب سطرهای خاص",
responses={
200: {
"description": "فایل PDF با موفقیت تولید شد",
"content": {
"application/pdf": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def export_referrals_pdf(
request: Request,
query_info: QueryInfo,
selected_only: bool = False,
selected_indices: str | None = None,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> Response:
"""
خروجی PDF لیست معرفی‌ها
پارامترها:
- selected_only: آیا فقط سطرهای انتخاب شده export شوند
- selected_indices: لیست ایندکس‌های انتخاب شده (JSON string)
- سایر پارامترهای QueryInfo برای فیلتر
"""
from app.services.pdf import PDFService
from app.services.auth_service import referral_stats
import json
# Parse selected indices if provided
indices = None
if selected_only and selected_indices:
try:
indices = json.loads(selected_indices)
except (json.JSONDecodeError, TypeError):
indices = None
# Get stats for the report
stats = None
try:
# Extract date range from filters if available
start_date = None
end_date = None
if query_info.filters:
for filter_item in query_info.filters:
if filter_item.property == 'created_at':
if filter_item.operator == '>=':
start_date = filter_item.value
elif filter_item.operator == '<':
end_date = filter_item.value
stats = referral_stats(
db=db,
user_id=ctx.user.id,
start=start_date,
end=end_date
)
except Exception:
pass # Continue without stats
# Get calendar type from request headers
calendar_header = request.headers.get("X-Calendar-Type", "jalali")
calendar_type = "jalali" if calendar_header.lower() in ["jalali", "persian", "shamsi"] else "gregorian"
# Generate PDF using new modular service
pdf_service = PDFService()
# Get locale from request headers
locale_header = request.headers.get("Accept-Language", "fa")
locale = "fa" if locale_header.startswith("fa") else "en"
pdf_bytes = pdf_service.generate_pdf(
module_name='marketing',
data={}, # Empty data - module will fetch its own data
calendar_type=calendar_type,
locale=locale,
db=db,
user_id=ctx.user.id,
query_info=query_info,
selected_indices=indices,
stats=stats
)
# Return PDF response
from fastapi.responses import Response
import datetime
filename = f"referrals_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(pdf_bytes))
}
)
@router.post("/referrals/export/excel",
summary="خروجی Excel لیست معرفی‌ها",
description="خروجی Excel لیست معرفی‌ها با قابلیت فیلتر و انتخاب سطرهای خاص",
responses={
200: {
"description": "فایل Excel با موفقیت تولید شد",
"content": {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
}
}
)
def export_referrals_excel(
request: Request,
query_info: QueryInfo,
selected_only: bool = False,
selected_indices: str | None = None,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
) -> Response:
"""
خروجی Excel لیست معرفی‌ها (فایل Excel واقعی برای دانلود)
پارامترها:
- selected_only: آیا فقط سطرهای انتخاب شده export شوند
- selected_indices: لیست ایندکس‌های انتخاب شده (JSON string)
- سایر پارامترهای QueryInfo برای فیلتر
"""
from app.services.pdf import PDFService
import json
import io
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
# Parse selected indices if provided
indices = None
if selected_only and selected_indices:
try:
indices = json.loads(selected_indices)
except (json.JSONDecodeError, TypeError):
indices = None
# Get calendar type from request headers
calendar_header = request.headers.get("X-Calendar-Type", "jalali")
calendar_type = "jalali" if calendar_header.lower() in ["jalali", "persian", "shamsi"] else "gregorian"
# Generate Excel data using new modular service
pdf_service = PDFService()
# Get locale from request headers
locale_header = request.headers.get("Accept-Language", "fa")
locale = "fa" if locale_header.startswith("fa") else "en"
excel_data = pdf_service.generate_excel_data(
module_name='marketing',
data={}, # Empty data - module will fetch its own data
calendar_type=calendar_type,
locale=locale,
db=db,
user_id=ctx.user.id,
query_info=query_info,
selected_indices=indices
)
# Create Excel workbook
wb = Workbook()
ws = wb.active
ws.title = "Referrals"
# Define styles
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_alignment = Alignment(horizontal="center", vertical="center")
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# Add headers
if excel_data:
headers = list(excel_data[0].keys())
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = border
# Add data rows
for row, data in enumerate(excel_data, 2):
for col, header in enumerate(headers, 1):
cell = ws.cell(row=row, column=col, value=data.get(header, ""))
cell.border = border
# Center align for numbers and dates
if header in ["ردیف", "Row", "تاریخ ثبت", "Registration Date"]:
cell.alignment = Alignment(horizontal="center")
# Auto-adjust column widths
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
# Save to BytesIO
excel_buffer = io.BytesIO()
wb.save(excel_buffer)
excel_buffer.seek(0)
# Generate filename
filename = f"referrals_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
# Return Excel file as response
return Response(
content=excel_buffer.getvalue(),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
}
)