mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
vbox funzionanti
This commit is contained in:
@@ -7,3 +7,5 @@ from app.models.archival import ArchivalBatch, ArchivalBatchMessage, ArchivalDip
|
||||
from app.models.audit_log import AuditLog # noqa: F401
|
||||
from app.models.label import Label, MessageLabel # noqa: F401
|
||||
from app.models.permission import MailboxPermission # noqa: F401
|
||||
from app.models.virtual_box import VirtualBox, VirtualBoxRule, VirtualBoxAssignment # noqa: F401
|
||||
from app.models.notification import NotificationChannel, NotificationRule, NotificationLog # noqa: F401
|
||||
|
||||
@@ -61,6 +61,7 @@ class Mailbox(Base):
|
||||
status: Mapped[str] = mapped_column(MailboxStatus, nullable=False, default="active")
|
||||
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_sync_uid: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
sent_last_sync_uid: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
sync_error_msg: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
sync_error_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Modelli Notifiche Multi-canale.
|
||||
|
||||
Struttura:
|
||||
NotificationChannel → configurazione canale (Webhook, Email, Telegram, WhatsApp)
|
||||
NotificationRule → regola evento → canale
|
||||
NotificationLog → log tentativi di invio (con retry e circuit breaker)
|
||||
|
||||
Canali supportati:
|
||||
webhook – POST HMAC-SHA256
|
||||
email – SMTP
|
||||
telegram – Bot API
|
||||
whatsapp – Meta Cloud API v18
|
||||
|
||||
Sicurezza:
|
||||
config_enc – configurazione sensibile cifrata AES-256-GCM
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
ChannelType = Enum(
|
||||
"webhook", "email", "telegram", "whatsapp",
|
||||
name="notification_channel_type",
|
||||
create_type=False,
|
||||
)
|
||||
|
||||
NotificationStatus = Enum(
|
||||
"pending", "sent", "failed", "skipped",
|
||||
name="notification_status",
|
||||
create_type=False,
|
||||
)
|
||||
|
||||
|
||||
class NotificationChannel(Base):
|
||||
"""
|
||||
Canale di notifica configurato da un tenant.
|
||||
|
||||
Il campo config_enc contiene la configurazione sensibile (API key,
|
||||
token, SMTP password…) cifrata AES-256-GCM a livello applicativo.
|
||||
"""
|
||||
|
||||
__tablename__ = "notification_channels"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
channel_type: Mapped[str] = mapped_column(ChannelType, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
# Configurazione NON sensibile (JSON libero: url, chat_id, from_email…)
|
||||
config: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
|
||||
# Configurazione sensibile cifrata (token, password…) – base64(GCM(json))
|
||||
config_enc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Circuit breaker
|
||||
consecutive_failures: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
circuit_open_until: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
rules: Mapped[list["NotificationRule"]] = relationship(
|
||||
"NotificationRule", back_populates="channel", cascade="all, delete-orphan"
|
||||
)
|
||||
logs: Mapped[list["NotificationLog"]] = relationship(
|
||||
"NotificationLog", back_populates="channel", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_notif_channel_tenant", "tenant_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<NotificationChannel {self.name!r} type={self.channel_type!r}>"
|
||||
|
||||
|
||||
class NotificationRule(Base):
|
||||
"""
|
||||
Regola: evento PecFlow → canale di notifica.
|
||||
|
||||
event_type: new_message | state_changed | anomaly | send_failed | ...
|
||||
filter: JSONB con condizioni opzionali (mailbox_id, state, ecc.)
|
||||
"""
|
||||
|
||||
__tablename__ = "notification_rules"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
channel_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("notification_channels.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
filter: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
channel: Mapped["NotificationChannel"] = relationship(
|
||||
"NotificationChannel", back_populates="rules"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_notif_rule_tenant", "tenant_id"),
|
||||
Index("idx_notif_rule_channel", "channel_id"),
|
||||
Index("idx_notif_rule_event", "event_type"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<NotificationRule {self.name!r} event={self.event_type!r}>"
|
||||
|
||||
|
||||
class NotificationLog(Base):
|
||||
"""
|
||||
Log di ogni tentativo di notifica.
|
||||
|
||||
Retry indipendente per canale: max 3 tentativi, backoff 5m → 30m → 2h.
|
||||
"""
|
||||
|
||||
__tablename__ = "notification_log"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
channel_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("notification_channels.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
rule_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("notification_rules.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
event_payload: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
|
||||
status: Mapped[str] = mapped_column(NotificationStatus, nullable=False, default="pending")
|
||||
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
max_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
||||
next_retry_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
http_status: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
sent_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
channel: Mapped["NotificationChannel"] = relationship(
|
||||
"NotificationChannel", back_populates="logs"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_notif_log_tenant", "tenant_id"),
|
||||
Index("idx_notif_log_channel", "channel_id"),
|
||||
Index("idx_notif_log_status", "status", "next_retry_at"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<NotificationLog channel={self.channel_id} status={self.status!r}>"
|
||||
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
Modelli Virtual Box – filtri nominati assegnabili agli utenti.
|
||||
|
||||
Struttura:
|
||||
VirtualBox → definisce il filtro (nome, label, mailbox scope)
|
||||
VirtualBoxRule → singola regola (field + pattern) dentro una VBox
|
||||
VirtualBoxAssignment → assegnazione VBox → User
|
||||
|
||||
Logica:
|
||||
- Le regole nella stessa VBox si combinano in AND.
|
||||
- Più VBox assegnate allo stesso utente si uniscono in OR.
|
||||
- Il filtro si applica automaticamente a inbox e ricerca.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
String,
|
||||
Table,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
# ─── Tabella di associazione VirtualBox ↔ Mailbox ─────────────────────────────
|
||||
virtual_box_mailboxes = Table(
|
||||
"virtual_box_mailboxes",
|
||||
Base.metadata,
|
||||
Column(
|
||||
"virtual_box_id",
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("virtual_boxes.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
Column(
|
||||
"mailbox_id",
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("mailboxes.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class VirtualBox(Base):
|
||||
"""Casella virtuale: contenitore di regole + etichetta opzionale."""
|
||||
|
||||
__tablename__ = "virtual_boxes"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
label: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
rules: Mapped[list["VirtualBoxRule"]] = relationship(
|
||||
"VirtualBoxRule", back_populates="virtual_box", cascade="all, delete-orphan",
|
||||
order_by="VirtualBoxRule.created_at"
|
||||
)
|
||||
assignments: Mapped[list["VirtualBoxAssignment"]] = relationship(
|
||||
"VirtualBoxAssignment", back_populates="virtual_box", cascade="all, delete-orphan"
|
||||
)
|
||||
mailboxes: Mapped[list["Mailbox"]] = relationship( # noqa: F821
|
||||
"Mailbox",
|
||||
secondary=virtual_box_mailboxes,
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "name", name="uq_vbox_name_tenant"),
|
||||
Index("idx_vbox_tenant", "tenant_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<VirtualBox {self.name!r}>"
|
||||
|
||||
|
||||
class VirtualBoxRule(Base):
|
||||
"""
|
||||
Singola regola di filtro all'interno di una VBox.
|
||||
|
||||
field : mailbox_id | imap_folder | subject | from_address | to_address
|
||||
operator : contains | equals | starts_with | ends_with | regex
|
||||
value : valore da confrontare
|
||||
|
||||
Può anche specificare un date_from / date_to per filtrare per periodo.
|
||||
"""
|
||||
|
||||
__tablename__ = "virtual_box_rules"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
virtual_box_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("virtual_boxes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
field: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
operator: Mapped[str] = mapped_column(String(20), nullable=False, default="contains")
|
||||
value: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
date_from: Mapped[str | None] = mapped_column(String(20), nullable=True) # ISO date
|
||||
date_to: Mapped[str | None] = mapped_column(String(20), nullable=True) # ISO date
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
virtual_box: Mapped["VirtualBox"] = relationship(
|
||||
"VirtualBox", back_populates="rules"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_vbox_rule_vbox", "virtual_box_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<VirtualBoxRule {self.field!r} {self.operator!r} {self.value!r}>"
|
||||
|
||||
|
||||
class VirtualBoxAssignment(Base):
|
||||
"""Assegnazione di una VBox a un utente."""
|
||||
|
||||
__tablename__ = "virtual_box_assignments"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
virtual_box_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("virtual_boxes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
assigned_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
assigned_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
# Relazioni
|
||||
virtual_box: Mapped["VirtualBox"] = relationship(
|
||||
"VirtualBox", back_populates="assignments"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("virtual_box_id", "user_id", name="uq_vbox_assignment"),
|
||||
Index("idx_vbox_assign_user", "user_id"),
|
||||
Index("idx_vbox_assign_vbox", "virtual_box_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<VirtualBoxAssignment vbox={self.virtual_box_id} user={self.user_id}>"
|
||||
Reference in New Issue
Block a user