api_key.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  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). Nothing else: no reading or
  52. # changing the channels themselves.
  53. can_send_notifications: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false())
  54. # Optional scope limits
  55. printer_ids: Mapped[list | None] = mapped_column(JSON, nullable=True) # null = all printers
  56. enabled: Mapped[bool] = mapped_column(Boolean, default=True)
  57. last_used: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  58. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  59. expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) # Optional expiry