feat: self-serve signup Phase 2 (frontend cutover) (#162)
Some checks failed
CI / e2e (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / backend (push) Has been cancelled
Mirror to GitHub / mirror (push) Has been cancelled

Co-authored-by: Michael Chihlas <michael@resolutionflow.com>
Co-committed-by: Michael Chihlas <michael@resolutionflow.com>
This commit was merged in pull request #162.
This commit is contained in:
2026-05-07 18:42:20 +00:00
committed by chihlasm
parent f918b766b0
commit f1be3abcc5
123 changed files with 11563 additions and 559 deletions

View File

@@ -0,0 +1,54 @@
"""Public endpoint for resolving an account invite code into display info.
Mounted as a public route (no tenant context, no auth) — used by the
/accept-invite page on the frontend so an invitee can see what account they
are about to join before they sign up. Uses the BYPASSRLS admin session
factory because account_invites is account-scoped under Phase 4 RLS but the
caller has no tenant identity yet.
"""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.core.admin_database import get_admin_db
from app.models.account_invite import AccountInvite
from app.schemas.oauth import InviteLookupResponse
router = APIRouter(prefix="/accounts", tags=["account-invite-lookup"])
@router.get("/invites/{code}/lookup", response_model=InviteLookupResponse)
async def lookup_invite(
code: str,
db: Annotated[AsyncSession, Depends(get_admin_db)],
) -> InviteLookupResponse:
"""Return minimal display data for a valid (unused, unexpired, not revoked)
invite. Returns 404 with `invite_invalid_or_expired_or_revoked` for any
invalid state — the AcceptInvitePage shows a single "ask the inviter to
resend" message regardless of which condition failed (anti-enumeration)."""
result = await db.execute(
select(AccountInvite)
.where(AccountInvite.code == code)
.options(
joinedload(AccountInvite.account),
joinedload(AccountInvite.invited_by),
)
)
invite = result.scalar_one_or_none()
if invite is None or not invite.is_valid:
raise HTTPException(
status_code=404,
detail={"error": "invite_invalid_or_expired_or_revoked"},
)
return InviteLookupResponse(
account_name=invite.account.name,
inviter_name=invite.invited_by.name,
invited_email=invite.email,
role=invite.role,
)