Files
devplacepy/devplacepy/routers/media.py
T
retoor 19cc85f409 fix: correct audit event names, attachment deletion, redirect safety, and add account deactivation check
- Fix audit event names in CLI prune/clear commands from `cli.seo.*` to `cli.seo_meta.*`
- Refactor `_delete_attachment_file` to accept full attachment dict instead of storage_path string, using directory and stored_name fields with ATTACHMENTS_DIR
- Add `safe_next` validation for referer header in validation error redirect and media redirect
- Add `is_active` check in login router to reject deactivated accounts with "Account is deactivated" error
- Replace raw `request.headers.get("Referer")` with `redirect_back()` utility in bookmarks, polls, reactions, and votes routers
- Move `mark_conversation_read` call from `get_conversation_messages` to `messages_page` to avoid side effects during message retrieval
- Fix poll audit link to use `option.get("label")` instead of `option.get("text")`
- Add `VOTABLE` set validation in votes router to reject invalid target types with 400 response
- Strip control characters (0x00-0x20) from URLs in `_safe_url` instead of simple strip
- Add `__getattr__` fallback in services `__init__.py` for dynamic attribute access
2026-06-21 16:46:27 +00:00

77 lines
2.8 KiB
Python

# retoor <retoor@molodetz.nl>
import logging
from fastapi import APIRouter, Request
from devplacepy.database import get_table
from devplacepy.attachments import soft_delete_attachment, restore_attachment
from devplacepy.utils import require_user, require_admin, is_admin, not_found, safe_next
from devplacepy.responses import action_result, wants_json, json_error
from devplacepy.services.audit import record as audit
logger = logging.getLogger(__name__)
router = APIRouter()
def _media_redirect(request: Request, attachment: dict) -> str:
referer = request.headers.get("referer", "")
if referer and safe_next(referer, "") == referer:
return referer
uploader = get_table("users").find_one(uid=attachment.get("user_uid"))
if uploader:
return f"/profile/{uploader['username']}?tab=media"
return "/feed"
@router.post("/{uid}/delete")
async def delete_media(request: Request, uid: str):
user = require_user(request)
attachment = get_table("attachments").find_one(uid=uid)
if not attachment:
raise not_found("Media not found")
if attachment.get("user_uid") != user["uid"] and not is_admin(user):
audit.record(
request,
"attachment.delete",
user=user,
result="denied",
target_type="attachment",
target_uid=uid,
target_label=attachment.get("filename"),
summary=f"user {user['username']} denied delete of attachment {uid}",
links=[audit.attachment_link(uid, attachment.get("filename"))],
)
if wants_json(request):
return json_error(403, "Not allowed")
return action_result(request, _media_redirect(request, attachment))
soft_delete_attachment(uid, deleted_by=user["uid"])
logger.info(f"Media {uid} soft-deleted by {user['username']}")
audit.record(
request,
"attachment.delete",
user=user,
target_type="attachment",
target_uid=uid,
target_label=attachment.get("filename"),
summary=f"user {user['username']} deleted attachment {attachment.get('filename') or uid}",
links=[audit.attachment_link(uid, attachment.get("filename"))],
)
return action_result(request, _media_redirect(request, attachment))
@router.post("/{uid}/restore")
async def restore_media(request: Request, uid: str):
admin = require_admin(request)
restore_attachment(uid)
logger.info(f"Media {uid} restored by {admin['username']}")
audit.record(
request,
"attachment.restore",
user=admin,
target_type="attachment",
target_uid=uid,
target_label=uid,
summary=f"admin {admin['username']} restored attachment {uid}",
links=[audit.attachment_link(uid)],
)
return action_result(request, "/admin/media")