Adds the invitee-side flow for self-serve signup Phase 2 (Task 36):
Backend
- Public GET /accounts/invites/{code}/lookup returns
{account_name, inviter_name, invited_email, role} for a valid invite,
404 invite_invalid_or_expired_or_revoked otherwise (collapses unknown /
expired / revoked / used into one anti-enumeration response). Mounted
in a new account_invite_lookup endpoints module on the public route
list, uses get_admin_db (BYPASSRLS) since the caller has no tenant.
- OAuthCallbackPayload gains optional account_invite_code + invited_email.
_sign_in_or_register honors them: a new OAuth user with a valid invite
joins the invited account (no personal account, no Pro trial), the
invite is marked used, and OAuth-profile-email vs invite-email mismatch
raises invite_email_mismatch (matching the email+password register
contract).
Frontend
- New public route /accept-invite -> AcceptInvitePage. Reads ?code=,
calls inviteApi.lookupAccountInvite, renders "Join {account} on
ResolutionFlow" with the invited email locked (rendered as a div, not
an input), three sign-in options (set password, Google, Microsoft),
and a clear "ask {inviter} to resend" + mailto: fallback for invalid
codes.
- OAuth state for invitees is base64url(JSON({csrf, accountInviteCode,
invitedEmail})). OAuthCallbackPage decodes both shapes, forwards the
invite fields to the backend, and surfaces invite_email_mismatch /
invite_invalid_or_expired_or_revoked errors with friendly text.
Successful invite-OAuth lands on /?welcome=teammate (suppresses the
welcome wizard for invitees per spec).
- UserCreate type + invite/auth API clients extended for the new fields.
Tests
- Backend: invite lookup happy path + four invalid-state collapse, OAuth
callback links invite when supplied + rejects on email mismatch.
- Frontend Vitest: AcceptInvitePage renders account name + locked email
+ accept buttons; resend message + mailto on invalid code.
All 43 backend auth/account/invite/email-verification tests green;
frontend Vitest 120/120 green; tsc -b clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
8.2 KiB
Python
184 lines
8.2 KiB
Python
from fastapi import APIRouter, Depends
|
|
|
|
from app.api.deps import (
|
|
require_tenant_context,
|
|
require_active_subscription,
|
|
require_verified_email_after_grace,
|
|
)
|
|
from app.api.endpoints import (
|
|
admin,
|
|
admin_audit,
|
|
admin_categories,
|
|
admin_dashboard,
|
|
admin_feature_flags,
|
|
admin_gallery,
|
|
admin_plan_limits,
|
|
admin_settings,
|
|
admin_survey,
|
|
ai_builder,
|
|
ai_chat,
|
|
ai_fix,
|
|
ai_sessions,
|
|
ai_suggestions,
|
|
analytics,
|
|
assistant_chat,
|
|
auth,
|
|
billing,
|
|
beta_feedback,
|
|
beta_signup,
|
|
sales_leads,
|
|
branding,
|
|
categories,
|
|
config as config_endpoints,
|
|
copilot,
|
|
device_types,
|
|
draft_templates,
|
|
feedback,
|
|
flow_proposals,
|
|
flowpilot_analytics,
|
|
folders,
|
|
integrations,
|
|
invite,
|
|
kb_accelerator,
|
|
maintenance_schedules,
|
|
network_diagrams,
|
|
notifications,
|
|
oauth as oauth_endpoints,
|
|
onboarding,
|
|
public_templates,
|
|
ratings,
|
|
scripts,
|
|
script_builder,
|
|
session_branches,
|
|
session_facts,
|
|
session_handoffs,
|
|
session_resolutions,
|
|
session_suggested_fixes,
|
|
sessions,
|
|
shared,
|
|
shares,
|
|
sidebar,
|
|
step_categories,
|
|
steps,
|
|
supporting_data,
|
|
survey,
|
|
tags,
|
|
target_lists,
|
|
tree_markdown,
|
|
tree_transfer,
|
|
trees,
|
|
uploads,
|
|
webhooks,
|
|
accounts,
|
|
account_invite_lookup,
|
|
)
|
|
|
|
api_router = APIRouter()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public / unauthenticated endpoints — no tenant context
|
|
#
|
|
# Note: auth.router contains both public endpoints (register, login,
|
|
# forgot-password, reset-password, email/verify) and authenticated endpoints
|
|
# (GET/PATCH /me, logout, change-password, email/send-verification).
|
|
# The authenticated auth endpoints only query the `users` table, which is
|
|
# excluded from Phase 1 RLS. They work correctly without tenant context
|
|
# in Phase 1. This will need revisiting in Phase 2 when `users` gets RLS.
|
|
# ---------------------------------------------------------------------------
|
|
api_router.include_router(auth.router)
|
|
api_router.include_router(oauth_endpoints.router)
|
|
api_router.include_router(billing.router) # Reachable when subscription locked
|
|
api_router.include_router(shared.router) # Public share links (no auth)
|
|
api_router.include_router(shares.public_router) # Public session share links (optional auth)
|
|
api_router.include_router(beta_signup.router)
|
|
api_router.include_router(sales_leads.router) # Talk-to-Sales (no auth, rate-limited)
|
|
api_router.include_router(webhooks.router) # Stripe webhook receiver
|
|
api_router.include_router(public_templates.router) # Public gallery (no auth, rate-limited)
|
|
api_router.include_router(survey.router) # Public survey flow (no auth, rate-limited)
|
|
api_router.include_router(config_endpoints.router) # Public runtime feature flags
|
|
api_router.include_router(account_invite_lookup.router) # Public invite-code lookup for /accept-invite
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Admin endpoints — super_admin only
|
|
# admin_categories, admin_gallery, admin_dashboard, admin query Phase 1 RLS
|
|
# tables and MUST use get_admin_db (migrated in Task 8). The remaining admin
|
|
# endpoints (admin_audit, admin_plan_limits, admin_feature_flags,
|
|
# admin_settings, admin_survey) are safe until Phase 2 extends RLS.
|
|
# ---------------------------------------------------------------------------
|
|
api_router.include_router(admin.router)
|
|
api_router.include_router(admin_dashboard.router)
|
|
api_router.include_router(admin_audit.router)
|
|
api_router.include_router(admin_plan_limits.router)
|
|
api_router.include_router(admin_feature_flags.router)
|
|
api_router.include_router(admin_settings.router)
|
|
api_router.include_router(admin_categories.router)
|
|
api_router.include_router(admin_survey.router)
|
|
api_router.include_router(admin_gallery.router)
|
|
# ---------------------------------------------------------------------------
|
|
# User-facing endpoints — tenant context required
|
|
#
|
|
# _tenant_deps: routers that only require an authenticated user inside a
|
|
# tenant (auth/account/admin/non-Pro feature surfaces).
|
|
# _pro_deps: routers gated behind an active Pro subscription. Adds
|
|
# require_active_subscription which raises 402 unless the
|
|
# account's Subscription is active/complimentary/past_due or
|
|
# trialing-with-time-remaining. Allowlisted paths in deps.py
|
|
# bypass the gate for billing/account admin/auth flows.
|
|
# ---------------------------------------------------------------------------
|
|
_tenant_deps = [Depends(require_tenant_context)]
|
|
_pro_deps = [
|
|
Depends(require_tenant_context),
|
|
Depends(require_active_subscription),
|
|
Depends(require_verified_email_after_grace),
|
|
]
|
|
|
|
api_router.include_router(trees.router, dependencies=_pro_deps)
|
|
api_router.include_router(sidebar.router, dependencies=_tenant_deps)
|
|
api_router.include_router(sessions.router, dependencies=_pro_deps)
|
|
api_router.include_router(invite.router, dependencies=_tenant_deps)
|
|
api_router.include_router(categories.router, dependencies=_tenant_deps)
|
|
api_router.include_router(tags.router, dependencies=_tenant_deps)
|
|
api_router.include_router(folders.router, dependencies=_tenant_deps)
|
|
api_router.include_router(step_categories.router, dependencies=_pro_deps)
|
|
api_router.include_router(steps.router, dependencies=_pro_deps)
|
|
api_router.include_router(accounts.router, dependencies=_tenant_deps)
|
|
api_router.include_router(shares.router, dependencies=_tenant_deps)
|
|
api_router.include_router(tree_markdown.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ratings.router, dependencies=_tenant_deps)
|
|
api_router.include_router(analytics.router, dependencies=_pro_deps)
|
|
api_router.include_router(target_lists.router, dependencies=_tenant_deps)
|
|
api_router.include_router(maintenance_schedules.router, dependencies=_tenant_deps)
|
|
api_router.include_router(feedback.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ai_builder.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ai_fix.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ai_chat.router, dependencies=_tenant_deps)
|
|
api_router.include_router(copilot.router, dependencies=_tenant_deps)
|
|
api_router.include_router(assistant_chat.router, dependencies=_pro_deps)
|
|
api_router.include_router(tree_transfer.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ai_suggestions.router, dependencies=_tenant_deps)
|
|
api_router.include_router(kb_accelerator.router, dependencies=_tenant_deps)
|
|
api_router.include_router(scripts.router, dependencies=_pro_deps)
|
|
api_router.include_router(integrations.router, dependencies=_pro_deps)
|
|
api_router.include_router(onboarding.router, dependencies=_tenant_deps)
|
|
api_router.include_router(branding.router, dependencies=_tenant_deps)
|
|
api_router.include_router(supporting_data.router, dependencies=_tenant_deps)
|
|
api_router.include_router(network_diagrams.router, dependencies=_tenant_deps)
|
|
# session_handoffs queue router must come before ai_sessions to avoid conflict
|
|
api_router.include_router(session_handoffs.queue_router, dependencies=_pro_deps)
|
|
api_router.include_router(session_resolutions.router, dependencies=_pro_deps)
|
|
# session_facts mounts under /ai-sessions/{id}/facts — register before ai_sessions
|
|
# so the {session_id}/facts subpaths take precedence over any future generic catchalls.
|
|
api_router.include_router(session_facts.router, dependencies=_pro_deps)
|
|
api_router.include_router(session_suggested_fixes.router, dependencies=_pro_deps)
|
|
api_router.include_router(draft_templates.router, dependencies=_tenant_deps)
|
|
api_router.include_router(ai_sessions.router, dependencies=_pro_deps)
|
|
api_router.include_router(flow_proposals.router, dependencies=_pro_deps)
|
|
api_router.include_router(flowpilot_analytics.router, dependencies=_pro_deps)
|
|
api_router.include_router(notifications.router, dependencies=_tenant_deps)
|
|
api_router.include_router(uploads.router, dependencies=_tenant_deps)
|
|
api_router.include_router(script_builder.router, dependencies=_pro_deps)
|
|
api_router.include_router(beta_feedback.router, dependencies=_tenant_deps)
|
|
api_router.include_router(session_branches.router, dependencies=_pro_deps)
|
|
api_router.include_router(session_handoffs.router, dependencies=_pro_deps)
|
|
api_router.include_router(device_types.router, dependencies=_tenant_deps)
|