- 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)
162 lines
6.0 KiB
Python
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"),
|
|
)
|