aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-14 15:53:10 -0600
committerstuppie2026-09-14 15:53:10 -0600
commitbb999c73ab563b27e469fc4f74a5f81a5f35fffd (patch)
tree278de09db82dc612ee653a00f21b5f515af3aadf
parentd6137a3ab6c584422d1c1e6880fd02c29df0a77b (diff)
downloadamt-jb-bb999c73ab563b27e469fc4f74a5f81a5f35fffd.tar.gz
amt-jb-bb999c73ab563b27e469fc4f74a5f81a5f35fffd.zip
add invite_amt_account_link view
-rw-r--r--jb/api/magic_token.py8
-rw-r--r--jb/settings.py1
-rw-r--r--jb/views/auth.py42
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)],