Phase 0 Complete: Tasks 0.7-0.20
- 0.7: UI-Design-Richtlinien (docs/ui-design-guidelines.md, 535 lines) - 0.8: Theme-Customization Backend (4 theme fields, migration 0023) - 0.9: Theme-Customization Frontend (SettingsTheme.tsx, themeStore.ts, live preview) - 0.10: RBAC-Audit (4 plugins secured, 53 routes with require_permission) - 0.11: LiteLLM-Cleanup (llm_client.py migrated from httpx to litellm) - 0.12: KI-Agent-Framework docs (plugin-development-guide.md, agent_capabilities field) - 0.13: Heartbeat configurable (ProactiveSettings, migration 0024, frontend UI) - 0.14: Unified Search Field-Level RBAC (resolve_permissions + filter_fields_by_permission) - 0.15: Undo/History-System (EntityHistory model, service, routes, migration 0025, HistoryViewer) - 0.16: Storage Backend (LocalStorage + S3Storage, DMS/attachments/mail updated) - 0.17: Import/Export unified Contact fields (firstname, surname, email_1, phone_1) - 0.18: .gitignore & Config-Cleanup (webui→frontend, python-jose removed, .env untracked) - 0.19: Mail-Salt Security-Fix (per-account random salt, migration 0026) - 0.20: AGPL replaced (PyMuPDF→pypdf, OnlyOffice→Collabora, LICENSE + THIRD_PARTY_LICENSES.md)
This commit is contained in:
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.contact import Contact, ContactPerson
|
||||
from app.services.entity_history_service import record_history
|
||||
|
||||
|
||||
def _compute_displayname(data: dict) -> str:
|
||||
@@ -239,7 +240,15 @@ async def create_contact(
|
||||
q = select(Contact).options(selectinload(Contact.contact_persons)).where(Contact.id == contact.id)
|
||||
result = await db.execute(q)
|
||||
contact = result.scalar_one()
|
||||
return _serialize_contact_detail(contact)
|
||||
serialized = _serialize_contact_detail(contact)
|
||||
|
||||
# Record history
|
||||
await record_history(
|
||||
db, tenant_id, user_id, "contact", contact.id,
|
||||
action="create", snapshot_after=serialized,
|
||||
)
|
||||
|
||||
return serialized
|
||||
|
||||
|
||||
async def update_contact(
|
||||
@@ -260,6 +269,9 @@ async def update_contact(
|
||||
if not contact:
|
||||
raise ValueError("Contact not found")
|
||||
|
||||
# Capture snapshot before update
|
||||
snapshot_before = _serialize_contact_detail(contact)
|
||||
|
||||
# Recompute displayname if name fields changed
|
||||
if any(k in data for k in ("type", "name", "firstname", "surname", "surfix")):
|
||||
merged = {**_serialize_contact(contact), **data}
|
||||
@@ -271,10 +283,30 @@ async def update_contact(
|
||||
contact.updated_by = user_id
|
||||
|
||||
await db.flush()
|
||||
return _serialize_contact_detail(contact)
|
||||
snapshot_after = _serialize_contact_detail(contact)
|
||||
|
||||
# Compute changes diff
|
||||
changes: dict = {}
|
||||
for key, new_val in snapshot_after.items():
|
||||
old_val = snapshot_before.get(key)
|
||||
if old_val != new_val:
|
||||
changes[key] = {"old": old_val, "new": new_val}
|
||||
|
||||
# Record history
|
||||
await record_history(
|
||||
db, tenant_id, user_id, "contact", contact.id,
|
||||
action="update",
|
||||
snapshot_before=snapshot_before,
|
||||
snapshot_after=snapshot_after,
|
||||
changes=changes or None,
|
||||
)
|
||||
|
||||
return snapshot_after
|
||||
|
||||
|
||||
async def delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> None:
|
||||
async def delete_contact(
|
||||
db: AsyncSession, tenant_id: uuid.UUID, contact_id: str, user_id: uuid.UUID | None = None
|
||||
) -> None:
|
||||
"""Soft-delete a contact."""
|
||||
q = select(Contact).where(
|
||||
Contact.id == uuid.UUID(contact_id),
|
||||
@@ -285,10 +317,28 @@ async def delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str
|
||||
contact = result.scalar_one_or_none()
|
||||
if not contact:
|
||||
raise ValueError("Contact not found")
|
||||
|
||||
# Capture snapshot before deletion
|
||||
from sqlalchemy.orm import selectinload
|
||||
q2 = (
|
||||
select(Contact)
|
||||
.options(selectinload(Contact.contact_persons))
|
||||
.where(Contact.id == contact.id)
|
||||
)
|
||||
result2 = await db.execute(q2)
|
||||
contact_full = result2.scalar_one()
|
||||
snapshot_before = _serialize_contact_detail(contact_full)
|
||||
|
||||
from datetime import datetime, timezone
|
||||
contact.deleted_at = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
|
||||
# Record history
|
||||
await record_history(
|
||||
db, tenant_id, user_id, "contact", contact.id,
|
||||
action="delete", snapshot_before=snapshot_before,
|
||||
)
|
||||
|
||||
|
||||
async def hard_delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> None:
|
||||
"""GDPR hard-delete a contact."""
|
||||
|
||||
Reference in New Issue
Block a user