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,270 +1,291 @@
# ================================================================ # 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:
# Check if user already exists # Get user from database
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() User.is_active == True
)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
) )
user = result.scalar_one_or_none()
# Hash password if not user:
hashed_password = security_manager.hash_password(user_data.password) logger.warning(f"User not found: {email}")
return None
# Create user if not SecurityManager.verify_password(password, user.hashed_password):
user = User( logger.warning(f"Invalid password for user: {email}")
email=user_data.email, return None
hashed_password=hashed_password,
full_name=user_data.full_name,
phone=user_data.phone,
language=user_data.language,
is_active=True,
is_verified=False # Email verification required
)
db.add(user) # Update last login
await db.commit() user.last_login = datetime.now(timezone.utc)
await db.refresh(user) await db.commit()
# Publish user registered event - SIMPLIFIED logger.info(f"User authenticated 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 Exception as e:
if not success: logger.error(f"Authentication error for {email}: {e}")
logger.warning("Failed to publish user registered event", user_id=str(user.id)) await db.rollback()
return None
logger.info(f"User registered: {user.email}")
return UserResponse(**user.to_dict())
@staticmethod @staticmethod
async def login_user(login_data: UserLogin, db: AsyncSession, ip_address: str, user_agent: str) -> TokenResponse: async def create_user(email: str, password: str, full_name: str, db: AsyncSession) -> User:
"""Authenticate user and create tokens""" """Create a new user"""
try:
# Check if user already exists
result = await db.execute(
select(User).where(User.email == email)
)
existing_user = result.scalar_one_or_none()
# Check login attempts if existing_user:
if not await security_manager.check_login_attempts(login_data.email): raise HTTPException(
raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST,
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Email already registered"
detail="Too many login attempts. Please try again later." )
# Create new user
hashed_password = SecurityManager.get_password_hash(password)
user = User(
email=email,
hashed_password=hashed_password,
full_name=full_name,
is_active=True,
is_verified=False
) )
# Get user db.add(user)
result = await db.execute( await db.commit()
select(User).where(User.email == login_data.email) await db.refresh(user)
)
user = result.scalar_one_or_none()
if not user or not security_manager.verify_password(login_data.password, user.hashed_password): logger.info(f"User created successfully: {email}")
await security_manager.increment_login_attempts(login_data.email) return user
except HTTPException:
raise
except Exception as e:
logger.error(f"User creation error: {e}")
await db.rollback()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid email or password" detail="Failed to create user"
) )
if not user.is_active:
raise HTTPException(
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
token_data = {
"sub": str(user.id), # Standard JWT claim for subject
"user_id": str(user.id),
"email": user.email,
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
"role": user.role
}
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)
await db.commit()
# Publish login event - SIMPLIFIED
event_data = {
"user_id": str(user.id),
"email": user.email,
"ip_address": ip_address,
"user_agent": user_agent,
"timestamp": datetime.now(timezone.utc).isoformat()
}
success = await publish_user_login(event_data)
if not success:
logger.warning("Failed to publish login event", user_id=str(user.id))
logger.info("User login successful", user_id=str(user.id), email=user.email)
return TokenResponse(
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 login(email: str, password: str, db: AsyncSession) -> dict:
"""Login user and return tokens"""
try:
# Authenticate user
user = await AuthService.authenticate_user(email, password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials"
)
# Get user's tenant memberships from tenant service
tenant_memberships = await AuthService._get_user_tenants(str(user.id))
# Create tokens
access_token = SecurityManager.create_access_token(
data={
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"tenants": tenant_memberships # Include tenant info in token
}
)
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()
logger.info(f"User logged in successfully: {email}")
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"user": user.to_dict(),
"tenants": tenant_memberships
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Login error for {email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@staticmethod
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
payload = SecurityManager.verify_token(refresh_token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
# Verify refresh token user_id = payload.get("user_id")
token_data = security_manager.verify_token(refresh_token) if not user_id:
if not token_data or token_data.get("type") != "refresh": raise HTTPException(
raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED,
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
detail="Invalid refresh token" )
# Check if refresh token exists and is valid
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)
)
) )
user_id = token_data.get("user_id") stored_token = result.scalar_one_or_none()
if not user_id: if not stored_token:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token data" detail="Invalid or expired refresh token"
)
# Get user
result = await db.execute(
select(User).where(User.id == user_id, User.is_active == True)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
# Get user's tenant memberships from tenant service
tenant_memberships = await AuthService._get_user_tenants(str(user.id))
# Create new access token
access_token = SecurityManager.create_access_token(
data={
"user_id": str(user.id),
"email": user.email,
"full_name": user.full_name,
"tenants": tenant_memberships
}
) )
# Check if refresh token exists in Redis return {
if not await security_manager.verify_refresh_token(user_id, refresh_token): "access_token": access_token,
"token_type": "bearer"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Token refresh error: {e}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Refresh token not found or expired" detail="Token refresh failed"
) )
# Get user
result = await db.execute(
select(User).where(User.id == user_id)
)
user = result.scalar_one_or_none()
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive"
)
# Create new tokens
new_token_data = {
"sub": str(user.id),
"user_id": str(user.id),
"email": user.email,
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
"role": user.role
}
new_access_token = security_manager.create_access_token(new_token_data)
new_refresh_token = security_manager.create_refresh_token(new_token_data)
# Revoke old refresh token
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 @staticmethod
async def verify_token(token: str) -> Dict[str, Any]: async def logout(refresh_token: str, db: AsyncSession) -> bool:
"""Verify access token and return user data""" """Logout user by revoking refresh token"""
token_data = security_manager.verify_token(token)
if not token_data or token_data.get("type") != "access":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid access token"
)
return token_data
@staticmethod
async def logout_user(user_id: str, refresh_token: str, db: AsyncSession) -> bool:
"""Logout user and revoke tokens"""
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)
) )
await db.commit() token = result.scalar_one_or_none()
logger.info(f"User logged out: {user_id}") if token:
token.is_revoked = True
token.revoked_at = datetime.now(timezone.utc)
await db.commit()
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})>"