Improve auth process

This commit is contained in:
Urtzi Alfaro
2025-07-20 08:22:17 +02:00
parent 5f56c2fd00
commit 8486d1db7c
5 changed files with 331 additions and 287 deletions

View File

@@ -36,7 +36,7 @@ async def register(
metrics = get_metrics_collector(request) metrics = get_metrics_collector(request)
try: try:
result = await AuthService.register_user(user_data, db) result = await AuthService.create_user(user_data, db)
# Record successful registration # Record successful registration
if metrics: if metrics:
@@ -76,7 +76,7 @@ async def login(
ip_address = request.client.host ip_address = request.client.host
user_agent = request.headers.get("user-agent", "") user_agent = request.headers.get("user-agent", "")
result = await AuthService.login_user(login_data, db, ip_address, user_agent) result = await AuthService.login(login_data, db, ip_address, user_agent)
# Record successful login # Record successful login
if metrics: if metrics:
@@ -113,7 +113,7 @@ async def refresh_token(
metrics = get_metrics_collector(request) metrics = get_metrics_collector(request)
try: try:
result = await AuthService.refresh_token(refresh_data.refresh_token, db) result = await security_manager.refresh_token(refresh_data.refresh_token, db)
# Record successful refresh # Record successful refresh
if metrics: if metrics:
@@ -156,7 +156,7 @@ async def verify_token(
) )
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
payload = await AuthService.verify_token(token) payload = await security_manager.verify_token(token)
# Record successful verification # Record successful verification
if metrics: if metrics:
@@ -199,7 +199,7 @@ async def logout(
) )
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
await AuthService.logout_user(token, db) await AuthService.logout(token, db)
# Record successful logout # Record successful logout
if metrics: if metrics:

View File

@@ -1,19 +1,18 @@
# services/auth/app/models/users.py - FIXED VERSION # services/auth/app/models/users.py - FIXED VERSION
# ================================================================
""" """
User models for authentication service - FIXED User models for authentication service - FIXED
Removed tenant relationships to eliminate cross-service dependencies
""" """
from sqlalchemy import Column, String, Boolean, DateTime, Text from sqlalchemy import Column, String, Boolean, DateTime, Text
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship # Import relationship
from datetime import datetime, timezone from datetime import datetime, timezone
import uuid import uuid
from shared.database.base import Base from shared.database.base import Base
class User(Base): class User(Base):
"""User model - FIXED timezone handling""" """User model - FIXED without cross-service relationships"""
__tablename__ = "users" __tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
@@ -22,22 +21,19 @@ class User(Base):
full_name = Column(String(255), nullable=False) full_name = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False) is_verified = Column(Boolean, default=False)
# Removed tenant_id and role from User model
# FIXED: Use timezone-aware datetime for all datetime fields # Timezone-aware datetime fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
last_login = Column(DateTime(timezone=True)) # FIXED: Now timezone-aware last_login = Column(DateTime(timezone=True))
# Profile fields # Profile fields
phone = Column(String(20)) phone = Column(String(20))
language = Column(String(10), default="es") language = Column(String(10), default="es")
timezone = Column(String(50), default="Europe/Madrid") timezone = Column(String(50), default="Europe/Madrid")
# Relationships # REMOVED: All tenant relationships - these are handled by tenant service
# Define the many-to-many relationship through TenantMember # No tenant_memberships, tenants relationships
tenant_memberships = relationship("TenantMember", back_populates="user", cascade="all, delete-orphan") # Changed back_populates to avoid conflict
tenants = relationship("Tenant", secondary="tenant_members", back_populates="users")
def __repr__(self): def __repr__(self):
return f"<User(id={self.id}, email={self.email})>" return f"<User(id={self.id}, email={self.email})>"
@@ -50,33 +46,26 @@ class User(Base):
"full_name": self.full_name, "full_name": self.full_name,
"is_active": self.is_active, "is_active": self.is_active,
"is_verified": self.is_verified, "is_verified": self.is_verified,
# Removed tenant_id and role from to_dict
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_login": self.last_login.isoformat() if self.last_login else None,
"phone": self.phone, "phone": self.phone,
"language": self.language, "language": self.language,
"timezone": self.timezone "timezone": self.timezone,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"last_login": self.last_login.isoformat() if self.last_login else None
} }
class RefreshToken(Base):
class UserSession(Base): """Refresh token model for JWT authentication"""
"""User session model - FIXED timezone handling""" __tablename__ = "refresh_tokens"
__tablename__ = "user_sessions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # No FK - cross-service
refresh_token_hash = Column(String(255), nullable=False) token = Column(String(255), unique=True, nullable=False)
is_active = Column(Boolean, default=True) expires_at = Column(DateTime(timezone=True), nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False) # FIXED: timezone-aware is_revoked = Column(Boolean, default=False)
# Session metadata
ip_address = Column(String(45))
user_agent = Column(Text)
device_info = Column(Text)
# FIXED: Use timezone-aware datetime
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) revoked_at = Column(DateTime(timezone=True))
def __repr__(self): def __repr__(self):
return f"<UserSession(id={self.id}, user_id={self.user_id})>" return f"<RefreshToken(user_id={self.user_id}, expires_at={self.expires_at})>"

View File

@@ -1,35 +1,66 @@
# ================================================================ # services/auth/app/services/auth_service.py - FIXED VERSION
# services/auth/app/services/auth_service.py (COMPLETE VERSION)
# ================================================================
""" """
Authentication service business logic - Complete implementation Authentication service - FIXED
Handles user authentication without cross-service dependencies
""" """
import structlog import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update from sqlalchemy import select
from fastapi import HTTPException, status from fastapi import HTTPException, status
import httpx
from app.models.users import User, RefreshToken
from app.core.security import SecurityManager
from app.core.config import settings from app.core.config import settings
from app.models.users import User, UserSession
from app.schemas.auth import UserRegistration, UserLogin, TokenResponse, UserResponse
from app.core.security import security_manager
from app.services.messaging import publish_user_registered, publish_user_login
logger = structlog.get_logger() logger = logging.getLogger(__name__)
class AuthService: class AuthService:
"""Authentication service business logic""" """Authentication service"""
@staticmethod @staticmethod
async def register_user(user_data: UserRegistration, db: AsyncSession) -> UserResponse: async def authenticate_user(email: str, password: str, db: AsyncSession) -> Optional[User]:
"""Register a new user""" """Authenticate user with email and password"""
try:
# Get user from database
result = await db.execute(
select(User).where(
User.email == email,
User.is_active == True
)
)
user = result.scalar_one_or_none()
if not user:
logger.warning(f"User not found: {email}")
return None
if not SecurityManager.verify_password(password, user.hashed_password):
logger.warning(f"Invalid password for user: {email}")
return None
# Update last login
user.last_login = datetime.now(timezone.utc)
await db.commit()
logger.info(f"User authenticated successfully: {email}")
return user
except Exception as e:
logger.error(f"Authentication error for {email}: {e}")
await db.rollback()
return None
@staticmethod
async def create_user(email: str, password: str, full_name: str, db: AsyncSession) -> User:
"""Create a new user"""
try:
# Check if user already exists # Check if user already exists
result = await db.execute( result = await db.execute(
select(User).where(User.email == user_data.email) select(User).where(User.email == email)
) )
existing_user = result.scalar_one_or_none() existing_user = result.scalar_one_or_none()
@@ -39,232 +70,222 @@ class AuthService:
detail="Email already registered" detail="Email already registered"
) )
# Hash password # Create new user
hashed_password = security_manager.hash_password(user_data.password) hashed_password = SecurityManager.get_password_hash(password)
# Create user
user = User( user = User(
email=user_data.email, email=email,
hashed_password=hashed_password, hashed_password=hashed_password,
full_name=user_data.full_name, full_name=full_name,
phone=user_data.phone,
language=user_data.language,
is_active=True, is_active=True,
is_verified=False # Email verification required is_verified=False
) )
db.add(user) db.add(user)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
# Publish user registered event - SIMPLIFIED logger.info(f"User created successfully: {email}")
event_data = { return user
"user_id": str(user.id),
"email": user.email,
"tenant_id": user.tenant_id,
"full_name": user.full_name,
"language": user.language,
"timestamp": datetime.now(timezone.utc).isoformat()
}
success = await publish_user_registered(event_data) except HTTPException:
if not success: raise
logger.warning("Failed to publish user registered event", user_id=str(user.id)) except Exception as e:
logger.error(f"User creation error: {e}")
logger.info(f"User registered: {user.email}") await db.rollback()
return UserResponse(**user.to_dict()) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
)
@staticmethod @staticmethod
async def login_user(login_data: UserLogin, db: AsyncSession, ip_address: str, user_agent: str) -> TokenResponse: async def login(email: str, password: str, db: AsyncSession) -> dict:
"""Authenticate user and create tokens""" """Login user and return tokens"""
try:
# Check login attempts # Authenticate user
if not await security_manager.check_login_attempts(login_data.email): user = await AuthService.authenticate_user(email, password, db)
raise HTTPException( if not user:
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many login attempts. Please try again later."
)
# Get user
result = await db.execute(
select(User).where(User.email == login_data.email)
)
user = result.scalar_one_or_none()
if not user or not security_manager.verify_password(login_data.password, user.hashed_password):
await security_manager.increment_login_attempts(login_data.email)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password" detail="Invalid credentials"
) )
if not user.is_active: # Get user's tenant memberships from tenant service
raise HTTPException( tenant_memberships = await AuthService._get_user_tenants(str(user.id))
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Account is inactive"
)
# Clear login attempts
await security_manager.clear_login_attempts(login_data.email)
# Update last login
await db.execute(
update(User)
.where(User.id == user.id)
.values(last_login=datetime.now(timezone.utc))
)
await db.commit()
# Create tokens # Create tokens
token_data = { access_token = SecurityManager.create_access_token(
"sub": str(user.id), # Standard JWT claim for subject data={
"user_id": str(user.id), "user_id": str(user.id),
"email": user.email, "email": user.email,
"tenant_id": str(user.tenant_id) if user.tenant_id else None, "full_name": user.full_name,
"role": user.role "tenants": tenant_memberships # Include tenant info in token
} }
access_token = security_manager.create_access_token(token_data)
refresh_token = security_manager.create_refresh_token(token_data)
# Store refresh token
await security_manager.store_refresh_token(str(user.id), refresh_token)
# Create session record
session = UserSession(
user_id=user.id,
refresh_token_hash=security_manager.hash_token(refresh_token),
ip_address=ip_address,
user_agent=user_agent,
expires_at=datetime.now(timezone.utc) + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS)
) )
db.add(session) refresh_token_value = SecurityManager.create_refresh_token(data={"user_id": str(user.id)})
# Store refresh token in database
refresh_token = RefreshToken(
user_id=user.id,
token=refresh_token_value,
expires_at=datetime.now(timezone.utc) + timedelta(days=30)
)
db.add(refresh_token)
await db.commit() await db.commit()
# Publish login event - SIMPLIFIED logger.info(f"User logged in successfully: {email}")
event_data = {
"user_id": str(user.id), return {
"email": user.email, "access_token": access_token,
"ip_address": ip_address, "refresh_token": refresh_token_value,
"user_agent": user_agent, "token_type": "bearer",
"timestamp": datetime.now(timezone.utc).isoformat() "user": user.to_dict(),
"tenants": tenant_memberships
} }
success = await publish_user_login(event_data) except HTTPException:
if not success: raise
logger.warning("Failed to publish login event", user_id=str(user.id)) except Exception as e:
logger.error(f"Login error for {email}: {e}")
logger.info("User login successful", user_id=str(user.id), email=user.email) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
return TokenResponse( detail="Login failed"
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60
) )
@staticmethod @staticmethod
async def refresh_token(refresh_token: str, db: AsyncSession) -> TokenResponse: async def refresh_access_token(refresh_token: str, db: AsyncSession) -> dict:
"""Refresh access token using refresh token""" """Refresh access token using refresh token"""
try:
# Verify refresh token # Verify refresh token
token_data = security_manager.verify_token(refresh_token) payload = SecurityManager.verify_token(refresh_token)
if not token_data or token_data.get("type") != "refresh": if not payload:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token" detail="Invalid refresh token"
) )
user_id = token_data.get("user_id") user_id = payload.get("user_id")
if not user_id: if not user_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token data" detail="Invalid refresh token"
) )
# Check if refresh token exists in Redis # Check if refresh token exists and is valid
if not await security_manager.verify_refresh_token(user_id, refresh_token): result = await db.execute(
select(RefreshToken).where(
RefreshToken.token == refresh_token,
RefreshToken.user_id == user_id,
RefreshToken.is_revoked == False,
RefreshToken.expires_at > datetime.now(timezone.utc)
)
)
stored_token = result.scalar_one_or_none()
if not stored_token:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token not found or expired" detail="Invalid or expired refresh token"
) )
# Get user # Get user
result = await db.execute( result = await db.execute(
select(User).where(User.id == user_id) select(User).where(User.id == user_id, User.is_active == True)
) )
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if not user or not user.is_active: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive" detail="User not found"
) )
# Create new tokens # Get user's tenant memberships from tenant service
new_token_data = { tenant_memberships = await AuthService._get_user_tenants(str(user.id))
"sub": str(user.id),
# Create new access token
access_token = SecurityManager.create_access_token(
data={
"user_id": str(user.id), "user_id": str(user.id),
"email": user.email, "email": user.email,
"tenant_id": str(user.tenant_id) if user.tenant_id else None, "full_name": user.full_name,
"role": user.role "tenants": tenant_memberships
}
)
return {
"access_token": access_token,
"token_type": "bearer"
} }
new_access_token = security_manager.create_access_token(new_token_data) except HTTPException:
new_refresh_token = security_manager.create_refresh_token(new_token_data) raise
except Exception as e:
# Revoke old refresh token logger.error(f"Token refresh error: {e}")
await security_manager.revoke_refresh_token(user_id, refresh_token)
# Store new refresh token
await security_manager.store_refresh_token(user_id, new_refresh_token)
logger.info(f"Token refreshed for user: {user.email}")
return TokenResponse(
access_token=new_access_token,
refresh_token=new_refresh_token,
token_type="bearer",
expires_in=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
@staticmethod
async def verify_token(token: str) -> Dict[str, Any]:
"""Verify access token and return user data"""
token_data = security_manager.verify_token(token)
if not token_data or token_data.get("type") != "access":
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid access token" detail="Token refresh failed"
) )
return token_data
@staticmethod @staticmethod
async def logout_user(user_id: str, refresh_token: str, db: AsyncSession) -> bool: async def logout(refresh_token: str, db: AsyncSession) -> bool:
"""Logout user and revoke tokens""" """Logout user by revoking refresh token"""
try: try:
# Revoke refresh token # Revoke refresh token
await security_manager.revoke_refresh_token(user_id, refresh_token) result = await db.execute(
select(RefreshToken).where(RefreshToken.token == refresh_token)
# Deactivate session
await db.execute(
update(UserSession)
.where(
UserSession.user_id == user_id,
UserSession.refresh_token_hash == security_manager.hash_token(refresh_token)
)
.values(is_active=False)
) )
token = result.scalar_one_or_none()
if token:
token.is_revoked = True
token.revoked_at = datetime.now(timezone.utc)
await db.commit() await db.commit()
logger.info(f"User logged out: {user_id}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error logging out user {user_id}: {e}") logger.error(f"Logout error: {e}")
await db.rollback() await db.rollback()
return False return False
@staticmethod
async def verify_user_token(token: str) -> dict:
"""Verify access token and return user info"""
try:
payload = SecurityManager.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
return payload
except Exception as e:
logger.error(f"Token verification error: {e}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
@staticmethod
async def _get_user_tenants(user_id: str) -> list:
"""Get user's tenant memberships from tenant service"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{settings.TENANT_SERVICE_URL}/api/v1/tenants/user/{user_id}/memberships",
timeout=5.0
)
if response.status_code == 200:
return response.json()
else:
logger.warning(f"Failed to get user tenants: {response.status_code}")
return []
except Exception as e:
logger.error(f"Error getting user tenants: {e}")
return []

View File

@@ -18,4 +18,4 @@ pytz==2023.3
python-logstash==0.4.8 python-logstash==0.4.8
structlog==23.2.0 structlog==23.2.0
python-dotenv==1.0.0 python-dotenv==1.0.0
httpx==0.25.2

View File

@@ -1,12 +1,13 @@
# services/tenant/app/models/tenants.py # services/tenant/app/models/tenants.py - FIXED VERSION
""" """
Tenant models for bakery management Tenant models for bakery management - FIXED
Removed cross-service User relationship to eliminate circular dependencies
""" """
from sqlalchemy import Column, String, Boolean, DateTime, Float, ForeignKey, Text from sqlalchemy import Column, String, Boolean, DateTime, Float, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime, timezone
import uuid import uuid
from shared.database.base import Base from shared.database.base import Base
@@ -37,30 +38,30 @@ class Tenant(Base):
# ML status # ML status
model_trained = Column(Boolean, default=False) model_trained = Column(Boolean, default=False)
last_training_date = Column(DateTime) last_training_date = Column(DateTime(timezone=True))
# Ownership (The user who created the tenant, still a direct link) # Ownership (user_id without FK - cross-service reference)
owner_id = Column(UUID(as_uuid=True), nullable=False, index=True) owner_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Timestamps # Timestamps
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships # Relationships - only within tenant service
# Define the many-to-many relationship through TenantMember
members = relationship("TenantMember", back_populates="tenant", cascade="all, delete-orphan") members = relationship("TenantMember", back_populates="tenant", cascade="all, delete-orphan")
users = relationship("User", secondary="tenant_members", back_populates="tenants")
# REMOVED: users relationship - no cross-service SQLAlchemy relationships
def __repr__(self): def __repr__(self):
return f"<Tenant(id={self.id}, name={self.name})>" return f"<Tenant(id={self.id}, name={self.name})>"
class TenantMember(Base): class TenantMember(Base):
"""Tenant membership model for team access - Association Table""" """Tenant membership model for team access"""
__tablename__ = "tenant_members" __tablename__ = "tenant_members"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False) tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) # Added ForeignKey to users.id user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # No FK - cross-service reference
# Role and permissions specific to this tenant # Role and permissions specific to this tenant
role = Column(String(50), default="member") # owner, admin, member, viewer role = Column(String(50), default="member") # owner, admin, member, viewer
@@ -68,15 +69,48 @@ class TenantMember(Base):
# Status # Status
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
invited_by = Column(UUID(as_uuid=True)) invited_by = Column(UUID(as_uuid=True)) # No FK - cross-service reference
invited_at = Column(DateTime, default=datetime.utcnow) invited_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
joined_at = Column(DateTime) joined_at = Column(DateTime(timezone=True))
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Relationships to access the associated Tenant and User objects # Relationships - only within tenant service
tenant = relationship("Tenant", back_populates="members") tenant = relationship("Tenant", back_populates="members")
user = relationship("User", back_populates="tenant_memberships") # Changed back_populates to avoid conflict
# REMOVED: user relationship - no cross-service SQLAlchemy relationships
def __repr__(self): def __repr__(self):
return f"<TenantMember(tenant_id={self.tenant_id}, user_id={self.user_id}, role={self.role})>" return f"<TenantMember(tenant_id={self.tenant_id}, user_id={self.user_id}, role={self.role})>"
# Additional models for subscriptions, plans, etc.
class Subscription(Base):
"""Subscription model for tenant billing"""
__tablename__ = "subscriptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
plan = Column(String(50), default="basic") # basic, professional, enterprise
status = Column(String(50), default="active") # active, suspended, cancelled
# Billing
monthly_price = Column(Float, default=0.0)
billing_cycle = Column(String(20), default="monthly") # monthly, yearly
next_billing_date = Column(DateTime(timezone=True))
trial_ends_at = Column(DateTime(timezone=True))
# Limits
max_users = Column(Integer, default=1)
max_locations = Column(Integer, default=1)
max_products = Column(Integer, default=50)
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
tenant = relationship("Tenant")
def __repr__(self):
return f"<Subscription(tenant_id={self.tenant_id}, plan={self.plan}, status={self.status})>"