from datetime import datetime, timezone import uuid from sqlalchemy import ( Column, String, Integer, BigInteger, Boolean, DateTime, ForeignKey, Text, Index ) from sqlalchemy.orm import relationship from app.core.database import Base def utc_now(): return datetime.now(timezone.utc) class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String(255), unique=True, index=True, nullable=False) hashed_password = Column(String(255), nullable=False) full_name = Column(String(255), nullable=True) role = Column(String(50), default="ADMIN", nullable=False) # ADMIN, OPERATOR, VIEWER is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) class Client(Base): __tablename__ = "clients" id = Column(Integer, primary_key=True, index=True) client_code = Column(String(50), unique=True, index=True, nullable=False) # e.g., CLIENT-0001 name = Column(String(255), nullable=False) alias = Column(String(255), nullable=True) hostname = Column(String(255), nullable=True) os_info = Column(String(255), nullable=True) ip_address = Column(String(100), nullable=True) agent_version = Column(String(50), default="1.0.0", nullable=False) status = Column(String(50), default="OFFLINE", nullable=False) # ONLINE, OFFLINE, SYNCING, ERROR storage_used_bytes = Column(BigInteger, default=0, nullable=False) storage_quota_bytes = Column(BigInteger, default=100 * 1024 * 1024 * 1024, nullable=False) # 100 GB default last_seen_at = Column(DateTime(timezone=True), nullable=True) last_backup_at = Column(DateTime(timezone=True), nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) credentials = relationship("ClientCredential", back_populates="client", cascade="all, delete-orphan") jobs = relationship("BackupJob", back_populates="client", cascade="all, delete-orphan") backup_files = relationship("BackupFile", back_populates="client", cascade="all, delete-orphan") backup_sessions = relationship("BackupSession", back_populates="client", cascade="all, delete-orphan") class ClientCredential(Base): __tablename__ = "client_credentials" id = Column(Integer, primary_key=True, index=True) client_id = Column(Integer, ForeignKey("clients.id", ondelete="CASCADE"), nullable=False) device_id = Column(String(100), unique=True, index=True, nullable=False) # Unique UUID token_hash = Column(String(255), unique=True, index=True, nullable=False) name = Column(String(255), default="Primary Windows Agent", nullable=False) is_revoked = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) last_used_at = Column(DateTime(timezone=True), nullable=True) client = relationship("Client", back_populates="credentials") class RegistrationCode(Base): __tablename__ = "registration_codes" id = Column(Integer, primary_key=True, index=True) code = Column(String(50), unique=True, index=True, nullable=False) # e.g., OED-A1B2-C3D4 is_used = Column(Boolean, default=False, nullable=False) client_name_hint = Column(String(255), nullable=True) client_id = Column(Integer, ForeignKey("clients.id"), nullable=True) expires_at = Column(DateTime(timezone=True), nullable=False) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) class BackupJob(Base): __tablename__ = "backup_jobs" id = Column(Integer, primary_key=True, index=True) job_code = Column(String(50), unique=True, index=True, nullable=False) # e.g., JOB-001 client_id = Column(Integer, ForeignKey("clients.id", ondelete="CASCADE"), nullable=False) name = Column(String(255), nullable=False) source_path = Column(String(1024), nullable=False) # e.g., C:\SQLBackups file_patterns = Column(String(255), default="*.bak,*.mdf", nullable=False) schedule_cron = Column(String(100), default="0 2 * * *", nullable=False) # default 02:00 AM daily is_active = Column(Boolean, default=True, nullable=False) # Retention policies keep_daily = Column(Integer, default=7, nullable=False) keep_weekly = Column(Integer, default=4, nullable=False) keep_monthly = Column(Integer, default=12, nullable=False) min_stable_time_seconds = Column(Integer, default=60, nullable=False) status = Column(String(50), default="IDLE", nullable=False) # IDLE, RUNNING, ERROR, SUCCESS last_run_at = Column(DateTime(timezone=True), nullable=True) next_run_at = Column(DateTime(timezone=True), nullable=True) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) client = relationship("Client", back_populates="jobs") backup_files = relationship("BackupFile", back_populates="job", cascade="all, delete-orphan") backup_sessions = relationship("BackupSession", back_populates="job", cascade="all, delete-orphan") class BackupSession(Base): __tablename__ = "backup_sessions" id = Column(Integer, primary_key=True, index=True) session_code = Column(String(100), unique=True, index=True, default=lambda: str(uuid.uuid4()), nullable=False) client_id = Column(Integer, ForeignKey("clients.id", ondelete="CASCADE"), nullable=False) job_id = Column(Integer, ForeignKey("backup_jobs.id", ondelete="CASCADE"), nullable=True) filename = Column(String(512), nullable=False) file_size = Column(BigInteger, nullable=False) chunk_size = Column(Integer, default=4 * 1024 * 1024, nullable=False) total_chunks = Column(Integer, nullable=False) received_chunks_count = Column(Integer, default=0, nullable=False) sha256_full = Column(String(64), nullable=False) status = Column(String(50), default="PENDING", nullable=False) # PENDING, UPLOADING, ASSEMBLING, VERIFYING, SUCCESS, FAILED, CANCELLED error_message = Column(Text, nullable=True) started_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) completed_at = Column(DateTime(timezone=True), nullable=True) client = relationship("Client", back_populates="backup_sessions") job = relationship("BackupJob", back_populates="backup_sessions") chunks = relationship("BackupChunk", back_populates="session", cascade="all, delete-orphan") __table_args__ = ( Index("idx_session_client_status", "client_id", "status"), ) class BackupChunk(Base): __tablename__ = "backup_chunks" id = Column(Integer, primary_key=True, index=True) session_id = Column(Integer, ForeignKey("backup_sessions.id", ondelete="CASCADE"), nullable=False) chunk_index = Column(Integer, nullable=False) chunk_size = Column(Integer, nullable=False) sha256 = Column(String(64), nullable=False) is_received = Column(Boolean, default=False, nullable=False) received_at = Column(DateTime(timezone=True), nullable=True) session = relationship("BackupSession", back_populates="chunks") __table_args__ = ( Index("idx_chunk_session_idx", "session_id", "chunk_index", unique=True), ) class BackupFile(Base): __tablename__ = "backup_files" id = Column(Integer, primary_key=True, index=True) client_id = Column(Integer, ForeignKey("clients.id", ondelete="CASCADE"), nullable=False) job_id = Column(Integer, ForeignKey("backup_jobs.id", ondelete="CASCADE"), nullable=True) session_id = Column(Integer, ForeignKey("backup_sessions.id", ondelete="SET NULL"), nullable=True) filename = Column(String(512), nullable=False) relative_path = Column(String(1024), nullable=False) file_size = Column(BigInteger, nullable=False) sha256 = Column(String(64), nullable=False) retention_tag = Column(String(50), default="DAILY", nullable=False) # DAILY, WEEKLY, MONTHLY, MANUAL is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False) client = relationship("Client", back_populates="backup_files") job = relationship("BackupJob", back_populates="backup_files") class EventLog(Base): __tablename__ = "event_logs" id = Column(Integer, primary_key=True, index=True) timestamp = Column(DateTime(timezone=True), default=utc_now, nullable=False, index=True) event_type = Column(String(100), nullable=False, index=True) # LOGIN, CLIENT_REGISTERED, BACKUP_STARTED, BACKUP_COMPLETED, BACKUP_FAILED, etc. severity = Column(String(50), default="INFO", nullable=False) # INFO, WARNING, ERROR, CRITICAL client_id = Column(Integer, nullable=True, index=True) job_id = Column(Integer, nullable=True, index=True) user_email = Column(String(255), nullable=True) ip_address = Column(String(100), nullable=True) message = Column(Text, nullable=False) details_json = Column(Text, nullable=True) class SystemSetting(Base): __tablename__ = "system_settings" key = Column(String(100), primary_key=True, index=True) value = Column(String(1024), nullable=False)