Files
resolutionflow/backend/app/api/endpoints/onboarding.py
chihlasm 3ff886363c fix: use BYPASSRLS session for all auth deps and user-mutation endpoints
Phase 4 enabled RLS on the users table. All code paths that touch users
(or other RLS-protected tables) before require_tenant_context sets
app.current_account_id must use get_admin_db (BYPASSRLS):

- deps.py: get_current_user and get_current_active_user → get_admin_db
- auth.py: all endpoints → get_admin_db (login, register, refresh, etc.
  run before tenant context exists; mutation endpoints also need session
  consistency since current_user is in the admin session)
- accounts.py: transfer_ownership, leave_account, delete_account
  → get_admin_db (these mutate current_user directly)
- onboarding.py: dismiss_onboarding → get_admin_db (same reason)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 03:25:18 +00:00

112 lines
3.9 KiB
Python

"""Onboarding status endpoints — track new-user checklist progress."""
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_active_user
from app.core.database import get_db
from app.core.admin_database import get_admin_db
from app.models.assistant_chat import AssistantChat
from app.models.psa_connection import PsaConnection
from app.models.session import Session
from app.models.tree import Tree
from app.models.user import User
from app.schemas.onboarding import OnboardingStatus
router = APIRouter(prefix="/users", tags=["onboarding"])
@router.get("/onboarding-status", response_model=OnboardingStatus)
async def get_onboarding_status(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> OnboardingStatus:
"""Return onboarding checklist completion status for the current user."""
# created_flow — user owns at least 1 tree
created_flow_q = await db.execute(
select(func.count()).select_from(Tree).where(Tree.author_id == current_user.id).limit(1)
)
created_flow = (created_flow_q.scalar() or 0) > 0
# ran_session — user has at least 1 session
ran_session_q = await db.execute(
select(func.count()).select_from(Session).where(Session.user_id == current_user.id).limit(1)
)
ran_session = (ran_session_q.scalar() or 0) > 0
# exported_session — user has at least 1 session with exported=True
exported_q = await db.execute(
select(func.count())
.select_from(Session)
.where(Session.user_id == current_user.id, Session.exported == True) # noqa: E712
.limit(1)
)
exported_session = (exported_q.scalar() or 0) > 0
# tried_ai_assistant — user has at least 1 assistant chat
ai_q = await db.execute(
select(func.count())
.select_from(AssistantChat)
.where(AssistantChat.user_id == current_user.id)
.limit(1)
)
tried_ai_assistant = (ai_q.scalar() or 0) > 0
# Team-dependent checks
is_team_user = current_user.team_id is not None
invited_teammate = False
connected_psa = False
if is_team_user:
# invited_teammate — team/account has more than 1 member
if current_user.account_id:
teammate_q = await db.execute(
select(func.count())
.select_from(User)
.where(
User.account_id == current_user.account_id,
User.deleted_at.is_(None),
)
)
invited_teammate = (teammate_q.scalar() or 0) > 1
# connected_psa — account has at least 1 PSA connection
if current_user.account_id:
psa_q = await db.execute(
select(func.count())
.select_from(PsaConnection)
.where(PsaConnection.account_id == current_user.account_id)
.limit(1)
)
connected_psa = (psa_q.scalar() or 0) > 0
return OnboardingStatus(
created_flow=created_flow,
ran_session=ran_session,
exported_session=exported_session,
tried_ai_assistant=tried_ai_assistant,
invited_teammate=invited_teammate,
connected_psa=connected_psa,
is_team_user=is_team_user,
dismissed=current_user.onboarding_dismissed,
)
@router.post("/onboarding-status/dismiss", response_model=OnboardingStatus)
async def dismiss_onboarding(
db: Annotated[AsyncSession, Depends(get_admin_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> OnboardingStatus:
"""Dismiss the onboarding checklist for the current user."""
current_user.onboarding_dismissed = True
await db.commit()
await db.refresh(current_user)
# Return updated status (reuse the GET logic)
return await get_onboarding_status(db=db, current_user=current_user)