journal: paste image from clipboard, cap embedded image size, fix media persistence

- Paste-to-embed: pasting an image into the markdown editor uploads it and
  inserts ![](url) at the cursor. Unlike gallery attachments these aren't
  tied to a journal_entry (the entry may not exist yet while composing), so
  they're stored per-user under app/media/journal/{user_id}/pasted/ with no
  DB row, served through an ownership-scoped route, and never cleaned up
  automatically when an entry is deleted -- an accepted tradeoff at this
  app's personal scale.
- The markdown sanitizer was stripping all <img> tags (not on the bleach
  allowlist), which would have silently deleted every pasted image on save;
  added img/src/alt/title while keeping event-handler attributes blocked.
- Cap embedded image width in both the editor pane and the rendered preview
  so a large pasted photo can't overflow its card.
- Fix real data loss risk found while testing this: docker-compose.yml had
  no volume for app/media, so every container recreate during a deploy wiped
  uploaded photos, and deploy_sftp.py was syncing app/media/ (runtime user
  data, not source) into the remote build context. Added the volume mount
  and excluded media/ from the sync script. Recovered and relocated the
  real attachments that had already landed in the wrong place on the NAS
  during earlier deploys this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 14:27:30 +09:00
co-authored by Claude Sonnet 5
parent bdf9d0bae7
commit 00c66f9df8
10 changed files with 224 additions and 8 deletions
+10 -3
View File
@@ -35,12 +35,19 @@ def test_javascript_href_is_neutralized():
assert "javascript:" not in html
def test_onerror_attribute_is_stripped():
html = render_markdown('<img src=x onerror="alert(1)">본문')
def test_onerror_attribute_is_stripped_even_though_img_is_allowed():
html = render_markdown('<img src="x.png" onerror="alert(1)">본문')
assert "onerror" not in html
assert "<img" not in html # img는 허용 태그 목록에 없음
assert '<img src="x.png">' in html # img 자체는 허용되지만 onerror 같은 이벤트 속성은 지워져야 함
def test_allowed_link_href_is_preserved():
html = render_markdown("[내 블로그](https://example.com)")
assert 'href="https://example.com"' in html
def test_pasted_image_markdown_renders_with_relative_src():
# 붙여넣은 이미지는 절대 URL이 아니라 /api/journal/pasted-media/... 같은 상대 경로로 참조된다 —
# bleach가 스킴 없는 상대 경로도 그대로 통과시키는지 확인.
html = render_markdown("![](/api/journal/pasted-media/abc123.png)")
assert 'src="/api/journal/pasted-media/abc123.png"' in html