Files
MCM/db/models.py
itdrui.de 6eb27a62b1 feat: Multi-User-Unterstützung mit JWT-Authentifizierung
- User-Modell (username, password_hash, role admin/user, is_active)
- Standard-Admin-Benutzer wird beim ersten Start automatisch angelegt
- JWT-Tokens (HS256) für Benutzer-Sessions, konfigurierbare Ablaufzeit
- API-Key bleibt für service-to-service-Calls (backward-compatible)
- POST /api/v1/auth/login → JWT-Token
- GET  /api/v1/auth/me   → aktueller Benutzer
- CRUD /api/v1/users/    → Benutzerverwaltung (nur Admin)
- TUI zeigt Login-Screen beim Start; nach Erfolg → MainScreen
- Passwort-Hashing mit bcrypt (python-jose für JWT)
2026-03-04 20:55:13 +01:00

162 lines
6.0 KiB
Python

import uuid
from datetime import datetime
from sqlalchemy import (
Boolean,
Column,
DateTime,
Enum,
ForeignKey,
Index,
Integer,
String,
Table,
Text,
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from db.database import Base
def _uuid() -> str:
return str(uuid.uuid4())
# ── User ───────────────────────────────────────────────────────────────────────
class User(Base):
__tablename__ = "users"
id = Column(String(36), primary_key=True, default=_uuid)
username = Column(String(64), nullable=False, unique=True, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(
Enum("admin", "user", name="user_role"),
nullable=False,
default="user",
)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
# ── Many-to-many: Conversation ↔ Contact ──────────────────────────────────────
conversation_participants = Table(
"conversation_participants",
Base.metadata,
Column("conversation_id", String, ForeignKey("conversations.id", ondelete="CASCADE")),
Column("contact_id", String, ForeignKey("contacts.id", ondelete="CASCADE")),
)
# ── Contact ────────────────────────────────────────────────────────────────────
class Contact(Base):
__tablename__ = "contacts"
id = Column(String, primary_key=True, default=_uuid)
name = Column(String(255), nullable=False, index=True)
phone = Column(String(32), nullable=True, unique=True)
email = Column(String(255), nullable=True)
telegram_id = Column(String(64), nullable=True, unique=True)
telegram_username = Column(String(255), nullable=True)
whatsapp_phone = Column(String(32), nullable=True)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
conversations = relationship(
"Conversation",
secondary=conversation_participants,
back_populates="participants",
)
messages_sent = relationship(
"Message",
back_populates="sender",
foreign_keys="Message.sender_id",
)
# ── Conversation ───────────────────────────────────────────────────────────────
class Conversation(Base):
__tablename__ = "conversations"
id = Column(String, primary_key=True, default=_uuid)
channel = Column(
Enum("telegram", "whatsapp", "sms", name="channel_type"),
nullable=False,
index=True,
)
channel_conversation_id = Column(String(255), nullable=True)
title = Column(String(255), nullable=True)
is_group = Column(Boolean, default=False, nullable=False)
is_archived = Column(Boolean, default=False, nullable=False)
last_message_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
participants = relationship(
"Contact",
secondary=conversation_participants,
back_populates="conversations",
)
messages = relationship(
"Message",
back_populates="conversation",
cascade="all, delete-orphan",
order_by="Message.created_at",
)
__table_args__ = (
Index("idx_conv_channel_native", "channel", "channel_conversation_id"),
)
# ── Message ────────────────────────────────────────────────────────────────────
class Message(Base):
__tablename__ = "messages"
id = Column(String, primary_key=True, default=_uuid)
conversation_id = Column(
String, ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, index=True
)
sender_id = Column(String, ForeignKey("contacts.id"), nullable=True)
channel = Column(
Enum("telegram", "whatsapp", "sms", name="channel_type"),
nullable=False,
index=True,
)
channel_message_id = Column(String(255), nullable=True)
direction = Column(
Enum("inbound", "outbound", name="message_direction"),
nullable=False,
default="outbound",
)
text = Column(Text, nullable=False)
status = Column(
Enum("pending", "sent", "delivered", "read", "failed", name="message_status"),
default="pending",
nullable=False,
index=True,
)
reply_to_id = Column(String, ForeignKey("messages.id"), nullable=True)
is_edited = Column(Boolean, default=False, nullable=False)
error_message = Column(Text, nullable=True)
retry_count = Column(Integer, default=0, nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False, index=True)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
sent_at = Column(DateTime, nullable=True)
delivered_at = Column(DateTime, nullable=True)
read_at = Column(DateTime, nullable=True)
conversation = relationship("Conversation", back_populates="messages")
sender = relationship("Contact", back_populates="messages_sent", foreign_keys=[sender_id])
reply_to = relationship("Message", remote_side="Message.id", backref="replies")
__table_args__ = (
Index("idx_msg_channel_native", "channel", "channel_message_id"),
Index("idx_msg_conv_created", "conversation_id", "created_at"),
)