config.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. """Where Bambuddy finds the Manyfold install, and how it signs in (#1471).
  2. Manyfold is self-hosted, so unlike MakerWorld there is no fixed host: an admin
  3. enters the install's URL and the client ID and secret of an OAuth application
  4. created in Manyfold (Settings -> API). The three values live in the settings
  5. table under their own keys. They are not part of ``AppSettings``, so the
  6. general settings API never returns them; the secret is never returned at all.
  7. """
  8. from __future__ import annotations
  9. from dataclasses import dataclass
  10. from urllib.parse import urlsplit
  11. from sqlalchemy import select
  12. from sqlalchemy.ext.asyncio import AsyncSession
  13. from backend.app.api.routes._url_safety import assert_safe_lan_service_url
  14. from backend.app.models.settings import Settings
  15. URL_KEY = "manyfold_url"
  16. CLIENT_ID_KEY = "manyfold_client_id"
  17. CLIENT_SECRET_KEY = "manyfold_client_secret"
  18. CONFIG_KEYS = (URL_KEY, CLIENT_ID_KEY, CLIENT_SECRET_KEY)
  19. @dataclass(frozen=True)
  20. class ManyfoldConfig:
  21. url: str = ""
  22. client_id: str = ""
  23. client_secret: str = ""
  24. @property
  25. def configured(self) -> bool:
  26. return bool(self.url and self.client_id and self.client_secret)
  27. def normalize_url(raw: str) -> str:
  28. """The install's base URL without a trailing slash.
  29. Accepts a sub-path (``https://example.com/manyfold``) for installs behind a
  30. reverse proxy. Raises ``ValueError`` for anything that is not a plain
  31. http(s) URL with a host, and, like Spoolman's, for what the LAN-service
  32. tier refuses (cloud-metadata endpoints, numeric-encoded IPs, ...).
  33. Loopback and private addresses stay allowed: Manyfold usually runs on
  34. the same host or LAN.
  35. """
  36. candidate = (raw or "").strip()
  37. try:
  38. parts = urlsplit(candidate)
  39. except ValueError as exc:
  40. raise ValueError("The Manyfold URL is not a valid URL") from exc
  41. if parts.scheme not in ("http", "https") or not parts.hostname:
  42. raise ValueError("The Manyfold URL must start with http:// or https:// and name a host")
  43. if parts.username or parts.password:
  44. raise ValueError("The Manyfold URL must not contain a user name or password")
  45. if parts.query or parts.fragment:
  46. raise ValueError("The Manyfold URL must not contain a query or fragment")
  47. assert_safe_lan_service_url(candidate, label="Manyfold URL")
  48. return f"{parts.scheme}://{parts.netloc}{parts.path.rstrip('/')}"
  49. async def load_config(db: AsyncSession) -> ManyfoldConfig:
  50. rows = await db.execute(select(Settings.key, Settings.value).where(Settings.key.in_(CONFIG_KEYS)))
  51. values = {key: value or "" for key, value in rows.all()}
  52. return ManyfoldConfig(
  53. url=values.get(URL_KEY, ""),
  54. client_id=values.get(CLIENT_ID_KEY, ""),
  55. client_secret=values.get(CLIENT_SECRET_KEY, ""),
  56. )