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 from fastapi import APIRouter, Request, HTTPException
@@ -17,16 +17,37 @@ async def create_tenant(request: Request):
"""Proxy tenant creation to tenant service""" """Proxy tenant creation to tenant service"""
try: try:
body = await request.body() 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: async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post( response = await client.post(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/register", f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/register",
content=body, content=body,
headers={ headers=headers
"Content-Type": "application/json",
"Authorization": auth_header
}
) )
return JSONResponse( return JSONResponse(
@@ -45,12 +66,81 @@ async def create_tenant(request: Request):
async def get_tenants(request: Request): async def get_tenants(request: Request):
"""Get tenants""" """Get tenants"""
try: 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: async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get( response = await client.get(
f"{settings.TENANT_SERVICE_URL}/tenants", f"{settings.TENANT_SERVICE_URL}/api/v1/tenants",
headers={"Authorization": auth_header} 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( return JSONResponse(

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ================================================================ # ================================================================
# Complete Authentication Test with Registration # Complete Authentication Test with Registration - FIXED VERSION
# Tests the full user lifecycle: registration → login → API access # 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_EMAIL="test-$(date +%s)@bakery.com" # Unique email for each test
TEST_PASSWORD="SecurePass123!" TEST_PASSWORD="SecurePass123!"
TEST_NAME="Test Baker" 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 # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
@@ -73,6 +74,12 @@ if ! check_service_health "http://localhost:8001" "Auth Service"; then
exit 1 exit 1
fi 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 # Check Data Service
if ! check_service_health "http://localhost:8004" "Data Service"; then if ! check_service_health "http://localhost:8004" "Data Service"; then
log_warning "Data Service is not running, but continuing with auth tests..." log_warning "Data Service is not running, but continuing with auth tests..."
@@ -149,13 +156,13 @@ fi
echo "" 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 # 3a. Get current user info
log_step "4a. Getting current user profile" log_step "3a. Getting current user profile"
USER_PROFILE_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/users/me" \ USER_PROFILE_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/users/me" \
-H "Authorization: Bearer $ACCESS_TOKEN") -H "Authorization: Bearer $ACCESS_TOKEN")
@@ -171,53 +178,11 @@ fi
echo "" echo ""
# 4b. Test data service through gateway
log_step "4b. Testing data service through gateway"
DATA_RESPONSE=$(curl -s -X GET "$API_BASE/api/v1/data/sales" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID")
echo "Data Service Response:"
echo "$DATA_RESPONSE" | jq '.'
if [ "$(echo "$DATA_RESPONSE" | jq -r '.status // "unknown"')" != "error" ]; then
log_success "Data service access successful!"
else
log_warning "Data service returned error (may be expected for new tenant)"
fi
echo ""
# 4c. Test training service through gateway
log_step "4c. Testing training service through gateway"
TRAINING_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/training/jobs" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-ID: $TENANT_ID" \
-H "Content-Type: application/json" \
-d '{
"include_weather": true,
"include_traffic": false,
"min_data_points": 30
}')
echo "Training Service Response:"
echo "$TRAINING_RESPONSE" | jq '.'
if echo "$TRAINING_RESPONSE" | jq -e '.job_id // .message' > /dev/null; then
log_success "Training service access successful!"
else
log_warning "Training service access may have issues"
fi
echo ""
# ================================================================ # ================================================================
# STEP 5: TENANT REGISTRATION (OPTIONAL) # STEP 4: TENANT REGISTRATION (BAKERY CREATION)
# ================================================================ # ================================================================
log_step "Step 5: Registering a bakery/tenant" log_step "Step 4: Registering a bakery/tenant"
BAKERY_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/register" \ BAKERY_RESPONSE=$(curl -s -X POST "$API_BASE/api/v1/tenants/register" \
-H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Authorization: Bearer $ACCESS_TOKEN" \
@@ -235,19 +200,80 @@ echo "Bakery Registration Response:"
echo "$BAKERY_RESPONSE" | jq '.' echo "$BAKERY_RESPONSE" | jq '.'
if echo "$BAKERY_RESPONSE" | jq -e '.id' > /dev/null; then 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') TENANT_ID=$(echo "$BAKERY_RESPONSE" | jq -r '.id')
log_success "Bakery registration successful! Tenant ID: $TENANT_ID" log_success "Bakery registration successful! Tenant ID: $TENANT_ID"
else else
log_warning "Bakery registration endpoint may not be fully implemented" log_error "Bakery registration failed!"
echo "Response: $BAKERY_RESPONSE"
# Continue with tests using placeholder UUID for other endpoints
fi fi
echo "" echo ""
# ================================================================ # ================================================================
# STEP 6: TOKEN REFRESH # STEP 5: TEST DATA SERVICE WITH TENANT ID
# ================================================================ # ================================================================
log_step "Step 6: Testing token refresh" 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")
echo "Data Service Response:"
echo "$DATA_RESPONSE" | jq '.'
if [ "$(echo "$DATA_RESPONSE" | jq -r '.status // "unknown"')" != "error" ]; then
log_success "Data service access successful!"
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 ""
# ================================================================
# 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" \
-H "Content-Type: application/json" \
-d '{
"include_weather": true,
"include_traffic": false,
"min_data_points": 30
}')
echo "Training Service Response:"
echo "$TRAINING_RESPONSE" | jq '.'
if echo "$TRAINING_RESPONSE" | jq -e '.job_id // .message' > /dev/null; then
log_success "Training service access successful!"
else
log_warning "Training service access may have issues"
fi
else
log_warning "Skipping training service test - no valid tenant ID"
fi
echo ""
# ================================================================
# STEP 7: TOKEN REFRESH
# ================================================================
log_step "Step 7: Testing token refresh"
REFRESH_RESPONSE=$(curl -s -X POST "$AUTH_BASE/refresh" \ REFRESH_RESPONSE=$(curl -s -X POST "$AUTH_BASE/refresh" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -268,19 +294,19 @@ fi
echo "" 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 # 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") AUTH_HEALTH=$(curl -s -X GET "http://localhost:8001/health")
echo "Auth Service Health:" echo "Auth Service Health:"
echo "$AUTH_HEALTH" | jq '.' echo "$AUTH_HEALTH" | jq '.'
# Test other services if available # 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") services=("8002:Training" "8003:Forecasting" "8004:Data" "8005:Tenant" "8006:Notification")
@@ -299,10 +325,10 @@ done
echo "" 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" \ LOGOUT_RESPONSE=$(curl -s -X POST "$AUTH_BASE/logout" \
-H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Authorization: Bearer $ACCESS_TOKEN" \
@@ -342,12 +368,12 @@ echo ""
echo "Services Tested:" echo "Services Tested:"
echo " 🌐 API Gateway" echo " 🌐 API Gateway"
echo " 🔐 Auth Service" echo " 🔐 Auth Service"
echo " 🏢 Tenant Service (bakery registration)"
echo " 📊 Data Service (through gateway)" echo " 📊 Data Service (through gateway)"
echo " 🤖 Training Service (through gateway)" echo " 🤖 Training Service (through gateway)"
echo " 🏢 Tenant Service (bakery registration)"
echo "" echo ""
if [ -n "$TENANT_ID" ]; then if [ "$TENANT_ID" != "00000000-0000-0000-0000-000000000000" ]; then
echo "Tenant Created:" echo "Tenant Created:"
echo " 🏪 Tenant ID: $TENANT_ID" echo " 🏪 Tenant ID: $TENANT_ID"
echo "" echo ""

View File

@@ -103,8 +103,14 @@ async def login(
metrics = get_metrics_collector(request) metrics = get_metrics_collector(request)
try: 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 # Attempt login
result = await AuthService.login(login_data.email, login_data.password, db) result = await AuthService.login(login_data.email, login_data.password, db)

View File

@@ -1,6 +1,6 @@
# services/tenant/app/main.py # services/tenant/app/main.py
""" """
Tenant Service FastAPI application Tenant Service FastAPI application - FIXED VERSION
""" """
import structlog import structlog
@@ -47,11 +47,35 @@ async def startup_event():
"""Initialize service on startup""" """Initialize service on startup"""
logger.info("Starting Tenant Service...") 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") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
"""Cleanup on shutdown""" """Cleanup on shutdown"""
logger.info("Shutting down Tenant Service...") logger.info("Shutting down Tenant Service...")
await database_manager.engine.dispose()
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") @app.get("/health")
async def health_check(): async def health_check():

View File

@@ -1,11 +1,12 @@
# services/tenant/app/schemas/tenants.py # services/tenant/app/schemas/tenants.py
""" """
Tenant schemas Tenant schemas - FIXED VERSION
""" """
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from datetime import datetime from datetime import datetime
from uuid import UUID
import re import re
class BakeryRegistration(BaseModel): class BakeryRegistration(BaseModel):
@@ -42,8 +43,8 @@ class BakeryRegistration(BaseModel):
return v return v
class TenantResponse(BaseModel): class TenantResponse(BaseModel):
"""Tenant response schema""" """Tenant response schema - FIXED VERSION"""
id: str id: str # ✅ Keep as str for Pydantic validation
name: str name: str
subdomain: Optional[str] subdomain: Optional[str]
business_type: str business_type: str
@@ -55,9 +56,17 @@ class TenantResponse(BaseModel):
subscription_tier: str subscription_tier: str
model_trained: bool model_trained: bool
last_training_date: Optional[datetime] last_training_date: Optional[datetime]
owner_id: str owner_id: str # ✅ Keep as str for Pydantic validation
created_at: datetime 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: class Config:
from_attributes = True from_attributes = True
@@ -68,16 +77,70 @@ class TenantAccessResponse(BaseModel):
permissions: List[str] permissions: List[str]
class TenantMemberResponse(BaseModel): class TenantMemberResponse(BaseModel):
"""Tenant member response""" """Tenant member response - FIXED VERSION"""
id: str id: str
user_id: str user_id: str
role: str role: str
is_active: bool is_active: bool
joined_at: Optional[datetime] 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): class TenantUpdate(BaseModel):
"""Tenant update schema""" """Tenant update schema"""
name: Optional[str] = Field(None, min_length=2, max_length=200) name: Optional[str] = Field(None, min_length=2, max_length=200)
address: Optional[str] = Field(None, min_length=10, max_length=500) address: Optional[str] = Field(None, min_length=10, max_length=500)
phone: Optional[str] = None phone: Optional[str] = None
business_type: 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 shared.messaging.rabbitmq import RabbitMQClient
from app.core.config import settings from app.core.config import settings
import structlog import structlog
from datetime import datetime
logger = structlog.get_logger() logger = structlog.get_logger()