api_key.py 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. from datetime import datetime
  2. from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, Integer, String, false, func, true
  3. from sqlalchemy.orm import Mapped, mapped_column
  4. from backend.app.core.database import Base
  5. class APIKey(Base):
  6. """API key for external webhook access."""
  7. __tablename__ = "api_keys"
  8. # SQLite would give a freed id to the next row; see core/database AUTOINCREMENT_TABLES
  9. __table_args__ = {"sqlite_autoincrement": True}
  10. id: Mapped[int] = mapped_column(primary_key=True)
  11. name: Mapped[str] = mapped_column(String(100)) # User-friendly name
  12. key_hash: Mapped[str] = mapped_column(String(255)) # bcrypt hash of the key
  13. key_prefix: Mapped[str] = mapped_column(String(20)) # First 8 chars + "..." for display
  14. # Owner — required for new keys, NULL only on legacy rows that predate per-user
  15. # ownership. Cloud routes reject calls from keys without an owner so callers are
  16. # forced to recreate them. CASCADE so deleting a user removes their keys.
  17. user_id: Mapped[int | None] = mapped_column(
  18. Integer,
  19. ForeignKey("users.id", ondelete="CASCADE"),
  20. nullable=True,
  21. index=True,
  22. )
  23. # Permissions
  24. can_queue: Mapped[bool] = mapped_column(Boolean, default=True) # Add to queue
  25. can_control_printer: Mapped[bool] = mapped_column(Boolean, default=False) # Start/stop/cancel
  26. can_read_status: Mapped[bool] = mapped_column(Boolean, default=True) # Query status
  27. can_manage_library: Mapped[bool] = mapped_column(
  28. Boolean, default=True, server_default=true()
  29. ) # Upload/rename/delete own library files + MakerWorld import
  30. can_manage_inventory: Mapped[bool] = mapped_column(
  31. Boolean, default=True, server_default=true()
  32. ) # Inventory write ops (incl. SpoolBuddy kiosk NFC/scale/system)
  33. can_manage_maintenance: Mapped[bool] = mapped_column(
  34. Boolean, default=True, server_default=true()
  35. ) # Log/reset per-printer maintenance, edit intervals, manage the type catalog (#1832 follow-up)
  36. can_manage_archives: Mapped[bool] = mapped_column(
  37. Boolean, default=True, server_default=true()
  38. ) # Create/update/delete print archives (not purge) (#1888)
  39. can_manage_projects: Mapped[bool] = mapped_column(
  40. Boolean, default=True, server_default=true()
  41. ) # Create/update/delete projects + manage membership (add archives) (#1893)
  42. can_access_cloud: Mapped[bool] = mapped_column(
  43. Boolean, default=False, server_default=false()
  44. ) # Read /cloud/* on the owner's behalf
  45. # Narrowly-scoped settings write: only POST /settings/electricity-price.
  46. # Lets HA/Tibber-style automations push dynamic tariff updates without
  47. # granting full SETTINGS_UPDATE (which is denied for API keys because it
  48. # could rewrite SMTP/LDAP/MQTT credentials).
  49. can_update_energy_cost: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false())
  50. # Send a message through the notification channels that accept app
  51. # messages (POST /notifications/app-message), and set the count on the
  52. # sidebar link pointing at the app (PUT /external-links/badge). Nothing
  53. # else: no reading or changing the channels or the links themselves.
  54. can_send_notifications: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false())
  55. # Optional scope limits
  56. printer_ids: Mapped[list | None] = mapped_column(JSON, nullable=True) # null = all printers
  57. enabled: Mapped[bool] = mapped_column(Boolean, default=True)
  58. last_used: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  59. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  60. expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # Optional expiry