Initial commit - production deployment
This commit is contained in:
31
services/auth/app/models/__init__.py
Normal file
31
services/auth/app/models/__init__.py
Normal 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",
|
||||
]
|
||||
110
services/auth/app/models/consent.py
Normal file
110
services/auth/app/models/consent.py
Normal 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,
|
||||
}
|
||||
64
services/auth/app/models/deletion_job.py
Normal file
64
services/auth/app/models/deletion_job.py
Normal 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}')>"
|
||||
91
services/auth/app/models/onboarding.py
Normal file
91
services/auth/app/models/onboarding.py
Normal 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
|
||||
}
|
||||
39
services/auth/app/models/password_reset_tokens.py
Normal file
39
services/auth/app/models/password_reset_tokens.py
Normal 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})>"
|
||||
92
services/auth/app/models/tokens.py
Normal file
92
services/auth/app/models/tokens.py
Normal 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})>"
|
||||
61
services/auth/app/models/users.py
Normal file
61
services/auth/app/models/users.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user