mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
feat: add personal access token API routes
This commit is contained in:
parent
cddfb3660b
commit
608facd7e7
4 changed files with 135 additions and 0 deletions
|
|
@ -54,6 +54,7 @@ from .notes_routes import router as notes_router
|
|||
from .notion_add_connector_route import router as notion_add_connector_router
|
||||
from .obsidian_plugin_routes import router as obsidian_plugin_router
|
||||
from .onedrive_add_connector_route import router as onedrive_add_connector_router
|
||||
from .personal_access_tokens_routes import router as personal_access_tokens_router
|
||||
from .prompts_routes import router as prompts_router
|
||||
from .public_chat_routes import router as public_chat_router
|
||||
from .rbac_routes import router as rbac_router
|
||||
|
|
@ -113,6 +114,7 @@ router.include_router(slack_add_connector_router)
|
|||
router.include_router(teams_add_connector_router)
|
||||
router.include_router(onedrive_add_connector_router)
|
||||
router.include_router(obsidian_plugin_router) # Obsidian plugin push API
|
||||
router.include_router(personal_access_tokens_router) # Personal access token manager
|
||||
router.include_router(discord_add_connector_router)
|
||||
router.include_router(jira_add_connector_router)
|
||||
router.include_router(confluence_add_connector_router)
|
||||
|
|
|
|||
104
surfsense_backend/app/routes/personal_access_tokens_routes.py
Normal file
104
surfsense_backend/app/routes/personal_access_tokens_routes.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.auth.context import AuthContext
|
||||
from app.config import config
|
||||
from app.db import PersonalAccessToken, get_async_session
|
||||
from app.schemas.pat import PATCreate, PATCreated, PATRead
|
||||
from app.users import require_session_context
|
||||
from app.utils.pat import generate_pat, hash_pat, token_prefix
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _expires_at(expires_in_days: int | None) -> datetime | None:
|
||||
max_expiry_days = config.PAT_MAX_EXPIRY_DAYS
|
||||
|
||||
if max_expiry_days is not None:
|
||||
if expires_in_days is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
"This deployment requires PATs to have an expiry of "
|
||||
f"{max_expiry_days} days or less"
|
||||
),
|
||||
)
|
||||
if expires_in_days > max_expiry_days:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"PAT expiry cannot exceed {max_expiry_days} days",
|
||||
)
|
||||
|
||||
if expires_in_days is None:
|
||||
return None
|
||||
|
||||
return datetime.now(UTC) + timedelta(days=expires_in_days)
|
||||
|
||||
|
||||
@router.post("/pats", response_model=PATCreated)
|
||||
async def create_personal_access_token(
|
||||
body: PATCreate,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
auth: AuthContext = Depends(require_session_context),
|
||||
) -> PATCreated:
|
||||
token = generate_pat()
|
||||
pat = PersonalAccessToken(
|
||||
user_id=auth.user.id,
|
||||
token_hash=hash_pat(token),
|
||||
token_prefix=token_prefix(token),
|
||||
label=body.label.strip(),
|
||||
expires_at=_expires_at(body.expires_in_days),
|
||||
)
|
||||
session.add(pat)
|
||||
await session.commit()
|
||||
await session.refresh(pat)
|
||||
|
||||
return PATCreated(
|
||||
id=pat.id,
|
||||
label=pat.label,
|
||||
token=token,
|
||||
prefix=pat.token_prefix,
|
||||
expires_at=pat.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pats", response_model=list[PATRead])
|
||||
async def list_personal_access_tokens(
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
auth: AuthContext = Depends(require_session_context),
|
||||
) -> list[PATRead]:
|
||||
result = await session.execute(
|
||||
select(PersonalAccessToken)
|
||||
.where(PersonalAccessToken.user_id == auth.user.id)
|
||||
.order_by(PersonalAccessToken.created_at.desc())
|
||||
)
|
||||
return [
|
||||
PATRead(
|
||||
id=pat.id,
|
||||
label=pat.label,
|
||||
prefix=pat.token_prefix,
|
||||
expires_at=pat.expires_at,
|
||||
last_used_at=pat.last_used_at,
|
||||
created_at=pat.created_at,
|
||||
)
|
||||
for pat in result.scalars().all()
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/pats/{pat_id}", status_code=204)
|
||||
async def delete_personal_access_token(
|
||||
pat_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
auth: AuthContext = Depends(require_session_context),
|
||||
) -> None:
|
||||
await session.execute(
|
||||
delete(PersonalAccessToken).where(
|
||||
PersonalAccessToken.id == pat_id,
|
||||
PersonalAccessToken.user_id == auth.user.id,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -104,6 +104,7 @@ from .search_source_connector import (
|
|||
SearchSourceConnectorUpdate,
|
||||
)
|
||||
from .search_space import (
|
||||
SearchSpaceApiAccessUpdate,
|
||||
SearchSpaceBase,
|
||||
SearchSpaceCreate,
|
||||
SearchSpaceRead,
|
||||
|
|
@ -243,6 +244,7 @@ __all__ = [
|
|||
"SearchSourceConnectorUpdate",
|
||||
# Search space schemas
|
||||
"SearchSpaceBase",
|
||||
"SearchSpaceApiAccessUpdate",
|
||||
"SearchSpaceCreate",
|
||||
"SearchSpaceRead",
|
||||
"SearchSpaceUpdate",
|
||||
|
|
|
|||
27
surfsense_backend/app/schemas/pat.py
Normal file
27
surfsense_backend/app/schemas/pat.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class PATCreate(BaseModel):
|
||||
label: str = Field(min_length=1, max_length=120)
|
||||
expires_in_days: int | None = Field(default=None, gt=0)
|
||||
|
||||
|
||||
class PATCreated(BaseModel):
|
||||
id: int
|
||||
label: str
|
||||
token: str
|
||||
prefix: str
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
class PATRead(BaseModel):
|
||||
id: int
|
||||
label: str
|
||||
prefix: str
|
||||
expires_at: datetime | None = None
|
||||
last_used_at: datetime | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue