Files

45 lines
1.3 KiB
Python
Raw Permalink Normal View History

"""Single-use signed action tokens for proposal email links (§9.1).
Tokens are random, stored in DB (single-use enforced there) and the URL carries
an itsdangerous signature so guessing/forging is infeasible even if the DB row
leaked. Action links must be idempotent (§15): a used 'resolve' link re-visited
shows "already resolved", it never errors or double-fires.
"""
from __future__ import annotations
import secrets
import uuid
from itsdangerous import BadSignature, URLSafeSerializer
from sqlalchemy.ext.asyncio import AsyncSession
from gogo.config import get_settings
from gogo.models import ActionToken
def _serializer() -> URLSafeSerializer:
return URLSafeSerializer(get_settings().secret_key, salt="proposal-action")
async def create_action_token(
session: AsyncSession, request_id: uuid.UUID, action: str, slot_index: int | None = None
) -> str:
"""Create a DB-backed token and return the signed URL-safe token string."""
raw = secrets.token_urlsafe(24)[:40]
session.add(
ActionToken(token=raw, request_id=request_id, action=action, slot_index=slot_index)
)
return _serializer().dumps(raw)
def action_url(signed: str) -> str:
return f"{get_settings().base_url}/a/{signed}"
def unsign(signed: str) -> str | None:
try:
return _serializer().loads(signed)
except BadSignature:
return None