Fix tenant register 2

This commit is contained in:
Urtzi Alfaro
2025-07-20 23:43:42 +02:00
parent 38e78e7163
commit 2ee5117d48
6 changed files with 290 additions and 80 deletions

View File

@@ -1,5 +1,5 @@
"""
Tenant routes for gateway
Tenant routes for gateway - FIXED VERSION
"""
from fastapi import APIRouter, Request, HTTPException
@@ -17,16 +17,37 @@ async def create_tenant(request: Request):
"""Proxy tenant creation to tenant service"""
try:
body = await request.body()
auth_header = request.headers.get("Authorization")
# ✅ FIX: Forward all headers AND add user context from gateway auth
headers = dict(request.headers)
headers.pop("host", None) # Remove host header
# ✅ ADD USER CONTEXT FROM GATEWAY AUTHENTICATION
# Gateway middleware already verified the token and added user to request.state
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
# Add tenant ID if it exists
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
permissions = request.state.user.get("permissions", [])
if permissions:
headers["X-User-Permissions"] = ",".join(permissions)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/register",
content=body,
headers={
"Content-Type": "application/json",
"Authorization": auth_header
}
headers=headers
)
return JSONResponse(
@@ -45,12 +66,81 @@ async def create_tenant(request: Request):
async def get_tenants(request: Request):
"""Get tenants"""
try:
auth_header = request.headers.get("Authorization")
# ✅ FIX: Same pattern for GET requests
headers = dict(request.headers)
headers.pop("host", None)
# Add user context from gateway auth
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/tenants",
headers={"Authorization": auth_header}
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants",
headers=headers
)
return JSONResponse(
status_code=response.status_code,
content=response.json()
)
except httpx.RequestError as e:
logger.error(f"Tenant service unavailable: {e}")
raise HTTPException(
status_code=503,
detail="Tenant service unavailable"
)
# ✅ ADD: Generic proxy function like the data service has
async def _proxy_tenant_request(request: Request, target_path: str, method: str = None):
"""Proxy request to tenant service with user context"""
try:
url = f"{settings.TENANT_SERVICE_URL}{target_path}"
# Forward headers with user context
headers = dict(request.headers)
headers.pop("host", None)
# Add user context from gateway authentication
if hasattr(request.state, 'user'):
headers["X-User-ID"] = str(request.state.user.get("user_id"))
headers["X-User-Email"] = request.state.user.get("email", "")
headers["X-User-Role"] = request.state.user.get("role", "user")
if hasattr(request.state, 'tenant_id') and request.state.tenant_id:
headers["X-Tenant-ID"] = str(request.state.tenant_id)
elif request.state.user.get("tenant_id"):
headers["X-Tenant-ID"] = str(request.state.user.get("tenant_id"))
roles = request.state.user.get("roles", [])
if roles:
headers["X-User-Roles"] = ",".join(roles)
# Get request body if present
body = None
request_method = method or request.method
if request_method in ["POST", "PUT", "PATCH"]:
body = await request.body()
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(
method=request_method,
url=url,
headers=headers,
content=body,
params=dict(request.query_params)
)
return JSONResponse(

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# ================================================================
# Complete Authentication Test with Registration
# Complete Authentication Test with Registration - FIXED VERSION
# Tests the full user lifecycle: registration → login → API access
# ================================================================
@@ -14,7 +14,8 @@ AUTH_BASE="$API_BASE/api/v1/auth"
TEST_EMAIL="test-$(date +%s)@bakery.com" # Unique email for each test
TEST_PASSWORD="SecurePass123!"
TEST_NAME="Test Baker"
TENANT_ID="test-tenant-$(date +%s)"
# ✅ FIX: Generate a proper UUID for tenant testing (will be replaced after bakery creation)
TENANT_ID=$(uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || echo "00000000-0000-0000-0000-000000000000")
# Colors for output
RED='\033[0;31m'
@@ -73,6 +74,12 @@ if ! check_service_health "http://localhost:8001" "Auth Service"; then
exit 1
fi
# Check Tenant Service
if ! check_service_health "http://localhost:8005" "Tenant Service"; then
log_error "Tenant Service is not running. Check: docker-compose logs tenant-service"
exit 1
fi
# Check Data Service
if ! check_service_health "http://localhost:8004" "Data Service"; then
log_warning "Data Service is not running, but continuing with auth tests..."
@@ -149,13 +156,13 @@ fi
echo ""
# ================================================================
# STEP 4: ACCESSING PROTECTED ENDPOINTS
# STEP 3: ACCESSING PROTECTED ENDPOINTS
# ================================================================
log_step "Step 4: Testing protected endpoints with authentication"
log_step "Step 3: Testing protected endpoints with authentication"
# 4a. Get current user info
log_step "4a. Getting current user profile"
# 3a. Get current user info
log_step "3a. Getting current user profile"
USER_PROFILE_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/users/me" \
-H "Authorization: Bearer $ACCESS_TOKEN")
@@ -171,9 +178,47 @@ fi
echo ""
# 4b. Test data service through gateway
log_step "4b. Testing data service through gateway"
# ================================================================
# STEP 4: TENANT REGISTRATION (BAKERY CREATION)
# ================================================================
log_step "Step 4: Registering a bakery/tenant"
BAKERY_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/register" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Test Bakery $(date +%H%M)\",
\"business_type\": \"bakery\",
\"address\": \"Calle Test 123\",
\"city\": \"Madrid\",
\"postal_code\": \"28001\",
\"phone\": \"+34600123456\"
}")
echo "Bakery Registration Response:"
echo "$BAKERY_RESPONSE" | jq '.'
if echo "$BAKERY_RESPONSE" | jq -e '.id' > /dev/null; then
# ✅ FIX: Use the actual tenant ID returned from bakery creation
TENANT_ID=$(echo "$BAKERY_RESPONSE" | jq -r '.id')
log_success "Bakery registration successful! Tenant ID: $TENANT_ID"
else
log_error "Bakery registration failed!"
echo "Response: $BAKERY_RESPONSE"
# Continue with tests using placeholder UUID for other endpoints
fi
echo ""
# ================================================================
# STEP 5: TEST DATA SERVICE WITH TENANT ID
# ================================================================
log_step "Step 5: Testing data service through gateway"
# Only test with valid tenant ID
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
DATA_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/data/sales" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID")
@@ -186,12 +231,20 @@ if [ "$(echo "$DATA_RESPONSE" | jq -r '.status // "unknown"')" != "error" ]; the
else
log_warning "Data service returned error (may be expected for new tenant)"
fi
else
log_warning "Skipping data service test - no valid tenant ID"
fi
echo ""
# 4c. Test training service through gateway
log_step "4c. Testing training service through gateway"
# ================================================================
# STEP 6: TEST TRAINING SERVICE WITH TENANT ID
# ================================================================
log_step "Step 6: Testing training service through gateway"
# Only test with valid tenant ID
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
TRAINING_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/training/jobs" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID" \
@@ -210,44 +263,17 @@ if echo "$TRAINING_RESPONSE" | jq -e '.job_id // .message' > /dev/null; then
else
log_warning "Training service access may have issues"
fi
echo ""
# ================================================================
# STEP 5: TENANT REGISTRATION (OPTIONAL)
# ================================================================
log_step "Step 5: Registering a bakery/tenant"
BAKERY_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/register" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Test Bakery $(date +%H%M)\",
\"business_type\": \"bakery\",
\"address\": \"Calle Test 123\",
\"city\": \"Madrid\",
\"postal_code\": \"28001\",
\"phone\": \"+34600123456\"
}")
echo "Bakery Registration Response:"
echo "$BAKERY_RESPONSE" | jq '.'
if echo "$BAKERY_RESPONSE" | jq -e '.id' > /dev/null; then
TENANT_ID=$(echo "$BAKERY_RESPONSE" | jq -r '.id')
log_success "Bakery registration successful! Tenant ID: $TENANT_ID"
else
log_warning "Bakery registration endpoint may not be fully implemented"
log_warning "Skipping training service test - no valid tenant ID"
fi
echo ""
# ================================================================
# STEP 6: TOKEN REFRESH
# STEP 7: TOKEN REFRESH
# ================================================================
log_step "Step 6: Testing token refresh"
log_step "Step 7: Testing token refresh"
REFRESH_RESPONSE=$(curl -s -X POST "$AUTH_BASE/refresh" \
-H "Content-Type: application/json" \
@@ -268,19 +294,19 @@ fi
echo ""
# ================================================================
# STEP 7: DIRECT SERVICE HEALTH CHECKS
# STEP 8: DIRECT SERVICE HEALTH CHECKS
# ================================================================
log_step "Step 7: Testing direct service access (without gateway)"
log_step "Step 8: Testing direct service access (without gateway)"
# Test auth service directly
log_step "7a. Auth service direct health check"
log_step "8a. Auth service direct health check"
AUTH_HEALTH=$(curl -s -X GET "http://localhost:8001/health")
echo "Auth Service Health:"
echo "$AUTH_HEALTH" | jq '.'
# Test other services if available
log_step "7b. Other services health check"
log_step "8b. Other services health check"
services=("8002:Training" "8003:Forecasting" "8004:Data" "8005:Tenant" "8006:Notification")
@@ -299,10 +325,10 @@ done
echo ""
# ================================================================
# STEP 8: LOGOUT
# STEP 9: LOGOUT
# ================================================================
log_step "Step 8: Logging out user"
log_step "Step 9: Logging out user"
LOGOUT_RESPONSE=$(curl -s -X POST "$AUTH_BASE/logout" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
@@ -342,12 +368,12 @@ echo ""
echo "Services Tested:"
echo " 🌐 API Gateway"
echo " 🔐 Auth Service"
echo " 🏢 Tenant Service (bakery registration)"
echo " 📊 Data Service (through gateway)"
echo " 🤖 Training Service (through gateway)"
echo " 🏢 Tenant Service (bakery registration)"
echo ""
if [ -n "$TENANT_ID" ]; then
if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
echo "Tenant Created:"
echo " 🏪 Tenant ID: $TENANT_ID"
echo ""

View File

@@ -103,8 +103,14 @@ async def login(
metrics = get_metrics_collector(request)
try:
# Check login attempts
# Check login attempts TODO
# if not await SecurityManager.check_login_attempts(login_data.email):
# if metrics:
# metrics.increment_counter("login_failure_total", labels={"reason": "rate_limited"})
# raise HTTPException(
# status_code=status.HTTP_429_TOO_MANY_REQUESTS,
# detail="Too many login attempts. Please try again later."
# )
# Attempt login
result = await AuthService.login(login_data.email, login_data.password, db)

View File

@@ -1,6 +1,6 @@
# services/tenant/app/main.py
"""
Tenant Service FastAPI application
Tenant Service FastAPI application - FIXED VERSION
"""
import structlog
@@ -47,11 +47,35 @@ async def startup_event():
"""Initialize service on startup"""
logger.info("Starting Tenant Service...")
try:
# ✅ FIX: Import models to ensure they're registered with SQLAlchemy
from app.models.tenants import Tenant, TenantMember, Subscription
logger.info("Tenant models imported successfully")
# ✅ FIX: Create database tables on startup
await database_manager.create_tables()
logger.info("Tenant database tables created successfully")
except Exception as e:
logger.error(f"Failed to initialize tenant service: {e}")
raise
logger.info("Tenant Service startup completed successfully")
@app.on_event("shutdown")
async def shutdown_event():
"""Cleanup on shutdown"""
logger.info("Shutting down Tenant Service...")
try:
# Close database connections properly
if hasattr(database_manager, 'engine') and database_manager.engine:
await database_manager.engine.dispose()
logger.info("Database connections closed")
except Exception as e:
logger.error(f"Error during shutdown: {e}")
logger.info("Tenant Service shutdown completed")
@app.get("/health")
async def health_check():

View File

@@ -1,11 +1,12 @@
# services/tenant/app/schemas/tenants.py
"""
Tenant schemas
Tenant schemas - FIXED VERSION
"""
from pydantic import BaseModel, Field, validator
from typing import Optional, List, Dict, Any
from datetime import datetime
from uuid import UUID
import re
class BakeryRegistration(BaseModel):
@@ -42,8 +43,8 @@ class BakeryRegistration(BaseModel):
return v
class TenantResponse(BaseModel):
"""Tenant response schema"""
id: str
"""Tenant response schema - FIXED VERSION"""
id: str # ✅ Keep as str for Pydantic validation
name: str
subdomain: Optional[str]
business_type: str
@@ -55,9 +56,17 @@ class TenantResponse(BaseModel):
subscription_tier: str
model_trained: bool
last_training_date: Optional[datetime]
owner_id: str
owner_id: str # ✅ Keep as str for Pydantic validation
created_at: datetime
# ✅ FIX: Add custom validator to convert UUID to string
@validator('id', 'owner_id', pre=True)
def convert_uuid_to_string(cls, v):
"""Convert UUID objects to strings for JSON serialization"""
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
@@ -68,16 +77,70 @@ class TenantAccessResponse(BaseModel):
permissions: List[str]
class TenantMemberResponse(BaseModel):
"""Tenant member response"""
"""Tenant member response - FIXED VERSION"""
id: str
user_id: str
role: str
is_active: bool
joined_at: Optional[datetime]
# ✅ FIX: Add custom validator to convert UUID to string
@validator('id', 'user_id', pre=True)
def convert_uuid_to_string(cls, v):
"""Convert UUID objects to strings for JSON serialization"""
if isinstance(v, UUID):
return str(v)
return v
class Config:
from_attributes = True
class TenantUpdate(BaseModel):
"""Tenant update schema"""
name: Optional[str] = Field(None, min_length=2, max_length=200)
address: Optional[str] = Field(None, min_length=10, max_length=500)
phone: Optional[str] = None
business_type: Optional[str] = None
class TenantListResponse(BaseModel):
"""Response schema for listing tenants"""
tenants: List[TenantResponse]
total: int
page: int
per_page: int
has_next: bool
has_prev: bool
class TenantMemberInvitation(BaseModel):
"""Schema for inviting a member to a tenant"""
email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$')
role: str = Field(..., pattern=r'^(admin|member|viewer)$')
message: Optional[str] = Field(None, max_length=500)
class TenantMemberUpdate(BaseModel):
"""Schema for updating tenant member"""
role: Optional[str] = Field(None, pattern=r'^(owner|admin|member|viewer)$')
is_active: Optional[bool] = None
class TenantSubscriptionUpdate(BaseModel):
"""Schema for updating tenant subscription"""
plan: str = Field(..., pattern=r'^(basic|professional|enterprise)$')
billing_cycle: str = Field(default="monthly", pattern=r'^(monthly|yearly)$')
class TenantStatsResponse(BaseModel):
"""Tenant statistics response"""
tenant_id: str
total_members: int
active_members: int
total_predictions: int
models_trained: int
last_training_date: Optional[datetime]
subscription_plan: str
subscription_status: str
@validator('tenant_id', pre=True)
def convert_uuid_to_string(cls, v):
"""Convert UUID objects to strings for JSON serialization"""
if isinstance(v, UUID):
return str(v)
return v

View File

@@ -5,6 +5,7 @@ Tenant service messaging for event publishing
from shared.messaging.rabbitmq import RabbitMQClient
from app.core.config import settings
import structlog
from datetime import datetime
logger = structlog.get_logger()