Coverage for src / idx_api / models / api_key.py: 81%
27 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:09 -0700
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:09 -0700
1"""API Key model for programmatic authentication."""
3from datetime import datetime
4from typing import TYPE_CHECKING, Optional
6from sqlalchemy import DateTime, ForeignKey, JSON, String, Text
7from sqlalchemy.orm import Mapped, mapped_column, relationship
9from idx_api.models.base import Base, TimestampMixin
11if TYPE_CHECKING:
12 from idx_api.models.brokerage import Brokerage
13 from idx_api.models.user import User
16class APIKey(Base, TimestampMixin):
17 """API key for programmatic access."""
19 __tablename__ = "api_keys"
21 id: Mapped[int] = mapped_column(primary_key=True)
23 # Key storage (NEVER store plaintext keys)
24 key_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
25 key_prefix: Mapped[str] = mapped_column(
26 String(20), nullable=False, index=True
27 ) # e.g., "idx_live_abc"
29 # Ownership
30 user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), index=True)
31 brokerage_id: Mapped[Optional[int]] = mapped_column(ForeignKey("brokerages.id"), index=True)
33 # Metadata
34 name: Mapped[str] = mapped_column(String(200), nullable=False) # Human-friendly name
35 role: Mapped[str] = mapped_column(
36 String(20), nullable=False, default="broker"
37 ) # Role for this key
38 scopes: Mapped[Optional[dict]] = mapped_column(JSON) # Optional permission scopes
40 # Lifecycle
41 expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
42 last_used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
43 revoked_at: Mapped[Optional[datetime]] = mapped_column(
44 DateTime(timezone=True)
45 ) # Soft delete
47 # Relationships
48 user: Mapped[Optional["User"]] = relationship(back_populates="api_keys")
49 brokerage: Mapped[Optional["Brokerage"]] = relationship(back_populates="api_keys")
51 @property
52 def is_active(self) -> bool:
53 """Check if API key is currently active."""
54 if self.revoked_at:
55 return False
56 if self.expires_at and datetime.now() > self.expires_at:
57 return False
58 return True
60 def __repr__(self) -> str:
61 return f"<APIKey(id={self.id}, prefix='{self.key_prefix}', name='{self.name}')>"