1
0
Fork 0
mirror of https://github.com/EDeev/tablo.git synced 2026-10-07 20:49:31 +03:00
tablo/app/routes/export.py
Egor Deev 164623417c Безопасность: права на экспорт, обязательный SECRET_KEY, ошибки ИИ не показываются пользователю
- экспорт в JSON/CSV/PDF/PNG пускал любого вошедшего к любому расписанию, у которого
  есть хоть одна ссылка-приглашение (достаточно перебрать id); теперь — только владелец
  и редакторы, как и просмотр по id;
- три копии проверки прав сведены в app/access.py;
- без SECRET_KEY приложение не запускается (кроме отладки и тестов): со значением по
  умолчанию cookie сессии можно было подделать и войти под любым пользователем;
- текст исключения от ИИ-провайдера больше не уходит в интерфейс, ошибка пишется в лог;
- DATABASE_URL как альтернатива DB_*; debug в run.py — из FLASK_DEBUG;
- неиспользуемые импорты и переменные удалены (ruff).
2026-10-02 13:10:20 +00:00

87 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import urllib.parse
from flask import Blueprint, Response, redirect, url_for, flash
from flask_login import login_required
from app.models.schedule import Schedule
from app.access import can_view as _can_view
bp = Blueprint('export', __name__)
@bp.route('/schedules/<int:schedule_id>/export/json')
@login_required
def export_json(schedule_id: int):
schedule = Schedule.query.get_or_404(schedule_id)
if not _can_view(schedule):
flash('Нет доступа', 'danger')
return redirect(url_for('schedules.profile'))
from app.services.export import export_json as svc_json
data = svc_json(schedule)
filename = _safe_name(schedule.name) + '.json'
return Response(
data,
mimetype='application/json',
headers={'Content-Disposition': _cd(filename)},
)
@bp.route('/schedules/<int:schedule_id>/export/csv')
@login_required
def export_csv(schedule_id: int):
schedule = Schedule.query.get_or_404(schedule_id)
if not _can_view(schedule):
flash('Нет доступа', 'danger')
return redirect(url_for('schedules.profile'))
from app.services.export import export_csv as svc_csv
data = svc_csv(schedule)
filename = _safe_name(schedule.name) + '.csv'
return Response(
data,
mimetype='text/csv; charset=utf-8-sig',
headers={'Content-Disposition': _cd(filename)},
)
@bp.route('/schedules/<int:schedule_id>/export/pdf')
@login_required
def export_pdf(schedule_id: int):
schedule = Schedule.query.get_or_404(schedule_id)
if not _can_view(schedule):
flash('Нет доступа', 'danger')
return redirect(url_for('schedules.profile'))
from app.services.export import export_pdf as svc_pdf
data = svc_pdf(schedule)
filename = _safe_name(schedule.name) + '.pdf'
return Response(
data,
mimetype='application/pdf',
headers={'Content-Disposition': _cd(filename)},
)
@bp.route('/schedules/<int:schedule_id>/export/png')
@login_required
def export_png(schedule_id: int):
schedule = Schedule.query.get_or_404(schedule_id)
if not _can_view(schedule):
flash('Нет доступа', 'danger')
return redirect(url_for('schedules.profile'))
from app.services.export import export_png as svc_png
data = svc_png(schedule)
filename = _safe_name(schedule.name) + '.png'
return Response(
data,
mimetype='image/png',
headers={'Content-Disposition': _cd(filename)},
)
def _safe_name(name: str) -> str:
safe = ''.join(c if c.isalnum() or c in ' ._-' else '_' for c in name)
return safe.strip() or 'schedule'
def _cd(filename: str) -> str:
"""Content-Disposition с поддержкой Unicode (RFC 5987)."""
ascii_name = filename.encode('ascii', 'replace').decode('ascii')
utf8_name = urllib.parse.quote(filename, safe='')
return f"attachment; filename=\"{ascii_name}\"; filename*=UTF-8''{utf8_name}"