harmonized module names
This commit is contained in:
parent
5c0ab3f893
commit
ccc41e7023
100 changed files with 3756 additions and 432 deletions
6
app.py
6
app.py
|
|
@ -292,6 +292,10 @@ async def lifespan(app: FastAPI):
|
|||
# --- Init Managers ---
|
||||
await featuresLifecycle.start(eventUser)
|
||||
eventManager.start()
|
||||
|
||||
# Register audit log cleanup scheduler
|
||||
from modules.shared.auditLogger import registerAuditLogCleanupScheduler
|
||||
registerAuditLogCleanupScheduler()
|
||||
|
||||
yield
|
||||
|
||||
|
|
@ -444,7 +448,7 @@ app.include_router(sharepointRouter)
|
|||
from modules.routes.routeDataAutomation import router as automationRouter
|
||||
app.include_router(automationRouter)
|
||||
|
||||
from modules.routes.routeFeatureAutomation import router as adminAutomationEventsRouter
|
||||
from modules.routes.routeFeatureWorkflow import router as adminAutomationEventsRouter
|
||||
app.include_router(adminAutomationEventsRouter)
|
||||
|
||||
from modules.routes.routeRbac import router as rbacRouter
|
||||
|
|
|
|||
1221
local/backup/migration_export_20260119_085558.json
Normal file
1221
local/backup/migration_export_20260119_085558.json
Normal file
File diff suppressed because it is too large
Load diff
208
modules/datamodels/datamodelAudit.py
Normal file
208
modules/datamodels/datamodelAudit.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Audit Log Data Model for database-based audit logging.
|
||||
|
||||
This model stores security-relevant audit events for GDPR compliance and security monitoring.
|
||||
|
||||
GDPR-Relevant Events:
|
||||
- User access: login, logout, failed login attempts
|
||||
- Data access: create, read, update, delete operations on personal data
|
||||
- Security events: password changes, token refresh, session management
|
||||
- Key access: encryption/decryption of sensitive data
|
||||
- GDPR actions: data export, data portability, account deletion
|
||||
- Mandate/permission changes: user added/removed from mandates, role changes
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
from modules.shared.attributeUtils import registerModelLabels
|
||||
|
||||
|
||||
class AuditCategory(str, Enum):
|
||||
"""Categories for audit log entries"""
|
||||
ACCESS = "access" # Login/logout events
|
||||
KEY = "key" # Encryption key access
|
||||
DATA = "data" # Data CRUD operations
|
||||
SECURITY = "security" # Security-related events
|
||||
GDPR = "gdpr" # GDPR-specific actions
|
||||
PERMISSION = "permission" # Permission/role changes
|
||||
SYSTEM = "system" # System-level events
|
||||
|
||||
|
||||
class AuditAction(str, Enum):
|
||||
"""Actions for audit log entries"""
|
||||
# Access actions
|
||||
LOGIN = "login"
|
||||
LOGIN_FAILED = "login_failed"
|
||||
LOGOUT = "logout"
|
||||
TOKEN_REFRESH = "token_refresh"
|
||||
TOKEN_REVOKE = "token_revoke"
|
||||
SESSION_EXPIRED = "session_expired"
|
||||
|
||||
# Key actions
|
||||
KEY_ENCODE = "encode"
|
||||
KEY_DECODE = "decode"
|
||||
KEY_ACCESS = "key_access"
|
||||
|
||||
# Data actions
|
||||
DATA_CREATE = "create"
|
||||
DATA_READ = "read"
|
||||
DATA_UPDATE = "update"
|
||||
DATA_DELETE = "delete"
|
||||
DATA_EXPORT = "export"
|
||||
|
||||
# Security actions
|
||||
PASSWORD_CHANGE = "password_change"
|
||||
PASSWORD_RESET = "password_reset"
|
||||
MFA_ENABLED = "mfa_enabled"
|
||||
MFA_DISABLED = "mfa_disabled"
|
||||
|
||||
# GDPR actions
|
||||
GDPR_DATA_EXPORT = "gdpr_data_export"
|
||||
GDPR_DATA_PORTABILITY = "gdpr_data_portability"
|
||||
GDPR_ACCOUNT_DELETION = "gdpr_account_deletion"
|
||||
GDPR_CONSENT_UPDATE = "gdpr_consent_update"
|
||||
|
||||
# Permission actions
|
||||
USER_ADDED_TO_MANDATE = "user_added_to_mandate"
|
||||
USER_REMOVED_FROM_MANDATE = "user_removed_from_mandate"
|
||||
ROLE_ASSIGNED = "role_assigned"
|
||||
ROLE_REVOKED = "role_revoked"
|
||||
FEATURE_ACCESS_GRANTED = "feature_access_granted"
|
||||
FEATURE_ACCESS_REVOKED = "feature_access_revoked"
|
||||
|
||||
# System actions
|
||||
SYSTEM_STARTUP = "system_startup"
|
||||
SYSTEM_SHUTDOWN = "system_shutdown"
|
||||
CONFIG_CHANGE = "config_change"
|
||||
|
||||
|
||||
class AuditLogEntry(BaseModel):
|
||||
"""
|
||||
Audit log entry for database storage.
|
||||
|
||||
Stores all security-relevant events for compliance and monitoring.
|
||||
Entries are immutable once created (append-only audit trail).
|
||||
"""
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
description="Unique identifier for the audit entry",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
# Timestamp
|
||||
timestamp: float = Field(
|
||||
default_factory=getUtcTimestamp,
|
||||
description="UTC timestamp when the event occurred",
|
||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True}
|
||||
)
|
||||
|
||||
# Actor identification
|
||||
userId: str = Field(
|
||||
description="ID of the user who performed the action (or 'system' for system events)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||
)
|
||||
|
||||
username: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Username at the time of the event (for historical reference)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
# Context
|
||||
mandateId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Mandate context (if applicable)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature instance context (if applicable)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
# Event classification
|
||||
category: str = Field(
|
||||
description="Event category (access, key, data, security, gdpr, permission, system)",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||
)
|
||||
|
||||
action: str = Field(
|
||||
description="Specific action performed",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||
)
|
||||
|
||||
# Event details
|
||||
resourceType: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Type of resource affected (e.g., 'User', 'ChatWorkflow', 'TrusteeContract')",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
resourceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="ID of the affected resource",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
details: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Additional details about the event",
|
||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
# Request metadata
|
||||
ipAddress: Optional[str] = Field(
|
||||
default=None,
|
||||
description="IP address of the client",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
userAgent: Optional[str] = Field(
|
||||
default=None,
|
||||
description="User agent string from the request",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
# Outcome
|
||||
success: bool = Field(
|
||||
default=True,
|
||||
description="Whether the action was successful",
|
||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
|
||||
)
|
||||
|
||||
errorMessage: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Error message if the action failed",
|
||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
|
||||
|
||||
# Register labels for internationalization
|
||||
registerModelLabels(
|
||||
"AuditLogEntry",
|
||||
{"en": "Audit Log Entry", "de": "Audit-Log-Eintrag", "fr": "Entrée du journal d'audit"},
|
||||
{
|
||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
||||
"timestamp": {"en": "Timestamp", "de": "Zeitstempel", "fr": "Horodatage"},
|
||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
||||
"username": {"en": "Username", "de": "Benutzername", "fr": "Nom d'utilisateur"},
|
||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID", "fr": "ID de l'instance"},
|
||||
"category": {"en": "Category", "de": "Kategorie", "fr": "Catégorie"},
|
||||
"action": {"en": "Action", "de": "Aktion", "fr": "Action"},
|
||||
"resourceType": {"en": "Resource Type", "de": "Ressourcentyp", "fr": "Type de ressource"},
|
||||
"resourceId": {"en": "Resource ID", "de": "Ressourcen-ID", "fr": "ID de ressource"},
|
||||
"details": {"en": "Details", "de": "Details", "fr": "Détails"},
|
||||
"ipAddress": {"en": "IP Address", "de": "IP-Adresse", "fr": "Adresse IP"},
|
||||
"userAgent": {"en": "User Agent", "de": "User-Agent", "fr": "Agent utilisateur"},
|
||||
"success": {"en": "Success", "de": "Erfolgreich", "fr": "Succès"},
|
||||
"errorMessage": {"en": "Error Message", "de": "Fehlermeldung", "fr": "Message d'erreur"},
|
||||
},
|
||||
)
|
||||
|
|
@ -14,6 +14,12 @@ class ChatStat(BaseModel):
|
|||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this stat belongs to"
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this stat belongs to"
|
||||
)
|
||||
workflowId: Optional[str] = Field(
|
||||
None, description="Foreign key to workflow (for workflow stats)"
|
||||
)
|
||||
|
|
@ -33,6 +39,8 @@ registerModelLabels(
|
|||
{"en": "Chat Statistics", "fr": "Statistiques de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
|
||||
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
|
||||
"bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
|
||||
|
|
@ -49,6 +57,12 @@ class ChatLog(BaseModel):
|
|||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this log belongs to"
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this log belongs to"
|
||||
)
|
||||
workflowId: str = Field(description="Foreign key to workflow")
|
||||
message: str = Field(description="Log message")
|
||||
type: str = Field(description="Log type (info, warning, error, etc.)")
|
||||
|
|
@ -79,6 +93,8 @@ registerModelLabels(
|
|||
{"en": "Chat Log", "fr": "Journal de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"message": {"en": "Message", "fr": "Message"},
|
||||
"type": {"en": "Type", "fr": "Type"},
|
||||
|
|
@ -94,6 +110,12 @@ class ChatDocument(BaseModel):
|
|||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this document belongs to"
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this document belongs to"
|
||||
)
|
||||
messageId: str = Field(description="Foreign key to message")
|
||||
fileId: str = Field(description="Foreign key to file")
|
||||
fileName: str = Field(description="Name of the file")
|
||||
|
|
@ -112,6 +134,8 @@ registerModelLabels(
|
|||
{"en": "Chat Document", "fr": "Document de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"messageId": {"en": "Message ID", "fr": "ID du message"},
|
||||
"fileId": {"en": "File ID", "fr": "ID du fichier"},
|
||||
"fileName": {"en": "File Name", "fr": "Nom du fichier"},
|
||||
|
|
@ -200,6 +224,12 @@ class ChatMessage(BaseModel):
|
|||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this message belongs to"
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this message belongs to"
|
||||
)
|
||||
workflowId: str = Field(description="Foreign key to workflow")
|
||||
parentMessageId: Optional[str] = Field(
|
||||
None, description="Parent message ID for threading"
|
||||
|
|
@ -251,6 +281,8 @@ registerModelLabels(
|
|||
{"en": "Chat Message", "fr": "Message de chat"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
|
||||
"documents": {"en": "Documents", "fr": "Documents"},
|
||||
|
|
@ -296,6 +328,7 @@ registerModelLabels(
|
|||
class ChatWorkflow(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="ID of the mandate this workflow belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance this workflow belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
status: str = Field(default="running", description="Current status of the workflow", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||
{"value": "running", "label": {"en": "Running", "fr": "En cours"}},
|
||||
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
|
||||
|
|
@ -370,6 +403,7 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"status": {"en": "Status", "fr": "Statut"},
|
||||
"name": {"en": "Name", "fr": "Nom"},
|
||||
"currentRound": {"en": "Current Round", "fr": "Tour actuel"},
|
||||
|
|
@ -993,6 +1027,7 @@ registerModelLabels(
|
|||
class AutomationDefinition(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="Mandate ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance this automation belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
label: str = Field(description="User-friendly name", json_schema_extra={"frontend_type": "text", "frontend_required": True})
|
||||
schedule: str = Field(description="Cron schedule pattern", json_schema_extra={"frontend_type": "select", "frontend_required": True, "frontend_options": [
|
||||
{"value": "0 */4 * * *", "label": {"en": "Every 4 hours", "fr": "Toutes les 4 heures"}},
|
||||
|
|
@ -1013,6 +1048,7 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"label": {"en": "Label", "fr": "Libellé"},
|
||||
"schedule": {"en": "Schedule", "fr": "Planification"},
|
||||
"template": {"en": "Template", "fr": "Modèle"},
|
||||
|
|
@ -13,6 +13,7 @@ import base64
|
|||
class FileItem(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="ID of the mandate this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
fileName: str = Field(description="Name of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
||||
mimeType: str = Field(description="MIME type of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
fileHash: str = Field(description="Hash of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
|
|
@ -25,6 +26,7 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
||||
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ class MessagingSubscription(BaseModel):
|
|||
description="ID of the mandate this subscription belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this subscription belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Description of the subscription",
|
||||
|
|
@ -92,6 +96,7 @@ registerModelLabels(
|
|||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||
"subscriptionLabel": {"en": "Subscription Label", "fr": "Label d'abonnement"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"description": {"en": "Description", "fr": "Description"},
|
||||
"isSystemSubscription": {"en": "System Subscription", "fr": "Abonnement système"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||
|
|
@ -110,6 +115,14 @@ class MessagingSubscriptionRegistration(BaseModel):
|
|||
description="Unique ID of the registration",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this registration belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this registration belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
subscriptionId: str = Field(
|
||||
description="ID of the subscription this registration belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||
|
|
@ -161,6 +174,8 @@ registerModelLabels(
|
|||
{"en": "Messaging Registration", "fr": "Inscription à la messagerie"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"channel": {"en": "Channel", "fr": "Canal"},
|
||||
|
|
@ -179,6 +194,14 @@ class MessagingDelivery(BaseModel):
|
|||
description="Unique ID of the delivery",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
mandateId: str = Field(
|
||||
description="ID of the mandate this delivery belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this delivery belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
)
|
||||
subscriptionId: str = Field(
|
||||
description="ID of the subscription this delivery belongs to",
|
||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||
|
|
@ -239,6 +262,8 @@ registerModelLabels(
|
|||
{"en": "Messaging Delivery", "fr": "Livraison de messagerie"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"channel": {"en": "Channel", "fr": "Canal"},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from modules.shared.attributeUtils import registerModelLabels
|
|||
class DataNeutraliserConfig(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the configuration", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="ID of the mandate this configuration belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance this configuration belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
userId: str = Field(description="ID of the user who created this configuration", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
enabled: bool = Field(default=True, description="Whether data neutralization is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False})
|
||||
namesToParse: str = Field(default="", description="Multiline list of names to parse for neutralization", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False})
|
||||
|
|
@ -22,6 +23,7 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
|
||||
|
|
@ -33,6 +35,7 @@ registerModelLabels(
|
|||
class DataNeutralizerAttributes(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the attribute mapping (used as UID in neutralized files)", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
mandateId: str = Field(description="ID of the mandate this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
userId: str = Field(description="ID of the user who created this attribute", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
originalText: str = Field(description="Original text that was neutralized", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
fileId: Optional[str] = Field(default=None, description="ID of the file this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
|
|
@ -43,6 +46,7 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"originalText": {"en": "Original Text", "fr": "Texte original"},
|
||||
"fileId": {"en": "File ID", "fr": "ID de fichier"},
|
||||
|
|
|
|||
|
|
@ -123,6 +123,12 @@ class Dokument(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance this document belongs to",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
label: str = Field(
|
||||
description="Document label",
|
||||
frontend_type="text",
|
||||
|
|
@ -207,6 +213,12 @@ class Land(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
label: str = Field(
|
||||
description="Country name (e.g. 'Schweiz')",
|
||||
frontend_type="text",
|
||||
|
|
@ -251,6 +263,12 @@ class Kanton(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
label: str = Field(
|
||||
description="Canton name (e.g. 'Zürich')",
|
||||
frontend_type="text",
|
||||
|
|
@ -302,6 +320,12 @@ class Gemeinde(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
label: str = Field(
|
||||
description="Municipality name (e.g. 'Zürich')",
|
||||
frontend_type="text",
|
||||
|
|
@ -359,6 +383,12 @@ class Parzelle(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
|
||||
# Grunddaten
|
||||
label: str = Field(
|
||||
|
|
@ -579,6 +609,12 @@ class Projekt(BaseModel):
|
|||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
featureInstanceId: str = Field(
|
||||
description="ID of the feature instance",
|
||||
frontend_type="text",
|
||||
frontend_readonly=True,
|
||||
frontend_required=False,
|
||||
)
|
||||
label: str = Field(
|
||||
description="Project designation",
|
||||
frontend_type="text",
|
||||
|
|
@ -643,6 +679,7 @@ registerModelLabels(
|
|||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -653,6 +690,7 @@ registerModelLabels(
|
|||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -662,6 +700,8 @@ registerModelLabels(
|
|||
{
|
||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ class TrusteeOrganisation(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector:
|
||||
# _createdAt, _modifiedAt, _createdBy, _modifiedBy
|
||||
|
||||
|
|
@ -56,6 +65,7 @@ registerModelLabels(
|
|||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -87,6 +97,15 @@ class TrusteeRole(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -97,6 +116,7 @@ registerModelLabels(
|
|||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -159,6 +179,15 @@ class TrusteeAccess(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -172,6 +201,7 @@ registerModelLabels(
|
|||
"userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
|
||||
"contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -222,6 +252,15 @@ class TrusteeContract(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -234,6 +273,7 @@ registerModelLabels(
|
|||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -309,6 +349,15 @@ class TrusteeDocument(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -323,6 +372,7 @@ registerModelLabels(
|
|||
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
||||
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -477,6 +527,15 @@ class TrusteePosition(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -499,6 +558,7 @@ registerModelLabels(
|
|||
"vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
|
||||
"vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -562,6 +622,15 @@ class TrusteePositionDocument(BaseModel):
|
|||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
featureInstanceId: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Feature Instance ID for instance-level isolation",
|
||||
json_schema_extra={
|
||||
"frontend_type": "text",
|
||||
"frontend_readonly": True,
|
||||
"frontend_required": False
|
||||
}
|
||||
)
|
||||
# System attributes are automatically set by DatabaseConnector
|
||||
|
||||
|
||||
|
|
@ -575,5 +644,6 @@ registerModelLabels(
|
|||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class VoiceSettings(BaseModel):
|
|||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
||||
userId: str = Field(description="ID of the user these settings belong to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
mandateId: str = Field(description="ID of the mandate these settings belong to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
featureInstanceId: str = Field(description="ID of the feature instance these settings belong to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
||||
sttLanguage: str = Field(default="de-DE", description="Speech-to-Text language", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True})
|
||||
ttsLanguage: str = Field(default="de-DE", description="Text-to-Speech language", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True})
|
||||
ttsVoice: str = Field(default="de-DE-KatjaNeural", description="Text-to-Speech voice", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True})
|
||||
|
|
@ -28,6 +29,7 @@ registerModelLabels(
|
|||
"id": {"en": "ID", "fr": "ID"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
||||
"sttLanguage": {"en": "STT Language", "fr": "Langue STT"},
|
||||
"ttsLanguage": {"en": "TTS Language", "fr": "Langue TTS"},
|
||||
"ttsVoice": {"en": "TTS Voice", "fr": "Voix TTS"},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from modules.datamodels.datamodelDocref import DocumentReferenceList
|
|||
|
||||
# Forward references for circular imports (use string annotations)
|
||||
if TYPE_CHECKING:
|
||||
from modules.datamodels.datamodelChat import ChatDocument, ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument, ActionResult
|
||||
from modules.datamodels.datamodelExtraction import ExtractionOptions
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
|
||||
from pydantic import BaseModel, Field
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ActionResult
|
||||
from modules.shared.frontendTypes import FrontendType
|
||||
from modules.shared.attributeUtils import registerModelLabels
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import asyncio
|
|||
import re
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog, ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog, ChatDocument
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
|
||||
|
|
@ -335,7 +335,7 @@ async def _emit_log_and_event(
|
|||
# Emit event directly for streaming (using correct signature)
|
||||
if created_log and event_manager:
|
||||
try:
|
||||
from modules.datamodels.datamodelChat import ChatLog
|
||||
from modules.datamodels.datamodelChatbot import ChatLog
|
||||
# Convert to dict if it's a Pydantic model
|
||||
if hasattr(created_log, "model_dump"):
|
||||
log_dict = created_log.model_dump()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from modules.datamodels.datamodelRealEstate import (
|
|||
Land,
|
||||
)
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
|
||||
from modules.interfaces.interfaceDbRealEstate import getInterface as getRealEstateInterface
|
||||
from modules.connectors.connectorSwissTopoMapServer import SwissTopoMapServerConnector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import logging
|
|||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, AutomationDefinition
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum, AutomationDefinition
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
from modules.shared.eventManagement import eventManager
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"""
|
||||
Utility functions for automation feature.
|
||||
|
||||
Moved from interfaces/interfaceDbChatObjects.py.
|
||||
Moved from interfaces/interfaceDbChatbot.py.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from modules.security.rbac import RbacClass
|
|||
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||
from modules.datamodels.datamodelUam import AccessLevel
|
||||
|
||||
from modules.datamodels.datamodelChat import (
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
ChatDocument,
|
||||
ChatStat,
|
||||
ChatLog,
|
||||
|
|
@ -178,18 +178,20 @@ class ChatObjects:
|
|||
Uses the JSON connector for data access with added language support.
|
||||
"""
|
||||
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None):
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Initializes the Chat Interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
# Initialize variables
|
||||
self.currentUser = currentUser # Store User object directly
|
||||
self.userId = currentUser.id if currentUser else None
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
self.rbac = None # RBAC interface
|
||||
|
||||
# Initialize services
|
||||
|
|
@ -200,7 +202,7 @@ class ChatObjects:
|
|||
|
||||
# Set user context if provided
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser, mandateId=mandateId)
|
||||
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
|
||||
|
||||
# ===== Generic Utility Methods =====
|
||||
|
||||
|
|
@ -263,17 +265,19 @@ class ChatObjects:
|
|||
def _initializeServices(self):
|
||||
pass
|
||||
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None):
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Sets the user context for the interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
self.currentUser = currentUser # Store User object directly
|
||||
self.userId = currentUser.id
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
|
||||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
|
@ -603,10 +607,12 @@ class ChatObjects:
|
|||
If pagination is None: List[Dict[str, Any]]
|
||||
If pagination is provided: PaginatedResult with items and metadata
|
||||
"""
|
||||
# Use RBAC filtering
|
||||
# Use RBAC filtering with featureInstanceId for instance-level isolation
|
||||
filteredWorkflows = getRecordsetWithRBAC(self.db,
|
||||
ChatWorkflow,
|
||||
self.currentUser
|
||||
self.currentUser,
|
||||
mandateId=self.mandateId,
|
||||
featureInstanceId=self.featureInstanceId
|
||||
)
|
||||
|
||||
# If no pagination requested, return all items (no sorting - frontend handles it)
|
||||
|
|
@ -638,11 +644,13 @@ class ChatObjects:
|
|||
|
||||
def getWorkflow(self, workflowId: str) -> Optional[ChatWorkflow]:
|
||||
"""Returns a workflow by ID if user has access."""
|
||||
# Use RBAC filtering
|
||||
# Use RBAC filtering with featureInstanceId for instance-level isolation
|
||||
workflows = getRecordsetWithRBAC(self.db,
|
||||
ChatWorkflow,
|
||||
self.currentUser,
|
||||
recordFilter={"id": workflowId}
|
||||
recordFilter={"id": workflowId},
|
||||
mandateId=self.mandateId,
|
||||
featureInstanceId=self.featureInstanceId
|
||||
)
|
||||
|
||||
if not workflows:
|
||||
|
|
@ -689,6 +697,12 @@ class ChatObjects:
|
|||
if "lastActivity" not in workflowData:
|
||||
workflowData["lastActivity"] = currentTime
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in workflowData or not workflowData["mandateId"]:
|
||||
workflowData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in workflowData or not workflowData["featureInstanceId"]:
|
||||
workflowData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Use generic field separation based on ChatWorkflow model
|
||||
simpleFields, objectFields = self._separateObjectFields(ChatWorkflow, workflowData)
|
||||
|
||||
|
|
@ -1009,6 +1023,12 @@ class ChatObjects:
|
|||
if "actionNumber" not in messageData:
|
||||
messageData["actionNumber"] = workflow.currentAction
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in messageData or not messageData["mandateId"]:
|
||||
messageData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in messageData or not messageData["featureInstanceId"]:
|
||||
messageData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Use generic field separation based on ChatMessage model
|
||||
simpleFields, objectFields = self._separateObjectFields(ChatMessage, messageData)
|
||||
|
||||
|
|
@ -1303,6 +1323,12 @@ class ChatObjects:
|
|||
def createDocument(self, documentData: Dict[str, Any]) -> ChatDocument:
|
||||
"""Creates a document for a message in normalized table."""
|
||||
try:
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in documentData or not documentData["mandateId"]:
|
||||
documentData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in documentData or not documentData["featureInstanceId"]:
|
||||
documentData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Validate and normalize document data to dict
|
||||
document = ChatDocument(**documentData)
|
||||
logger.debug(f"Creating document in database: fileName={document.fileName}, fileId={document.fileId}, messageId={document.messageId}")
|
||||
|
|
@ -1422,6 +1448,12 @@ class ChatObjects:
|
|||
if "timestamp" not in logData:
|
||||
logData["timestamp"] = getUtcTimestamp()
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in logData or not logData["mandateId"]:
|
||||
logData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in logData or not logData["featureInstanceId"]:
|
||||
logData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Add status information if not present
|
||||
if "status" not in logData and "type" in logData:
|
||||
if logData["type"] == "error":
|
||||
|
|
@ -1508,6 +1540,12 @@ class ChatObjects:
|
|||
if "workflowId" not in statData:
|
||||
raise ValueError("workflowId is required in statData")
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in statData or not statData["mandateId"]:
|
||||
statData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in statData or not statData["featureInstanceId"]:
|
||||
statData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Validate the stat data against ChatStat model
|
||||
stat = ChatStat(**statData)
|
||||
|
||||
|
|
@ -1768,9 +1806,11 @@ class ChatObjects:
|
|||
if "id" not in automationData or not automationData["id"]:
|
||||
automationData["id"] = str(uuid.uuid4())
|
||||
|
||||
# Ensure mandateId is set
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if "mandateId" not in automationData:
|
||||
automationData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in automationData:
|
||||
automationData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Ensure database connector has correct userId context
|
||||
# The connector should have been initialized with userId, but ensure it's updated
|
||||
|
|
@ -1894,7 +1934,7 @@ class ChatObjects:
|
|||
logger.error(f"Error notifying automation change: {str(e)}")
|
||||
|
||||
|
||||
def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] = None) -> 'ChatObjects':
|
||||
def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> 'ChatObjects':
|
||||
"""
|
||||
Returns a ChatObjects instance for the current user.
|
||||
Handles initialization of database and records.
|
||||
|
|
@ -1902,20 +1942,22 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
|||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header).
|
||||
"""
|
||||
if not currentUser:
|
||||
raise ValueError("Invalid user context: user is required")
|
||||
|
||||
effectiveMandateId = str(mandateId) if mandateId else None
|
||||
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||
|
||||
# Create context key
|
||||
contextKey = f"{effectiveMandateId}_{currentUser.id}"
|
||||
# Create context key including featureInstanceId for proper isolation
|
||||
contextKey = f"{effectiveMandateId}_{effectiveFeatureInstanceId}_{currentUser.id}"
|
||||
|
||||
# Create new instance if not exists
|
||||
if contextKey not in _chatInterfaces:
|
||||
_chatInterfaces[contextKey] = ChatObjects(currentUser, mandateId=effectiveMandateId)
|
||||
_chatInterfaces[contextKey] = ChatObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
else:
|
||||
# Update user context if needed
|
||||
_chatInterfaces[contextKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
||||
_chatInterfaces[contextKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
|
||||
return _chatInterfaces[contextKey]
|
||||
|
|
@ -76,12 +76,13 @@ class ComponentObjects:
|
|||
# Initialize standard records if needed
|
||||
self._initRecords()
|
||||
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None):
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Sets the user context for the interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
if not currentUser:
|
||||
logger.info("Initializing interface without user context")
|
||||
|
|
@ -91,6 +92,7 @@ class ComponentObjects:
|
|||
self.userId = currentUser.id
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
|
||||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
|
@ -986,12 +988,14 @@ class ComponentObjects:
|
|||
fileSize = len(content)
|
||||
fileHash = hashlib.sha256(content).hexdigest()
|
||||
|
||||
# Use mandateId from context
|
||||
# Use mandateId and featureInstanceId from context for proper data isolation
|
||||
mandateId = self.mandateId
|
||||
featureInstanceId = self.featureInstanceId
|
||||
|
||||
# Create FileItem instance
|
||||
fileItem = FileItem(
|
||||
mandateId=mandateId,
|
||||
featureInstanceId=featureInstanceId,
|
||||
fileName=uniqueName,
|
||||
mimeType=mimeType,
|
||||
fileSize=fileSize,
|
||||
|
|
@ -1327,9 +1331,11 @@ class ComponentObjects:
|
|||
if "userId" not in settingsData:
|
||||
settingsData["userId"] = self.userId
|
||||
|
||||
# Ensure mandateId is set from context
|
||||
# Ensure mandateId and featureInstanceId are set from context
|
||||
if "mandateId" not in settingsData:
|
||||
settingsData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in settingsData:
|
||||
settingsData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Check if settings already exist for this user
|
||||
existingSettings = self.getVoiceSettings(settingsData["userId"])
|
||||
|
|
@ -1501,9 +1507,11 @@ class ComponentObjects:
|
|||
if not all(c.isalpha() or c == "_" for c in subscriptionId):
|
||||
raise ValueError("subscriptionId must contain only letters and underscores")
|
||||
|
||||
# Set mandateId from context
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in subscriptionData:
|
||||
subscriptionData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in subscriptionData:
|
||||
subscriptionData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
createdRecord = self.db.recordCreate(MessagingSubscription, subscriptionData)
|
||||
if not createdRecord or not createdRecord.get("id"):
|
||||
|
|
@ -1605,6 +1613,12 @@ class ComponentObjects:
|
|||
if "userId" not in registrationData:
|
||||
registrationData["userId"] = self.userId
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in registrationData:
|
||||
registrationData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in registrationData:
|
||||
registrationData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
createdRecord = self.db.recordCreate(MessagingSubscriptionRegistration, registrationData)
|
||||
if not createdRecord or not createdRecord.get("id"):
|
||||
raise ValueError("Failed to create registration record")
|
||||
|
|
@ -1679,6 +1693,13 @@ class ComponentObjects:
|
|||
def createDelivery(self, delivery: MessagingDelivery) -> Dict[str, Any]:
|
||||
"""Creates a new delivery record."""
|
||||
deliveryData = delivery.model_dump() if isinstance(delivery, MessagingDelivery) else delivery
|
||||
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
if "mandateId" not in deliveryData or not deliveryData["mandateId"]:
|
||||
deliveryData["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in deliveryData or not deliveryData["featureInstanceId"]:
|
||||
deliveryData["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
createdRecord = self.db.recordCreate(MessagingDelivery, deliveryData)
|
||||
if not createdRecord or not createdRecord.get("id"):
|
||||
raise ValueError("Failed to create delivery record")
|
||||
|
|
@ -1748,7 +1769,7 @@ class ComponentObjects:
|
|||
return MessagingDelivery(**filteredDeliveries[0]) if filteredDeliveries else None
|
||||
|
||||
|
||||
def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] = None) -> 'ComponentObjects':
|
||||
def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> 'ComponentObjects':
|
||||
"""
|
||||
Returns a ComponentObjects instance.
|
||||
If currentUser is provided, initializes with user context.
|
||||
|
|
@ -1757,8 +1778,10 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
|||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header).
|
||||
"""
|
||||
effectiveMandateId = str(mandateId) if mandateId else None
|
||||
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||
|
||||
# Create new instance if not exists
|
||||
if "default" not in _instancesManagement:
|
||||
|
|
@ -1767,7 +1790,7 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
|||
interface = _instancesManagement["default"]
|
||||
|
||||
if currentUser:
|
||||
interface.setUserContext(currentUser, mandateId=effectiveMandateId)
|
||||
interface.setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
else:
|
||||
logger.info("Returning interface without user context")
|
||||
|
||||
|
|
|
|||
|
|
@ -39,17 +39,19 @@ class RealEstateObjects:
|
|||
Handles CRUD operations on Real Estate entities.
|
||||
"""
|
||||
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None):
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Initializes the Real Estate Interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
self.currentUser = currentUser
|
||||
self.userId = currentUser.id if currentUser else None
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
self.rbac = None # RBAC interface
|
||||
|
||||
# Initialize database
|
||||
|
|
@ -57,7 +59,7 @@ class RealEstateObjects:
|
|||
|
||||
# Set user context if provided
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser, mandateId=mandateId)
|
||||
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
|
||||
|
||||
def _initializeDatabase(self):
|
||||
"""Initialize PostgreSQL database connection."""
|
||||
|
|
@ -107,17 +109,19 @@ class RealEstateObjects:
|
|||
logger.warning(f"Error ensuring supporting tables exist: {e}")
|
||||
# Don't raise - tables will be created on-demand anyway
|
||||
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None):
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Sets the user context for the interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
self.currentUser = currentUser
|
||||
self.userId = currentUser.id
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
|
||||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
|
@ -145,9 +149,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Projekt, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create projects")
|
||||
|
||||
# Ensure mandateId is set
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not projekt.mandateId:
|
||||
projekt.mandateId = self.mandateId
|
||||
if not projekt.featureInstanceId:
|
||||
projekt.featureInstanceId = self.featureInstanceId
|
||||
|
||||
# Save to database - use mode='json' to ensure nested Pydantic models are serialized
|
||||
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
|
||||
|
|
@ -231,8 +237,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Parzelle, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create plots")
|
||||
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not parzelle.mandateId:
|
||||
parzelle.mandateId = self.mandateId
|
||||
if not parzelle.featureInstanceId:
|
||||
parzelle.featureInstanceId = self.featureInstanceId
|
||||
|
||||
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
|
||||
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
|
||||
|
|
@ -438,8 +447,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Dokument, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create documents")
|
||||
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not dokument.mandateId:
|
||||
dokument.mandateId = self.mandateId
|
||||
if not dokument.featureInstanceId:
|
||||
dokument.featureInstanceId = self.featureInstanceId
|
||||
|
||||
self.db.recordCreate(Dokument, dokument.model_dump())
|
||||
|
||||
|
|
@ -504,8 +516,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Gemeinde, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create municipalities")
|
||||
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not gemeinde.mandateId:
|
||||
gemeinde.mandateId = self.mandateId
|
||||
if not gemeinde.featureInstanceId:
|
||||
gemeinde.featureInstanceId = self.featureInstanceId
|
||||
|
||||
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
|
||||
|
||||
|
|
@ -570,8 +585,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Kanton, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create cantons")
|
||||
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not kanton.mandateId:
|
||||
kanton.mandateId = self.mandateId
|
||||
if not kanton.featureInstanceId:
|
||||
kanton.featureInstanceId = self.featureInstanceId
|
||||
|
||||
self.db.recordCreate(Kanton, kanton.model_dump())
|
||||
|
||||
|
|
@ -636,8 +654,11 @@ class RealEstateObjects:
|
|||
if not self.checkRbacPermission(Land, "create"):
|
||||
raise PermissionError(f"User {self.userId} cannot create countries")
|
||||
|
||||
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||
if not land.mandateId:
|
||||
land.mandateId = self.mandateId
|
||||
if not land.featureInstanceId:
|
||||
land.featureInstanceId = self.featureInstanceId
|
||||
|
||||
self.db.recordCreate(Land, land.model_dump())
|
||||
|
||||
|
|
@ -792,7 +813,7 @@ class RealEstateObjects:
|
|||
raise
|
||||
|
||||
|
||||
def getInterface(currentUser: User, mandateId: Optional[str] = None) -> RealEstateObjects:
|
||||
def getInterface(currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None) -> RealEstateObjects:
|
||||
"""
|
||||
Factory function to get or create a Real Estate interface instance for a user.
|
||||
Uses singleton pattern per user.
|
||||
|
|
@ -800,16 +821,19 @@ def getInterface(currentUser: User, mandateId: Optional[str] = None) -> RealEsta
|
|||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header).
|
||||
"""
|
||||
effectiveMandateId = str(mandateId) if mandateId else None
|
||||
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||
|
||||
userKey = f"{currentUser.id}_{effectiveMandateId}"
|
||||
# Include featureInstanceId in key for proper isolation
|
||||
userKey = f"{currentUser.id}_{effectiveMandateId}_{effectiveFeatureInstanceId}"
|
||||
|
||||
if userKey not in _realEstateInterfaces:
|
||||
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser, mandateId=effectiveMandateId)
|
||||
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
else:
|
||||
# Update user context if needed
|
||||
_realEstateInterfaces[userKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
||||
_realEstateInterfaces[userKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
|
||||
return _realEstateInterfaces[userKey]
|
||||
|
||||
|
|
@ -33,12 +33,13 @@ logger = logging.getLogger(__name__)
|
|||
_trusteeInterfaces = {}
|
||||
|
||||
|
||||
def getInterface(currentUser: User, mandateId: Optional[Union[str, uuid.UUID]] = None) -> "TrusteeObjects":
|
||||
def getInterface(currentUser: User, mandateId: Optional[Union[str, uuid.UUID]] = None, featureInstanceId: Optional[str] = None) -> "TrusteeObjects":
|
||||
"""Get or create a TrusteeObjects instance for the given user context.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header).
|
||||
"""
|
||||
global _trusteeInterfaces
|
||||
|
||||
|
|
@ -46,14 +47,16 @@ def getInterface(currentUser: User, mandateId: Optional[Union[str, uuid.UUID]] =
|
|||
raise ValueError("Valid user context required")
|
||||
|
||||
effectiveMandateId = str(mandateId) if mandateId else None
|
||||
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||
|
||||
cacheKey = f"{currentUser.id}_{effectiveMandateId}"
|
||||
# Include featureInstanceId in cache key for proper isolation
|
||||
cacheKey = f"{currentUser.id}_{effectiveMandateId}_{effectiveFeatureInstanceId}"
|
||||
|
||||
if cacheKey not in _trusteeInterfaces:
|
||||
_trusteeInterfaces[cacheKey] = TrusteeObjects(currentUser, mandateId=effectiveMandateId)
|
||||
_trusteeInterfaces[cacheKey] = TrusteeObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
else:
|
||||
# Update user context if needed
|
||||
_trusteeInterfaces[cacheKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
||||
_trusteeInterfaces[cacheKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||
|
||||
return _trusteeInterfaces[cacheKey]
|
||||
|
||||
|
|
@ -64,17 +67,19 @@ class TrusteeObjects:
|
|||
Manages trustee organisations, roles, access, contracts, documents, and positions.
|
||||
"""
|
||||
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None):
|
||||
def __init__(self, currentUser: Optional[User] = None, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Initializes the Trustee Interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
self.currentUser = currentUser
|
||||
self.userId = currentUser.id if currentUser else None
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
self.rbac = None
|
||||
|
||||
# Initialize database
|
||||
|
|
@ -82,14 +87,15 @@ class TrusteeObjects:
|
|||
|
||||
# Set user context if provided
|
||||
if currentUser:
|
||||
self.setUserContext(currentUser, mandateId=mandateId)
|
||||
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
|
||||
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None):
|
||||
def setUserContext(self, currentUser: User, mandateId: Optional[str] = None, featureInstanceId: Optional[str] = None):
|
||||
"""Sets the user context for the interface.
|
||||
|
||||
Args:
|
||||
currentUser: The authenticated user
|
||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||
"""
|
||||
if not currentUser:
|
||||
logger.info("Initializing interface without user context")
|
||||
|
|
@ -99,6 +105,7 @@ class TrusteeObjects:
|
|||
self.userId = currentUser.id
|
||||
# Use mandateId from parameter (Request-Context), not from user object
|
||||
self.mandateId = mandateId
|
||||
self.featureInstanceId = featureInstanceId
|
||||
|
||||
if not self.userId:
|
||||
raise ValueError("Invalid user context: id is required")
|
||||
|
|
@ -204,8 +211,10 @@ class TrusteeObjects:
|
|||
logger.warning(f"User {self.userId} lacks permission to create organisation")
|
||||
return None
|
||||
|
||||
# Set mandateId from current user
|
||||
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Validate ID format (alphanumeric, hyphens, underscores, 3-50 chars)
|
||||
orgId = data.get("id", "")
|
||||
|
|
@ -307,6 +316,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
roleId = data.get("id", "")
|
||||
|
||||
if not roleId:
|
||||
|
|
@ -414,6 +425,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
accessId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -603,6 +616,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
contractId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -729,6 +744,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
documentId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -879,6 +896,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
# Calculate VAT amount if not provided
|
||||
if "vatAmount" not in data or data.get("vatAmount") == 0:
|
||||
|
|
@ -1028,6 +1047,8 @@ class TrusteeObjects:
|
|||
return None
|
||||
|
||||
data["mandateId"] = self.mandateId
|
||||
if "featureInstanceId" not in data:
|
||||
data["featureInstanceId"] = self.featureInstanceId
|
||||
|
||||
import uuid
|
||||
linkId = data.get("id") or str(uuid.uuid4())
|
||||
|
|
@ -13,9 +13,9 @@ import logging
|
|||
import json
|
||||
|
||||
# Import interfaces and models
|
||||
from modules.interfaces.interfaceDbChatObjects import getInterface as getChatInterface
|
||||
from modules.interfaces.interfaceDbChatbot import getInterface as getChatInterface
|
||||
from modules.auth import getCurrentUser, limiter
|
||||
from modules.datamodels.datamodelChat import AutomationDefinition, ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import AutomationDefinition, ChatWorkflow
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||
from modules.features.workflow import executeAutomation
|
||||
|
|
|
|||
|
|
@ -472,12 +472,15 @@ async def addUserToMandate(
|
|||
roleIds=data.roleIds
|
||||
)
|
||||
|
||||
# 8. Audit
|
||||
audit_logger.logSecurityEvent(
|
||||
# 8. Audit - Log permission change with IP address
|
||||
audit_logger.logPermissionChange(
|
||||
userId=str(context.user.id),
|
||||
mandateId=mandateId,
|
||||
action="user_added_to_mandate",
|
||||
details=f"targetUser={data.targetUserId}, roles={data.roleIds}"
|
||||
targetUserId=data.targetUserId,
|
||||
details=f"Roles assigned: {data.roleIds}",
|
||||
resourceType="UserMandate",
|
||||
resourceId=str(userMandate.id)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
@ -557,12 +560,14 @@ async def removeUserFromMandate(
|
|||
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
|
||||
rootInterface.deleteUserMandate(targetUserId, mandateId)
|
||||
|
||||
# Audit
|
||||
audit_logger.logSecurityEvent(
|
||||
# Audit - Log permission change
|
||||
audit_logger.logPermissionChange(
|
||||
userId=str(context.user.id),
|
||||
mandateId=mandateId,
|
||||
action="user_removed_from_mandate",
|
||||
details=f"targetUser={targetUserId}"
|
||||
targetUserId=targetUserId,
|
||||
details="User removed from mandate",
|
||||
resourceType="UserMandate"
|
||||
)
|
||||
|
||||
logger.info(f"User {context.user.id} removed user {targetUserId} from mandate {mandateId}")
|
||||
|
|
@ -657,12 +662,15 @@ async def updateUserRolesInMandate(
|
|||
for roleId in roleIds:
|
||||
rootInterface.addRoleToUserMandate(str(membership.id), roleId)
|
||||
|
||||
# Audit
|
||||
audit_logger.logSecurityEvent(
|
||||
# Audit - Log role assignment change
|
||||
audit_logger.logPermissionChange(
|
||||
userId=str(context.user.id),
|
||||
mandateId=mandateId,
|
||||
action="user_roles_updated_in_mandate",
|
||||
details=f"targetUser={targetUserId}, newRoles={roleIds}"
|
||||
action="role_assigned",
|
||||
targetUserId=targetUserId,
|
||||
details=f"New roles: {roleIds}",
|
||||
resourceType="UserMandateRole",
|
||||
resourceId=str(membership.id)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -360,7 +360,9 @@ async def reset_user_password(
|
|||
userId=str(context.user.id),
|
||||
mandateId=str(context.mandateId) if context.mandateId else "system",
|
||||
action="password_reset",
|
||||
details=f"Reset password for user {userId}"
|
||||
details=f"Reset password for user {userId}",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -439,7 +441,9 @@ async def change_password(
|
|||
userId=str(context.user.id),
|
||||
mandateId=str(context.mandateId) if context.mandateId else "system",
|
||||
action="password_change",
|
||||
details="User changed their own password"
|
||||
details="User changed their own password",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Reques
|
|||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
|
||||
# Import interfaces
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
# Import models
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
||||
|
||||
# Import workflow control functions
|
||||
from modules.features.workflow import chatStart, chatStop
|
||||
|
|
@ -32,7 +32,7 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
def _getServiceChat(context: RequestContext):
|
||||
return interfaceDbChatObjects.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
|
||||
return interfaceDbChatbot.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
|
||||
|
||||
# Workflow start endpoint
|
||||
@router.post("/start", response_model=ChatWorkflow)
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ from modules.shared.timeUtils import parseTimestamp
|
|||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
|
||||
# Import interfaces
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||
|
||||
# Import models
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse
|
||||
|
||||
# Import chatbot feature
|
||||
|
|
@ -43,7 +43,7 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
def _getServiceChat(context: RequestContext):
|
||||
return interfaceDbChatObjects.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
|
||||
return interfaceDbChatbot.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
|
||||
|
||||
# Chatbot streaming endpoint (SSE)
|
||||
@router.post("/start/stream")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from modules.datamodels.datamodelRealEstate import (
|
|||
)
|
||||
|
||||
# Import interfaces
|
||||
from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
|
||||
from modules.interfaces.interfaceDbRealEstate import getInterface as getRealEstateInterface
|
||||
|
||||
# Import feature logic for AI-powered commands
|
||||
from modules.features.realEstate.mainRealEstate import (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
"""
|
||||
Routes for Trustee feature data management.
|
||||
Implements CRUD operations for organisations, roles, access, contracts, documents, and positions.
|
||||
|
||||
URL Structure: /api/trustee/{instanceId}/{entity}
|
||||
- instanceId is the FeatureInstance ID (required for all operations)
|
||||
- This ensures proper multi-tenant isolation at the URL level
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response
|
||||
|
|
@ -14,7 +18,9 @@ import json
|
|||
import io
|
||||
|
||||
from modules.auth import limiter, getRequestContext, RequestContext
|
||||
from modules.interfaces.interfaceDbTrusteeObjects import getInterface
|
||||
from modules.interfaces.interfaceDbTrustee import getInterface
|
||||
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
||||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||
from modules.datamodels.datamodelTrustee import (
|
||||
TrusteeOrganisation,
|
||||
TrusteeRole,
|
||||
|
|
@ -59,22 +65,72 @@ def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
|
|||
return None
|
||||
|
||||
|
||||
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||
"""
|
||||
Validate that the user has access to the feature instance.
|
||||
Returns the mandateId for the instance.
|
||||
|
||||
Args:
|
||||
instanceId: The FeatureInstance ID from URL
|
||||
context: The request context with user info
|
||||
|
||||
Returns:
|
||||
mandateId of the instance
|
||||
|
||||
Raises:
|
||||
HTTPException 404 if instance not found
|
||||
HTTPException 403 if user doesn't have access
|
||||
"""
|
||||
rootInterface = getRootInterface()
|
||||
featureInterface = getFeatureInterface(rootInterface.db)
|
||||
|
||||
instance = featureInterface.getFeatureInstance(instanceId)
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Feature instance '{instanceId}' not found"
|
||||
)
|
||||
|
||||
# Verify it's a trustee instance
|
||||
if instance.featureCode != "trustee":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Instance '{instanceId}' is not a trustee instance"
|
||||
)
|
||||
|
||||
# Verify user has access to this instance
|
||||
if not context.isSysAdmin:
|
||||
# Check if user has FeatureAccess for this instance
|
||||
featureAccesses = rootInterface.getFeatureAccessesForUser(str(context.user.id))
|
||||
hasAccess = any(
|
||||
str(fa.featureInstanceId) == instanceId and fa.enabled
|
||||
for fa in featureAccesses
|
||||
)
|
||||
if not hasAccess:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Access denied to feature instance '{instanceId}'"
|
||||
)
|
||||
|
||||
return str(instance.mandateId)
|
||||
|
||||
|
||||
# ===== Organisation Routes =====
|
||||
|
||||
@router.get("/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
||||
@router.get("/{instanceId}/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisations(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteeOrganisation]:
|
||||
"""Get all organisations with optional pagination."""
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(f"getOrganisations called for user {context.user.id}, mandateId: {context.mandateId}")
|
||||
"""Get all organisations for a feature instance with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllOrganisations(paginationParams)
|
||||
logger.debug(f"getOrganisations returned {len(result.items)} items")
|
||||
|
||||
if paginationParams:
|
||||
return PaginatedResponse(
|
||||
|
|
@ -91,46 +147,55 @@ async def getOrganisations(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@router.get("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("30/minute")
|
||||
async def getOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Get a single organisation by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
org = interface.getOrganisation(orgId)
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
return org
|
||||
|
||||
|
||||
@router.post("/organisations", response_model=TrusteeOrganisation, status_code=201)
|
||||
@router.post("/{instanceId}/organisations", response_model=TrusteeOrganisation, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeOrganisation = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Create a new organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createOrganisation(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create organisation")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
data: TrusteeOrganisation = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeOrganisation:
|
||||
"""Update an organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getOrganisation(orgId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
|
|
@ -141,15 +206,18 @@ async def updateOrganisation(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/organisations/{orgId}")
|
||||
@router.delete("/{instanceId}/organisations/{orgId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(..., description="Organisation ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete an organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getOrganisation(orgId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||
|
|
@ -162,16 +230,19 @@ async def deleteOrganisation(
|
|||
|
||||
# ===== Role Routes =====
|
||||
|
||||
@router.get("/roles", response_model=PaginatedResponse[TrusteeRole])
|
||||
@router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole])
|
||||
@limiter.limit("30/minute")
|
||||
async def getRoles(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteeRole]:
|
||||
"""Get all roles with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllRoles(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -189,46 +260,55 @@ async def getRoles(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/roles/{roleId}", response_model=TrusteeRole)
|
||||
@router.get("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("30/minute")
|
||||
async def getRole(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(..., description="Role ID"),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeRole:
|
||||
"""Get a single role by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
role = interface.getRole(roleId)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
return role
|
||||
|
||||
|
||||
@router.post("/roles", response_model=TrusteeRole, status_code=201)
|
||||
@router.post("/{instanceId}/roles", response_model=TrusteeRole, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createRole(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeRole = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeRole:
|
||||
"""Create a new role (sysadmin only)."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createRole(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create role")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/roles/{roleId}", response_model=TrusteeRole)
|
||||
@router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateRole(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(...),
|
||||
data: TrusteeRole = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeRole:
|
||||
"""Update a role (sysadmin only)."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getRole(roleId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
|
|
@ -239,15 +319,18 @@ async def updateRole(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/roles/{roleId}")
|
||||
@router.delete("/{instanceId}/roles/{roleId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteRole(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
roleId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a role (sysadmin only, fails if in use)."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getRole(roleId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||
|
|
@ -260,16 +343,19 @@ async def deleteRole(
|
|||
|
||||
# ===== Access Routes =====
|
||||
|
||||
@router.get("/access", response_model=PaginatedResponse[TrusteeAccess])
|
||||
@router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAllAccess(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteeAccess]:
|
||||
"""Get all access records with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllAccess(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -287,70 +373,85 @@ async def getAllAccess(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/access/{accessId}", response_model=TrusteeAccess)
|
||||
@router.get("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccess(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeAccess:
|
||||
"""Get a single access record by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
access = interface.getAccess(accessId)
|
||||
if not access:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
return access
|
||||
|
||||
|
||||
@router.get("/access/organisation/{orgId}", response_model=List[TrusteeAccess])
|
||||
@router.get("/{instanceId}/access/organisation/{orgId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteeAccess]:
|
||||
"""Get all access records for an organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getAccessByOrganisation(orgId)
|
||||
|
||||
|
||||
@router.get("/access/user/{userId}", response_model=List[TrusteeAccess])
|
||||
@router.get("/{instanceId}/access/user/{userId}", response_model=List[TrusteeAccess])
|
||||
@limiter.limit("30/minute")
|
||||
async def getAccessByUser(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
userId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteeAccess]:
|
||||
"""Get all access records for a user."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getAccessByUser(userId)
|
||||
|
||||
|
||||
@router.post("/access", response_model=TrusteeAccess, status_code=201)
|
||||
@router.post("/{instanceId}/access", response_model=TrusteeAccess, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createAccess(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeAccess = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeAccess:
|
||||
"""Create a new access record."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createAccess(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create access")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/access/{accessId}", response_model=TrusteeAccess)
|
||||
@router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateAccess(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
data: TrusteeAccess = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeAccess:
|
||||
"""Update an access record."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getAccess(accessId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
|
|
@ -361,15 +462,18 @@ async def updateAccess(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/access/{accessId}")
|
||||
@router.delete("/{instanceId}/access/{accessId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteAccess(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
accessId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete an access record."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getAccess(accessId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||
|
|
@ -382,16 +486,19 @@ async def deleteAccess(
|
|||
|
||||
# ===== Contract Routes =====
|
||||
|
||||
@router.get("/contracts", response_model=PaginatedResponse[TrusteeContract])
|
||||
@router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContracts(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteeContract]:
|
||||
"""Get all contracts with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllContracts(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -409,58 +516,70 @@ async def getContracts(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@router.get("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("30/minute")
|
||||
async def getContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeContract:
|
||||
"""Get a single contract by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
contract = interface.getContract(contractId)
|
||||
if not contract:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
return contract
|
||||
|
||||
|
||||
@router.get("/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
|
||||
@router.get("/{instanceId}/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
|
||||
@limiter.limit("30/minute")
|
||||
async def getContractsByOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteeContract]:
|
||||
"""Get all contracts for an organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getContractsByOrganisation(orgId)
|
||||
|
||||
|
||||
@router.post("/contracts", response_model=TrusteeContract, status_code=201)
|
||||
@router.post("/{instanceId}/contracts", response_model=TrusteeContract, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeContract = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeContract:
|
||||
"""Create a new contract."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createContract(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create contract")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
data: TrusteeContract = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeContract:
|
||||
"""Update a contract (organisationId is immutable)."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getContract(contractId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
|
|
@ -471,15 +590,18 @@ async def updateContract(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/contracts/{contractId}")
|
||||
@router.delete("/{instanceId}/contracts/{contractId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a contract."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getContract(contractId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||
|
|
@ -492,16 +614,19 @@ async def deleteContract(
|
|||
|
||||
# ===== Document Routes =====
|
||||
|
||||
@router.get("/documents", response_model=PaginatedResponse[TrusteeDocument])
|
||||
@router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocuments(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteeDocument]:
|
||||
"""Get all documents (metadata only) with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllDocuments(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -519,30 +644,36 @@ async def getDocuments(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@router.get("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeDocument:
|
||||
"""Get document metadata by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
doc = interface.getDocument(documentId)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
return doc
|
||||
|
||||
|
||||
@router.get("/documents/{documentId}/data")
|
||||
@router.get("/{instanceId}/documents/{documentId}/data")
|
||||
@limiter.limit("10/minute")
|
||||
async def getDocumentData(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
):
|
||||
"""Download document binary data."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
doc = interface.getDocument(documentId)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
|
@ -558,43 +689,52 @@ async def getDocumentData(
|
|||
)
|
||||
|
||||
|
||||
@router.get("/documents/contract/{contractId}", response_model=List[TrusteeDocument])
|
||||
@router.get("/{instanceId}/documents/contract/{contractId}", response_model=List[TrusteeDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsByContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteeDocument]:
|
||||
"""Get all documents for a contract."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getDocumentsByContract(contractId)
|
||||
|
||||
|
||||
@router.post("/documents", response_model=TrusteeDocument, status_code=201)
|
||||
@router.post("/{instanceId}/documents", response_model=TrusteeDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteeDocument = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeDocument:
|
||||
"""Create a new document."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createDocument(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
|
||||
@limiter.limit("10/minute")
|
||||
async def updateDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
data: TrusteeDocument = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteeDocument:
|
||||
"""Update document metadata."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getDocument(documentId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
|
@ -605,15 +745,18 @@ async def updateDocument(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/documents/{documentId}")
|
||||
@router.delete("/{instanceId}/documents/{documentId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deleteDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a document."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getDocument(documentId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||
|
|
@ -626,16 +769,19 @@ async def deleteDocument(
|
|||
|
||||
# ===== Position Routes =====
|
||||
|
||||
@router.get("/positions", response_model=PaginatedResponse[TrusteePosition])
|
||||
@router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositions(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteePosition]:
|
||||
"""Get all positions with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllPositions(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -653,70 +799,85 @@ async def getPositions(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/positions/{positionId}", response_model=TrusteePosition)
|
||||
@router.get("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPosition(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteePosition:
|
||||
"""Get a single position by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
position = interface.getPosition(positionId)
|
||||
if not position:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
return position
|
||||
|
||||
|
||||
@router.get("/positions/contract/{contractId}", response_model=List[TrusteePosition])
|
||||
@router.get("/{instanceId}/positions/contract/{contractId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByContract(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
contractId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteePosition]:
|
||||
"""Get all positions for a contract."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getPositionsByContract(contractId)
|
||||
|
||||
|
||||
@router.get("/positions/organisation/{orgId}", response_model=List[TrusteePosition])
|
||||
@router.get("/{instanceId}/positions/organisation/{orgId}", response_model=List[TrusteePosition])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsByOrganisation(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
orgId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteePosition]:
|
||||
"""Get all positions for an organisation."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getPositionsByOrganisation(orgId)
|
||||
|
||||
|
||||
@router.post("/positions", response_model=TrusteePosition, status_code=201)
|
||||
@router.post("/{instanceId}/positions", response_model=TrusteePosition, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPosition(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteePosition = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteePosition:
|
||||
"""Create a new position."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createPosition(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create position")
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/positions/{positionId}", response_model=TrusteePosition)
|
||||
@router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
|
||||
@limiter.limit("10/minute")
|
||||
async def updatePosition(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
data: TrusteePosition = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteePosition:
|
||||
"""Update a position."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getPosition(positionId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
|
|
@ -727,15 +888,18 @@ async def updatePosition(
|
|||
return result
|
||||
|
||||
|
||||
@router.delete("/positions/{positionId}")
|
||||
@router.delete("/{instanceId}/positions/{positionId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePosition(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a position."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getPosition(positionId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||
|
|
@ -748,16 +912,19 @@ async def deletePosition(
|
|||
|
||||
# ===== Position-Document Link Routes =====
|
||||
|
||||
@router.get("/position-documents", response_model=PaginatedResponse[TrusteePositionDocument])
|
||||
@router.get("/{instanceId}/position-documents", response_model=PaginatedResponse[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocuments(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
pagination: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> PaginatedResponse[TrusteePositionDocument]:
|
||||
"""Get all position-document links with optional pagination."""
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
paginationParams = _parsePagination(pagination)
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.getAllPositionDocuments(paginationParams)
|
||||
|
||||
if paginationParams:
|
||||
|
|
@ -775,69 +942,84 @@ async def getPositionDocuments(
|
|||
return PaginatedResponse(items=result.items, pagination=None)
|
||||
|
||||
|
||||
@router.get("/position-documents/{linkId}", response_model=TrusteePositionDocument)
|
||||
@router.get("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument)
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
linkId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteePositionDocument:
|
||||
"""Get a single position-document link by ID."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
link = interface.getPositionDocument(linkId)
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||
return link
|
||||
|
||||
|
||||
@router.get("/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
|
||||
@router.get("/{instanceId}/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getDocumentsForPosition(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
positionId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteePositionDocument]:
|
||||
"""Get all document links for a position."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getDocumentsForPosition(positionId)
|
||||
|
||||
|
||||
@router.get("/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument])
|
||||
@router.get("/{instanceId}/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument])
|
||||
@limiter.limit("30/minute")
|
||||
async def getPositionsForDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
documentId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> List[TrusteePositionDocument]:
|
||||
"""Get all position links for a document."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
return interface.getPositionsForDocument(documentId)
|
||||
|
||||
|
||||
@router.post("/position-documents", response_model=TrusteePositionDocument, status_code=201)
|
||||
@router.post("/{instanceId}/position-documents", response_model=TrusteePositionDocument, status_code=201)
|
||||
@limiter.limit("10/minute")
|
||||
async def createPositionDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
data: TrusteePositionDocument = Body(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> TrusteePositionDocument:
|
||||
"""Create a new position-document link."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
result = interface.createPositionDocument(data.model_dump())
|
||||
if not result:
|
||||
raise HTTPException(status_code=400, detail="Failed to create link")
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/position-documents/{linkId}")
|
||||
@router.delete("/{instanceId}/position-documents/{linkId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def deletePositionDocument(
|
||||
request: Request,
|
||||
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||
linkId: str = Path(...),
|
||||
context: RequestContext = Depends(getRequestContext)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a position-document link."""
|
||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
||||
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
existing = interface.getPositionDocument(linkId)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from fastapi import status
|
|||
import logging
|
||||
|
||||
# Import interfaces and models
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
from modules.auth import limiter, getRequestContext, requireSysAdmin, RequestContext
|
||||
|
||||
# Configure logger
|
||||
|
|
@ -75,7 +75,7 @@ async def sync_all_automation_events(
|
|||
This will register/remove events based on active flags.
|
||||
"""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbChatObjects import getInterface as getChatInterface
|
||||
from modules.interfaces.interfaceDbChatbot import getInterface as getChatInterface
|
||||
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
||||
from modules.features.workflow import syncAutomationEvents
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ async def remove_event(
|
|||
# Update automation's eventId if it exists
|
||||
if eventId.startswith("automation."):
|
||||
automation_id = eventId.replace("automation.", "")
|
||||
chatInterface = interfaceDbChatObjects.getInterface(context.user)
|
||||
chatInterface = interfaceDbChatbot.getInterface(context.user)
|
||||
automation = chatInterface.getAutomationDefinition(automation_id)
|
||||
if automation and getattr(automation, "eventId", None) == eventId:
|
||||
chatInterface.updateAutomationDefinition(automation_id, {"eventId": None})
|
||||
|
|
@ -204,12 +204,13 @@ async def exportUserData(
|
|||
for inv in invitationsUsed
|
||||
]
|
||||
|
||||
# Audit log
|
||||
audit_logger.logSecurityEvent(
|
||||
# Audit log - GDPR Article 15 data export
|
||||
audit_logger.logGdprEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="gdpr_data_export",
|
||||
details="User requested data export (Article 15)"
|
||||
details="User requested data export (GDPR Article 15 - Right of Access)",
|
||||
ipAddress=request.client.host if request.client else None
|
||||
)
|
||||
|
||||
logger.info(f"User {currentUser.id} exported personal data (GDPR Art. 15)")
|
||||
|
|
@ -304,12 +305,13 @@ async def exportPortableData(
|
|||
"about": portableData
|
||||
}
|
||||
|
||||
# Audit log
|
||||
audit_logger.logSecurityEvent(
|
||||
# Audit log - GDPR Article 20 data portability
|
||||
audit_logger.logGdprEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="gdpr_data_portability",
|
||||
details="User requested portable data export (Article 20)"
|
||||
details="User requested portable data export (GDPR Article 20 - Right to Data Portability)",
|
||||
ipAddress=request.client.host if request.client else None
|
||||
)
|
||||
|
||||
logger.info(f"User {currentUser.id} exported portable data (GDPR Art. 20)")
|
||||
|
|
@ -431,12 +433,13 @@ async def deleteAccount(
|
|||
rootInterface.db.recordDelete(User, str(currentUser.id))
|
||||
deletedData.append("User account deleted")
|
||||
|
||||
# Audit log (before user is deleted)
|
||||
audit_logger.logSecurityEvent(
|
||||
# Audit log (before user is deleted) - GDPR Article 17 account deletion
|
||||
audit_logger.logGdprEvent(
|
||||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="gdpr_account_deletion",
|
||||
details=f"User deleted own account (Article 17). Data: {', '.join(deletedData)}"
|
||||
details=f"User deleted own account (GDPR Article 17 - Right to Erasure). Data: {', '.join(deletedData)}",
|
||||
ipAddress=request.client.host if request.client else None
|
||||
)
|
||||
|
||||
logger.info(f"User {currentUser.id} deleted own account (GDPR Art. 17)")
|
||||
|
|
|
|||
|
|
@ -624,7 +624,10 @@ async def logout(
|
|||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="logout",
|
||||
successInfo="google_auth_logout"
|
||||
successInfo="google_auth_logout",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
userAgent=request.headers.get("user-agent"),
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if audit logging fails
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ async def login(
|
|||
userId=str(user.id),
|
||||
mandateId="system",
|
||||
action="login",
|
||||
successInfo="local_auth_success"
|
||||
successInfo="local_auth_success",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
userAgent=request.headers.get("user-agent"),
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if audit logging fails
|
||||
|
|
@ -171,10 +174,13 @@ async def login(
|
|||
try:
|
||||
from modules.shared.auditLogger import audit_logger
|
||||
audit_logger.logUserAccess(
|
||||
userId="unknown",
|
||||
mandateId="unknown",
|
||||
action="login",
|
||||
successInfo=f"failed: {error_msg}"
|
||||
userId=formData.username or "unknown",
|
||||
mandateId="system",
|
||||
action="login_failed",
|
||||
successInfo=f"failed: {error_msg}",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
userAgent=request.headers.get("user-agent"),
|
||||
success=False
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if audit logging fails
|
||||
|
|
@ -438,7 +444,10 @@ async def logout(request: Request, response: Response, currentUser: User = Depen
|
|||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="logout",
|
||||
successInfo=f"revoked_tokens: {revoked}"
|
||||
successInfo=f"revoked_tokens: {revoked}",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
userAgent=request.headers.get("user-agent"),
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if audit logging fails
|
||||
|
|
|
|||
|
|
@ -634,7 +634,10 @@ async def logout(
|
|||
userId=str(currentUser.id),
|
||||
mandateId="system",
|
||||
action="logout",
|
||||
successInfo="microsoft_auth_logout"
|
||||
successInfo="microsoft_auth_logout",
|
||||
ipAddress=request.client.host if request.client else None,
|
||||
userAgent=request.headers.get("user-agent"),
|
||||
success=True
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail if audit logging fails
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Respon
|
|||
from modules.auth import limiter, getCurrentUser
|
||||
|
||||
# Import interfaces
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
from modules.interfaces.interfaceDbChatObjects import getInterface
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
from modules.interfaces.interfaceDbChatbot import getInterface
|
||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||
|
||||
# Import models
|
||||
from modules.datamodels.datamodelChat import (
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
ChatWorkflow,
|
||||
ChatMessage,
|
||||
ChatLog,
|
||||
|
|
@ -45,7 +45,7 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
def getServiceChat(currentUser: User):
|
||||
return interfaceDbChatObjects.getInterface(currentUser)
|
||||
return interfaceDbChatbot.getInterface(currentUser)
|
||||
|
||||
# Consolidated endpoint for getting all workflows
|
||||
@router.get("/", response_model=PaginatedResponse[ChatWorkflow])
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
|
||||
class PublicService:
|
||||
"""Lightweight proxy exposing only public callable attributes of a target.
|
||||
|
|
@ -49,7 +49,7 @@ class Services:
|
|||
|
||||
# Initialize interfaces with explicit mandateId
|
||||
|
||||
from modules.interfaces.interfaceDbChatObjects import getInterface as getChatInterface
|
||||
from modules.interfaces.interfaceDbChatbot import getInterface as getChatInterface
|
||||
self.interfaceDbChat = getChatInterface(user, mandateId=mandateId)
|
||||
|
||||
from modules.interfaces.interfaceDbAppObjects import getInterface as getAppInterface
|
||||
|
|
@ -58,7 +58,7 @@ class Services:
|
|||
from modules.interfaces.interfaceDbComponentObjects import getInterface as getComponentInterface
|
||||
self.interfaceDbComponent = getComponentInterface(user, mandateId=mandateId)
|
||||
|
||||
from modules.interfaces.interfaceDbTrusteeObjects import getInterface as getTrusteeInterface
|
||||
from modules.interfaces.interfaceDbTrustee import getInterface as getTrusteeInterface
|
||||
self.interfaceDbTrustee = getTrusteeInterface(user, mandateId=mandateId)
|
||||
|
||||
# Expose RBAC directly on services for convenience
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import re
|
|||
import time
|
||||
import base64
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import PromptPlaceholder, ChatDocument
|
||||
from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService
|
||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import logging
|
|||
import base64
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import json
|
|||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
from modules.datamodels.datamodelExtraction import DocumentIntent
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.datamodels.datamodelUam import User, UserConnection
|
||||
from modules.datamodels.datamodelChat import ChatDocument, ChatMessage, ChatStat, ChatLog
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument, ChatMessage, ChatStat, ChatLog
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.shared.progressLogger import ProgressLogger
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import json
|
|||
from .subRegistry import ExtractorRegistry, ChunkerRegistry
|
||||
from .subPipeline import runExtraction
|
||||
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart, MergeStrategy, ExtractionOptions, PartResult, DocumentIntent
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
from modules.datamodels.datamodelAi import AiCallResponse, AiCallRequest, AiCallOptions, OperationTypeEnum, AiModelCall
|
||||
from modules.aicore.aicoreModelRegistry import modelRegistry
|
||||
from modules.aicore.aicoreModelSelector import modelSelector
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import base64
|
|||
import traceback
|
||||
from typing import Any, Dict, List, Optional, Callable
|
||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
from modules.services.serviceGeneration.subDocumentUtility import (
|
||||
getFileExtension,
|
||||
getMimeTypeFromExtension,
|
||||
|
|
|
|||
|
|
@ -157,11 +157,11 @@ class UtilsService:
|
|||
|
||||
def storeDebugMessageAndDocuments(self, message, currentUser):
|
||||
"""
|
||||
Wrapper to store debug messages and documents via interfaceDbChatObjects.
|
||||
Mirrors storeDebugMessageAndDocuments() in modules.interfaces.interfaceDbChatObjects.
|
||||
Wrapper to store debug messages and documents via interfaceDbChatbot.
|
||||
Mirrors storeDebugMessageAndDocuments() in modules.interfaces.interfaceDbChatbot.
|
||||
"""
|
||||
try:
|
||||
from modules.interfaces.interfaceDbChatObjects import storeDebugMessageAndDocuments as _storeDebugMessageAndDocuments
|
||||
from modules.interfaces.interfaceDbChatbot import storeDebugMessageAndDocuments as _storeDebugMessageAndDocuments
|
||||
_storeDebugMessageAndDocuments(message, currentUser)
|
||||
except Exception:
|
||||
# Silent fail to never break main flow
|
||||
|
|
|
|||
|
|
@ -4,201 +4,471 @@
|
|||
Audit Logging System for PowerOn Gateway
|
||||
|
||||
This module provides centralized audit logging functionality for security events,
|
||||
user actions, and system access patterns.
|
||||
user actions, and system access patterns. Logs are stored in the database for
|
||||
GDPR compliance and security monitoring.
|
||||
|
||||
GDPR Requirements Addressed:
|
||||
- Article 5(1)(f): Integrity and confidentiality - secure audit trail
|
||||
- Article 17: Right to erasure - audit log retention with automatic cleanup
|
||||
- Article 30: Records of processing activities - comprehensive event logging
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.shared.timeUtils import getUtcTimestamp
|
||||
|
||||
|
||||
class DailyRotatingFileHandler(RotatingFileHandler):
|
||||
"""
|
||||
A rotating file handler that automatically switches to a new file when the date changes.
|
||||
The log file name includes the current date and switches at midnight.
|
||||
"""
|
||||
|
||||
def __init__(self, logDir, filenamePrefix, maxBytes=10485760, backupCount=5, **kwargs):
|
||||
self.logDir = logDir
|
||||
self.filenamePrefix = filenamePrefix
|
||||
self.currentDate = None
|
||||
self.currentFile = None
|
||||
|
||||
# Initialize with today's file
|
||||
self._updateFileIfNeeded()
|
||||
|
||||
# Call parent constructor with current file
|
||||
super().__init__(self.currentFile, maxBytes=maxBytes, backupCount=backupCount, **kwargs)
|
||||
|
||||
def _updateFileIfNeeded(self):
|
||||
"""Update the log file if the date has changed"""
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
if self.currentDate != today:
|
||||
self.currentDate = today
|
||||
newFile = os.path.join(self.logDir, f"{self.filenamePrefix}_{today}.log")
|
||||
|
||||
if self.currentFile != newFile:
|
||||
self.currentFile = newFile
|
||||
return True
|
||||
return False
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a log record, switching files if date has changed"""
|
||||
# Check if we need to switch to a new file
|
||||
if self._updateFileIfNeeded():
|
||||
# Close current file and open new one
|
||||
if self.stream:
|
||||
self.stream.close()
|
||||
self.stream = None
|
||||
|
||||
# Update the baseFilename for the parent class
|
||||
self.baseFilename = self.currentFile
|
||||
# Reopen the stream
|
||||
if not self.delay:
|
||||
self.stream = self._open()
|
||||
|
||||
# Call parent emit method
|
||||
super().emit(record)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditLogger:
|
||||
"""Centralized audit logging system"""
|
||||
"""
|
||||
Centralized audit logging system with database storage.
|
||||
|
||||
Logs security-relevant events to PostgreSQL for:
|
||||
- GDPR compliance
|
||||
- Security monitoring
|
||||
- Access tracking
|
||||
- Incident investigation
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = None
|
||||
self._setupAuditLogger()
|
||||
|
||||
def _setupAuditLogger(self):
|
||||
"""Setup the audit logger with daily file rotation"""
|
||||
self._db = None
|
||||
self._modelClass = None
|
||||
self._initialized = False
|
||||
self._fallbackToStdout = True
|
||||
|
||||
def _ensureInitialized(self) -> bool:
|
||||
"""Lazily initialize database connection to avoid circular imports."""
|
||||
if self._initialized:
|
||||
return self._db is not None
|
||||
|
||||
self._initialized = True
|
||||
|
||||
try:
|
||||
# Get log directory from config
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
# If relative path, make it relative to the gateway directory
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
from modules.datamodels.datamodelAudit import AuditLogEntry
|
||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||
|
||||
# Ensure log directory exists
|
||||
os.makedirs(logDir, exist_ok=True)
|
||||
self._modelClass = AuditLogEntry
|
||||
|
||||
# Create audit logger
|
||||
self.logger = logging.getLogger('audit')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
# Get database configuration
|
||||
dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
|
||||
dbDatabase = "poweron_app" # Store audit logs in the main app database
|
||||
dbUser = APP_CONFIG.get("DB_USER")
|
||||
dbPassword = APP_CONFIG.get("DB_PASSWORD_SECRET")
|
||||
dbPort = int(APP_CONFIG.get("DB_PORT", 5432))
|
||||
|
||||
# Remove any existing handlers to avoid duplicates
|
||||
for handler in self.logger.handlers[:]:
|
||||
self.logger.removeHandler(handler)
|
||||
|
||||
# Create daily rotating file handler for audit log
|
||||
rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB
|
||||
backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5))
|
||||
|
||||
fileHandler = DailyRotatingFileHandler(
|
||||
logDir=logDir,
|
||||
filenamePrefix="log_audit",
|
||||
maxBytes=rotationSize,
|
||||
backupCount=backupCount
|
||||
# Create database connector with system user context
|
||||
self._db = DatabaseConnector(
|
||||
dbHost=dbHost,
|
||||
dbDatabase=dbDatabase,
|
||||
dbUser=dbUser,
|
||||
dbPassword=dbPassword,
|
||||
dbPort=dbPort,
|
||||
userId="system" # Audit logs are created by system
|
||||
)
|
||||
|
||||
# Create formatter for audit log
|
||||
auditFormatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
fileHandler.setFormatter(auditFormatter)
|
||||
# Initialize database and ensure table exists
|
||||
self._db.initDbSystem()
|
||||
self._db._ensureTableExists(AuditLogEntry)
|
||||
|
||||
# Add handler to logger
|
||||
self.logger.addHandler(fileHandler)
|
||||
|
||||
# Prevent propagation to root logger
|
||||
self.logger.propagate = False
|
||||
logger.info("AuditLogger database connection initialized successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to standard logger if audit setup fails
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.error(f"Failed to setup audit logger: {str(e)}")
|
||||
logger.warning(f"AuditLogger database initialization failed, using fallback logging: {e}")
|
||||
self._db = None
|
||||
return False
|
||||
|
||||
def logEvent(self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
category: str,
|
||||
action: str,
|
||||
details: str = "",
|
||||
timestamp: Optional[datetime] = None) -> None:
|
||||
def _logToFallback(self, entry: Dict[str, Any]) -> None:
|
||||
"""Log to standard logger as fallback when database is unavailable."""
|
||||
if self._fallbackToStdout:
|
||||
fallbackMsg = (
|
||||
f"AUDIT | {entry.get('timestamp', '')} | "
|
||||
f"{entry.get('userId', '')} | {entry.get('mandateId', '')} | "
|
||||
f"{entry.get('category', '')} | {entry.get('action', '')} | "
|
||||
f"{entry.get('details', '')}"
|
||||
)
|
||||
logging.getLogger('audit.fallback').info(fallbackMsg)
|
||||
|
||||
def logEvent(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: Optional[str] = None,
|
||||
category: str = "system",
|
||||
action: str = "",
|
||||
details: str = "",
|
||||
featureInstanceId: Optional[str] = None,
|
||||
resourceType: Optional[str] = None,
|
||||
resourceId: Optional[str] = None,
|
||||
ipAddress: Optional[str] = None,
|
||||
userAgent: Optional[str] = None,
|
||||
success: bool = True,
|
||||
errorMessage: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
timestamp: Optional[float] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Log an audit event
|
||||
Log an audit event to the database.
|
||||
|
||||
Args:
|
||||
userId: User identifier
|
||||
mandateId: Mandate identifier (can be empty if not applicable)
|
||||
category: Event category (e.g., 'key', 'access', 'data')
|
||||
action: Specific action (e.g., 'decode', 'login', 'logout')
|
||||
userId: User identifier (or 'system' for system events)
|
||||
mandateId: Mandate context (can be None for system-level events)
|
||||
category: Event category (access, key, data, security, gdpr, permission, system)
|
||||
action: Specific action performed
|
||||
details: Additional details about the event
|
||||
featureInstanceId: Feature instance context (if applicable)
|
||||
resourceType: Type of resource affected
|
||||
resourceId: ID of the affected resource
|
||||
ipAddress: Client IP address
|
||||
userAgent: Client user agent
|
||||
success: Whether the action was successful
|
||||
errorMessage: Error message if action failed
|
||||
username: Username at the time of event (for historical reference)
|
||||
timestamp: Optional custom timestamp (defaults to current time)
|
||||
|
||||
Returns:
|
||||
ID of the created audit log entry, or None if logging failed
|
||||
"""
|
||||
try:
|
||||
if not self.logger:
|
||||
return
|
||||
|
||||
# Use provided timestamp or current time
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now()
|
||||
|
||||
# Format the audit log entry
|
||||
# Format: timestamp | userid | mandateid | category | action | details
|
||||
auditEntry = f"{userId} | {mandateId} | {category} | {action} | {details}"
|
||||
|
||||
# Log the event
|
||||
self.logger.info(auditEntry)
|
||||
# Prepare the entry data
|
||||
entryData = {
|
||||
"timestamp": timestamp if timestamp else getUtcTimestamp(),
|
||||
"userId": userId or "unknown",
|
||||
"username": username,
|
||||
"mandateId": mandateId,
|
||||
"featureInstanceId": featureInstanceId,
|
||||
"category": category,
|
||||
"action": action,
|
||||
"resourceType": resourceType,
|
||||
"resourceId": resourceId,
|
||||
"details": details if details else None,
|
||||
"ipAddress": ipAddress,
|
||||
"userAgent": userAgent,
|
||||
"success": success,
|
||||
"errorMessage": errorMessage
|
||||
}
|
||||
|
||||
# Try to write to database
|
||||
if self._ensureInitialized() and self._db:
|
||||
from modules.datamodels.datamodelAudit import AuditLogEntry
|
||||
|
||||
entry = AuditLogEntry(**entryData)
|
||||
created = self._db.recordCreate(AuditLogEntry, entry.model_dump())
|
||||
|
||||
if created and created.get("id"):
|
||||
return created["id"]
|
||||
else:
|
||||
self._logToFallback(entryData)
|
||||
return None
|
||||
else:
|
||||
# Use fallback logging
|
||||
self._logToFallback(entryData)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
# Use standard logger as fallback
|
||||
logging.getLogger(__name__).error(f"Failed to log audit event: {str(e)}")
|
||||
logger.error(f"Failed to log audit event: {e}")
|
||||
# Try fallback
|
||||
try:
|
||||
self._logToFallback(entryData)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def logKeyAccess(self, userId: str, mandateId: str, keyName: str, action: str) -> None:
|
||||
"""Log key access events (decode/encode)"""
|
||||
self.logEvent(
|
||||
# ===== Convenience Methods for Common Event Types =====
|
||||
|
||||
def logKeyAccess(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
keyName: str,
|
||||
action: str,
|
||||
ipAddress: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Log key access events (encode/decode)."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="key",
|
||||
action=action,
|
||||
details=keyName
|
||||
details=f"Key: {keyName}",
|
||||
resourceType="EncryptionKey",
|
||||
resourceId=keyName,
|
||||
ipAddress=ipAddress
|
||||
)
|
||||
|
||||
def logUserAccess(self, userId: str, mandateId: str, action: str, successInfo: str = "") -> None:
|
||||
"""Log user access events (login/logout)"""
|
||||
self.logEvent(
|
||||
def logUserAccess(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
action: str,
|
||||
successInfo: str = "",
|
||||
ipAddress: Optional[str] = None,
|
||||
userAgent: Optional[str] = None,
|
||||
success: bool = True
|
||||
) -> Optional[str]:
|
||||
"""Log user access events (login/logout)."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="access",
|
||||
action=action,
|
||||
details=successInfo
|
||||
details=successInfo,
|
||||
ipAddress=ipAddress,
|
||||
userAgent=userAgent,
|
||||
success=success
|
||||
)
|
||||
|
||||
def logDataAccess(self, userId: str, mandateId: str, action: str, details: str = "") -> None:
|
||||
"""Log data access events"""
|
||||
self.logEvent(
|
||||
def logDataAccess(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
action: str,
|
||||
details: str = "",
|
||||
resourceType: Optional[str] = None,
|
||||
resourceId: Optional[str] = None,
|
||||
featureInstanceId: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Log data access events (CRUD operations)."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="data",
|
||||
action=action,
|
||||
details=details
|
||||
details=details,
|
||||
resourceType=resourceType,
|
||||
resourceId=resourceId,
|
||||
featureInstanceId=featureInstanceId
|
||||
)
|
||||
|
||||
def logSecurityEvent(self, userId: str, mandateId: str, action: str, details: str = "") -> None:
|
||||
"""Log security-related events"""
|
||||
self.logEvent(
|
||||
def logSecurityEvent(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
action: str,
|
||||
details: str = "",
|
||||
ipAddress: Optional[str] = None,
|
||||
success: bool = True,
|
||||
errorMessage: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Log security-related events."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="security",
|
||||
action=action,
|
||||
details=details
|
||||
details=details,
|
||||
ipAddress=ipAddress,
|
||||
success=success,
|
||||
errorMessage=errorMessage
|
||||
)
|
||||
|
||||
def logGdprEvent(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
action: str,
|
||||
details: str = "",
|
||||
ipAddress: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Log GDPR-specific events (data export, deletion, etc.)."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="gdpr",
|
||||
action=action,
|
||||
details=details,
|
||||
ipAddress=ipAddress
|
||||
)
|
||||
|
||||
def logPermissionChange(
|
||||
self,
|
||||
userId: str,
|
||||
mandateId: str,
|
||||
action: str,
|
||||
targetUserId: str,
|
||||
details: str = "",
|
||||
resourceType: Optional[str] = None,
|
||||
resourceId: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Log permission/role changes."""
|
||||
return self.logEvent(
|
||||
userId=userId,
|
||||
mandateId=mandateId,
|
||||
category="permission",
|
||||
action=action,
|
||||
details=f"Target user: {targetUserId}. {details}",
|
||||
resourceType=resourceType,
|
||||
resourceId=resourceId
|
||||
)
|
||||
|
||||
# ===== Audit Log Query Methods =====
|
||||
|
||||
def getAuditLogs(
|
||||
self,
|
||||
userId: Optional[str] = None,
|
||||
mandateId: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
fromTimestamp: Optional[float] = None,
|
||||
toTimestamp: Optional[float] = None,
|
||||
limit: int = 100
|
||||
) -> list:
|
||||
"""
|
||||
Query audit logs from database.
|
||||
|
||||
Args:
|
||||
userId: Filter by user ID
|
||||
mandateId: Filter by mandate ID
|
||||
category: Filter by category
|
||||
action: Filter by action
|
||||
fromTimestamp: Filter events after this timestamp
|
||||
toTimestamp: Filter events before this timestamp
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List of audit log entries
|
||||
"""
|
||||
if not self._ensureInitialized() or not self._db:
|
||||
return []
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelAudit import AuditLogEntry
|
||||
|
||||
# Build filter
|
||||
recordFilter = {}
|
||||
if userId:
|
||||
recordFilter["userId"] = userId
|
||||
if mandateId:
|
||||
recordFilter["mandateId"] = mandateId
|
||||
if category:
|
||||
recordFilter["category"] = category
|
||||
if action:
|
||||
recordFilter["action"] = action
|
||||
|
||||
# Query database
|
||||
records = self._db.getRecordset(
|
||||
AuditLogEntry,
|
||||
recordFilter=recordFilter if recordFilter else None,
|
||||
orderBy="timestamp DESC"
|
||||
)
|
||||
|
||||
# Apply timestamp filtering in Python (PostgreSQL connector may not support $gt/$lt)
|
||||
if fromTimestamp or toTimestamp:
|
||||
filteredRecords = []
|
||||
for record in records:
|
||||
ts = record.get("timestamp", 0)
|
||||
if fromTimestamp and ts < fromTimestamp:
|
||||
continue
|
||||
if toTimestamp and ts > toTimestamp:
|
||||
continue
|
||||
filteredRecords.append(record)
|
||||
records = filteredRecords
|
||||
|
||||
# Apply limit
|
||||
return records[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query audit logs: {e}")
|
||||
return []
|
||||
|
||||
# ===== Cleanup Methods =====
|
||||
|
||||
def cleanupOldEntries(self, retentionDays: int = 365) -> int:
|
||||
"""
|
||||
Remove audit log entries older than the retention period.
|
||||
|
||||
GDPR Note: Audit logs should be retained for a reasonable period
|
||||
for security and compliance purposes, but not indefinitely.
|
||||
Default retention is 1 year (365 days).
|
||||
|
||||
Args:
|
||||
retentionDays: Number of days to retain audit logs
|
||||
|
||||
Returns:
|
||||
Number of entries deleted
|
||||
"""
|
||||
if not self._ensureInitialized() or not self._db:
|
||||
logger.warning("Cannot cleanup audit logs: database not initialized")
|
||||
return 0
|
||||
|
||||
try:
|
||||
from modules.datamodels.datamodelAudit import AuditLogEntry
|
||||
import time
|
||||
|
||||
# Calculate cutoff timestamp
|
||||
cutoffTimestamp = time.time() - (retentionDays * 24 * 60 * 60)
|
||||
|
||||
# Query old entries
|
||||
allRecords = self._db.getRecordset(AuditLogEntry)
|
||||
oldRecords = [r for r in allRecords if r.get("timestamp", 0) < cutoffTimestamp]
|
||||
|
||||
# Delete old entries
|
||||
deletedCount = 0
|
||||
for record in oldRecords:
|
||||
recordId = record.get("id")
|
||||
if recordId:
|
||||
if self._db.recordDelete(AuditLogEntry, recordId):
|
||||
deletedCount += 1
|
||||
|
||||
logger.info(f"Audit log cleanup: removed {deletedCount} entries older than {retentionDays} days")
|
||||
|
||||
# Log the cleanup action itself
|
||||
self.logEvent(
|
||||
userId="system",
|
||||
mandateId="system",
|
||||
category="system",
|
||||
action="audit_cleanup",
|
||||
details=f"Removed {deletedCount} entries older than {retentionDays} days"
|
||||
)
|
||||
|
||||
return deletedCount
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup audit logs: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
# Global audit logger instance
|
||||
audit_logger = AuditLogger()
|
||||
|
||||
|
||||
# ===== Scheduler Integration =====
|
||||
|
||||
async def runAuditLogCleanup() -> None:
|
||||
"""
|
||||
Scheduled task to cleanup old audit log entries.
|
||||
Called by the event scheduler.
|
||||
"""
|
||||
try:
|
||||
retentionDays = int(APP_CONFIG.get("AUDIT_LOG_RETENTION_DAYS", 365))
|
||||
deletedCount = audit_logger.cleanupOldEntries(retentionDays=retentionDays)
|
||||
logger.info(f"Scheduled audit log cleanup completed: {deletedCount} entries removed")
|
||||
except Exception as e:
|
||||
logger.error(f"Scheduled audit log cleanup failed: {e}")
|
||||
|
||||
|
||||
def registerAuditLogCleanupScheduler() -> None:
|
||||
"""
|
||||
Register the audit log cleanup job with the event scheduler.
|
||||
Should be called during application startup.
|
||||
"""
|
||||
try:
|
||||
from modules.shared.eventManagement import eventManager
|
||||
|
||||
# Run cleanup daily at 3 AM
|
||||
eventManager.registerCron(
|
||||
jobId="audit_log_cleanup",
|
||||
func=runAuditLogCleanup,
|
||||
cronKwargs={
|
||||
"hour": "3",
|
||||
"minute": "0"
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("Audit log cleanup scheduler registered (daily at 03:00)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register audit log cleanup scheduler: {e}")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ActionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import time
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelAi import AiCallOptions
|
||||
from modules.datamodels.datamodelExtraction import ContentPart
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ActionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ActionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import time
|
||||
import re
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import json
|
|||
import time
|
||||
from typing import Dict, Any
|
||||
from modules.workflows.methods.methodBase import action
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.connectors.connectorPreprocessor import PreprocessorConnector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList
|
||||
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy, ContentExtracted, ContentPart
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList
|
||||
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import json
|
||||
import aiohttp
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import json
|
||||
import uuid
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import csv as csv_module
|
|||
from io import StringIO
|
||||
from datetime import datetime, UTC
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import csv as csv_module
|
|||
from io import BytesIO
|
||||
from datetime import datetime, UTC
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, List
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import json
|
|||
import io
|
||||
import pandas as pd
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import json
|
|||
import pandas as pd
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import json
|
|||
import base64
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import json
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import json
|
|||
import base64
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import urllib.parse
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import urllib.parse
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import base64
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
import json
|
||||
import urllib.parse
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import json
|
||||
import base64
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from modules.datamodels.datamodelChat import ActionResult, ActionItem, TaskStep
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import ActionResult, ActionItem, TaskStep
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.workflows.processing.shared.methodDiscovery import methods
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from modules.datamodels.datamodelChat import TaskPlan, TaskStep, ActionResult, ReviewResult
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import TaskPlan, TaskStep, ActionResult, ReviewResult
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan
|
||||
from modules.datamodels.datamodelChatbot import TaskStep, TaskContext, TaskPlan
|
||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
|
||||
from modules.workflows.processing.shared.promptGenerationTaskplan import (
|
||||
generateTaskPlanningPrompt
|
||||
|
|
@ -51,7 +51,7 @@ class TaskPlanner:
|
|||
|
||||
# Analyze user intent to obtain cleaned user objective for planning
|
||||
# SKIP intent analysis for AUTOMATION mode - it uses predefined JSON plans
|
||||
from modules.datamodels.datamodelChat import WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import WorkflowModeEnum
|
||||
workflowMode = getattr(workflow, 'workflowMode', None)
|
||||
skipIntentionAnalysis = (workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import json
|
|||
import logging
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
from modules.datamodels.datamodelChat import (
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
||||
TaskPlan, ActionResult
|
||||
)
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.workflows.processing.modes.modeBase import BaseMode
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
from modules.shared.timeUtils import parseTimestamp
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskResult, ActionItem
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import TaskStep, TaskContext, TaskResult, ActionItem
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.workflows.processing.core.taskPlanner import TaskPlanner
|
||||
from modules.workflows.processing.core.actionExecutor import ActionExecutor
|
||||
from modules.workflows.processing.core.messageCreator import MessageCreator
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import re
|
|||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Any
|
||||
from modules.datamodels.datamodelChat import (
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
||||
ActionResult, Observation, ObservationPreview, ReviewResult
|
||||
)
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.workflows.processing.modes.modeBase import BaseMode
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
from modules.shared.timeUtils import parseTimestamp
|
||||
|
|
@ -893,7 +893,7 @@ class DynamicMode(BaseMode):
|
|||
async def _refineDecide(self, context: TaskContext, observation: Observation) -> ReviewResult:
|
||||
"""Refine: decide continue or stop, with reason"""
|
||||
# Create proper ReviewContext for extractReviewContent
|
||||
from modules.datamodels.datamodelChat import ReviewContext
|
||||
from modules.datamodels.datamodelChatbot import ReviewContext
|
||||
# Convert observation to dict for extractReviewContent (temporary compatibility)
|
||||
observationDict = {
|
||||
'success': observation.success,
|
||||
|
|
@ -1042,7 +1042,7 @@ class DynamicMode(BaseMode):
|
|||
|
||||
# Parse response using structured parsing with ReviewResult model
|
||||
from modules.shared.jsonUtils import parseJsonWithModel
|
||||
from modules.datamodels.datamodelChat import ReviewResult
|
||||
from modules.datamodels.datamodelChatbot import ReviewResult
|
||||
|
||||
if not resp:
|
||||
return ReviewResult(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from modules.datamodels.datamodelChat import TaskStep, ActionResult, Observation
|
||||
from modules.datamodels.datamodelChatbot import TaskStep, ActionResult, Observation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ def extractReviewContent(context: Any) -> str:
|
|||
elif hasattr(context, 'observation') and context.observation:
|
||||
# For observation data, show full content but handle documents specially
|
||||
# Handle both Pydantic Observation model and dict format
|
||||
from modules.datamodels.datamodelChat import Observation
|
||||
from modules.datamodels.datamodelChatbot import Observation
|
||||
|
||||
if isinstance(context.observation, Observation):
|
||||
# Convert Pydantic model to dict
|
||||
|
|
@ -371,7 +371,7 @@ def extractReviewContent(context: Any) -> str:
|
|||
# For observation data in stepResult, show full content but handle documents specially
|
||||
observation = context.stepResult['observation']
|
||||
# Handle both Pydantic Observation model and dict format
|
||||
from modules.datamodels.datamodelChat import Observation
|
||||
from modules.datamodels.datamodelChatbot import Observation
|
||||
|
||||
if isinstance(observation, Observation):
|
||||
# Convert Pydantic model to dict
|
||||
|
|
@ -452,10 +452,10 @@ def extractLatestRefinementFeedback(context: Any) -> str:
|
|||
# First check for ERROR level logs in workflow
|
||||
if hasattr(context, 'workflow') and context.workflow:
|
||||
try:
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
||||
rootInterface = getRootInterface()
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(rootInterface.currentUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(rootInterface.currentUser)
|
||||
|
||||
# Get workflow logs
|
||||
chatData = interfaceDbChat.getUnifiedChatData(context.workflow.id, None)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Handles prompt templates for dynamic mode action handling.
|
|||
|
||||
import json
|
||||
from typing import Any, List
|
||||
from modules.datamodels.datamodelChat import PromptBundle, PromptPlaceholder
|
||||
from modules.datamodels.datamodelChatbot import PromptBundle, PromptPlaceholder
|
||||
from modules.workflows.processing.shared.placeholderFactory import (
|
||||
extractUserPrompt,
|
||||
extractUserLanguage,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Handles prompt templates and extraction functions for task planning phase.
|
|||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from modules.datamodels.datamodelChat import PromptBundle, PromptPlaceholder
|
||||
from modules.datamodels.datamodelChatbot import PromptBundle, PromptPlaceholder
|
||||
from modules.workflows.processing.shared.placeholderFactory import (
|
||||
extractUserPrompt,
|
||||
extractAvailableDocumentsSummary,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
||||
from modules.datamodels import datamodelChat
|
||||
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan, ActionResult, ActionDocument, ChatDocument, ChatMessage
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, WorkflowModeEnum
|
||||
from modules.datamodels import datamodelChatbot
|
||||
from modules.datamodels.datamodelChatbot import TaskStep, TaskContext, TaskPlan, ActionResult, ActionDocument, ChatDocument, ChatMessage
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, WorkflowModeEnum
|
||||
from modules.workflows.processing.modes.modeBase import BaseMode
|
||||
from modules.workflows.processing.modes.modeDynamic import DynamicMode
|
||||
from modules.workflows.processing.modes.modeAutomation import AutomationMode
|
||||
|
|
@ -102,7 +102,7 @@ class WorkflowProcessor:
|
|||
self.services.chat.progressLogFinish(operationId, False)
|
||||
raise
|
||||
|
||||
async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> datamodelChat.TaskResult:
|
||||
async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> datamodelChatbot.TaskResult:
|
||||
"""Execute a task step using the appropriate mode"""
|
||||
import time
|
||||
|
||||
|
|
@ -494,7 +494,7 @@ class WorkflowProcessor:
|
|||
|
||||
# Create ActionResult with response
|
||||
# For fast path, we create a simple text document with the response
|
||||
from modules.datamodels.datamodelChat import ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ActionDocument
|
||||
|
||||
responseDoc = ActionDocument(
|
||||
documentName="fast_path_response.txt",
|
||||
|
|
@ -626,7 +626,7 @@ class WorkflowProcessor:
|
|||
ChatMessage with persisted documents
|
||||
"""
|
||||
try:
|
||||
from modules.datamodels.datamodelChat import ChatMessage, ChatDocument, ActionDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatMessage, ChatDocument, ActionDocument
|
||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||
|
||||
# Check workflow status
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import uuid
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from modules.datamodels.datamodelChat import (
|
||||
from modules.datamodels.datamodelChatbot import (
|
||||
UserInputRequest,
|
||||
ChatMessage,
|
||||
ChatWorkflow,
|
||||
ChatDocument,
|
||||
WorkflowModeEnum
|
||||
)
|
||||
from modules.datamodels.datamodelChat import TaskContext
|
||||
from modules.datamodels.datamodelChatbot import TaskContext
|
||||
from modules.workflows.processing.workflowProcessor import WorkflowProcessor
|
||||
from modules.workflows.processing.shared.stateTools import WorkflowStoppedException, checkWorkflowStopped
|
||||
|
||||
|
|
@ -606,7 +606,7 @@ The following is the user's original input message. Analyze intent, normalize th
|
|||
|
||||
# Collect file info
|
||||
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
doc = ChatDocument(
|
||||
fileId=fileItem.id,
|
||||
fileName=fileInfo.get("fileName", fileName) if fileInfo else fileName,
|
||||
|
|
@ -792,7 +792,7 @@ The following is the user's original input message. Analyze intent, normalize th
|
|||
|
||||
# Collect file info
|
||||
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
doc = ChatDocument(
|
||||
fileId=fileItem.id,
|
||||
fileName=fileInfo.get("fileName", fileName) if fileInfo else fileName,
|
||||
|
|
@ -921,7 +921,7 @@ The following is the user's original input message. Analyze intent, normalize th
|
|||
# Persist task result for cross-task/round document references
|
||||
# Convert ChatTaskResult to WorkflowTaskResult for persistence
|
||||
from modules.datamodels.datamodelWorkflow import TaskResult as WorkflowTaskResult
|
||||
from modules.datamodels.datamodelChat import ActionResult
|
||||
from modules.datamodels.datamodelChatbot import ActionResult
|
||||
|
||||
# Get final ActionResult from task execution (last action result)
|
||||
finalActionResult = None
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class AIModelsTester:
|
|||
self.services.extraction = ExtractionService(self.services)
|
||||
|
||||
# Create a minimal workflow context
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, WorkflowModeEnum
|
||||
import uuid
|
||||
|
||||
self.services.currentWorkflow = ChatWorkflow(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ if _gateway_path not in sys.path:
|
|||
sys.path.insert(0, _gateway_path)
|
||||
|
||||
from modules.datamodels.datamodelAi import OperationTypeEnum
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, ChatDocument, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, ChatDocument, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
|
||||
|
||||
|
|
@ -94,8 +94,8 @@ class MethodAiOperationsTester:
|
|||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
# Import and initialize services - use the same approach as routeChatPlayground
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
|
||||
# Import and initialize services
|
||||
from modules.services import getInterface as getServices
|
||||
|
|
@ -174,7 +174,7 @@ class MethodAiOperationsTester:
|
|||
imageData = f.read()
|
||||
|
||||
# Create a ChatDocument
|
||||
from modules.datamodels.datamodelChat import ChatDocument
|
||||
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||
import uuid
|
||||
|
||||
testImageDoc = ChatDocument(
|
||||
|
|
@ -186,7 +186,7 @@ class MethodAiOperationsTester:
|
|||
)
|
||||
|
||||
# Create a message with this document
|
||||
from modules.datamodels.datamodelChat import ChatMessage
|
||||
from modules.datamodels.datamodelChatbot import ChatMessage
|
||||
import time
|
||||
|
||||
testMessage = ChatMessage(
|
||||
|
|
@ -201,8 +201,8 @@ class MethodAiOperationsTester:
|
|||
|
||||
# Save message to database
|
||||
if self.services.workflow:
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
messageDict = testMessage.model_dump()
|
||||
interfaceDbChat.createMessage(messageDict)
|
||||
|
||||
|
|
@ -283,8 +283,8 @@ class MethodAiOperationsTester:
|
|||
maxSteps=5
|
||||
)
|
||||
# Save workflow to database
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflowDict = testWorkflow.model_dump()
|
||||
interfaceDbChat.createWorkflow(workflowDict)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ class AIBehaviorTester:
|
|||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
# Create and save workflow in database using the interface
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, WorkflowModeEnum
|
||||
import uuid
|
||||
import time
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
currentTimestamp = time.time()
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ class AIBehaviorTester:
|
|||
)
|
||||
|
||||
# SAVE workflow to database so it exists for access control
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflowDict = testWorkflow.model_dump()
|
||||
interfaceDbChat.createWorkflow(workflowDict)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ if _gateway_path not in sys.path:
|
|||
|
||||
# Import the service initialization
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.datamodels.datamodelChat import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.features.workflow import chatStart
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
|
||||
class WorkflowWithDocumentsTester:
|
||||
|
|
@ -192,7 +192,7 @@ class WorkflowWithDocumentsTester:
|
|||
return False
|
||||
|
||||
# Get current workflow status
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
currentWorkflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||
|
||||
if not currentWorkflow:
|
||||
|
|
@ -225,7 +225,7 @@ class WorkflowWithDocumentsTester:
|
|||
if not self.workflow:
|
||||
return {"error": "No workflow to analyze"}
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||
|
||||
if not workflow:
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ if _gateway_path not in sys.path:
|
|||
|
||||
# Import the service initialization
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.datamodels.datamodelChat import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.features.workflow import chatStart
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
|
||||
class WorkflowPromptVariationsTester:
|
||||
|
|
@ -115,7 +115,7 @@ class WorkflowPromptVariationsTester:
|
|||
return False
|
||||
|
||||
# Get current workflow status
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
currentWorkflow = interfaceDbChat.getWorkflow(workflow.id)
|
||||
|
||||
if not currentWorkflow:
|
||||
|
|
@ -140,7 +140,7 @@ class WorkflowPromptVariationsTester:
|
|||
|
||||
def _analyzeWorkflowResults(self, workflow: Any) -> Dict[str, Any]:
|
||||
"""Analyze workflow results and extract information."""
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflow = interfaceDbChat.getWorkflow(workflow.id)
|
||||
|
||||
if not workflow:
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ if _gateway_path not in sys.path:
|
|||
|
||||
# Import the service initialization
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.datamodels.datamodelChat import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.features.workflow import chatStart
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
|
||||
class DocumentGenerationFormatsTester:
|
||||
|
|
@ -251,7 +251,7 @@ class DocumentGenerationFormatsTester:
|
|||
startTime = time.time()
|
||||
lastStatus = None
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
|
||||
if timeout is None:
|
||||
print("Waiting indefinitely (no timeout)")
|
||||
|
|
@ -296,7 +296,7 @@ class DocumentGenerationFormatsTester:
|
|||
if not self.workflow:
|
||||
return {"error": "No workflow to analyze"}
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||
|
||||
if not workflow:
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ if _gateway_path not in sys.path:
|
|||
|
||||
# Import the service initialization
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.datamodels.datamodelChat import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.features.workflow import chatStart
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
|
||||
class DocumentGenerationFormatsTester10:
|
||||
|
|
@ -248,7 +248,7 @@ class DocumentGenerationFormatsTester10:
|
|||
startTime = time.time()
|
||||
lastStatus = None
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
|
||||
if timeout is None:
|
||||
print("Waiting indefinitely (no timeout)")
|
||||
|
|
@ -293,7 +293,7 @@ class DocumentGenerationFormatsTester10:
|
|||
if not self.workflow:
|
||||
return {"error": "No workflow to analyze"}
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||
|
||||
if not workflow:
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ if _gateway_path not in sys.path:
|
|||
|
||||
# Import the service initialization
|
||||
from modules.services import getInterface as getServices
|
||||
from modules.datamodels.datamodelChat import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelChatbot import UserInputRequest, WorkflowModeEnum
|
||||
from modules.datamodels.datamodelUam import User
|
||||
from modules.features.workflow import chatStart
|
||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||
|
||||
|
||||
class CodeGenerationFormatsTester11:
|
||||
|
|
@ -190,7 +190,7 @@ class CodeGenerationFormatsTester11:
|
|||
startTime = time.time()
|
||||
lastStatus = None
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
|
||||
if timeout is None:
|
||||
print("Waiting indefinitely (no timeout)")
|
||||
|
|
@ -235,7 +235,7 @@ class CodeGenerationFormatsTester11:
|
|||
if not self.workflow:
|
||||
return {"error": "No workflow to analyze"}
|
||||
|
||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
||||
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||
|
||||
if not workflow:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import pytest
|
|||
import uuid
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, TaskContext, TaskStep
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, TaskContext, TaskStep
|
||||
from modules.datamodels.datamodelWorkflow import ActionDefinition
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference, DocumentItemReference
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Tests state increment methods, helper methods, and updateFromSelection.
|
|||
import pytest
|
||||
import uuid
|
||||
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow, TaskContext, TaskStep
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow, TaskContext, TaskStep
|
||||
from modules.datamodels.datamodelWorkflow import ActionDefinition
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|||
|
||||
from modules.datamodels.datamodelWorkflow import ActionDefinition, AiResponse
|
||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference
|
||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
||||
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||
from modules.shared.jsonUtils import parseJsonWithModel
|
||||
|
||||
|
||||
|
|
|
|||
508
tool_db_export_migration.py
Normal file
508
tool_db_export_migration.py
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Datenbank Export-Tool für Migration.
|
||||
|
||||
Dieses Script exportiert alle Daten aus ALLEN PowerOn PostgreSQL-Datenbanken
|
||||
in eine JSON-Datei, die als Migrationsdatensatz verwendet werden kann.
|
||||
|
||||
Datenbanken:
|
||||
- poweron_app (User, Mandate, RBAC, Features, etc.)
|
||||
- poweron_chat (Chat-Konversationen und Nachrichten)
|
||||
- poweron_management (Workflows, Prompts, Connections, etc.)
|
||||
- poweron_realestate (Real Estate Daten)
|
||||
- poweron_trustee (Trustee Daten)
|
||||
|
||||
Verwendung:
|
||||
python tool_db_export_migration.py [--output <pfad>] [--pretty]
|
||||
|
||||
Optionen:
|
||||
--output, -o Pfad zur Ausgabedatei (Standard: migration_export_<timestamp>.json)
|
||||
--pretty, -p JSON formatiert ausgeben (für bessere Lesbarkeit)
|
||||
--exclude Komma-getrennte Liste von Tabellen, die ausgeschlossen werden sollen
|
||||
--include-meta System-Metadaten (_createdAt, _modifiedAt, etc.) beibehalten
|
||||
--db Nur bestimmte Datenbank(en) exportieren (komma-getrennt)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Alle PowerOn Datenbanken
|
||||
ALL_DATABASES = [
|
||||
"poweron_app", # Haupt-App: User, Mandate, RBAC, Features
|
||||
"poweron_chat", # Chat-Konversationen
|
||||
"poweron_management", # Workflows, Prompts, Connections
|
||||
"poweron_realestate", # Real Estate
|
||||
"poweron_trustee", # Trustee
|
||||
]
|
||||
|
||||
|
||||
def _loadEnvConfig() -> Dict[str, str]:
|
||||
"""Lädt die Konfiguration direkt aus der .env Datei."""
|
||||
config = {}
|
||||
envPath = Path(__file__).parent / '.env'
|
||||
|
||||
if not envPath.exists():
|
||||
logger.warning(f"Environment file not found at {envPath}")
|
||||
return config
|
||||
|
||||
# Versuche verschiedene Encodings
|
||||
encodings = ['utf-8', 'utf-8-sig', 'latin-1', 'cp1252']
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
with open(envPath, 'r', encoding=encoding) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
# Erfolgreich geladen
|
||||
return config
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading .env file with {encoding}: {e}")
|
||||
continue
|
||||
|
||||
logger.error(f"Could not load .env file with any encoding")
|
||||
return config
|
||||
|
||||
|
||||
# Globale Konfiguration laden
|
||||
_ENV_CONFIG = _loadEnvConfig()
|
||||
|
||||
|
||||
def _getConfigValue(key: str, default: str = None) -> str:
|
||||
"""Holt einen Konfigurationswert."""
|
||||
return _ENV_CONFIG.get(key, os.environ.get(key, default))
|
||||
|
||||
|
||||
def _databaseExists(dbDatabase: str) -> bool:
|
||||
"""Prüft ob eine Datenbank existiert."""
|
||||
dbHost = _getConfigValue("DB_HOST", "localhost")
|
||||
dbUser = _getConfigValue("DB_USER")
|
||||
dbPassword = _getConfigValue("DB_PASSWORD_SECRET")
|
||||
dbPort = int(_getConfigValue("DB_PORT", "5432"))
|
||||
|
||||
try:
|
||||
# Verbinde zur postgres Datenbank um zu prüfen
|
||||
conn = psycopg2.connect(
|
||||
host=dbHost,
|
||||
port=dbPort,
|
||||
database="postgres",
|
||||
user=dbUser,
|
||||
password=dbPassword
|
||||
)
|
||||
conn.autocommit = True
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM pg_database WHERE datname = %s",
|
||||
(dbDatabase,)
|
||||
)
|
||||
exists = cursor.fetchone() is not None
|
||||
|
||||
conn.close()
|
||||
return exists
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen der Datenbank {dbDatabase}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _getDbConnection(dbDatabase: str):
|
||||
"""Erstellt eine Verbindung zu einer spezifischen PostgreSQL-Datenbank."""
|
||||
# Erst prüfen ob Datenbank existiert
|
||||
if not _databaseExists(dbDatabase):
|
||||
logger.warning(f"Datenbank '{dbDatabase}' existiert nicht - übersprungen")
|
||||
return None
|
||||
|
||||
dbHost = _getConfigValue("DB_HOST", "localhost")
|
||||
dbUser = _getConfigValue("DB_USER")
|
||||
dbPassword = _getConfigValue("DB_PASSWORD_SECRET")
|
||||
dbPort = int(_getConfigValue("DB_PORT", "5432"))
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=dbHost,
|
||||
port=dbPort,
|
||||
database=dbDatabase,
|
||||
user=dbUser,
|
||||
password=dbPassword,
|
||||
cursor_factory=psycopg2.extras.RealDictCursor
|
||||
)
|
||||
conn.set_client_encoding('UTF8')
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error(f"Datenbankverbindung zu {dbDatabase} fehlgeschlagen: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def _getTables(conn) -> List[str]:
|
||||
"""Gibt alle Tabellennamen in der Datenbank zurück."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
tables = [row["table_name"] for row in cursor.fetchall()]
|
||||
return tables
|
||||
|
||||
|
||||
def _getTableData(conn, tableName: str, includeMeta: bool = False) -> List[Dict[str, Any]]:
|
||||
"""Liest alle Daten aus einer Tabelle."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(f'SELECT * FROM "{tableName}"')
|
||||
rows = cursor.fetchall()
|
||||
|
||||
records = []
|
||||
for row in rows:
|
||||
record = dict(row)
|
||||
|
||||
# Optional: System-Metadaten entfernen
|
||||
if not includeMeta:
|
||||
metaFields = ["_createdAt", "_modifiedAt", "_createdBy", "_modifiedBy"]
|
||||
for field in metaFields:
|
||||
record.pop(field, None)
|
||||
|
||||
# Konvertiere JSONB-Felder (sind bereits als Dict/List von psycopg2)
|
||||
for key, value in record.items():
|
||||
if isinstance(value, (int, float)):
|
||||
record[key] = float(value) if isinstance(value, float) else int(value)
|
||||
|
||||
records.append(record)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def _getTableRowCount(conn, tableName: str) -> int:
|
||||
"""Zählt die Anzahl der Zeilen in einer Tabelle."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(f'SELECT COUNT(*) as count FROM "{tableName}"')
|
||||
result = cursor.fetchone()
|
||||
return result["count"] if result else 0
|
||||
|
||||
|
||||
def _exportSingleDatabase(
|
||||
dbDatabase: str,
|
||||
excludeTables: List[str],
|
||||
includeMeta: bool
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Exportiert eine einzelne Datenbank."""
|
||||
conn = _getDbConnection(dbDatabase)
|
||||
|
||||
if conn is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
allTables = _getTables(conn)
|
||||
|
||||
# System-Tabellen ausschliessen
|
||||
systemTables = ["_system"]
|
||||
tablesToExport = [
|
||||
t for t in allTables
|
||||
if t not in systemTables and t not in excludeTables
|
||||
]
|
||||
|
||||
dbExport = {
|
||||
"tables": {},
|
||||
"summary": {},
|
||||
"tableCount": len(tablesToExport),
|
||||
"totalRecords": 0
|
||||
}
|
||||
|
||||
for tableName in tablesToExport:
|
||||
try:
|
||||
records = _getTableData(conn, tableName, includeMeta)
|
||||
rowCount = len(records)
|
||||
dbExport["totalRecords"] += rowCount
|
||||
|
||||
dbExport["tables"][tableName] = records
|
||||
dbExport["summary"][tableName] = {"recordCount": rowCount}
|
||||
|
||||
if rowCount > 0:
|
||||
logger.info(f" {tableName}: {rowCount} Datensätze")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" Fehler bei Tabelle {tableName}: {e}")
|
||||
dbExport["tables"][tableName] = []
|
||||
dbExport["summary"][tableName] = {"recordCount": 0, "error": str(e)}
|
||||
|
||||
return dbExport
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def exportDatabase(
|
||||
outputPath: Optional[str] = None,
|
||||
prettyPrint: bool = False,
|
||||
excludeTables: Optional[List[str]] = None,
|
||||
includeMeta: bool = False,
|
||||
onlyDatabases: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Exportiert alle Datenbanken in eine JSON-Datei.
|
||||
|
||||
Args:
|
||||
outputPath: Pfad zur Ausgabedatei (optional)
|
||||
prettyPrint: JSON formatiert ausgeben
|
||||
excludeTables: Liste von Tabellen, die ausgeschlossen werden sollen
|
||||
includeMeta: System-Metadaten beibehalten
|
||||
onlyDatabases: Nur diese Datenbanken exportieren
|
||||
|
||||
Returns:
|
||||
Pfad zur erstellten Exportdatei
|
||||
"""
|
||||
excludeTables = excludeTables or []
|
||||
|
||||
# Welche Datenbanken exportieren?
|
||||
databasesToExport = onlyDatabases if onlyDatabases else ALL_DATABASES
|
||||
|
||||
# Standard-Ausgabepfad generieren (im Log-Ordner)
|
||||
if not outputPath:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
logDir = _getConfigValue("APP_LOGGING_LOG_DIR")
|
||||
if logDir and os.path.isabs(logDir):
|
||||
outputDir = logDir
|
||||
else:
|
||||
outputDir = os.path.join(os.path.dirname(__file__), "local", "logs")
|
||||
os.makedirs(outputDir, exist_ok=True)
|
||||
outputPath = os.path.join(outputDir, f"migration_export_{timestamp}.json")
|
||||
|
||||
logger.info(f"Starte Export von {len(databasesToExport)} Datenbank(en)...")
|
||||
logger.info(f"Datenbanken: {', '.join(databasesToExport)}")
|
||||
|
||||
# Export-Struktur erstellen
|
||||
exportData = {
|
||||
"meta": {
|
||||
"exportedAt": datetime.utcnow().isoformat() + "Z",
|
||||
"exportedFrom": _getConfigValue("APP_ENV_LABEL", "unknown"),
|
||||
"version": "1.0",
|
||||
"databaseCount": 0,
|
||||
"totalTables": 0,
|
||||
"totalRecords": 0,
|
||||
"excludedTables": excludeTables,
|
||||
"includesMeta": includeMeta
|
||||
},
|
||||
"databases": {}
|
||||
}
|
||||
|
||||
# Jede Datenbank exportieren
|
||||
for dbName in databasesToExport:
|
||||
logger.info(f"Exportiere Datenbank: {dbName}")
|
||||
|
||||
dbExport = _exportSingleDatabase(dbName, excludeTables, includeMeta)
|
||||
|
||||
if dbExport is not None:
|
||||
exportData["databases"][dbName] = dbExport
|
||||
exportData["meta"]["databaseCount"] += 1
|
||||
exportData["meta"]["totalTables"] += dbExport["tableCount"]
|
||||
exportData["meta"]["totalRecords"] += dbExport["totalRecords"]
|
||||
logger.info(f" -> {dbExport['tableCount']} Tabellen, {dbExport['totalRecords']} Datensätze")
|
||||
else:
|
||||
logger.info(f" -> Übersprungen (existiert nicht)")
|
||||
|
||||
# JSON-Datei schreiben
|
||||
logger.info(f"Schreibe Exportdatei: {outputPath}")
|
||||
|
||||
with open(outputPath, "w", encoding="utf-8") as f:
|
||||
if prettyPrint:
|
||||
json.dump(exportData, f, indent=2, ensure_ascii=False, default=str)
|
||||
else:
|
||||
json.dump(exportData, f, ensure_ascii=False, default=str)
|
||||
|
||||
# Dateigrösse berechnen
|
||||
fileSize = os.path.getsize(outputPath)
|
||||
fileSizeStr = _formatFileSize(fileSize)
|
||||
|
||||
logger.info(f"Export abgeschlossen!")
|
||||
logger.info(f" Datenbanken: {exportData['meta']['databaseCount']}")
|
||||
logger.info(f" Tabellen: {exportData['meta']['totalTables']}")
|
||||
logger.info(f" Datensätze: {exportData['meta']['totalRecords']}")
|
||||
logger.info(f" Dateigrösse: {fileSizeStr}")
|
||||
logger.info(f" Ausgabedatei: {outputPath}")
|
||||
|
||||
return outputPath
|
||||
|
||||
|
||||
def _formatFileSize(sizeBytes: int) -> str:
|
||||
"""Formatiert Dateigrösse in lesbares Format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if sizeBytes < 1024:
|
||||
return f"{sizeBytes:.2f} {unit}"
|
||||
sizeBytes /= 1024
|
||||
return f"{sizeBytes:.2f} TB"
|
||||
|
||||
|
||||
def printDatabaseSummary():
|
||||
"""Zeigt eine Zusammenfassung aller Datenbanken an."""
|
||||
print("\n" + "=" * 70)
|
||||
print("DATENBANK ZUSAMMENFASSUNG - ALLE POWEREON DATENBANKEN")
|
||||
print("=" * 70)
|
||||
print(f"Umgebung: {_getConfigValue('APP_ENV_LABEL', 'unknown')}")
|
||||
print(f"Host: {_getConfigValue('DB_HOST', 'localhost')}")
|
||||
print("=" * 70)
|
||||
|
||||
grandTotalRecords = 0
|
||||
grandTotalTables = 0
|
||||
|
||||
for dbName in ALL_DATABASES:
|
||||
print(f"\n{dbName}")
|
||||
print("-" * 70)
|
||||
|
||||
conn = _getDbConnection(dbName)
|
||||
if conn is None:
|
||||
print(" (Datenbank existiert nicht)")
|
||||
continue
|
||||
|
||||
try:
|
||||
tables = _getTables(conn)
|
||||
dbTotalRecords = 0
|
||||
|
||||
print(f" {'Tabelle':<45} {'Datensätze':>15}")
|
||||
print(f" {'-' * 45} {'-' * 15}")
|
||||
|
||||
for tableName in tables:
|
||||
if tableName.startswith("_"):
|
||||
continue # System-Tabellen überspringen
|
||||
count = _getTableRowCount(conn, tableName)
|
||||
dbTotalRecords += count
|
||||
if count > 0: # Nur nicht-leere Tabellen anzeigen
|
||||
print(f" {tableName:<45} {count:>15}")
|
||||
|
||||
print(f" {'-' * 45} {'-' * 15}")
|
||||
print(f" {'Gesamt':<45} {dbTotalRecords:>15}")
|
||||
|
||||
grandTotalRecords += dbTotalRecords
|
||||
grandTotalTables += len([t for t in tables if not t.startswith("_")])
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"GESAMTÜBERSICHT")
|
||||
print(f" Datenbanken: {len(ALL_DATABASES)}")
|
||||
print(f" Tabellen: {grandTotalTables}")
|
||||
print(f" Datensätze: {grandTotalRecords}")
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Exportiert alle PowerOn Datenbank-Daten für Migration",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Datenbanken:
|
||||
poweron_app - User, Mandate, RBAC, Features
|
||||
poweron_chat - Chat-Konversationen
|
||||
poweron_management - Workflows, Prompts, Connections
|
||||
poweron_realestate - Real Estate Daten
|
||||
poweron_trustee - Trustee Daten
|
||||
|
||||
Beispiele:
|
||||
python tool_db_export_migration.py
|
||||
python tool_db_export_migration.py --pretty
|
||||
python tool_db_export_migration.py -o backup.json --pretty
|
||||
python tool_db_export_migration.py --db poweron_app,poweron_chat
|
||||
python tool_db_export_migration.py --exclude Token,AuthEvent --include-meta
|
||||
python tool_db_export_migration.py --summary
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
help="Pfad zur Ausgabedatei",
|
||||
type=str,
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-p", "--pretty",
|
||||
help="JSON formatiert ausgeben",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--exclude",
|
||||
help="Komma-getrennte Liste von Tabellen zum Ausschliessen",
|
||||
type=str,
|
||||
default=""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--include-meta",
|
||||
help="System-Metadaten (_createdAt, etc.) beibehalten",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
help="Nur bestimmte Datenbank(en) exportieren (komma-getrennt)",
|
||||
type=str,
|
||||
default=""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--summary",
|
||||
help="Nur Zusammenfassung anzeigen (kein Export)",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Nur Zusammenfassung anzeigen
|
||||
if args.summary:
|
||||
printDatabaseSummary()
|
||||
return
|
||||
|
||||
# Exclude-Liste parsen
|
||||
excludeTables = []
|
||||
if args.exclude:
|
||||
excludeTables = [t.strip() for t in args.exclude.split(",") if t.strip()]
|
||||
|
||||
# Datenbank-Liste parsen
|
||||
onlyDatabases = None
|
||||
if args.db:
|
||||
onlyDatabases = [db.strip() for db in args.db.split(",") if db.strip()]
|
||||
|
||||
# Export durchführen
|
||||
try:
|
||||
outputPath = exportDatabase(
|
||||
outputPath=args.output,
|
||||
prettyPrint=args.pretty,
|
||||
excludeTables=excludeTables,
|
||||
includeMeta=args.include_meta,
|
||||
onlyDatabases=onlyDatabases
|
||||
)
|
||||
print(f"\n Export erfolgreich: {outputPath}\n")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Export fehlgeschlagen: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
612
tool_db_import_migration.py
Normal file
612
tool_db_import_migration.py
Normal file
|
|
@ -0,0 +1,612 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Datenbank Import-Tool für Migration.
|
||||
|
||||
Dieses Script importiert Daten aus einer JSON-Migrationsdatei
|
||||
in ALLE PowerOn PostgreSQL-Datenbanken.
|
||||
|
||||
ACHTUNG: Dieses Script kann bestehende Daten überschreiben!
|
||||
Bitte vor dem Import ein Backup erstellen.
|
||||
|
||||
Datenbanken:
|
||||
- poweron_app (User, Mandate, RBAC, Features, etc.)
|
||||
- poweron_chat (Chat-Konversationen und Nachrichten)
|
||||
- poweron_management (Workflows, Prompts, Connections, etc.)
|
||||
- poweron_realestate (Real Estate Daten)
|
||||
- poweron_trustee (Trustee Daten)
|
||||
|
||||
Verwendung:
|
||||
python tool_db_import_migration.py <import_file.json> [--dry-run] [--force]
|
||||
|
||||
Optionen:
|
||||
--dry-run Simuliert den Import ohne Änderungen
|
||||
--force Bestätigung überspringen
|
||||
--clear-first Tabellen vor dem Import leeren
|
||||
--only-tables Komma-getrennte Liste von Tabellen (nur diese importieren)
|
||||
--only-db Komma-getrennte Liste von Datenbanken (nur diese importieren)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
# Logging konfigurieren
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Alle PowerOn Datenbanken
|
||||
ALL_DATABASES = [
|
||||
"poweron_app",
|
||||
"poweron_chat",
|
||||
"poweron_management",
|
||||
"poweron_realestate",
|
||||
"poweron_trustee",
|
||||
]
|
||||
|
||||
|
||||
def _loadEnvConfig() -> Dict[str, str]:
|
||||
"""Lädt die Konfiguration direkt aus der .env Datei."""
|
||||
config = {}
|
||||
envPath = Path(__file__).parent / '.env'
|
||||
|
||||
if not envPath.exists():
|
||||
logger.warning(f"Environment file not found at {envPath}")
|
||||
return config
|
||||
|
||||
# Versuche verschiedene Encodings
|
||||
encodings = ['utf-8', 'utf-8-sig', 'latin-1', 'cp1252']
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
with open(envPath, 'r', encoding=encoding) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
# Erfolgreich geladen
|
||||
return config
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading .env file with {encoding}: {e}")
|
||||
continue
|
||||
|
||||
logger.error(f"Could not load .env file with any encoding")
|
||||
return config
|
||||
|
||||
|
||||
# Globale Konfiguration laden
|
||||
_ENV_CONFIG = _loadEnvConfig()
|
||||
|
||||
|
||||
def _getConfigValue(key: str, default: str = None) -> str:
|
||||
"""Holt einen Konfigurationswert."""
|
||||
return _ENV_CONFIG.get(key, os.environ.get(key, default))
|
||||
|
||||
|
||||
def _getUtcTimestamp() -> float:
|
||||
"""Gibt den aktuellen UTC-Timestamp zurück."""
|
||||
return time.time()
|
||||
|
||||
|
||||
def _databaseExists(dbDatabase: str) -> bool:
|
||||
"""Prüft ob eine Datenbank existiert."""
|
||||
dbHost = _getConfigValue("DB_HOST", "localhost")
|
||||
dbUser = _getConfigValue("DB_USER")
|
||||
dbPassword = _getConfigValue("DB_PASSWORD_SECRET")
|
||||
dbPort = int(_getConfigValue("DB_PORT", "5432"))
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=dbHost,
|
||||
port=dbPort,
|
||||
database="postgres",
|
||||
user=dbUser,
|
||||
password=dbPassword
|
||||
)
|
||||
conn.autocommit = True
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM pg_database WHERE datname = %s",
|
||||
(dbDatabase,)
|
||||
)
|
||||
exists = cursor.fetchone() is not None
|
||||
|
||||
conn.close()
|
||||
return exists
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Prüfen der Datenbank {dbDatabase}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _getDbConnection(dbDatabase: str, autocommit: bool = False):
|
||||
"""Erstellt eine Verbindung zu einer spezifischen PostgreSQL-Datenbank."""
|
||||
# Erst prüfen ob Datenbank existiert
|
||||
if not _databaseExists(dbDatabase):
|
||||
logger.warning(f"Datenbank '{dbDatabase}' existiert nicht")
|
||||
return None
|
||||
|
||||
dbHost = _getConfigValue("DB_HOST", "localhost")
|
||||
dbUser = _getConfigValue("DB_USER")
|
||||
dbPassword = _getConfigValue("DB_PASSWORD_SECRET")
|
||||
dbPort = int(_getConfigValue("DB_PORT", "5432"))
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=dbHost,
|
||||
port=dbPort,
|
||||
database=dbDatabase,
|
||||
user=dbUser,
|
||||
password=dbPassword,
|
||||
cursor_factory=psycopg2.extras.RealDictCursor
|
||||
)
|
||||
conn.set_client_encoding('UTF8')
|
||||
conn.autocommit = autocommit
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error(f"Datenbankverbindung zu {dbDatabase} fehlgeschlagen: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def _getExistingTables(conn) -> List[str]:
|
||||
"""Gibt alle Tabellennamen in der Datenbank zurück."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
tables = [row["table_name"] for row in cursor.fetchall()]
|
||||
return tables
|
||||
|
||||
|
||||
def _getTableColumns(conn, tableName: str) -> List[str]:
|
||||
"""Gibt alle Spalten einer Tabelle zurück."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = %s AND table_schema = 'public'
|
||||
""", (tableName,))
|
||||
columns = [row["column_name"] for row in cursor.fetchall()]
|
||||
return columns
|
||||
|
||||
|
||||
def _clearTable(conn, tableName: str):
|
||||
"""Löscht alle Daten aus einer Tabelle."""
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(f'DELETE FROM "{tableName}"')
|
||||
|
||||
|
||||
def _insertRecord(conn, tableName: str, record: Dict[str, Any], existingColumns: List[str]) -> bool:
|
||||
"""Fügt einen Datensatz in eine Tabelle ein (UPSERT)."""
|
||||
filteredRecord = {k: v for k, v in record.items() if k in existingColumns}
|
||||
|
||||
if not filteredRecord:
|
||||
return False
|
||||
|
||||
# Metadaten hinzufügen falls nicht vorhanden
|
||||
currentTime = _getUtcTimestamp()
|
||||
if "_createdAt" not in filteredRecord and "_createdAt" in existingColumns:
|
||||
filteredRecord["_createdAt"] = currentTime
|
||||
if "_modifiedAt" in existingColumns:
|
||||
filteredRecord["_modifiedAt"] = currentTime
|
||||
|
||||
columns = list(filteredRecord.keys())
|
||||
values = []
|
||||
|
||||
for col in columns:
|
||||
value = filteredRecord[col]
|
||||
if isinstance(value, (dict, list)):
|
||||
values.append(json.dumps(value))
|
||||
else:
|
||||
values.append(value)
|
||||
|
||||
colNames = ", ".join([f'"{col}"' for col in columns])
|
||||
placeholders = ", ".join(["%s"] * len(columns))
|
||||
|
||||
updateCols = [col for col in columns if col not in ["id", "_createdAt", "_createdBy"]]
|
||||
updateClause = ", ".join([f'"{col}" = EXCLUDED."{col}"' for col in updateCols])
|
||||
|
||||
if updateClause:
|
||||
sql = f'''
|
||||
INSERT INTO "{tableName}" ({colNames})
|
||||
VALUES ({placeholders})
|
||||
ON CONFLICT ("id") DO UPDATE SET {updateClause}
|
||||
'''
|
||||
else:
|
||||
sql = f'''
|
||||
INSERT INTO "{tableName}" ({colNames})
|
||||
VALUES ({placeholders})
|
||||
ON CONFLICT ("id") DO NOTHING
|
||||
'''
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(sql, values)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Einfügen in {tableName}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def loadMigrationFile(filePath: str) -> Dict[str, Any]:
|
||||
"""Lädt die Migrationsdatei."""
|
||||
logger.info(f"Lade Migrationsdatei: {filePath}")
|
||||
|
||||
if not os.path.exists(filePath):
|
||||
raise FileNotFoundError(f"Datei nicht gefunden: {filePath}")
|
||||
|
||||
with open(filePath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Validierung - unterstütze beide Formate (alt: tables, neu: databases)
|
||||
if "databases" not in data and "tables" not in data:
|
||||
raise ValueError("Ungültiges Migrationsformat: 'databases' oder 'tables' erforderlich")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _importSingleDatabase(
|
||||
dbName: str,
|
||||
dbData: Dict[str, Any],
|
||||
dryRun: bool,
|
||||
clearFirst: bool,
|
||||
onlyTables: Optional[List[str]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Importiert Daten in eine einzelne Datenbank."""
|
||||
stats = {
|
||||
"imported": {},
|
||||
"skipped": {},
|
||||
"errors": {},
|
||||
"totalImported": 0,
|
||||
"totalSkipped": 0,
|
||||
"totalErrors": 0
|
||||
}
|
||||
|
||||
conn = _getDbConnection(dbName)
|
||||
if conn is None:
|
||||
logger.warning(f" Datenbank '{dbName}' existiert nicht - übersprungen")
|
||||
return stats
|
||||
|
||||
try:
|
||||
existingTables = _getExistingTables(conn)
|
||||
tables = dbData.get("tables", {})
|
||||
|
||||
tablesToImport = list(tables.keys())
|
||||
if onlyTables:
|
||||
tablesToImport = [t for t in tablesToImport if t in onlyTables]
|
||||
|
||||
for tableName in tablesToImport:
|
||||
records = tables[tableName]
|
||||
|
||||
if tableName not in existingTables:
|
||||
logger.warning(f" Tabelle '{tableName}' existiert nicht - übersprungen")
|
||||
stats["skipped"][tableName] = len(records)
|
||||
stats["totalSkipped"] += len(records)
|
||||
continue
|
||||
|
||||
if dryRun:
|
||||
stats["imported"][tableName] = len(records)
|
||||
stats["totalImported"] += len(records)
|
||||
continue
|
||||
|
||||
if clearFirst:
|
||||
_clearTable(conn, tableName)
|
||||
|
||||
existingColumns = _getTableColumns(conn, tableName)
|
||||
|
||||
imported = 0
|
||||
errors = 0
|
||||
|
||||
for record in records:
|
||||
if _insertRecord(conn, tableName, record, existingColumns):
|
||||
imported += 1
|
||||
else:
|
||||
errors += 1
|
||||
|
||||
stats["imported"][tableName] = imported
|
||||
stats["totalImported"] += imported
|
||||
|
||||
if errors > 0:
|
||||
stats["errors"][tableName] = errors
|
||||
stats["totalErrors"] += errors
|
||||
|
||||
if imported > 0:
|
||||
logger.info(f" {tableName}: {imported} importiert, {errors} Fehler")
|
||||
|
||||
if not dryRun:
|
||||
conn.commit()
|
||||
else:
|
||||
conn.rollback()
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error(f" Import fehlgeschlagen: {e}")
|
||||
raise
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def importDatabase(
|
||||
filePath: str,
|
||||
dryRun: bool = False,
|
||||
clearFirst: bool = False,
|
||||
onlyTables: Optional[List[str]] = None,
|
||||
onlyDatabases: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Importiert Daten aus einer Migrationsdatei.
|
||||
|
||||
Args:
|
||||
filePath: Pfad zur Migrationsdatei
|
||||
dryRun: Nur simulieren
|
||||
clearFirst: Tabellen vor Import leeren
|
||||
onlyTables: Nur diese Tabellen importieren
|
||||
onlyDatabases: Nur diese Datenbanken importieren
|
||||
|
||||
Returns:
|
||||
Import-Statistiken
|
||||
"""
|
||||
migrationData = loadMigrationFile(filePath)
|
||||
meta = migrationData.get("meta", {})
|
||||
|
||||
logger.info(f"Migrationsdatei geladen:")
|
||||
logger.info(f" Exportiert am: {meta.get('exportedAt', 'unbekannt')}")
|
||||
logger.info(f" Quelle: {meta.get('exportedFrom', 'unbekannt')}")
|
||||
|
||||
stats = {
|
||||
"databases": {},
|
||||
"totalImported": 0,
|
||||
"totalSkipped": 0,
|
||||
"totalErrors": 0
|
||||
}
|
||||
|
||||
# Neues Format (mehrere Datenbanken)
|
||||
if "databases" in migrationData:
|
||||
databases = migrationData["databases"]
|
||||
logger.info(f" Datenbanken: {len(databases)}")
|
||||
logger.info(f" Tabellen: {meta.get('totalTables', 'unbekannt')}")
|
||||
logger.info(f" Datensätze: {meta.get('totalRecords', 'unbekannt')}")
|
||||
|
||||
for dbName, dbData in databases.items():
|
||||
if onlyDatabases and dbName not in onlyDatabases:
|
||||
continue
|
||||
|
||||
logger.info(f"Importiere Datenbank: {dbName}")
|
||||
dbStats = _importSingleDatabase(dbName, dbData, dryRun, clearFirst, onlyTables)
|
||||
|
||||
stats["databases"][dbName] = dbStats
|
||||
stats["totalImported"] += dbStats["totalImported"]
|
||||
stats["totalSkipped"] += dbStats["totalSkipped"]
|
||||
stats["totalErrors"] += dbStats["totalErrors"]
|
||||
|
||||
# Altes Format (einzelne Datenbank - poweron_app)
|
||||
elif "tables" in migrationData:
|
||||
logger.info(" Format: Legacy (einzelne Datenbank)")
|
||||
dbName = "poweron_app"
|
||||
dbData = {"tables": migrationData["tables"]}
|
||||
|
||||
if not onlyDatabases or dbName in onlyDatabases:
|
||||
logger.info(f"Importiere Datenbank: {dbName}")
|
||||
dbStats = _importSingleDatabase(dbName, dbData, dryRun, clearFirst, onlyTables)
|
||||
|
||||
stats["databases"][dbName] = dbStats
|
||||
stats["totalImported"] = dbStats["totalImported"]
|
||||
stats["totalSkipped"] = dbStats["totalSkipped"]
|
||||
stats["totalErrors"] = dbStats["totalErrors"]
|
||||
|
||||
if dryRun:
|
||||
logger.info("Dry-Run: Keine Änderungen vorgenommen")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def printImportPreview(filePath: str):
|
||||
"""Zeigt eine Vorschau der zu importierenden Daten."""
|
||||
migrationData = loadMigrationFile(filePath)
|
||||
meta = migrationData.get("meta", {})
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("IMPORT VORSCHAU")
|
||||
print("=" * 70)
|
||||
print(f"Datei: {filePath}")
|
||||
print(f"Exportiert am: {meta.get('exportedAt', 'unbekannt')}")
|
||||
print(f"Quelle: {meta.get('exportedFrom', 'unbekannt')}")
|
||||
|
||||
# Neues Format
|
||||
if "databases" in migrationData:
|
||||
databases = migrationData["databases"]
|
||||
print(f"Datenbanken: {len(databases)}")
|
||||
print("=" * 70)
|
||||
|
||||
grandTotal = 0
|
||||
for dbName, dbData in databases.items():
|
||||
tables = dbData.get("tables", {})
|
||||
dbTotal = sum(len(records) for records in tables.values())
|
||||
grandTotal += dbTotal
|
||||
|
||||
print(f"\n{dbName} ({dbTotal} Datensätze)")
|
||||
print("-" * 70)
|
||||
print(f" {'Tabelle':<45} {'Datensätze':>15}")
|
||||
print(f" {'-' * 45} {'-' * 15}")
|
||||
|
||||
for tableName, records in sorted(tables.items()):
|
||||
if len(records) > 0:
|
||||
print(f" {tableName:<45} {len(records):>15}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"GESAMT: {grandTotal} Datensätze")
|
||||
|
||||
# Altes Format
|
||||
elif "tables" in migrationData:
|
||||
tables = migrationData["tables"]
|
||||
print(f"Format: Legacy (poweron_app)")
|
||||
print("-" * 70)
|
||||
print(f"{'Tabelle':<45} {'Datensätze':>15}")
|
||||
print("-" * 70)
|
||||
|
||||
totalRecords = 0
|
||||
for tableName, records in sorted(tables.items()):
|
||||
count = len(records)
|
||||
totalRecords += count
|
||||
if count > 0:
|
||||
print(f"{tableName:<45} {count:>15}")
|
||||
|
||||
print("-" * 70)
|
||||
print(f"{'GESAMT':<45} {totalRecords:>15}")
|
||||
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Importiert Datenbank-Daten aus einer Migrationsdatei",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Datenbanken:
|
||||
poweron_app - User, Mandate, RBAC, Features
|
||||
poweron_chat - Chat-Konversationen
|
||||
poweron_management - Workflows, Prompts, Connections
|
||||
poweron_realestate - Real Estate Daten
|
||||
poweron_trustee - Trustee Daten
|
||||
|
||||
Beispiele:
|
||||
python tool_db_import_migration.py migration_export.json --dry-run
|
||||
python tool_db_import_migration.py migration_export.json --preview
|
||||
python tool_db_import_migration.py migration_export.json --force
|
||||
python tool_db_import_migration.py migration_export.json --clear-first --force
|
||||
python tool_db_import_migration.py migration_export.json --only-db poweron_app
|
||||
python tool_db_import_migration.py migration_export.json --only-tables UserInDB,Mandate
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"import_file",
|
||||
help="Pfad zur Migrationsdatei (JSON)",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
help="Simuliert den Import ohne Änderungen",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
help="Bestätigung überspringen",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--clear-first",
|
||||
help="Tabellen vor dem Import leeren",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--only-tables",
|
||||
help="Nur diese Tabellen importieren (komma-getrennt)",
|
||||
type=str,
|
||||
default=""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--only-db",
|
||||
help="Nur diese Datenbank(en) importieren (komma-getrennt)",
|
||||
type=str,
|
||||
default=""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--preview",
|
||||
help="Nur Vorschau anzeigen (kein Import)",
|
||||
action="store_true"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Nur Vorschau anzeigen
|
||||
if args.preview:
|
||||
printImportPreview(args.import_file)
|
||||
return
|
||||
|
||||
# Listen parsen
|
||||
onlyTables = None
|
||||
if args.only_tables:
|
||||
onlyTables = [t.strip() for t in args.only_tables.split(",") if t.strip()]
|
||||
|
||||
onlyDatabases = None
|
||||
if args.only_db:
|
||||
onlyDatabases = [db.strip() for db in args.only_db.split(",") if db.strip()]
|
||||
|
||||
# Bestätigung einholen
|
||||
if not args.dry_run and not args.force:
|
||||
printImportPreview(args.import_file)
|
||||
|
||||
if args.clear_first:
|
||||
print("WARNUNG: --clear-first wird ALLE bestehenden Daten in den Zieltabellen löschen!")
|
||||
|
||||
response = input("\nMöchten Sie den Import starten? [y/N]: ")
|
||||
if response.lower() not in ["y", "yes", "j", "ja"]:
|
||||
print("Import abgebrochen.")
|
||||
return
|
||||
|
||||
# Import durchführen
|
||||
try:
|
||||
if args.dry_run:
|
||||
logger.info("=== DRY-RUN MODUS ===")
|
||||
|
||||
stats = importDatabase(
|
||||
filePath=args.import_file,
|
||||
dryRun=args.dry_run,
|
||||
clearFirst=args.clear_first,
|
||||
onlyTables=onlyTables,
|
||||
onlyDatabases=onlyDatabases
|
||||
)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("IMPORT ERGEBNIS")
|
||||
print("=" * 70)
|
||||
print(f"Importiert: {stats['totalImported']} Datensätze")
|
||||
print(f"Übersprungen: {stats['totalSkipped']} Datensätze")
|
||||
print(f"Fehler: {stats['totalErrors']} Datensätze")
|
||||
|
||||
if args.dry_run:
|
||||
print("\n(Dry-Run: Keine tatsächlichen Änderungen vorgenommen)")
|
||||
else:
|
||||
print("\n Import erfolgreich abgeschlossen!")
|
||||
|
||||
print("=" * 70 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Import fehlgeschlagen: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue