feat(pat): add personal access token storage and settings

A personal_access_tokens table (migration 0032) holds scoped user-level API
credentials. Only the SHA-256 of the secret is stored, like device session
tokens. Lookups exclude revoked and expired tokens and the tokens of
deactivated users. PAT_* settings cover the feature switch, default and
maximum lifetime, the operator opt-in for non-expiring tokens and the
per-user cap.
This commit is contained in:
arc53-machine committed 2026-09-19 23:39:58 +01:00
1 parent 0952a05aab
commit 22ecc0aee3
7 files changed
+494

No files matched your search

+41
View File
@@ -1079,3 +1079,44 @@ device_auto_approve_patterns_table = Table(
Column("created_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
UniqueConstraint("device_id", "user_id", "pattern", name="device_auto_approve_uidx"),
)
# --- Personal access tokens (migration 0032) --------------------------------
# Scoped user-level API credentials. Only the SHA-256 of the secret is stored.
personal_access_tokens_table = Table(
"personal_access_tokens",
metadata,
Column("id", UUID(as_uuid=True), primary_key=True, server_default=func.gen_random_uuid()),
Column("user_id", Text, nullable=False),
Column("name", Text, nullable=False),
Column("token_hash", Text, nullable=False),
Column("token_prefix", Text, nullable=False),
Column("scopes", ARRAY(Text), nullable=False, server_default="{}"),
Column("resource_filter", JSONB, nullable=False, server_default=text("'{}'::jsonb")),
Column("status", Text, nullable=False, server_default="active"),
Column("expires_at", DateTime(timezone=True)),
Column("last_used_at", DateTime(timezone=True)),
Column("last_used_ip", Text),
Column("created_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
Column("revoked_at", DateTime(timezone=True)),
Column("revoke_reason", Text),
CheckConstraint("status IN ('active', 'revoked')", name="personal_access_tokens_status_check"),
)
Index(
"personal_access_tokens_hash_uidx",
personal_access_tokens_table.c.token_hash,
unique=True,
)
Index(
"personal_access_tokens_user_name_uidx",
personal_access_tokens_table.c.user_id,
personal_access_tokens_table.c.name,
unique=True,
postgresql_where=personal_access_tokens_table.c.status == "active",
)
Index(
"personal_access_tokens_user_idx",
personal_access_tokens_table.c.user_id,
personal_access_tokens_table.c.created_at.desc(),
)