Security fixes: P0-P2 complete (22 fixes)
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""Generic finite state machine for domain entity status transitions.
|
||||
|
||||
Defines allowed state transitions for Contact and Workflow entities.
|
||||
Usage in Commands:
|
||||
|
||||
from app.core.state_machine import contact_state_machine
|
||||
contact_state_machine.transition(contact.status, "qualified")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class StateMachineError(Exception):
|
||||
"""Raised when an invalid state transition is attempted."""
|
||||
|
||||
|
||||
class StateMachine:
|
||||
"""Finite state machine that validates and executes state transitions.
|
||||
|
||||
Attributes:
|
||||
transitions: Mapping from a state to the list of states it can transition to.
|
||||
"""
|
||||
|
||||
def __init__(self, transitions: dict[str, list[str]]) -> None:
|
||||
self.transitions: dict[str, list[str]] = transitions
|
||||
|
||||
def can_transition(self, current: str, target: str) -> bool:
|
||||
"""Return True if transitioning from *current* to *target* is allowed."""
|
||||
allowed = self.transitions.get(current, [])
|
||||
return target in allowed
|
||||
|
||||
def transition(self, current: str, target: str) -> str:
|
||||
"""Validate and return the new state.
|
||||
|
||||
Raises:
|
||||
StateMachineError: if the transition is not allowed.
|
||||
"""
|
||||
if not self.can_transition(current, target):
|
||||
raise StateMachineError(
|
||||
f"Invalid state transition: '{current}' -> '{target}'. "
|
||||
f"Allowed targets from '{current}': {self.transitions.get(current, [])}"
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
# ── Contact lifecycle: lead → qualified → customer → inactive ──
|
||||
# Allows skipping 'qualified' and reactivation from inactive.
|
||||
contact_state_machine = StateMachine(
|
||||
transitions={
|
||||
"lead": ["qualified", "customer", "inactive"],
|
||||
"qualified": ["customer", "lead", "inactive"],
|
||||
"customer": ["inactive"],
|
||||
"inactive": ["lead"],
|
||||
}
|
||||
)
|
||||
|
||||
# ── Workflow lifecycle: draft → active → paused → completed → cancelled ──
|
||||
workflow_state_machine = StateMachine(
|
||||
transitions={
|
||||
"draft": ["active", "cancelled"],
|
||||
"active": ["paused", "completed", "cancelled"],
|
||||
"paused": ["active", "completed", "cancelled"],
|
||||
"completed": [],
|
||||
"cancelled": [],
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user