mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-04 22:02:16 +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 .notion_add_connector_route import router as notion_add_connector_router
|
||||||
from .obsidian_plugin_routes import router as obsidian_plugin_router
|
from .obsidian_plugin_routes import router as obsidian_plugin_router
|
||||||
from .onedrive_add_connector_route import router as onedrive_add_connector_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 .prompts_routes import router as prompts_router
|
||||||
from .public_chat_routes import router as public_chat_router
|
from .public_chat_routes import router as public_chat_router
|
||||||
from .rbac_routes import router as rbac_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(teams_add_connector_router)
|
||||||
router.include_router(onedrive_add_connector_router)
|
router.include_router(onedrive_add_connector_router)
|
||||||
router.include_router(obsidian_plugin_router) # Obsidian plugin push API
|
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(discord_add_connector_router)
|
||||||
router.include_router(jira_add_connector_router)
|
router.include_router(jira_add_connector_router)
|
||||||
router.include_router(confluence_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,
|
SearchSourceConnectorUpdate,
|
||||||
)
|
)
|
||||||
from .search_space import (
|
from .search_space import (
|
||||||
|
SearchSpaceApiAccessUpdate,
|
||||||
SearchSpaceBase,
|
SearchSpaceBase,
|
||||||
SearchSpaceCreate,
|
SearchSpaceCreate,
|
||||||
SearchSpaceRead,
|
SearchSpaceRead,
|
||||||
|
|
@ -243,6 +244,7 @@ __all__ = [
|
||||||
"SearchSourceConnectorUpdate",
|
"SearchSourceConnectorUpdate",
|
||||||
# Search space schemas
|
# Search space schemas
|
||||||
"SearchSpaceBase",
|
"SearchSpaceBase",
|
||||||
|
"SearchSpaceApiAccessUpdate",
|
||||||
"SearchSpaceCreate",
|
"SearchSpaceCreate",
|
||||||
"SearchSpaceRead",
|
"SearchSpaceRead",
|
||||||
"SearchSpaceUpdate",
|
"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