Files
life-echo/api/app/features/auth/preset_avatars.py
Kevin 7ad52fce89 feat(profile): avatar presets, upload, nickname editing
- FastAPI: preset assets 01–08, GET list/static, PUT /me/avatar/preset,
  safer uploaded-avatar path validation, preset_avatars + HTTP tests.
- Expo: personal-info (library + presets), profile tab avatar,
  resolveApiMediaUrl, auth hooks cache sync, Web multipart helper,
  partial-save messaging + profile i18n.
- Includes existing edits to conversation screen and voice use-player.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 13:51:43 +08:00

69 lines
2.1 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.
"""服务端托管的预设头像(白名单文件名)。"""
from __future__ import annotations
from pathlib import Path
_PRESETS_DIR = Path(__file__).resolve().parent / "avatar_presets"
# 与仓库内静态文件一致01.png … 08.png
ALLOWED_PRESET_FILENAMES: frozenset[str] = frozenset(f"{i:02d}.png" for i in range(1, 9))
PRESET_IDS: tuple[str, ...] = tuple(f"{i:02d}" for i in range(1, 9))
def preset_filename_for_id(preset_id: str) -> str | None:
"""preset_id 形如 \"01\",返回 \"01.png\";非法则 None。"""
stripped = preset_id.strip()
name = f"{stripped}.png"
if name in ALLOWED_PRESET_FILENAMES:
return name
return None
def avatar_url_for_preset_filename(filename: str) -> str:
return f"/api/auth/avatar-presets/{filename}"
def list_preset_items() -> list[tuple[str, str]]:
"""(preset_id, avatar_url) 列表,供 GET /avatar-presets。"""
return [
(pid, avatar_url_for_preset_filename(f"{pid}.png")) for pid in PRESET_IDS
]
def preset_file_path(filename: str) -> Path | None:
if filename not in ALLOWED_PRESET_FILENAMES:
return None
path = (_PRESETS_DIR / filename).resolve()
try:
path.relative_to(_PRESETS_DIR.resolve())
except ValueError:
return None
return path
def _avatar_upload_stem_allowed(stem: str) -> bool:
"""允许 UUID、数字 ID、含连字符/下划线的安全文件名主干。"""
if not stem or len(stem) > 128:
return False
return all(c.isalnum() or c in "-_" for c in stem)
def safe_avatar_upload_path(filename: str, avatar_dir: Path) -> Path | None:
"""用户上传头像文件名形如 {user_id}.jpg防路径穿越。"""
if "/" in filename or "\\" in filename or ".." in filename:
return None
if not filename.endswith(".jpg"):
return None
stem = filename[:-4]
if not _avatar_upload_stem_allowed(stem):
return None
base = avatar_dir.resolve()
path = (avatar_dir / filename).resolve()
try:
path.relative_to(base)
except ValueError:
return None
return path