Skip to content

Commit 1e7628e

Browse files
authored
Update forge user creation flow (#95)
1 parent 95fc33f commit 1e7628e

4 files changed

Lines changed: 72 additions & 122 deletions

File tree

app/api/dependencies.py

Lines changed: 4 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
1414
from jose import JWTError, jwt
1515
from sqlalchemy import select
16-
from sqlalchemy.exc import IntegrityError
1716
from sqlalchemy.ext.asyncio import AsyncSession
1817
from sqlalchemy.orm import Session, joinedload, selectinload
1918

@@ -33,7 +32,6 @@
3332
)
3433
from app.models.forge_api_key import ForgeApiKey
3534
from app.models.user import User
36-
from app.services.provider_service import create_default_tensorblock_provider_for_user
3735

3836
logger = get_logger(name="dependencies")
3937

@@ -402,111 +400,11 @@ async def get_current_user_from_clerk(
402400
.filter(User.clerk_user_id == clerk_user_id)
403401
)
404402
user = result.scalar_one_or_none()
405-
406-
# User doesn't exist yet, create one
407403
if not user:
408-
# Fetch user data from Clerk API
409-
# https://clerk.com/docs/reference/backend-api/tag/users/get/users/%7Buser_id%7D
410-
if not CLERK_API_KEY:
411-
raise HTTPException(
412-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
413-
detail="Clerk API key not configured",
414-
)
415-
416-
# Call Clerk API to get user info
417-
url = f"{CLERK_API_URL}/users/{clerk_user_id}"
418-
419-
try:
420-
async with aiohttp.ClientSession() as session:
421-
async with session.get(url, headers={"Authorization": f"Bearer {CLERK_API_KEY}"}) as response:
422-
response.raise_for_status()
423-
user_data = await response.json()
424-
425-
# Extract email address
426-
email = None
427-
if user_data.get("primary_email_address_id") and user_data.get(
428-
"email_addresses"
429-
):
430-
for email_obj in user_data.get("email_addresses", []):
431-
if email_obj["id"] == user_data["primary_email_address_id"]:
432-
email = email_obj.get("email_address")
433-
break
434-
if email is None:
435-
raise ValueError("No email found in Clerk user data")
436-
437-
# Use email as username directly
438-
username = email
439-
except Exception as e:
440-
logger.exception(f"Error fetching Clerk user data: {e}")
441-
raise HTTPException(
442-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
443-
detail="Failed to fetch user data from Clerk",
444-
)
445-
446-
# Check if user exists with this email
447-
result = await db.execute(select(User).filter(User.email == email))
448-
existing_user = result.scalar_one_or_none()
449-
if existing_user:
450-
# Link existing user to Clerk ID
451-
try:
452-
existing_user.clerk_user_id = clerk_user_id
453-
await db.commit()
454-
return existing_user
455-
except IntegrityError:
456-
# Another request might have already linked this user or created a new one
457-
await db.rollback()
458-
# Retry the query to get the user
459-
result = await db.execute(
460-
select(User).filter(User.clerk_user_id == clerk_user_id)
461-
)
462-
user = result.scalar_one_or_none()
463-
if user:
464-
return user
465-
# If still no user, continue with creation attempt
466-
467-
# Create new user
468-
try:
469-
user = User(
470-
email=email,
471-
username=username,
472-
clerk_user_id=clerk_user_id,
473-
is_active=True,
474-
hashed_password="", # Clerk handles authentication
475-
)
476-
db.add(user)
477-
await db.commit()
478-
await db.refresh(user)
479-
480-
# Create default TensorBlock provider for the new user
481-
try:
482-
await create_default_tensorblock_provider_for_user(user.id, db)
483-
except Exception as e:
484-
# Log error but don't fail user creation
485-
logger.warning(
486-
f"Failed to create default TensorBlock provider for user {user.id}: {e}"
487-
)
488-
489-
return user
490-
except IntegrityError as e:
491-
# Handle race condition: another request might have created the user
492-
await db.rollback()
493-
if "users_clerk_user_id_key" in str(e) or "clerk_user_id" in str(e):
494-
# Retry the query to get the user that was created by another request
495-
result = await db.execute(
496-
select(User).filter(User.clerk_user_id == clerk_user_id)
497-
)
498-
user = result.scalar_one_or_none()
499-
if user:
500-
return user
501-
else:
502-
# This shouldn't happen, but handle it gracefully
503-
raise HTTPException(
504-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
505-
detail="Failed to create or retrieve user due to database constraint",
506-
)
507-
else:
508-
# Re-raise other integrity errors
509-
raise
404+
raise HTTPException(
405+
status_code=status.HTTP_401_UNAUTHORIZED,
406+
detail="User not found",
407+
)
510408

511409
return user
512410

app/api/routes/users.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ async def create_user(
2222
user_in: UserCreate, db: AsyncSession = Depends(get_async_db)
2323
) -> Any:
2424
"""
25-
Create a new user.
25+
Create a new user for cli usage.
26+
27+
This is a deprecated endpoint and should not be used in staging/production.
28+
Users should be created via the Clerk webhook.
2629
"""
2730
# Check if email already exists
2831
result = await db.execute(
@@ -52,7 +55,6 @@ async def create_user(
5255
hashed_password=hashed_password,
5356
)
5457
db.add(db_user)
55-
await db.commit()
5658
await db.refresh(db_user)
5759

5860
# Create default TensorBlock provider for the new user
@@ -66,7 +68,7 @@ async def create_user(
6668
"error": str(e),
6769
}
6870
})
69-
71+
await db.commit()
7072
return db_user
7173

7274

app/api/routes/webhooks.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from fastapi import APIRouter, Depends, HTTPException, Request, status
55
import stripe
6+
from sqlalchemy.exc import IntegrityError
67
from sqlalchemy import update, delete, select
78
from sqlalchemy.ext.asyncio import AsyncSession
89
from svix import Webhook, WebhookVerificationError
@@ -14,6 +15,7 @@
1415
from app.models.user import User
1516
from app.models.admin_users import AdminUsers
1617
from app.services.wallet_service import WalletService
18+
from app.services.provider_service import create_default_tensorblock_provider_for_user
1719

1820
logger = get_logger(name="webhooks")
1921

@@ -27,12 +29,17 @@
2729
@router.post("/clerk")
2830
async def clerk_webhook_handler(request: Request, db: AsyncSession = Depends(get_async_db)):
2931
"""
30-
Handle Clerk webhooks for user events.
32+
Handle Clerk webhooks for user/organization membership events.
3133
3234
Key events to handle:
35+
# Organization membership events
3336
- organizationMembership.created: Add user to admin users table
3437
- organizationMembership.updated: Update user in admin users table
3538
- organizationMembership.deleted: Remove user from admin users table
39+
40+
# User events
41+
- user.created: Upsert user record
42+
- user.updated: Upsert user record
3643
"""
3744
# Get the request body
3845
payload = await request.body()
@@ -71,10 +78,12 @@ async def clerk_webhook_handler(request: Request, db: AsyncSession = Depends(get
7178
event_type = event_data.get("type")
7279
logger.info(f"Received Clerk webhook: {event_type}")
7380

74-
if event_type == "organizationMembership.created" or event_type == "organizationMembership.updated":
81+
if event_type in ["organizationMembership.created", "organizationMembership.updated"]:
7582
await handle_organization_membership_created(event_data, db)
7683
elif event_type == "organizationMembership.deleted":
7784
await handle_organization_membership_deleted(event_data, db)
85+
elif event_type in ["user.created", "user.updated"]:
86+
await handle_clerk_user_created(event_data, db)
7887
else:
7988
logger.warning(f"Unhandled Clerk event type: {event_type}")
8089
except json.JSONDecodeError:
@@ -122,6 +131,55 @@ async def handle_organization_membership_deleted(event_data: dict, db: AsyncSess
122131
await db.commit()
123132

124133

134+
async def handle_clerk_user_created(event_data: dict, db: AsyncSession):
135+
data = event_data['data']
136+
clerk_user_id = data['id']
137+
138+
# extract the primary email address
139+
if not data.get('primary_email_address_id') or not data.get('email_addresses'):
140+
logger.error(f"No primary email address or email addresses found for user {clerk_user_id}")
141+
raise HTTPException(status_code=400, detail="No primary email address or email addresses found for user")
142+
143+
email = None
144+
primary_email_address_id = data['primary_email_address_id']
145+
for email_address in data['email_addresses']:
146+
if email_address['id'] == primary_email_address_id:
147+
email = email_address['email_address']
148+
break
149+
150+
if not email:
151+
logger.error(f"No email address found for user {clerk_user_id}")
152+
raise HTTPException(status_code=400, detail="No email address found for user")
153+
154+
# upsert user record
155+
try:
156+
result = await db.execute(
157+
insert(User).values(
158+
email=email,
159+
username=email, # Use email as username
160+
clerk_user_id=clerk_user_id,
161+
is_active=True,
162+
hashed_password="", # Clerk handles authentication
163+
).on_conflict_do_update(
164+
index_elements=[User.clerk_user_id],
165+
set_=dict(
166+
email=email,
167+
username=email,
168+
is_active=True,
169+
hashed_password="", # Clerk handles authentication
170+
)
171+
).returning(User.id)
172+
)
173+
user_id = result.scalar_one()
174+
await create_default_tensorblock_provider_for_user(user_id, db)
175+
await db.commit()
176+
except IntegrityError:
177+
logger.exception("Error upserting user record for clerk user")
178+
raise HTTPException(status_code=400, detail="Error upserting user record for clerk user")
179+
180+
logger.info(f"Upserted user record for clerk user {clerk_user_id}/{email}")
181+
182+
125183
@router.post("/stripe")
126184
async def stripe_webhook_handler(request: Request, db: AsyncSession = Depends(get_async_db)):
127185
"""

app/services/provider_service.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -859,15 +859,7 @@ async def create_default_tensorblock_provider_for_user(
859859
)
860860

861861
db.add(provider_key)
862-
await db.commit()
863-
864862
logger.info(f"Created default TensorBlock provider for user {user_id}")
865-
866-
except Exception as e:
867-
await db.rollback()
868-
logger.error(
869-
"Error creating default TensorBlock provider for user {}: {}",
870-
user_id,
871-
e,
872-
)
873-
# Don't raise the exception - this is optional functionality
863+
except Exception:
864+
logger.exception(f"Error creating default TensorBlock provider for user {user_id}")
865+
raise

0 commit comments

Comments
 (0)