feat: update all endpoints and schemas for account-based model
Replace team_id with account_id across all API endpoints (trees, categories, tags, steps, step_categories, admin, auth). Add new accounts and webhooks endpoints. Registration now atomically creates Account + Subscription, with account_invite_code bypassing the platform invite gate. Schemas updated for account_id/account_role. 82 tests passing including 18 new tests for accounts, subscriptions, and permissions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
@@ -18,6 +20,9 @@ from app.core.security import (
|
||||
from app.models.user import User
|
||||
from app.models.invite_code import InviteCode
|
||||
from app.models.refresh_token import RefreshToken
|
||||
from app.models.account import Account
|
||||
from app.models.subscription import Subscription
|
||||
from app.models.account_invite import AccountInvite
|
||||
from app.schemas.user import UserCreate, UserResponse, UserLogin
|
||||
from app.schemas.token import Token
|
||||
from app.api.deps import get_current_active_user, get_refresh_token_payload
|
||||
@@ -37,6 +42,12 @@ async def _store_refresh_token(db: AsyncSession, refresh_token_str: str, user_id
|
||||
db.add(token_record)
|
||||
|
||||
|
||||
def _generate_display_code() -> str:
|
||||
"""Generate a random 8-character alphanumeric display code."""
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return ''.join(secrets.choice(chars) for _ in range(8))
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
@limiter.limit("3/minute")
|
||||
async def register(
|
||||
@@ -44,10 +55,46 @@ async def register(
|
||||
user_data: UserCreate,
|
||||
db: Annotated[AsyncSession, Depends(get_db)]
|
||||
):
|
||||
"""Register a new user."""
|
||||
# Validate invite code if required
|
||||
"""Register a new user.
|
||||
|
||||
Supports two flows:
|
||||
- account_invite_code: Join an existing account (bypasses platform invite gate)
|
||||
- invite_code: Platform invite code (when REQUIRE_INVITE_CODE is enabled)
|
||||
|
||||
After user creation, if no account invite was used, a personal Account
|
||||
and free Subscription are created automatically.
|
||||
"""
|
||||
# Check for account invite code FIRST — bypasses platform invite gate
|
||||
account_invite_record = None
|
||||
if user_data.account_invite_code:
|
||||
result = await db.execute(
|
||||
select(AccountInvite).where(
|
||||
AccountInvite.code == user_data.account_invite_code
|
||||
)
|
||||
)
|
||||
account_invite_record = result.scalar_one_or_none()
|
||||
|
||||
if not account_invite_record:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid account invite code"
|
||||
)
|
||||
|
||||
if account_invite_record.is_used:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Account invite code has already been used"
|
||||
)
|
||||
|
||||
if account_invite_record.is_expired:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Account invite code has expired"
|
||||
)
|
||||
|
||||
# Validate platform invite code if required (skip if account invite was provided)
|
||||
invite_code_record = None
|
||||
if settings.REQUIRE_INVITE_CODE:
|
||||
if not account_invite_record and settings.REQUIRE_INVITE_CODE:
|
||||
if not user_data.invite_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -96,8 +143,37 @@ async def register(
|
||||
invite_code_id=invite_code_record.id if invite_code_record else None
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush() # Get user ID before creating account
|
||||
|
||||
# Mark invite code as used
|
||||
if account_invite_record:
|
||||
# Join existing account via account invite
|
||||
new_user.account_id = account_invite_record.account_id
|
||||
new_user.account_role = account_invite_record.role
|
||||
|
||||
# Mark account invite as used
|
||||
account_invite_record.accepted_by_id = new_user.id
|
||||
account_invite_record.used_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
# Create personal Account + free Subscription
|
||||
new_account = Account(
|
||||
name=f"{user_data.name}'s Account",
|
||||
display_code=_generate_display_code(),
|
||||
owner_id=new_user.id,
|
||||
)
|
||||
db.add(new_account)
|
||||
await db.flush() # Get account ID
|
||||
|
||||
new_subscription = Subscription(
|
||||
account_id=new_account.id,
|
||||
plan="free",
|
||||
status="active",
|
||||
)
|
||||
db.add(new_subscription)
|
||||
|
||||
new_user.account_id = new_account.id
|
||||
new_user.account_role = "owner"
|
||||
|
||||
# Mark platform invite code as used
|
||||
if invite_code_record:
|
||||
invite_code_record.used_by_id = new_user.id
|
||||
invite_code_record.used_at = datetime.now(timezone.utc)
|
||||
|
||||
Reference in New Issue
Block a user