New token arch

This commit is contained in:
Urtzi Alfaro
2026-01-10 21:45:37 +01:00
parent cc53037552
commit bf1db7cb9e
26 changed files with 1751 additions and 107 deletions

View File

@@ -1004,13 +1004,48 @@ async def upgrade_subscription_plan(
error=str(cache_error))
# Don't fail the upgrade if cache invalidation fails
# SECURITY: Invalidate all existing tokens for this tenant
# Forces users to re-authenticate and get new JWT with updated tier
try:
await _invalidate_tenant_tokens(tenant_id, redis_client)
logger.info("Invalidated all tokens for tenant after subscription upgrade",
tenant_id=str(tenant_id))
except Exception as token_error:
logger.error("Failed to invalidate tenant tokens after upgrade",
tenant_id=str(tenant_id),
error=str(token_error))
# Don't fail the upgrade if token invalidation fails
# Also publish event for real-time notification
try:
from shared.messaging import UnifiedEventPublisher
event_publisher = UnifiedEventPublisher()
await event_publisher.publish_business_event(
event_type="subscription.changed",
tenant_id=str(tenant_id),
data={
"tenant_id": str(tenant_id),
"old_tier": active_subscription.plan,
"new_tier": new_plan,
"action": "upgrade"
}
)
logger.info("Published subscription change event",
tenant_id=str(tenant_id),
event_type="subscription.changed")
except Exception as event_error:
logger.error("Failed to publish subscription change event",
tenant_id=str(tenant_id),
error=str(event_error))
return {
"success": True,
"message": f"Plan successfully upgraded to {new_plan}",
"old_plan": active_subscription.plan,
"new_plan": new_plan,
"new_monthly_price": updated_subscription.monthly_price,
"validation": validation
"validation": validation,
"requires_token_refresh": True # Signal to frontend
}
except HTTPException:
@@ -1192,3 +1227,33 @@ async def get_invoices(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get invoices"
)
async def _invalidate_tenant_tokens(tenant_id: str, redis_client):
"""
Invalidate all tokens for users in this tenant.
Forces re-authentication to get fresh subscription data.
"""
try:
# Set a "subscription_changed_at" timestamp for this tenant
# Gateway will check this and reject tokens issued before this time
import datetime
from datetime import timezone
changed_timestamp = datetime.datetime.now(timezone.utc).timestamp()
await redis_client.set(
f"tenant:{tenant_id}:subscription_changed_at",
str(changed_timestamp),
ex=86400 # 24 hour TTL
)
logger.info("Set subscription change timestamp for token invalidation",
tenant_id=tenant_id,
timestamp=changed_timestamp)
except Exception as e:
logger.error("Failed to invalidate tenant tokens",
tenant_id=tenant_id,
error=str(e))
raise

View File

@@ -771,6 +771,43 @@ class EnhancedTenantService:
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to remove team member"
)
async def get_user_memberships(self, user_id: str) -> List[Dict[str, Any]]:
"""Get all tenant memberships for a user (for authentication service)"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
# Get all user memberships
memberships = await self.member_repo.get_user_memberships(user_id, active_only=False)
# Convert to response format
result = []
for membership in memberships:
result.append({
"id": str(membership.id),
"tenant_id": str(membership.tenant_id),
"user_id": str(membership.user_id),
"role": membership.role,
"is_active": membership.is_active,
"joined_at": membership.joined_at.isoformat() if membership.joined_at else None,
"invited_by": str(membership.invited_by) if membership.invited_by else None
})
logger.info("Retrieved user memberships",
user_id=user_id,
membership_count=len(result))
return result
except Exception as e:
logger.error("Failed to get user memberships",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user memberships"
)
async def update_model_status(
self,