mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
feat: add PAT storage and API access fields
This commit is contained in:
parent
7b981b6d16
commit
4463990ca4
4 changed files with 136 additions and 0 deletions
|
|
@ -84,6 +84,9 @@ SECRET_KEY=SECRET
|
|||
# JWT Token Lifetimes (optional, defaults shown)
|
||||
# ACCESS_TOKEN_LIFETIME_SECONDS=86400 # 1 day
|
||||
# REFRESH_TOKEN_LIFETIME_SECONDS=1209600 # 2 weeks
|
||||
# Personal Access Tokens (PATs). Empty/unset = no maximum; users may create
|
||||
# never-expiring PATs. When set, PAT creation requires an expiry <= this many days.
|
||||
# PAT_MAX_EXPIRY_DAYS=
|
||||
|
||||
NEXT_FRONTEND_URL=http://localhost:3000
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
"""Add personal access tokens and search-space API access gate.
|
||||
|
||||
Revision ID: 166
|
||||
Revises: 165
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "166"
|
||||
down_revision: str | None = "165"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS personal_access_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL,
|
||||
token_prefix VARCHAR(16) NOT NULL,
|
||||
label VARCHAR NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE,
|
||||
last_used_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS ix_personal_access_tokens_token_hash "
|
||||
"ON personal_access_tokens (token_hash)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_personal_access_tokens_user_id "
|
||||
"ON personal_access_tokens (user_id)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_personal_access_tokens_id "
|
||||
"ON personal_access_tokens (id)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_personal_access_tokens_created_at "
|
||||
"ON personal_access_tokens (created_at)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_personal_access_tokens_expires_at "
|
||||
"ON personal_access_tokens (expires_at)"
|
||||
)
|
||||
|
||||
bind = op.get_bind()
|
||||
api_access_column_exists = bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'searchspaces'
|
||||
AND column_name = 'api_access_enabled'
|
||||
)
|
||||
"""
|
||||
)
|
||||
).scalar()
|
||||
|
||||
op.execute(
|
||||
"ALTER TABLE searchspaces ADD COLUMN IF NOT EXISTS "
|
||||
"api_access_enabled BOOLEAN NOT NULL DEFAULT false"
|
||||
)
|
||||
|
||||
if not api_access_column_exists:
|
||||
op.execute("UPDATE searchspaces SET api_access_enabled = true")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE searchspaces DROP COLUMN IF EXISTS api_access_enabled"
|
||||
)
|
||||
op.execute("DROP TABLE IF EXISTS personal_access_tokens")
|
||||
|
|
@ -919,6 +919,10 @@ class Config:
|
|||
REFRESH_TOKEN_LIFETIME_SECONDS = int(
|
||||
os.getenv("REFRESH_TOKEN_LIFETIME_SECONDS", str(14 * 24 * 60 * 60)) # 2 weeks
|
||||
)
|
||||
_PAT_MAX_EXPIRY_DAYS = os.getenv("PAT_MAX_EXPIRY_DAYS", "").strip()
|
||||
PAT_MAX_EXPIRY_DAYS = (
|
||||
int(_PAT_MAX_EXPIRY_DAYS) if _PAT_MAX_EXPIRY_DAYS else None
|
||||
)
|
||||
|
||||
# ETL Service
|
||||
ETL_SERVICE = os.getenv("ETL_SERVICE")
|
||||
|
|
|
|||
|
|
@ -368,6 +368,9 @@ class Permission(StrEnum):
|
|||
SETTINGS_UPDATE = "settings:update"
|
||||
SETTINGS_DELETE = "settings:delete" # Delete the entire search space
|
||||
|
||||
# API Access
|
||||
API_ACCESS_MANAGE = "api_access:manage"
|
||||
|
||||
# Public Sharing
|
||||
PUBLIC_SHARING_VIEW = "public_sharing:view"
|
||||
PUBLIC_SHARING_CREATE = "public_sharing:create"
|
||||
|
|
@ -1693,6 +1696,9 @@ class SearchSpace(BaseModel, TimestampMixin):
|
|||
citations_enabled = Column(
|
||||
Boolean, nullable=False, default=True
|
||||
) # Enable/disable citations
|
||||
api_access_enabled = Column(
|
||||
Boolean, nullable=False, default=False, server_default="false"
|
||||
)
|
||||
qna_custom_instructions = Column(
|
||||
Text, nullable=True, default=""
|
||||
) # User's custom instructions
|
||||
|
|
@ -2330,6 +2336,11 @@ if config.AUTH_TYPE == "GOOGLE":
|
|||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
personal_access_tokens = relationship(
|
||||
"PersonalAccessToken",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
|
|
@ -2462,6 +2473,11 @@ else:
|
|||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
personal_access_tokens = relationship(
|
||||
"PersonalAccessToken",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
|
||||
class AgentActionLog(BaseModel):
|
||||
|
|
@ -2712,6 +2728,36 @@ class RefreshToken(Base, TimestampMixin):
|
|||
return not self.is_expired and not self.is_revoked
|
||||
|
||||
|
||||
class PersonalAccessToken(BaseModel, TimestampMixin):
|
||||
"""
|
||||
Stores hashed Personal Access Tokens for programmatic API access.
|
||||
Plaintext tokens are shown once on creation and are never persisted.
|
||||
"""
|
||||
|
||||
__tablename__ = "personal_access_tokens"
|
||||
|
||||
user_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("user.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
user = relationship("User", back_populates="personal_access_tokens")
|
||||
token_hash = Column(String(64), unique=True, nullable=False, index=True)
|
||||
token_prefix = Column(String(16), nullable=False)
|
||||
label = Column(String, nullable=False)
|
||||
expires_at = Column(TIMESTAMP(timezone=True), nullable=True, index=True)
|
||||
last_used_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return self.expires_at is not None and datetime.now(UTC) >= self.expires_at
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
return not self.is_expired
|
||||
|
||||
|
||||
# Register model packages that live outside this file so their classes
|
||||
# are present in Base.metadata before configure_mappers() resolves any
|
||||
# string-based relationship() references.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue