login_background.py 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. """登录界面自定义背景图接口。
  2. 用户将 background.jpg 或 background.png 放到服务端运行目录
  3. (打包模式为 exe 同级目录,开发模式为仓库根目录)即可让登录界面
  4. 自动展示自定义背景;两个文件同时存在时优先 jpg。
  5. 按主题定制(契约1.31):可选查询参数 theme=blue|red|green 时优先
  6. 返回 background-{theme}.jpg/png,主题图不存在时回落通用 background;
  7. 登录页按当前主题请求,实现每主题一张背景图。
  8. 安全说明:
  9. - 接口公开(登录前无凭证可访问),不返回任何敏感信息;
  10. - 候选文件名为固定白名单(通用 + 三主题),theme 参数经白名单校验,
  11. 不接收路径参数,无目录穿越风险。
  12. """
  13. from __future__ import annotations
  14. import sys
  15. from pathlib import Path
  16. from flask import current_app, request, send_file
  17. from dms.api.v1.blueprint import api_v1
  18. from dms.common.errors import FileNotFoundError as DmsFileNotFoundError
  19. # 主题白名单:界面主题三选一;theme 缺省/非法时仅查通用兜底文件
  20. _BACKGROUND_THEMES: tuple[str, ...] = ("blue", "red", "green")
  21. def _runtime_root() -> Path:
  22. """服务端运行目录,与 launcher._app_root 同语义。
  23. - PyInstaller 打包模式:exe 同级目录(用户可见、可写)
  24. - 开发模式:本文件位于 backend/dms/api/v1/ 下,仓库根在上级第四层
  25. """
  26. if getattr(sys, "frozen", False):
  27. return Path(sys.executable).resolve().parent
  28. return Path(__file__).resolve().parents[4]
  29. def _candidate_paths() -> list[tuple[Path, str]]:
  30. # 测试与特殊部署可通过 DMS_LOGIN_BACKGROUND_DIR 覆盖运行目录
  31. root = Path(
  32. current_app.config.get("DMS_LOGIN_BACKGROUND_DIR") or _runtime_root()
  33. )
  34. theme = (request.args.get("theme") or "").strip().lower()
  35. names: list[tuple[str, str]] = []
  36. if theme in _BACKGROUND_THEMES:
  37. names += [
  38. (f"background-{theme}.jpg", "image/jpeg"),
  39. (f"background-{theme}.png", "image/png"),
  40. ]
  41. # 通用兜底:主题图缺失或未指定 theme 时使用
  42. names += [
  43. ("background.jpg", "image/jpeg"),
  44. ("background.png", "image/png"),
  45. ]
  46. return [(root / name, mime) for name, mime in names]
  47. @api_v1.get("/config/login-background")
  48. def get_login_background():
  49. for path, mime in _candidate_paths():
  50. if path.is_file():
  51. response = send_file(path, mimetype=mime, conditional=True)
  52. response.headers["X-Content-Type-Options"] = "nosniff"
  53. # 背景图变更频率低,允许浏览器短缓存减少首屏请求
  54. response.headers["Cache-Control"] = "public, max-age=60"
  55. return response
  56. raise DmsFileNotFoundError("运行目录下不存在 background.jpg/png 背景图")