214 lines
7 KiB
Python
214 lines
7 KiB
Python
from sqlalchemy import (
|
|
Column,
|
|
Integer,
|
|
String,
|
|
DateTime,
|
|
Boolean,
|
|
Text,
|
|
ForeignKey,
|
|
Index,
|
|
Enum as SQLEnum,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
from database.models.generic_model import Base
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(String(36), primary_key=True)
|
|
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
hashed_password = Column(String(255), nullable=False)
|
|
|
|
nom = Column(String(100), nullable=False)
|
|
prenom = Column(String(100), nullable=False)
|
|
role = Column(String(50), default="user")
|
|
|
|
is_verified = Column(Boolean, default=False, index=True)
|
|
verification_token = Column(String(255), nullable=True, unique=True, index=True)
|
|
verification_token_expires = Column(DateTime, nullable=True)
|
|
|
|
is_active = Column(Boolean, default=True, index=True)
|
|
failed_login_attempts = Column(Integer, default=0)
|
|
locked_until = Column(DateTime, nullable=True)
|
|
|
|
reset_token = Column(String(255), nullable=True, unique=True, index=True)
|
|
reset_token_expires = Column(DateTime, nullable=True)
|
|
|
|
password_changed_at = Column(DateTime, nullable=True)
|
|
must_change_password = Column(Boolean, default=False)
|
|
|
|
created_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
|
last_login = Column(DateTime, nullable=True)
|
|
last_login_ip = Column(String(45), nullable=True)
|
|
|
|
refresh_tokens = relationship(
|
|
"RefreshToken", back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
|
|
audit_logs = relationship(
|
|
"AuditLog", back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<User {self.email} verified={self.is_verified}>"
|
|
|
|
|
|
class RefreshToken(Base):
|
|
__tablename__ = "refresh_tokens"
|
|
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(
|
|
String(36),
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
token_hash = Column(String(64), unique=True, nullable=False, index=True)
|
|
token_id = Column(String(32), unique=True, nullable=False, index=True)
|
|
|
|
fingerprint_hash = Column(String(64), nullable=True)
|
|
device_info = Column(String(500), nullable=True)
|
|
ip_address = Column(String(45), nullable=True)
|
|
|
|
is_revoked = Column(Boolean, default=False, index=True)
|
|
revoked_at = Column(DateTime, nullable=True)
|
|
revoked_reason = Column(String(100), nullable=True)
|
|
|
|
is_used = Column(Boolean, default=False)
|
|
used_at = Column(DateTime, nullable=True)
|
|
replaced_by = Column(String(36), nullable=True)
|
|
|
|
expires_at = Column(DateTime, nullable=False, index=True)
|
|
created_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
last_used_at = Column(DateTime, nullable=True)
|
|
|
|
user = relationship("User", back_populates="refresh_tokens")
|
|
|
|
__table_args__ = (
|
|
Index("ix_refresh_tokens_user_valid", "user_id", "is_revoked", "expires_at"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<RefreshToken {self.token_id[:8]}... user={self.user_id}>"
|
|
|
|
|
|
class AuditEventType(str, Enum):
|
|
LOGIN_SUCCESS = "login_success"
|
|
LOGIN_FAILED = "login_failed"
|
|
LOGOUT = "logout"
|
|
PASSWORD_CHANGE = "password_change"
|
|
PASSWORD_RESET_REQUEST = "password_reset_request"
|
|
PASSWORD_RESET_COMPLETE = "password_reset_complete"
|
|
EMAIL_VERIFICATION = "email_verification"
|
|
ACCOUNT_LOCKED = "account_locked"
|
|
ACCOUNT_UNLOCKED = "account_unlocked"
|
|
TOKEN_REFRESH = "token_refresh"
|
|
TOKEN_REVOKED = "token_revoked"
|
|
SUSPICIOUS_ACTIVITY = "suspicious_activity"
|
|
SESSION_CREATED = "session_created"
|
|
SESSION_TERMINATED = "session_terminated"
|
|
|
|
|
|
class AuditLog(Base):
|
|
__tablename__ = "audit_logs"
|
|
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(
|
|
String(36),
|
|
ForeignKey("users.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
|
|
event_type = Column(SQLEnum(AuditEventType), nullable=False, index=True)
|
|
event_description = Column(Text, nullable=True)
|
|
|
|
ip_address = Column(String(45), nullable=True, index=True)
|
|
user_agent = Column(String(500), nullable=True)
|
|
fingerprint_hash = Column(String(64), nullable=True)
|
|
|
|
resource_type = Column(String(50), nullable=True)
|
|
resource_id = Column(String(100), nullable=True)
|
|
|
|
request_method = Column(String(10), nullable=True)
|
|
request_path = Column(String(500), nullable=True)
|
|
|
|
meta = Column("metadata", Text, nullable=True)
|
|
|
|
success = Column(Boolean, default=True)
|
|
failure_reason = Column(String(255), nullable=True)
|
|
|
|
created_at = Column(DateTime, default=datetime.now, nullable=False, index=True)
|
|
|
|
user = relationship("User", back_populates="audit_logs")
|
|
|
|
__table_args__ = (
|
|
Index("ix_audit_logs_user_event", "user_id", "event_type", "created_at"),
|
|
Index("ix_audit_logs_ip_event", "ip_address", "event_type", "created_at"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<AuditLog {self.event_type.value} user={self.user_id}>"
|
|
|
|
|
|
class LoginAttempt(Base):
|
|
__tablename__ = "login_attempts"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
email = Column(String(255), nullable=False, index=True)
|
|
ip_address = Column(String(45), nullable=True, index=True)
|
|
user_agent = Column(String(500), nullable=True)
|
|
fingerprint_hash = Column(String(64), nullable=True)
|
|
|
|
success = Column(Boolean, default=False, index=True)
|
|
failure_reason = Column(String(255), nullable=True)
|
|
|
|
timestamp = Column(DateTime, default=datetime.now, nullable=False, index=True)
|
|
|
|
__table_args__ = (
|
|
Index("ix_login_attempts_email_time", "email", "timestamp"),
|
|
Index("ix_login_attempts_ip_time", "ip_address", "timestamp"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<LoginAttempt {self.email} success={self.success}>"
|
|
|
|
|
|
class UserSession(Base):
|
|
__tablename__ = "user_sessions"
|
|
|
|
id = Column(String(36), primary_key=True)
|
|
user_id = Column(
|
|
String(36),
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
|
|
session_token_hash = Column(String(64), unique=True, nullable=False, index=True)
|
|
refresh_token_id = Column(String(36), nullable=True)
|
|
|
|
device_info = Column(String(500), nullable=True)
|
|
ip_address = Column(String(45), nullable=True)
|
|
fingerprint_hash = Column(String(64), nullable=True)
|
|
location = Column(String(255), nullable=True)
|
|
|
|
is_active = Column(Boolean, default=True, index=True)
|
|
terminated_at = Column(DateTime, nullable=True)
|
|
termination_reason = Column(String(100), nullable=True)
|
|
|
|
created_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
last_activity = Column(DateTime, default=datetime.now, nullable=False)
|
|
expires_at = Column(DateTime, nullable=False)
|
|
|
|
__table_args__ = (Index("ix_user_sessions_user_active", "user_id", "is_active"),)
|
|
|
|
def __repr__(self):
|
|
return f"<UserSession {self.id[:8]}... user={self.user_id}>"
|