diff options
| author | stuppie | 2026-09-14 15:53:10 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-14 15:53:10 -0600 |
| commit | bb999c73ab563b27e469fc4f74a5f81a5f35fffd (patch) | |
| tree | 278de09db82dc612ee653a00f21b5f515af3aadf | |
| parent | d6137a3ab6c584422d1c1e6880fd02c29df0a77b (diff) | |
| download | amt-jb-bb999c73ab563b27e469fc4f74a5f81a5f35fffd.tar.gz amt-jb-bb999c73ab563b27e469fc4f74a5f81a5f35fffd.zip | |
add invite_amt_account_link view
| -rw-r--r-- | jb/api/magic_token.py | 8 | ||||
| -rw-r--r-- | jb/settings.py | 1 | ||||
| -rw-r--r-- | jb/views/auth.py | 42 |
3 files changed, 48 insertions, 3 deletions
diff --git a/jb/api/magic_token.py b/jb/api/magic_token.py index dc05686..189db02 100644 --- a/jb/api/magic_token.py +++ b/jb/api/magic_token.py @@ -10,6 +10,7 @@ from jb.models.auth import AmtAccountLink MAGIC_TOKEN_PREFIX = "auth:magic:" AMT_ACCOUNT_LINK_TOKEN_PREFIX = "auth:amt-account-link:" MAGIC_TOKEN_TTL: int = 3 * 60 * 60 # 3hrs, in seconds +INVITE_AMT_LINK_TOKEN_TTL: int = 45 * 24 * 60 * 60 # 45 days def redis_token_key(token: str, prefix: str = MAGIC_TOKEN_PREFIX) -> str: @@ -56,7 +57,10 @@ def consume_magic_token(token: str, redis_config: RedisConfig | None = None) -> def create_amt_account_link_token( - email: str, amt_worker_id: str, redis_config: RedisConfig | None = None + email: str, + amt_worker_id: str, + redis_config: RedisConfig | None = None, + ttl_seconds: int = MAGIC_TOKEN_TTL, ) -> str: """Bind an email and AMT worker ID to an opaque, short-lived token.""" if redis_config is None: @@ -72,7 +76,7 @@ def create_amt_account_link_token( redis_client.set( redis_token_key(token, AMT_ACCOUNT_LINK_TOKEN_PREFIX), data.model_dump_json(), - ex=MAGIC_TOKEN_TTL, + ex=ttl_seconds, ) return token diff --git a/jb/settings.py b/jb/settings.py index 543a0e8..1bf65bc 100644 --- a/jb/settings.py +++ b/jb/settings.py @@ -63,6 +63,7 @@ class Settings(GRLBaseSettings): session_jwt_secret: SecretStr | None = Field(default=None, min_length=32) magic_token_salt: SecretStr | None = Field(default=None, min_length=32) + invite_amt_account_token: SecretStr | None = Field(default=None, min_length=32) gr_api_host: HttpUrl = Field(default=HttpUrl("https://generalresearch.com/api/v2/")) gr_api_token: SecretStr | None = Field(default=None, min_length=1) diff --git a/jb/views/auth.py b/jb/views/auth.py index f1c2081..1b4c364 100644 --- a/jb/views/auth.py +++ b/jb/views/auth.py @@ -1,7 +1,8 @@ +import secrets from typing import Annotated from urllib.parse import urlencode -from fastapi import APIRouter, Depends, HTTPException, Response, status +from fastapi import APIRouter, Depends, Header, HTTPException, Response, status from fastapi.responses import HTMLResponse, RedirectResponse from jb.api.auth import ( @@ -10,6 +11,7 @@ from jb.api.auth import ( get_authenticated_user, ) from jb.api.magic_token import ( + INVITE_AMT_LINK_TOKEN_TTL, consume_amt_account_link_token, consume_magic_token, create_amt_account_link_token, @@ -35,6 +37,29 @@ from jb.settings import BASE_HTML auth_router = APIRouter(prefix="/auth", tags=["Auth"]) +def authenticate_invite_amt_account_link( + authorization: Annotated[str | None, Header()] = None, +) -> None: + expected_token = settings.invite_amt_account_token + if expected_token is None: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="not configured", + ) + + scheme, _, supplied_token = (authorization or "").partition(" ") + authenticated = scheme.lower() == "bearer" and secrets.compare_digest( + supplied_token, + expected_token.get_secret_value(), + ) + if not authenticated: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid scheduler authentication token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + @auth_router.post("/magic-link/request/") def request_magic_link(body: AccountLogin) -> dict[str, str]: """Create a magic link.""" @@ -119,6 +144,21 @@ def link_amt_account(body: AmtAccountLink) -> dict[str, str]: return {} +@auth_router.post("/link-amt/invite/") +def invite_amt_account_link( + body: AmtAccountLink, + _authenticated: Annotated[None, Depends(authenticate_invite_amt_account_link)], +) -> dict[str, str]: + """Create a long-lived AMT account-link URL without sending an email.""" + token = create_amt_account_link_token( + email=str(body.email), + amt_worker_id=body.amt_worker_id, + ttl_seconds=INVITE_AMT_LINK_TOKEN_TTL, + ) + query = urlencode({"token": token}) + return {"magic_link": f"{settings.base_url}auth/link-amt/?{query}"} + + @auth_router.get("/debug/", response_class=HTMLResponse, include_in_schema=False) def link_amt_account_landing_page( gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], |
