Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# services/auth/app/models/__init__.py
"""
Models export for auth service
"""
# Import AuditLog model for this service
from shared.security import create_audit_log_model
from shared.database.base import Base
# Create audit log model for this service
AuditLog = create_audit_log_model(Base)
from .users import User
from .tokens import RefreshToken, LoginAttempt
from .onboarding import UserOnboardingProgress, UserOnboardingSummary
from .consent import UserConsent, ConsentHistory
from .deletion_job import DeletionJob
from .password_reset_tokens import PasswordResetToken
__all__ = [
'User',
'RefreshToken',
'LoginAttempt',
'UserOnboardingProgress',
'UserOnboardingSummary',
'UserConsent',
'ConsentHistory',
'DeletionJob',
'PasswordResetToken',
"AuditLog",
]

View File

@@ -0,0 +1,110 @@
"""
User consent tracking models for GDPR compliance
"""
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Index
from sqlalchemy.dialects.postgresql import UUID, JSON
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class UserConsent(Base):
"""
Tracks user consent for various data processing activities
GDPR Article 7 - Conditions for consent
"""
__tablename__ = "user_consents"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Consent types
terms_accepted = Column(Boolean, nullable=False, default=False)
privacy_accepted = Column(Boolean, nullable=False, default=False)
marketing_consent = Column(Boolean, nullable=False, default=False)
analytics_consent = Column(Boolean, nullable=False, default=False)
# Consent metadata
consent_version = Column(String(20), nullable=False, default="1.0")
consent_method = Column(String(50), nullable=False) # registration, settings_update, cookie_banner
ip_address = Column(String(45), nullable=True)
user_agent = Column(Text, nullable=True)
# Consent text at time of acceptance
terms_text_hash = Column(String(64), nullable=True)
privacy_text_hash = Column(String(64), nullable=True)
# Timestamps
consented_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
withdrawn_at = Column(DateTime(timezone=True), nullable=True)
# Additional metadata (renamed from 'metadata' to avoid SQLAlchemy reserved word)
extra_data = Column(JSON, nullable=True)
__table_args__ = (
Index('idx_user_consent_user_id', 'user_id'),
Index('idx_user_consent_consented_at', 'consented_at'),
)
def __repr__(self):
return f"<UserConsent(user_id={self.user_id}, version={self.consent_version})>"
def to_dict(self):
return {
"id": str(self.id),
"user_id": str(self.user_id),
"terms_accepted": self.terms_accepted,
"privacy_accepted": self.privacy_accepted,
"marketing_consent": self.marketing_consent,
"analytics_consent": self.analytics_consent,
"consent_version": self.consent_version,
"consent_method": self.consent_method,
"consented_at": self.consented_at.isoformat() if self.consented_at else None,
"withdrawn_at": self.withdrawn_at.isoformat() if self.withdrawn_at else None,
}
class ConsentHistory(Base):
"""
Historical record of all consent changes
Provides audit trail for GDPR compliance
"""
__tablename__ = "consent_history"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
consent_id = Column(UUID(as_uuid=True), ForeignKey("user_consents.id", ondelete="SET NULL"), nullable=True)
# Action type
action = Column(String(50), nullable=False) # granted, updated, withdrawn, revoked
# Consent state at time of action
consent_snapshot = Column(JSON, nullable=False)
# Context
ip_address = Column(String(45), nullable=True)
user_agent = Column(Text, nullable=True)
consent_method = Column(String(50), nullable=True)
# Timestamp
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
__table_args__ = (
Index('idx_consent_history_user_id', 'user_id'),
Index('idx_consent_history_created_at', 'created_at'),
Index('idx_consent_history_action', 'action'),
)
def __repr__(self):
return f"<ConsentHistory(user_id={self.user_id}, action={self.action})>"
def to_dict(self):
return {
"id": str(self.id),
"user_id": str(self.user_id),
"action": self.action,
"consent_snapshot": self.consent_snapshot,
"created_at": self.created_at.isoformat() if self.created_at else None,
}

View File

@@ -0,0 +1,64 @@
"""
Deletion Job Model
Tracks tenant deletion jobs for persistence and recovery
"""
from sqlalchemy import Column, String, DateTime, Text, JSON, Index, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
import uuid
from shared.database.base import Base
class DeletionJob(Base):
"""
Persistent storage for tenant deletion jobs
Enables job recovery and tracking across service restarts
"""
__tablename__ = "deletion_jobs"
# Primary identifiers
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
job_id = Column(String(100), nullable=False, unique=True, index=True) # External job ID
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Job Metadata
tenant_name = Column(String(255), nullable=True)
initiated_by = Column(UUID(as_uuid=True), nullable=True) # User ID who started deletion
# Job Status
status = Column(String(50), nullable=False, default="pending", index=True) # pending, in_progress, completed, failed, rolled_back
# Service Results
service_results = Column(JSON, nullable=True) # Dict of service_name -> result details
# Progress Tracking
total_items_deleted = Column(Integer, default=0, nullable=False)
services_completed = Column(Integer, default=0, nullable=False)
services_failed = Column(Integer, default=0, nullable=False)
# Error Tracking
error_log = Column(JSON, nullable=True) # Array of error messages
# Timestamps
started_at = Column(DateTime(timezone=True), nullable=True, index=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
# Additional Context
notes = Column(Text, nullable=True)
extra_metadata = Column(JSON, nullable=True) # Additional job-specific data
# Indexes for performance
__table_args__ = (
Index('idx_deletion_job_id', 'job_id'),
Index('idx_deletion_tenant_id', 'tenant_id'),
Index('idx_deletion_status', 'status'),
Index('idx_deletion_started_at', 'started_at'),
Index('idx_deletion_tenant_status', 'tenant_id', 'status'),
)
def __repr__(self):
return f"<DeletionJob(job_id='{self.job_id}', tenant_id={self.tenant_id}, status='{self.status}')>"

View File

@@ -0,0 +1,91 @@
# services/auth/app/models/onboarding.py
"""
User onboarding progress models
"""
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, JSON, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class UserOnboardingProgress(Base):
"""User onboarding progress tracking model"""
__tablename__ = "user_onboarding_progress"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
# Step tracking
step_name = Column(String(50), nullable=False)
completed = Column(Boolean, default=False, nullable=False)
completed_at = Column(DateTime(timezone=True))
# Additional step data (JSON field for flexibility)
step_data = Column(JSON, default=dict)
# 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))
# Unique constraint to prevent duplicate step entries per user
__table_args__ = (
UniqueConstraint('user_id', 'step_name', name='uq_user_step'),
{'extend_existing': True}
)
def __repr__(self):
return f"<UserOnboardingProgress(id={self.id}, user_id={self.user_id}, step={self.step_name}, completed={self.completed})>"
def to_dict(self):
"""Convert to dictionary"""
return {
"id": str(self.id),
"user_id": str(self.user_id),
"step_name": self.step_name,
"completed": self.completed,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"step_data": self.step_data or {},
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
class UserOnboardingSummary(Base):
"""User onboarding summary for quick lookups"""
__tablename__ = "user_onboarding_summary"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
# Summary fields
current_step = Column(String(50), nullable=False, default="user_registered")
next_step = Column(String(50))
completion_percentage = Column(String(50), default="0.0") # Store as string for precision
fully_completed = Column(Boolean, default=False)
# Progress tracking
steps_completed_count = Column(String(50), default="0") # Store as string: "3/5"
# 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))
last_activity_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
def __repr__(self):
return f"<UserOnboardingSummary(user_id={self.user_id}, current_step={self.current_step}, completion={self.completion_percentage}%)>"
def to_dict(self):
"""Convert to dictionary"""
return {
"id": str(self.id),
"user_id": str(self.user_id),
"current_step": self.current_step,
"next_step": self.next_step,
"completion_percentage": float(self.completion_percentage) if self.completion_percentage else 0.0,
"fully_completed": self.fully_completed,
"steps_completed_count": self.steps_completed_count,
"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_activity_at": self.last_activity_at.isoformat() if self.last_activity_at else None
}

View File

@@ -0,0 +1,39 @@
# services/auth/app/models/password_reset_tokens.py
"""
Password reset token model for authentication service
"""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, String, DateTime, Boolean, Index
from sqlalchemy.dialects.postgresql import UUID
from shared.database.base import Base
class PasswordResetToken(Base):
"""
Password reset token model
Stores temporary tokens for password reset functionality
"""
__tablename__ = "password_reset_tokens"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
token = Column(String(255), nullable=False, unique=True, index=True)
expires_at = Column(DateTime(timezone=True), nullable=False)
is_used = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
used_at = Column(DateTime(timezone=True), nullable=True)
# Add indexes for better performance
__table_args__ = (
Index('ix_password_reset_tokens_user_id', 'user_id'),
Index('ix_password_reset_tokens_token', 'token'),
Index('ix_password_reset_tokens_expires_at', 'expires_at'),
Index('ix_password_reset_tokens_is_used', 'is_used'),
)
def __repr__(self):
return f"<PasswordResetToken(id={self.id}, user_id={self.user_id}, token={self.token[:10]}..., is_used={self.is_used})>"

View File

@@ -0,0 +1,92 @@
# ================================================================
# services/auth/app/models/tokens.py
# ================================================================
"""
Token models for authentication service
"""
import hashlib
import uuid
from datetime import datetime, timezone
from sqlalchemy import Column, String, Boolean, DateTime, Text, Index
from sqlalchemy.dialects.postgresql import UUID
from shared.database.base import Base
class RefreshToken(Base):
"""
Refresh token model - FIXED to prevent duplicate constraint violations
"""
__tablename__ = "refresh_tokens"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# ✅ FIX 1: Use TEXT instead of VARCHAR to handle longer tokens
token = Column(Text, nullable=False)
# ✅ FIX 2: Add token hash for uniqueness instead of full token
token_hash = Column(String(255), nullable=True, unique=True)
expires_at = Column(DateTime(timezone=True), nullable=False)
is_revoked = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
revoked_at = Column(DateTime(timezone=True), nullable=True)
# ✅ FIX 3: Add indexes for better performance
__table_args__ = (
Index('ix_refresh_tokens_user_id_active', 'user_id', 'is_revoked'),
Index('ix_refresh_tokens_expires_at', 'expires_at'),
Index('ix_refresh_tokens_token_hash', 'token_hash'),
)
def __init__(self, **kwargs):
"""Initialize refresh token with automatic hash generation"""
super().__init__(**kwargs)
if self.token and not self.token_hash:
self.token_hash = self._generate_token_hash(self.token)
@staticmethod
def _generate_token_hash(token: str) -> str:
"""Generate a hash of the token for uniqueness checking"""
return hashlib.sha256(token.encode()).hexdigest()
def update_token(self, new_token: str):
"""Update token and regenerate hash"""
self.token = new_token
self.token_hash = self._generate_token_hash(new_token)
@classmethod
async def create_refresh_token(cls, user_id: uuid.UUID, token: str, expires_at: datetime):
"""
Create a new refresh token with proper hash generation
"""
return cls(
id=uuid.uuid4(),
user_id=user_id,
token=token,
token_hash=cls._generate_token_hash(token),
expires_at=expires_at,
is_revoked=False,
created_at=datetime.now(timezone.utc)
)
def __repr__(self):
return f"<RefreshToken(id={self.id}, user_id={self.user_id}, expires_at={self.expires_at})>"
class LoginAttempt(Base):
"""Login attempt tracking model"""
__tablename__ = "login_attempts"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), nullable=False, index=True)
ip_address = Column(String(45), nullable=False)
user_agent = Column(Text)
success = Column(Boolean, default=False)
failure_reason = Column(String(255))
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
def __repr__(self):
return f"<LoginAttempt(id={self.id}, email={self.email}, success={self.success})>"

View File

@@ -0,0 +1,61 @@
# services/auth/app/models/users.py - FIXED VERSION
"""
User models for authentication service - FIXED
Removed tenant relationships to eliminate cross-service dependencies
"""
from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class User(Base):
"""User model - FIXED without cross-service relationships"""
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String(255), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
# Timezone-aware datetime fields
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))
last_login = Column(DateTime(timezone=True))
# Profile fields
phone = Column(String(20))
language = Column(String(10), default="es")
timezone = Column(String(50), default="Europe/Madrid")
role = Column(String(20), nullable=False)
# Payment integration fields
payment_customer_id = Column(String(255), nullable=True, index=True)
default_payment_method_id = Column(String(255), nullable=True)
# REMOVED: All tenant relationships - these are handled by tenant service
# No tenant_memberships, tenants relationships
def __repr__(self):
return f"<User(id={self.id}, email={self.email})>"
def to_dict(self):
"""Convert user to dictionary"""
return {
"id": str(self.id),
"email": self.email,
"full_name": self.full_name,
"is_active": self.is_active,
"is_verified": self.is_verified,
"phone": self.phone,
"language": self.language,
"timezone": self.timezone,
"role": self.role,
"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
}