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 ---
|
# --- Init Managers ---
|
||||||
await featuresLifecycle.start(eventUser)
|
await featuresLifecycle.start(eventUser)
|
||||||
eventManager.start()
|
eventManager.start()
|
||||||
|
|
||||||
|
# Register audit log cleanup scheduler
|
||||||
|
from modules.shared.auditLogger import registerAuditLogCleanupScheduler
|
||||||
|
registerAuditLogCleanupScheduler()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
@ -444,7 +448,7 @@ app.include_router(sharepointRouter)
|
||||||
from modules.routes.routeDataAutomation import router as automationRouter
|
from modules.routes.routeDataAutomation import router as automationRouter
|
||||||
app.include_router(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)
|
app.include_router(adminAutomationEventsRouter)
|
||||||
|
|
||||||
from modules.routes.routeRbac import router as rbacRouter
|
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(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
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(
|
workflowId: Optional[str] = Field(
|
||||||
None, description="Foreign key to workflow (for workflow stats)"
|
None, description="Foreign key to workflow (for workflow stats)"
|
||||||
)
|
)
|
||||||
|
|
@ -33,6 +39,8 @@ registerModelLabels(
|
||||||
{"en": "Chat Statistics", "fr": "Statistiques de chat"},
|
{"en": "Chat Statistics", "fr": "Statistiques de chat"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
|
||||||
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
|
"processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
|
||||||
"bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
|
"bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
|
||||||
|
|
@ -49,6 +57,12 @@ class ChatLog(BaseModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
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")
|
workflowId: str = Field(description="Foreign key to workflow")
|
||||||
message: str = Field(description="Log message")
|
message: str = Field(description="Log message")
|
||||||
type: str = Field(description="Log type (info, warning, error, etc.)")
|
type: str = Field(description="Log type (info, warning, error, etc.)")
|
||||||
|
|
@ -79,6 +93,8 @@ registerModelLabels(
|
||||||
{"en": "Chat Log", "fr": "Journal de chat"},
|
{"en": "Chat Log", "fr": "Journal de chat"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||||
"message": {"en": "Message", "fr": "Message"},
|
"message": {"en": "Message", "fr": "Message"},
|
||||||
"type": {"en": "Type", "fr": "Type"},
|
"type": {"en": "Type", "fr": "Type"},
|
||||||
|
|
@ -94,6 +110,12 @@ class ChatDocument(BaseModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
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")
|
messageId: str = Field(description="Foreign key to message")
|
||||||
fileId: str = Field(description="Foreign key to file")
|
fileId: str = Field(description="Foreign key to file")
|
||||||
fileName: str = Field(description="Name of the file")
|
fileName: str = Field(description="Name of the file")
|
||||||
|
|
@ -112,6 +134,8 @@ registerModelLabels(
|
||||||
{"en": "Chat Document", "fr": "Document de chat"},
|
{"en": "Chat Document", "fr": "Document de chat"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"messageId": {"en": "Message ID", "fr": "ID du message"},
|
||||||
"fileId": {"en": "File ID", "fr": "ID du fichier"},
|
"fileId": {"en": "File ID", "fr": "ID du fichier"},
|
||||||
"fileName": {"en": "File Name", "fr": "Nom du fichier"},
|
"fileName": {"en": "File Name", "fr": "Nom du fichier"},
|
||||||
|
|
@ -200,6 +224,12 @@ class ChatMessage(BaseModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
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")
|
workflowId: str = Field(description="Foreign key to workflow")
|
||||||
parentMessageId: Optional[str] = Field(
|
parentMessageId: Optional[str] = Field(
|
||||||
None, description="Parent message ID for threading"
|
None, description="Parent message ID for threading"
|
||||||
|
|
@ -251,6 +281,8 @@ registerModelLabels(
|
||||||
{"en": "Chat Message", "fr": "Message de chat"},
|
{"en": "Chat Message", "fr": "Message de chat"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
|
||||||
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
|
"parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
|
||||||
"documents": {"en": "Documents", "fr": "Documents"},
|
"documents": {"en": "Documents", "fr": "Documents"},
|
||||||
|
|
@ -296,6 +328,7 @@ registerModelLabels(
|
||||||
class ChatWorkflow(BaseModel):
|
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})
|
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})
|
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": [
|
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": "running", "label": {"en": "Running", "fr": "En cours"}},
|
||||||
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
|
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
|
||||||
|
|
@ -370,6 +403,7 @@ registerModelLabels(
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
"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"},
|
"status": {"en": "Status", "fr": "Statut"},
|
||||||
"name": {"en": "Name", "fr": "Nom"},
|
"name": {"en": "Name", "fr": "Nom"},
|
||||||
"currentRound": {"en": "Current Round", "fr": "Tour actuel"},
|
"currentRound": {"en": "Current Round", "fr": "Tour actuel"},
|
||||||
|
|
@ -993,6 +1027,7 @@ registerModelLabels(
|
||||||
class AutomationDefinition(BaseModel):
|
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})
|
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})
|
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})
|
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": [
|
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"}},
|
{"value": "0 */4 * * *", "label": {"en": "Every 4 hours", "fr": "Toutes les 4 heures"}},
|
||||||
|
|
@ -1013,6 +1048,7 @@ registerModelLabels(
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
"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é"},
|
"label": {"en": "Label", "fr": "Libellé"},
|
||||||
"schedule": {"en": "Schedule", "fr": "Planification"},
|
"schedule": {"en": "Schedule", "fr": "Planification"},
|
||||||
"template": {"en": "Template", "fr": "Modèle"},
|
"template": {"en": "Template", "fr": "Modèle"},
|
||||||
|
|
@ -13,6 +13,7 @@ import base64
|
||||||
class FileItem(BaseModel):
|
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})
|
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})
|
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})
|
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})
|
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})
|
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"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
"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"},
|
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
||||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
||||||
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ class MessagingSubscription(BaseModel):
|
||||||
description="ID of the mandate this subscription belongs to",
|
description="ID of the mandate this subscription belongs to",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
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(
|
description: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Description of the subscription",
|
description="Description of the subscription",
|
||||||
|
|
@ -92,6 +96,7 @@ registerModelLabels(
|
||||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||||
"subscriptionLabel": {"en": "Subscription Label", "fr": "Label d'abonnement"},
|
"subscriptionLabel": {"en": "Subscription Label", "fr": "Label d'abonnement"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
"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"},
|
"description": {"en": "Description", "fr": "Description"},
|
||||||
"isSystemSubscription": {"en": "System Subscription", "fr": "Abonnement système"},
|
"isSystemSubscription": {"en": "System Subscription", "fr": "Abonnement système"},
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||||
|
|
@ -110,6 +115,14 @@ class MessagingSubscriptionRegistration(BaseModel):
|
||||||
description="Unique ID of the registration",
|
description="Unique ID of the registration",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
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(
|
subscriptionId: str = Field(
|
||||||
description="ID of the subscription this registration belongs to",
|
description="ID of the subscription this registration belongs to",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||||
|
|
@ -161,6 +174,8 @@ registerModelLabels(
|
||||||
{"en": "Messaging Registration", "fr": "Inscription à la messagerie"},
|
{"en": "Messaging Registration", "fr": "Inscription à la messagerie"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||||
"channel": {"en": "Channel", "fr": "Canal"},
|
"channel": {"en": "Channel", "fr": "Canal"},
|
||||||
|
|
@ -179,6 +194,14 @@ class MessagingDelivery(BaseModel):
|
||||||
description="Unique ID of the delivery",
|
description="Unique ID of the delivery",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
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(
|
subscriptionId: str = Field(
|
||||||
description="ID of the subscription this delivery belongs to",
|
description="ID of the subscription this delivery belongs to",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
|
|
@ -239,6 +262,8 @@ registerModelLabels(
|
||||||
{"en": "Messaging Delivery", "fr": "Livraison de messagerie"},
|
{"en": "Messaging Delivery", "fr": "Livraison de messagerie"},
|
||||||
{
|
{
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
"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"},
|
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||||
"channel": {"en": "Channel", "fr": "Canal"},
|
"channel": {"en": "Channel", "fr": "Canal"},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from modules.shared.attributeUtils import registerModelLabels
|
||||||
class DataNeutraliserConfig(BaseModel):
|
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})
|
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})
|
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})
|
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})
|
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})
|
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"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
"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"},
|
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||||
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
|
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
|
||||||
|
|
@ -33,6 +35,7 @@ registerModelLabels(
|
||||||
class DataNeutralizerAttributes(BaseModel):
|
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})
|
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})
|
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})
|
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})
|
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})
|
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"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
"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"},
|
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||||
"originalText": {"en": "Original Text", "fr": "Texte original"},
|
"originalText": {"en": "Original Text", "fr": "Texte original"},
|
||||||
"fileId": {"en": "File ID", "fr": "ID de fichier"},
|
"fileId": {"en": "File ID", "fr": "ID de fichier"},
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,12 @@ class Dokument(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
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(
|
label: str = Field(
|
||||||
description="Document label",
|
description="Document label",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
|
|
@ -207,6 +213,12 @@ class Land(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
)
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
description="ID of the feature instance",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Country name (e.g. 'Schweiz')",
|
description="Country name (e.g. 'Schweiz')",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
|
|
@ -251,6 +263,12 @@ class Kanton(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
)
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
description="ID of the feature instance",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Canton name (e.g. 'Zürich')",
|
description="Canton name (e.g. 'Zürich')",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
|
|
@ -302,6 +320,12 @@ class Gemeinde(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
)
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
description="ID of the feature instance",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Municipality name (e.g. 'Zürich')",
|
description="Municipality name (e.g. 'Zürich')",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
|
|
@ -359,6 +383,12 @@ class Parzelle(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
)
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
description="ID of the feature instance",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
|
|
||||||
# Grunddaten
|
# Grunddaten
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
|
|
@ -579,6 +609,12 @@ class Projekt(BaseModel):
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
)
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
description="ID of the feature instance",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Project designation",
|
description="Project designation",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
|
|
@ -643,6 +679,7 @@ registerModelLabels(
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||||
"statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
|
"statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
"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"},
|
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
"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"},
|
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
"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
|
"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:
|
# System attributes are automatically set by DatabaseConnector:
|
||||||
# _createdAt, _modifiedAt, _createdBy, _modifiedBy
|
# _createdAt, _modifiedAt, _createdBy, _modifiedBy
|
||||||
|
|
||||||
|
|
@ -56,6 +65,7 @@ registerModelLabels(
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,6 +116,7 @@ registerModelLabels(
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
||||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -172,6 +201,7 @@ registerModelLabels(
|
||||||
"userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
|
"userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
|
||||||
"contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
|
"contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -234,6 +273,7 @@ registerModelLabels(
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -323,6 +372,7 @@ registerModelLabels(
|
||||||
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
||||||
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -499,6 +558,7 @@ registerModelLabels(
|
||||||
"vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
|
"vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
|
||||||
"vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
|
"vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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
|
"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
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -575,5 +644,6 @@ registerModelLabels(
|
||||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
||||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
"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})
|
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})
|
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})
|
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})
|
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})
|
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})
|
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"},
|
"id": {"en": "ID", "fr": "ID"},
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
"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"},
|
"sttLanguage": {"en": "STT Language", "fr": "Langue STT"},
|
||||||
"ttsLanguage": {"en": "TTS Language", "fr": "Langue TTS"},
|
"ttsLanguage": {"en": "TTS Language", "fr": "Langue TTS"},
|
||||||
"ttsVoice": {"en": "TTS Voice", "fr": "Voix 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)
|
# Forward references for circular imports (use string annotations)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from modules.datamodels.datamodelChat import ChatDocument, ActionResult
|
from modules.datamodels.datamodelChatbot import ChatDocument, ActionResult
|
||||||
from modules.datamodels.datamodelExtraction import ExtractionOptions
|
from modules.datamodels.datamodelExtraction import ExtractionOptions
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
|
from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
|
||||||
from pydantic import BaseModel, Field
|
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.frontendTypes import FrontendType
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.attributeUtils import registerModelLabels
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import asyncio
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Dict, Any, List
|
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.datamodelUam import User
|
||||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
|
||||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
|
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)
|
# Emit event directly for streaming (using correct signature)
|
||||||
if created_log and event_manager:
|
if created_log and event_manager:
|
||||||
try:
|
try:
|
||||||
from modules.datamodels.datamodelChat import ChatLog
|
from modules.datamodels.datamodelChatbot import ChatLog
|
||||||
# Convert to dict if it's a Pydantic model
|
# Convert to dict if it's a Pydantic model
|
||||||
if hasattr(created_log, "model_dump"):
|
if hasattr(created_log, "model_dump"):
|
||||||
log_dict = created_log.model_dump()
|
log_dict = created_log.model_dump()
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from modules.datamodels.datamodelRealEstate import (
|
||||||
Land,
|
Land,
|
||||||
)
|
)
|
||||||
from modules.services import getInterface as getServices
|
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
|
from modules.connectors.connectorSwissTopoMapServer import SwissTopoMapServerConnector
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, Optional
|
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.datamodels.datamodelUam import User
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
from modules.shared.eventManagement import eventManager
|
from modules.shared.eventManagement import eventManager
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"""
|
"""
|
||||||
Utility functions for automation feature.
|
Utility functions for automation feature.
|
||||||
|
|
||||||
Moved from interfaces/interfaceDbChatObjects.py.
|
Moved from interfaces/interfaceDbChatbot.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from modules.security.rbac import RbacClass
|
||||||
from modules.datamodels.datamodelRbac import AccessRuleContext
|
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||||
from modules.datamodels.datamodelUam import AccessLevel
|
from modules.datamodels.datamodelUam import AccessLevel
|
||||||
|
|
||||||
from modules.datamodels.datamodelChat import (
|
from modules.datamodels.datamodelChatbot import (
|
||||||
ChatDocument,
|
ChatDocument,
|
||||||
ChatStat,
|
ChatStat,
|
||||||
ChatLog,
|
ChatLog,
|
||||||
|
|
@ -178,18 +178,20 @@ class ChatObjects:
|
||||||
Uses the JSON connector for data access with added language support.
|
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.
|
"""Initializes the Chat Interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
||||||
|
featureInstanceId: The feature instance ID from RequestContext (X-Feature-Instance-Id header)
|
||||||
"""
|
"""
|
||||||
# Initialize variables
|
# Initialize variables
|
||||||
self.currentUser = currentUser # Store User object directly
|
self.currentUser = currentUser # Store User object directly
|
||||||
self.userId = currentUser.id if currentUser else None
|
self.userId = currentUser.id if currentUser else None
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
self.rbac = None # RBAC interface
|
self.rbac = None # RBAC interface
|
||||||
|
|
||||||
# Initialize services
|
# Initialize services
|
||||||
|
|
@ -200,7 +202,7 @@ class ChatObjects:
|
||||||
|
|
||||||
# Set user context if provided
|
# Set user context if provided
|
||||||
if currentUser:
|
if currentUser:
|
||||||
self.setUserContext(currentUser, mandateId=mandateId)
|
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
|
||||||
|
|
||||||
# ===== Generic Utility Methods =====
|
# ===== Generic Utility Methods =====
|
||||||
|
|
||||||
|
|
@ -263,17 +265,19 @@ class ChatObjects:
|
||||||
def _initializeServices(self):
|
def _initializeServices(self):
|
||||||
pass
|
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.
|
"""Sets the user context for the interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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.currentUser = currentUser # Store User object directly
|
||||||
self.userId = currentUser.id
|
self.userId = currentUser.id
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
|
|
||||||
if not self.userId:
|
if not self.userId:
|
||||||
raise ValueError("Invalid user context: id is required")
|
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 None: List[Dict[str, Any]]
|
||||||
If pagination is provided: PaginatedResult with items and metadata
|
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,
|
filteredWorkflows = getRecordsetWithRBAC(self.db,
|
||||||
ChatWorkflow,
|
ChatWorkflow,
|
||||||
self.currentUser
|
self.currentUser,
|
||||||
|
mandateId=self.mandateId,
|
||||||
|
featureInstanceId=self.featureInstanceId
|
||||||
)
|
)
|
||||||
|
|
||||||
# If no pagination requested, return all items (no sorting - frontend handles it)
|
# 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]:
|
def getWorkflow(self, workflowId: str) -> Optional[ChatWorkflow]:
|
||||||
"""Returns a workflow by ID if user has access."""
|
"""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,
|
workflows = getRecordsetWithRBAC(self.db,
|
||||||
ChatWorkflow,
|
ChatWorkflow,
|
||||||
self.currentUser,
|
self.currentUser,
|
||||||
recordFilter={"id": workflowId}
|
recordFilter={"id": workflowId},
|
||||||
|
mandateId=self.mandateId,
|
||||||
|
featureInstanceId=self.featureInstanceId
|
||||||
)
|
)
|
||||||
|
|
||||||
if not workflows:
|
if not workflows:
|
||||||
|
|
@ -689,6 +697,12 @@ class ChatObjects:
|
||||||
if "lastActivity" not in workflowData:
|
if "lastActivity" not in workflowData:
|
||||||
workflowData["lastActivity"] = currentTime
|
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
|
# Use generic field separation based on ChatWorkflow model
|
||||||
simpleFields, objectFields = self._separateObjectFields(ChatWorkflow, workflowData)
|
simpleFields, objectFields = self._separateObjectFields(ChatWorkflow, workflowData)
|
||||||
|
|
||||||
|
|
@ -1009,6 +1023,12 @@ class ChatObjects:
|
||||||
if "actionNumber" not in messageData:
|
if "actionNumber" not in messageData:
|
||||||
messageData["actionNumber"] = workflow.currentAction
|
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
|
# Use generic field separation based on ChatMessage model
|
||||||
simpleFields, objectFields = self._separateObjectFields(ChatMessage, messageData)
|
simpleFields, objectFields = self._separateObjectFields(ChatMessage, messageData)
|
||||||
|
|
||||||
|
|
@ -1303,6 +1323,12 @@ class ChatObjects:
|
||||||
def createDocument(self, documentData: Dict[str, Any]) -> ChatDocument:
|
def createDocument(self, documentData: Dict[str, Any]) -> ChatDocument:
|
||||||
"""Creates a document for a message in normalized table."""
|
"""Creates a document for a message in normalized table."""
|
||||||
try:
|
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
|
# Validate and normalize document data to dict
|
||||||
document = ChatDocument(**documentData)
|
document = ChatDocument(**documentData)
|
||||||
logger.debug(f"Creating document in database: fileName={document.fileName}, fileId={document.fileId}, messageId={document.messageId}")
|
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:
|
if "timestamp" not in logData:
|
||||||
logData["timestamp"] = getUtcTimestamp()
|
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
|
# Add status information if not present
|
||||||
if "status" not in logData and "type" in logData:
|
if "status" not in logData and "type" in logData:
|
||||||
if logData["type"] == "error":
|
if logData["type"] == "error":
|
||||||
|
|
@ -1508,6 +1540,12 @@ class ChatObjects:
|
||||||
if "workflowId" not in statData:
|
if "workflowId" not in statData:
|
||||||
raise ValueError("workflowId is required 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
|
# Validate the stat data against ChatStat model
|
||||||
stat = ChatStat(**statData)
|
stat = ChatStat(**statData)
|
||||||
|
|
||||||
|
|
@ -1768,9 +1806,11 @@ class ChatObjects:
|
||||||
if "id" not in automationData or not automationData["id"]:
|
if "id" not in automationData or not automationData["id"]:
|
||||||
automationData["id"] = str(uuid.uuid4())
|
automationData["id"] = str(uuid.uuid4())
|
||||||
|
|
||||||
# Ensure mandateId is set
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if "mandateId" not in automationData:
|
if "mandateId" not in automationData:
|
||||||
automationData["mandateId"] = self.mandateId
|
automationData["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in automationData:
|
||||||
|
automationData["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
# Ensure database connector has correct userId context
|
# Ensure database connector has correct userId context
|
||||||
# The connector should have been initialized with userId, but ensure it's updated
|
# 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)}")
|
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.
|
Returns a ChatObjects instance for the current user.
|
||||||
Handles initialization of database and records.
|
Handles initialization of database and records.
|
||||||
|
|
@ -1902,20 +1942,22 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
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:
|
if not currentUser:
|
||||||
raise ValueError("Invalid user context: user is required")
|
raise ValueError("Invalid user context: user is required")
|
||||||
|
|
||||||
effectiveMandateId = str(mandateId) if mandateId else None
|
effectiveMandateId = str(mandateId) if mandateId else None
|
||||||
|
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||||
|
|
||||||
# Create context key
|
# Create context key including featureInstanceId for proper isolation
|
||||||
contextKey = f"{effectiveMandateId}_{currentUser.id}"
|
contextKey = f"{effectiveMandateId}_{effectiveFeatureInstanceId}_{currentUser.id}"
|
||||||
|
|
||||||
# Create new instance if not exists
|
# Create new instance if not exists
|
||||||
if contextKey not in _chatInterfaces:
|
if contextKey not in _chatInterfaces:
|
||||||
_chatInterfaces[contextKey] = ChatObjects(currentUser, mandateId=effectiveMandateId)
|
_chatInterfaces[contextKey] = ChatObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
else:
|
else:
|
||||||
# Update user context if needed
|
# Update user context if needed
|
||||||
_chatInterfaces[contextKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
_chatInterfaces[contextKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
|
|
||||||
return _chatInterfaces[contextKey]
|
return _chatInterfaces[contextKey]
|
||||||
|
|
@ -76,12 +76,13 @@ class ComponentObjects:
|
||||||
# Initialize standard records if needed
|
# Initialize standard records if needed
|
||||||
self._initRecords()
|
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.
|
"""Sets the user context for the interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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:
|
if not currentUser:
|
||||||
logger.info("Initializing interface without user context")
|
logger.info("Initializing interface without user context")
|
||||||
|
|
@ -91,6 +92,7 @@ class ComponentObjects:
|
||||||
self.userId = currentUser.id
|
self.userId = currentUser.id
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
|
|
||||||
if not self.userId:
|
if not self.userId:
|
||||||
raise ValueError("Invalid user context: id is required")
|
raise ValueError("Invalid user context: id is required")
|
||||||
|
|
@ -986,12 +988,14 @@ class ComponentObjects:
|
||||||
fileSize = len(content)
|
fileSize = len(content)
|
||||||
fileHash = hashlib.sha256(content).hexdigest()
|
fileHash = hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
# Use mandateId from context
|
# Use mandateId and featureInstanceId from context for proper data isolation
|
||||||
mandateId = self.mandateId
|
mandateId = self.mandateId
|
||||||
|
featureInstanceId = self.featureInstanceId
|
||||||
|
|
||||||
# Create FileItem instance
|
# Create FileItem instance
|
||||||
fileItem = FileItem(
|
fileItem = FileItem(
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
|
featureInstanceId=featureInstanceId,
|
||||||
fileName=uniqueName,
|
fileName=uniqueName,
|
||||||
mimeType=mimeType,
|
mimeType=mimeType,
|
||||||
fileSize=fileSize,
|
fileSize=fileSize,
|
||||||
|
|
@ -1327,9 +1331,11 @@ class ComponentObjects:
|
||||||
if "userId" not in settingsData:
|
if "userId" not in settingsData:
|
||||||
settingsData["userId"] = self.userId
|
settingsData["userId"] = self.userId
|
||||||
|
|
||||||
# Ensure mandateId is set from context
|
# Ensure mandateId and featureInstanceId are set from context
|
||||||
if "mandateId" not in settingsData:
|
if "mandateId" not in settingsData:
|
||||||
settingsData["mandateId"] = self.mandateId
|
settingsData["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in settingsData:
|
||||||
|
settingsData["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
# Check if settings already exist for this user
|
# Check if settings already exist for this user
|
||||||
existingSettings = self.getVoiceSettings(settingsData["userId"])
|
existingSettings = self.getVoiceSettings(settingsData["userId"])
|
||||||
|
|
@ -1501,9 +1507,11 @@ class ComponentObjects:
|
||||||
if not all(c.isalpha() or c == "_" for c in subscriptionId):
|
if not all(c.isalpha() or c == "_" for c in subscriptionId):
|
||||||
raise ValueError("subscriptionId must contain only letters and underscores")
|
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:
|
if "mandateId" not in subscriptionData:
|
||||||
subscriptionData["mandateId"] = self.mandateId
|
subscriptionData["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in subscriptionData:
|
||||||
|
subscriptionData["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
createdRecord = self.db.recordCreate(MessagingSubscription, subscriptionData)
|
createdRecord = self.db.recordCreate(MessagingSubscription, subscriptionData)
|
||||||
if not createdRecord or not createdRecord.get("id"):
|
if not createdRecord or not createdRecord.get("id"):
|
||||||
|
|
@ -1605,6 +1613,12 @@ class ComponentObjects:
|
||||||
if "userId" not in registrationData:
|
if "userId" not in registrationData:
|
||||||
registrationData["userId"] = self.userId
|
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)
|
createdRecord = self.db.recordCreate(MessagingSubscriptionRegistration, registrationData)
|
||||||
if not createdRecord or not createdRecord.get("id"):
|
if not createdRecord or not createdRecord.get("id"):
|
||||||
raise ValueError("Failed to create registration record")
|
raise ValueError("Failed to create registration record")
|
||||||
|
|
@ -1679,6 +1693,13 @@ class ComponentObjects:
|
||||||
def createDelivery(self, delivery: MessagingDelivery) -> Dict[str, Any]:
|
def createDelivery(self, delivery: MessagingDelivery) -> Dict[str, Any]:
|
||||||
"""Creates a new delivery record."""
|
"""Creates a new delivery record."""
|
||||||
deliveryData = delivery.model_dump() if isinstance(delivery, MessagingDelivery) else delivery
|
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)
|
createdRecord = self.db.recordCreate(MessagingDelivery, deliveryData)
|
||||||
if not createdRecord or not createdRecord.get("id"):
|
if not createdRecord or not createdRecord.get("id"):
|
||||||
raise ValueError("Failed to create delivery record")
|
raise ValueError("Failed to create delivery record")
|
||||||
|
|
@ -1748,7 +1769,7 @@ class ComponentObjects:
|
||||||
return MessagingDelivery(**filteredDeliveries[0]) if filteredDeliveries else None
|
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.
|
Returns a ComponentObjects instance.
|
||||||
If currentUser is provided, initializes with user context.
|
If currentUser is provided, initializes with user context.
|
||||||
|
|
@ -1757,8 +1778,10 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
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
|
effectiveMandateId = str(mandateId) if mandateId else None
|
||||||
|
effectiveFeatureInstanceId = str(featureInstanceId) if featureInstanceId else None
|
||||||
|
|
||||||
# Create new instance if not exists
|
# Create new instance if not exists
|
||||||
if "default" not in _instancesManagement:
|
if "default" not in _instancesManagement:
|
||||||
|
|
@ -1767,7 +1790,7 @@ def getInterface(currentUser: Optional[User] = None, mandateId: Optional[str] =
|
||||||
interface = _instancesManagement["default"]
|
interface = _instancesManagement["default"]
|
||||||
|
|
||||||
if currentUser:
|
if currentUser:
|
||||||
interface.setUserContext(currentUser, mandateId=effectiveMandateId)
|
interface.setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
else:
|
else:
|
||||||
logger.info("Returning interface without user context")
|
logger.info("Returning interface without user context")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,19 @@ class RealEstateObjects:
|
||||||
Handles CRUD operations on Real Estate entities.
|
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.
|
"""Initializes the Real Estate Interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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.currentUser = currentUser
|
||||||
self.userId = currentUser.id if currentUser else None
|
self.userId = currentUser.id if currentUser else None
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
self.rbac = None # RBAC interface
|
self.rbac = None # RBAC interface
|
||||||
|
|
||||||
# Initialize database
|
# Initialize database
|
||||||
|
|
@ -57,7 +59,7 @@ class RealEstateObjects:
|
||||||
|
|
||||||
# Set user context if provided
|
# Set user context if provided
|
||||||
if currentUser:
|
if currentUser:
|
||||||
self.setUserContext(currentUser, mandateId=mandateId)
|
self.setUserContext(currentUser, mandateId=mandateId, featureInstanceId=featureInstanceId)
|
||||||
|
|
||||||
def _initializeDatabase(self):
|
def _initializeDatabase(self):
|
||||||
"""Initialize PostgreSQL database connection."""
|
"""Initialize PostgreSQL database connection."""
|
||||||
|
|
@ -107,17 +109,19 @@ class RealEstateObjects:
|
||||||
logger.warning(f"Error ensuring supporting tables exist: {e}")
|
logger.warning(f"Error ensuring supporting tables exist: {e}")
|
||||||
# Don't raise - tables will be created on-demand anyway
|
# 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.
|
"""Sets the user context for the interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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.currentUser = currentUser
|
||||||
self.userId = currentUser.id
|
self.userId = currentUser.id
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
|
|
||||||
if not self.userId:
|
if not self.userId:
|
||||||
raise ValueError("Invalid user context: id is required")
|
raise ValueError("Invalid user context: id is required")
|
||||||
|
|
@ -145,9 +149,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Projekt, "create"):
|
if not self.checkRbacPermission(Projekt, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create projects")
|
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:
|
if not projekt.mandateId:
|
||||||
projekt.mandateId = self.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
|
# Save to database - use mode='json' to ensure nested Pydantic models are serialized
|
||||||
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
|
self.db.recordCreate(Projekt, projekt.model_dump(mode='json'))
|
||||||
|
|
@ -231,8 +237,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Parzelle, "create"):
|
if not self.checkRbacPermission(Parzelle, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create plots")
|
raise PermissionError(f"User {self.userId} cannot create plots")
|
||||||
|
|
||||||
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if not parzelle.mandateId:
|
if not parzelle.mandateId:
|
||||||
parzelle.mandateId = self.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
|
# Use mode='json' to ensure nested Pydantic models (like GeoPolylinie) are serialized
|
||||||
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
|
self.db.recordCreate(Parzelle, parzelle.model_dump(mode='json'))
|
||||||
|
|
@ -438,8 +447,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Dokument, "create"):
|
if not self.checkRbacPermission(Dokument, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create documents")
|
raise PermissionError(f"User {self.userId} cannot create documents")
|
||||||
|
|
||||||
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if not dokument.mandateId:
|
if not dokument.mandateId:
|
||||||
dokument.mandateId = self.mandateId
|
dokument.mandateId = self.mandateId
|
||||||
|
if not dokument.featureInstanceId:
|
||||||
|
dokument.featureInstanceId = self.featureInstanceId
|
||||||
|
|
||||||
self.db.recordCreate(Dokument, dokument.model_dump())
|
self.db.recordCreate(Dokument, dokument.model_dump())
|
||||||
|
|
||||||
|
|
@ -504,8 +516,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Gemeinde, "create"):
|
if not self.checkRbacPermission(Gemeinde, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create municipalities")
|
raise PermissionError(f"User {self.userId} cannot create municipalities")
|
||||||
|
|
||||||
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if not gemeinde.mandateId:
|
if not gemeinde.mandateId:
|
||||||
gemeinde.mandateId = self.mandateId
|
gemeinde.mandateId = self.mandateId
|
||||||
|
if not gemeinde.featureInstanceId:
|
||||||
|
gemeinde.featureInstanceId = self.featureInstanceId
|
||||||
|
|
||||||
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
|
self.db.recordCreate(Gemeinde, gemeinde.model_dump())
|
||||||
|
|
||||||
|
|
@ -570,8 +585,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Kanton, "create"):
|
if not self.checkRbacPermission(Kanton, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create cantons")
|
raise PermissionError(f"User {self.userId} cannot create cantons")
|
||||||
|
|
||||||
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if not kanton.mandateId:
|
if not kanton.mandateId:
|
||||||
kanton.mandateId = self.mandateId
|
kanton.mandateId = self.mandateId
|
||||||
|
if not kanton.featureInstanceId:
|
||||||
|
kanton.featureInstanceId = self.featureInstanceId
|
||||||
|
|
||||||
self.db.recordCreate(Kanton, kanton.model_dump())
|
self.db.recordCreate(Kanton, kanton.model_dump())
|
||||||
|
|
||||||
|
|
@ -636,8 +654,11 @@ class RealEstateObjects:
|
||||||
if not self.checkRbacPermission(Land, "create"):
|
if not self.checkRbacPermission(Land, "create"):
|
||||||
raise PermissionError(f"User {self.userId} cannot create countries")
|
raise PermissionError(f"User {self.userId} cannot create countries")
|
||||||
|
|
||||||
|
# Ensure mandateId and featureInstanceId are set for proper data isolation
|
||||||
if not land.mandateId:
|
if not land.mandateId:
|
||||||
land.mandateId = self.mandateId
|
land.mandateId = self.mandateId
|
||||||
|
if not land.featureInstanceId:
|
||||||
|
land.featureInstanceId = self.featureInstanceId
|
||||||
|
|
||||||
self.db.recordCreate(Land, land.model_dump())
|
self.db.recordCreate(Land, land.model_dump())
|
||||||
|
|
||||||
|
|
@ -792,7 +813,7 @@ class RealEstateObjects:
|
||||||
raise
|
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.
|
Factory function to get or create a Real Estate interface instance for a user.
|
||||||
Uses singleton pattern per user.
|
Uses singleton pattern per user.
|
||||||
|
|
@ -800,16 +821,19 @@ def getInterface(currentUser: User, mandateId: Optional[str] = None) -> RealEsta
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
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
|
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:
|
if userKey not in _realEstateInterfaces:
|
||||||
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser, mandateId=effectiveMandateId)
|
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
else:
|
else:
|
||||||
# Update user context if needed
|
# Update user context if needed
|
||||||
_realEstateInterfaces[userKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
_realEstateInterfaces[userKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
|
|
||||||
return _realEstateInterfaces[userKey]
|
return _realEstateInterfaces[userKey]
|
||||||
|
|
||||||
|
|
@ -33,12 +33,13 @@ logger = logging.getLogger(__name__)
|
||||||
_trusteeInterfaces = {}
|
_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.
|
"""Get or create a TrusteeObjects instance for the given user context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header). Required.
|
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
|
global _trusteeInterfaces
|
||||||
|
|
||||||
|
|
@ -46,14 +47,16 @@ def getInterface(currentUser: User, mandateId: Optional[Union[str, uuid.UUID]] =
|
||||||
raise ValueError("Valid user context required")
|
raise ValueError("Valid user context required")
|
||||||
|
|
||||||
effectiveMandateId = str(mandateId) if mandateId else None
|
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:
|
if cacheKey not in _trusteeInterfaces:
|
||||||
_trusteeInterfaces[cacheKey] = TrusteeObjects(currentUser, mandateId=effectiveMandateId)
|
_trusteeInterfaces[cacheKey] = TrusteeObjects(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
else:
|
else:
|
||||||
# Update user context if needed
|
# Update user context if needed
|
||||||
_trusteeInterfaces[cacheKey].setUserContext(currentUser, mandateId=effectiveMandateId)
|
_trusteeInterfaces[cacheKey].setUserContext(currentUser, mandateId=effectiveMandateId, featureInstanceId=effectiveFeatureInstanceId)
|
||||||
|
|
||||||
return _trusteeInterfaces[cacheKey]
|
return _trusteeInterfaces[cacheKey]
|
||||||
|
|
||||||
|
|
@ -64,17 +67,19 @@ class TrusteeObjects:
|
||||||
Manages trustee organisations, roles, access, contracts, documents, and positions.
|
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.
|
"""Initializes the Trustee Interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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.currentUser = currentUser
|
||||||
self.userId = currentUser.id if currentUser else None
|
self.userId = currentUser.id if currentUser else None
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
self.rbac = None
|
self.rbac = None
|
||||||
|
|
||||||
# Initialize database
|
# Initialize database
|
||||||
|
|
@ -82,14 +87,15 @@ class TrusteeObjects:
|
||||||
|
|
||||||
# Set user context if provided
|
# Set user context if provided
|
||||||
if currentUser:
|
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.
|
"""Sets the user context for the interface.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
currentUser: The authenticated user
|
currentUser: The authenticated user
|
||||||
mandateId: The mandate ID from RequestContext (X-Mandate-Id header)
|
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:
|
if not currentUser:
|
||||||
logger.info("Initializing interface without user context")
|
logger.info("Initializing interface without user context")
|
||||||
|
|
@ -99,6 +105,7 @@ class TrusteeObjects:
|
||||||
self.userId = currentUser.id
|
self.userId = currentUser.id
|
||||||
# Use mandateId from parameter (Request-Context), not from user object
|
# Use mandateId from parameter (Request-Context), not from user object
|
||||||
self.mandateId = mandateId
|
self.mandateId = mandateId
|
||||||
|
self.featureInstanceId = featureInstanceId
|
||||||
|
|
||||||
if not self.userId:
|
if not self.userId:
|
||||||
raise ValueError("Invalid user context: id is required")
|
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")
|
logger.warning(f"User {self.userId} lacks permission to create organisation")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Set mandateId from current user
|
# Set mandateId and featureInstanceId from context for proper data isolation
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
# Validate ID format (alphanumeric, hyphens, underscores, 3-50 chars)
|
# Validate ID format (alphanumeric, hyphens, underscores, 3-50 chars)
|
||||||
orgId = data.get("id", "")
|
orgId = data.get("id", "")
|
||||||
|
|
@ -307,6 +316,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
roleId = data.get("id", "")
|
roleId = data.get("id", "")
|
||||||
|
|
||||||
if not roleId:
|
if not roleId:
|
||||||
|
|
@ -414,6 +425,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
accessId = data.get("id") or str(uuid.uuid4())
|
accessId = data.get("id") or str(uuid.uuid4())
|
||||||
|
|
@ -603,6 +616,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
contractId = data.get("id") or str(uuid.uuid4())
|
contractId = data.get("id") or str(uuid.uuid4())
|
||||||
|
|
@ -729,6 +744,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
documentId = data.get("id") or str(uuid.uuid4())
|
documentId = data.get("id") or str(uuid.uuid4())
|
||||||
|
|
@ -879,6 +896,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
# Calculate VAT amount if not provided
|
# Calculate VAT amount if not provided
|
||||||
if "vatAmount" not in data or data.get("vatAmount") == 0:
|
if "vatAmount" not in data or data.get("vatAmount") == 0:
|
||||||
|
|
@ -1028,6 +1047,8 @@ class TrusteeObjects:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data["mandateId"] = self.mandateId
|
data["mandateId"] = self.mandateId
|
||||||
|
if "featureInstanceId" not in data:
|
||||||
|
data["featureInstanceId"] = self.featureInstanceId
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
linkId = data.get("id") or str(uuid.uuid4())
|
linkId = data.get("id") or str(uuid.uuid4())
|
||||||
|
|
@ -13,9 +13,9 @@ import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Import interfaces and models
|
# 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.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.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||||
from modules.features.workflow import executeAutomation
|
from modules.features.workflow import executeAutomation
|
||||||
|
|
|
||||||
|
|
@ -472,12 +472,15 @@ async def addUserToMandate(
|
||||||
roleIds=data.roleIds
|
roleIds=data.roleIds
|
||||||
)
|
)
|
||||||
|
|
||||||
# 8. Audit
|
# 8. Audit - Log permission change with IP address
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logPermissionChange(
|
||||||
userId=str(context.user.id),
|
userId=str(context.user.id),
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
action="user_added_to_mandate",
|
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(
|
logger.info(
|
||||||
|
|
@ -557,12 +560,14 @@ async def removeUserFromMandate(
|
||||||
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
|
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
|
||||||
rootInterface.deleteUserMandate(targetUserId, mandateId)
|
rootInterface.deleteUserMandate(targetUserId, mandateId)
|
||||||
|
|
||||||
# Audit
|
# Audit - Log permission change
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logPermissionChange(
|
||||||
userId=str(context.user.id),
|
userId=str(context.user.id),
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
action="user_removed_from_mandate",
|
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}")
|
logger.info(f"User {context.user.id} removed user {targetUserId} from mandate {mandateId}")
|
||||||
|
|
@ -657,12 +662,15 @@ async def updateUserRolesInMandate(
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
rootInterface.addRoleToUserMandate(str(membership.id), roleId)
|
rootInterface.addRoleToUserMandate(str(membership.id), roleId)
|
||||||
|
|
||||||
# Audit
|
# Audit - Log role assignment change
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logPermissionChange(
|
||||||
userId=str(context.user.id),
|
userId=str(context.user.id),
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
action="user_roles_updated_in_mandate",
|
action="role_assigned",
|
||||||
details=f"targetUser={targetUserId}, newRoles={roleIds}"
|
targetUserId=targetUserId,
|
||||||
|
details=f"New roles: {roleIds}",
|
||||||
|
resourceType="UserMandateRole",
|
||||||
|
resourceId=str(membership.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,9 @@ async def reset_user_password(
|
||||||
userId=str(context.user.id),
|
userId=str(context.user.id),
|
||||||
mandateId=str(context.mandateId) if context.mandateId else "system",
|
mandateId=str(context.mandateId) if context.mandateId else "system",
|
||||||
action="password_reset",
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -439,7 +441,9 @@ async def change_password(
|
||||||
userId=str(context.user.id),
|
userId=str(context.user.id),
|
||||||
mandateId=str(context.mandateId) if context.mandateId else "system",
|
mandateId=str(context.mandateId) if context.mandateId else "system",
|
||||||
action="password_change",
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Reques
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
|
|
||||||
# Import interfaces
|
# Import interfaces
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
# Import models
|
# Import models
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
from modules.datamodels.datamodelChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum
|
||||||
|
|
||||||
# Import workflow control functions
|
# Import workflow control functions
|
||||||
from modules.features.workflow import chatStart, chatStop
|
from modules.features.workflow import chatStart, chatStop
|
||||||
|
|
@ -32,7 +32,7 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
def _getServiceChat(context: RequestContext):
|
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
|
# Workflow start endpoint
|
||||||
@router.post("/start", response_model=ChatWorkflow)
|
@router.post("/start", response_model=ChatWorkflow)
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ from modules.shared.timeUtils import parseTimestamp
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
|
|
||||||
# Import interfaces
|
# Import interfaces
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||||
|
|
||||||
# Import models
|
# 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
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse
|
||||||
|
|
||||||
# Import chatbot feature
|
# Import chatbot feature
|
||||||
|
|
@ -43,7 +43,7 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
def _getServiceChat(context: RequestContext):
|
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)
|
# Chatbot streaming endpoint (SSE)
|
||||||
@router.post("/start/stream")
|
@router.post("/start/stream")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from modules.datamodels.datamodelRealEstate import (
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import interfaces
|
# 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
|
# Import feature logic for AI-powered commands
|
||||||
from modules.features.realEstate.mainRealEstate import (
|
from modules.features.realEstate.mainRealEstate import (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
"""
|
"""
|
||||||
Routes for Trustee feature data management.
|
Routes for Trustee feature data management.
|
||||||
Implements CRUD operations for organisations, roles, access, contracts, documents, and positions.
|
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
|
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query, Response
|
||||||
|
|
@ -14,7 +18,9 @@ import json
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
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 (
|
from modules.datamodels.datamodelTrustee import (
|
||||||
TrusteeOrganisation,
|
TrusteeOrganisation,
|
||||||
TrusteeRole,
|
TrusteeRole,
|
||||||
|
|
@ -59,22 +65,72 @@ def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
|
||||||
return None
|
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 =====
|
# ===== Organisation Routes =====
|
||||||
|
|
||||||
@router.get("/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
@router.get("/{instanceId}/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getOrganisations(
|
async def getOrganisations(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteeOrganisation]:
|
) -> PaginatedResponse[TrusteeOrganisation]:
|
||||||
"""Get all organisations with optional pagination."""
|
"""Get all organisations for a feature instance with optional pagination."""
|
||||||
logger = logging.getLogger(__name__)
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
logger.debug(f"getOrganisations called for user {context.user.id}, mandateId: {context.mandateId}")
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllOrganisations(paginationParams)
|
result = interface.getAllOrganisations(paginationParams)
|
||||||
logger.debug(f"getOrganisations returned {len(result.items)} items")
|
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
return PaginatedResponse(
|
return PaginatedResponse(
|
||||||
|
|
@ -91,46 +147,55 @@ async def getOrganisations(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getOrganisation(
|
async def getOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(..., description="Organisation ID"),
|
orgId: str = Path(..., description="Organisation ID"),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeOrganisation:
|
) -> TrusteeOrganisation:
|
||||||
"""Get a single organisation by ID."""
|
"""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)
|
org = interface.getOrganisation(orgId)
|
||||||
if not org:
|
if not org:
|
||||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||||
return org
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createOrganisation(
|
async def createOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteeOrganisation = Body(...),
|
data: TrusteeOrganisation = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeOrganisation:
|
) -> TrusteeOrganisation:
|
||||||
"""Create a new organisation."""
|
"""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())
|
result = interface.createOrganisation(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create organisation")
|
raise HTTPException(status_code=400, detail="Failed to create organisation")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/organisations/{orgId}", response_model=TrusteeOrganisation)
|
@router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updateOrganisation(
|
async def updateOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(..., description="Organisation ID"),
|
orgId: str = Path(..., description="Organisation ID"),
|
||||||
data: TrusteeOrganisation = Body(...),
|
data: TrusteeOrganisation = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeOrganisation:
|
) -> TrusteeOrganisation:
|
||||||
"""Update an organisation."""
|
"""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)
|
existing = interface.getOrganisation(orgId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||||
|
|
@ -141,15 +206,18 @@ async def updateOrganisation(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/organisations/{orgId}")
|
@router.delete("/{instanceId}/organisations/{orgId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deleteOrganisation(
|
async def deleteOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(..., description="Organisation ID"),
|
orgId: str = Path(..., description="Organisation ID"),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete an organisation."""
|
"""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)
|
existing = interface.getOrganisation(orgId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
raise HTTPException(status_code=404, detail=f"Organisation {orgId} not found")
|
||||||
|
|
@ -162,16 +230,19 @@ async def deleteOrganisation(
|
||||||
|
|
||||||
# ===== Role Routes =====
|
# ===== Role Routes =====
|
||||||
|
|
||||||
@router.get("/roles", response_model=PaginatedResponse[TrusteeRole])
|
@router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getRoles(
|
async def getRoles(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteeRole]:
|
) -> PaginatedResponse[TrusteeRole]:
|
||||||
"""Get all roles with optional pagination."""
|
"""Get all roles with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllRoles(paginationParams)
|
result = interface.getAllRoles(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -189,46 +260,55 @@ async def getRoles(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getRole(
|
async def getRole(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
roleId: str = Path(..., description="Role ID"),
|
roleId: str = Path(..., description="Role ID"),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeRole:
|
) -> TrusteeRole:
|
||||||
"""Get a single role by ID."""
|
"""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)
|
role = interface.getRole(roleId)
|
||||||
if not role:
|
if not role:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
return role
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createRole(
|
async def createRole(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteeRole = Body(...),
|
data: TrusteeRole = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeRole:
|
) -> TrusteeRole:
|
||||||
"""Create a new role (sysadmin only)."""
|
"""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())
|
result = interface.createRole(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create role")
|
raise HTTPException(status_code=400, detail="Failed to create role")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/roles/{roleId}", response_model=TrusteeRole)
|
@router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updateRole(
|
async def updateRole(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
roleId: str = Path(...),
|
roleId: str = Path(...),
|
||||||
data: TrusteeRole = Body(...),
|
data: TrusteeRole = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeRole:
|
) -> TrusteeRole:
|
||||||
"""Update a role (sysadmin only)."""
|
"""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)
|
existing = interface.getRole(roleId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
|
|
@ -239,15 +319,18 @@ async def updateRole(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/roles/{roleId}")
|
@router.delete("/{instanceId}/roles/{roleId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deleteRole(
|
async def deleteRole(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
roleId: str = Path(...),
|
roleId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete a role (sysadmin only, fails if in use)."""
|
"""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)
|
existing = interface.getRole(roleId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
|
|
@ -260,16 +343,19 @@ async def deleteRole(
|
||||||
|
|
||||||
# ===== Access Routes =====
|
# ===== Access Routes =====
|
||||||
|
|
||||||
@router.get("/access", response_model=PaginatedResponse[TrusteeAccess])
|
@router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getAllAccess(
|
async def getAllAccess(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteeAccess]:
|
) -> PaginatedResponse[TrusteeAccess]:
|
||||||
"""Get all access records with optional pagination."""
|
"""Get all access records with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllAccess(paginationParams)
|
result = interface.getAllAccess(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -287,70 +373,85 @@ async def getAllAccess(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getAccess(
|
async def getAccess(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
accessId: str = Path(...),
|
accessId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeAccess:
|
) -> TrusteeAccess:
|
||||||
"""Get a single access record by ID."""
|
"""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)
|
access = interface.getAccess(accessId)
|
||||||
if not access:
|
if not access:
|
||||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||||
return access
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getAccessByOrganisation(
|
async def getAccessByOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(...),
|
orgId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteeAccess]:
|
) -> List[TrusteeAccess]:
|
||||||
"""Get all access records for an organisation."""
|
"""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)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getAccessByUser(
|
async def getAccessByUser(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
userId: str = Path(...),
|
userId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteeAccess]:
|
) -> List[TrusteeAccess]:
|
||||||
"""Get all access records for a user."""
|
"""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)
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createAccess(
|
async def createAccess(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteeAccess = Body(...),
|
data: TrusteeAccess = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeAccess:
|
) -> TrusteeAccess:
|
||||||
"""Create a new access record."""
|
"""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())
|
result = interface.createAccess(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create access")
|
raise HTTPException(status_code=400, detail="Failed to create access")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/access/{accessId}", response_model=TrusteeAccess)
|
@router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updateAccess(
|
async def updateAccess(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
accessId: str = Path(...),
|
accessId: str = Path(...),
|
||||||
data: TrusteeAccess = Body(...),
|
data: TrusteeAccess = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeAccess:
|
) -> TrusteeAccess:
|
||||||
"""Update an access record."""
|
"""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)
|
existing = interface.getAccess(accessId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||||
|
|
@ -361,15 +462,18 @@ async def updateAccess(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/access/{accessId}")
|
@router.delete("/{instanceId}/access/{accessId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deleteAccess(
|
async def deleteAccess(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
accessId: str = Path(...),
|
accessId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete an access record."""
|
"""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)
|
existing = interface.getAccess(accessId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
raise HTTPException(status_code=404, detail=f"Access {accessId} not found")
|
||||||
|
|
@ -382,16 +486,19 @@ async def deleteAccess(
|
||||||
|
|
||||||
# ===== Contract Routes =====
|
# ===== Contract Routes =====
|
||||||
|
|
||||||
@router.get("/contracts", response_model=PaginatedResponse[TrusteeContract])
|
@router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getContracts(
|
async def getContracts(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteeContract]:
|
) -> PaginatedResponse[TrusteeContract]:
|
||||||
"""Get all contracts with optional pagination."""
|
"""Get all contracts with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllContracts(paginationParams)
|
result = interface.getAllContracts(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -409,58 +516,70 @@ async def getContracts(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getContract(
|
async def getContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
contractId: str = Path(...),
|
contractId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeContract:
|
) -> TrusteeContract:
|
||||||
"""Get a single contract by ID."""
|
"""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)
|
contract = interface.getContract(contractId)
|
||||||
if not contract:
|
if not contract:
|
||||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||||
return contract
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getContractsByOrganisation(
|
async def getContractsByOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(...),
|
orgId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteeContract]:
|
) -> List[TrusteeContract]:
|
||||||
"""Get all contracts for an organisation."""
|
"""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)
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createContract(
|
async def createContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteeContract = Body(...),
|
data: TrusteeContract = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeContract:
|
) -> TrusteeContract:
|
||||||
"""Create a new contract."""
|
"""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())
|
result = interface.createContract(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create contract")
|
raise HTTPException(status_code=400, detail="Failed to create contract")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/contracts/{contractId}", response_model=TrusteeContract)
|
@router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updateContract(
|
async def updateContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
contractId: str = Path(...),
|
contractId: str = Path(...),
|
||||||
data: TrusteeContract = Body(...),
|
data: TrusteeContract = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeContract:
|
) -> TrusteeContract:
|
||||||
"""Update a contract (organisationId is immutable)."""
|
"""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)
|
existing = interface.getContract(contractId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||||
|
|
@ -471,15 +590,18 @@ async def updateContract(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/contracts/{contractId}")
|
@router.delete("/{instanceId}/contracts/{contractId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deleteContract(
|
async def deleteContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
contractId: str = Path(...),
|
contractId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete a contract."""
|
"""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)
|
existing = interface.getContract(contractId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
raise HTTPException(status_code=404, detail=f"Contract {contractId} not found")
|
||||||
|
|
@ -492,16 +614,19 @@ async def deleteContract(
|
||||||
|
|
||||||
# ===== Document Routes =====
|
# ===== Document Routes =====
|
||||||
|
|
||||||
@router.get("/documents", response_model=PaginatedResponse[TrusteeDocument])
|
@router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getDocuments(
|
async def getDocuments(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteeDocument]:
|
) -> PaginatedResponse[TrusteeDocument]:
|
||||||
"""Get all documents (metadata only) with optional pagination."""
|
"""Get all documents (metadata only) with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllDocuments(paginationParams)
|
result = interface.getAllDocuments(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -519,30 +644,36 @@ async def getDocuments(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getDocument(
|
async def getDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
documentId: str = Path(...),
|
documentId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeDocument:
|
) -> TrusteeDocument:
|
||||||
"""Get document metadata by ID."""
|
"""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)
|
doc = interface.getDocument(documentId)
|
||||||
if not doc:
|
if not doc:
|
||||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
@router.get("/documents/{documentId}/data")
|
@router.get("/{instanceId}/documents/{documentId}/data")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def getDocumentData(
|
async def getDocumentData(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
documentId: str = Path(...),
|
documentId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
):
|
):
|
||||||
"""Download document binary data."""
|
"""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)
|
doc = interface.getDocument(documentId)
|
||||||
if not doc:
|
if not doc:
|
||||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getDocumentsByContract(
|
async def getDocumentsByContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
contractId: str = Path(...),
|
contractId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteeDocument]:
|
) -> List[TrusteeDocument]:
|
||||||
"""Get all documents for a contract."""
|
"""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)
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createDocument(
|
async def createDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteeDocument = Body(...),
|
data: TrusteeDocument = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeDocument:
|
) -> TrusteeDocument:
|
||||||
"""Create a new document."""
|
"""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())
|
result = interface.createDocument(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
raise HTTPException(status_code=400, detail="Failed to create document")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/documents/{documentId}", response_model=TrusteeDocument)
|
@router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updateDocument(
|
async def updateDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
documentId: str = Path(...),
|
documentId: str = Path(...),
|
||||||
data: TrusteeDocument = Body(...),
|
data: TrusteeDocument = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteeDocument:
|
) -> TrusteeDocument:
|
||||||
"""Update document metadata."""
|
"""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)
|
existing = interface.getDocument(documentId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||||
|
|
@ -605,15 +745,18 @@ async def updateDocument(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/documents/{documentId}")
|
@router.delete("/{instanceId}/documents/{documentId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deleteDocument(
|
async def deleteDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
documentId: str = Path(...),
|
documentId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete a document."""
|
"""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)
|
existing = interface.getDocument(documentId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
raise HTTPException(status_code=404, detail=f"Document {documentId} not found")
|
||||||
|
|
@ -626,16 +769,19 @@ async def deleteDocument(
|
||||||
|
|
||||||
# ===== Position Routes =====
|
# ===== Position Routes =====
|
||||||
|
|
||||||
@router.get("/positions", response_model=PaginatedResponse[TrusteePosition])
|
@router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition])
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getPositions(
|
async def getPositions(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteePosition]:
|
) -> PaginatedResponse[TrusteePosition]:
|
||||||
"""Get all positions with optional pagination."""
|
"""Get all positions with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllPositions(paginationParams)
|
result = interface.getAllPositions(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -653,70 +799,85 @@ async def getPositions(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getPosition(
|
async def getPosition(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
positionId: str = Path(...),
|
positionId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteePosition:
|
) -> TrusteePosition:
|
||||||
"""Get a single position by ID."""
|
"""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)
|
position = interface.getPosition(positionId)
|
||||||
if not position:
|
if not position:
|
||||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||||
return position
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getPositionsByContract(
|
async def getPositionsByContract(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
contractId: str = Path(...),
|
contractId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteePosition]:
|
) -> List[TrusteePosition]:
|
||||||
"""Get all positions for a contract."""
|
"""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)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getPositionsByOrganisation(
|
async def getPositionsByOrganisation(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
orgId: str = Path(...),
|
orgId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteePosition]:
|
) -> List[TrusteePosition]:
|
||||||
"""Get all positions for an organisation."""
|
"""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)
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createPosition(
|
async def createPosition(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteePosition = Body(...),
|
data: TrusteePosition = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteePosition:
|
) -> TrusteePosition:
|
||||||
"""Create a new position."""
|
"""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())
|
result = interface.createPosition(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create position")
|
raise HTTPException(status_code=400, detail="Failed to create position")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/positions/{positionId}", response_model=TrusteePosition)
|
@router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def updatePosition(
|
async def updatePosition(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
positionId: str = Path(...),
|
positionId: str = Path(...),
|
||||||
data: TrusteePosition = Body(...),
|
data: TrusteePosition = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteePosition:
|
) -> TrusteePosition:
|
||||||
"""Update a position."""
|
"""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)
|
existing = interface.getPosition(positionId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||||
|
|
@ -727,15 +888,18 @@ async def updatePosition(
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/positions/{positionId}")
|
@router.delete("/{instanceId}/positions/{positionId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deletePosition(
|
async def deletePosition(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
positionId: str = Path(...),
|
positionId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete a position."""
|
"""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)
|
existing = interface.getPosition(positionId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
raise HTTPException(status_code=404, detail=f"Position {positionId} not found")
|
||||||
|
|
@ -748,16 +912,19 @@ async def deletePosition(
|
||||||
|
|
||||||
# ===== Position-Document Link Routes =====
|
# ===== 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")
|
@limiter.limit("30/minute")
|
||||||
async def getPositionDocuments(
|
async def getPositionDocuments(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
pagination: Optional[str] = Query(None),
|
pagination: Optional[str] = Query(None),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> PaginatedResponse[TrusteePositionDocument]:
|
) -> PaginatedResponse[TrusteePositionDocument]:
|
||||||
"""Get all position-document links with optional pagination."""
|
"""Get all position-document links with optional pagination."""
|
||||||
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
paginationParams = _parsePagination(pagination)
|
paginationParams = _parsePagination(pagination)
|
||||||
interface = getInterface(context.user, mandateId=context.mandateId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.getAllPositionDocuments(paginationParams)
|
result = interface.getAllPositionDocuments(paginationParams)
|
||||||
|
|
||||||
if paginationParams:
|
if paginationParams:
|
||||||
|
|
@ -775,69 +942,84 @@ async def getPositionDocuments(
|
||||||
return PaginatedResponse(items=result.items, pagination=None)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getPositionDocument(
|
async def getPositionDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
linkId: str = Path(...),
|
linkId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteePositionDocument:
|
) -> TrusteePositionDocument:
|
||||||
"""Get a single position-document link by ID."""
|
"""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)
|
link = interface.getPositionDocument(linkId)
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||||
return link
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getDocumentsForPosition(
|
async def getDocumentsForPosition(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
positionId: str = Path(...),
|
positionId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteePositionDocument]:
|
) -> List[TrusteePositionDocument]:
|
||||||
"""Get all document links for a position."""
|
"""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)
|
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")
|
@limiter.limit("30/minute")
|
||||||
async def getPositionsForDocument(
|
async def getPositionsForDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
documentId: str = Path(...),
|
documentId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> List[TrusteePositionDocument]:
|
) -> List[TrusteePositionDocument]:
|
||||||
"""Get all position links for a document."""
|
"""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)
|
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")
|
@limiter.limit("10/minute")
|
||||||
async def createPositionDocument(
|
async def createPositionDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
data: TrusteePositionDocument = Body(...),
|
data: TrusteePositionDocument = Body(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> TrusteePositionDocument:
|
) -> TrusteePositionDocument:
|
||||||
"""Create a new position-document link."""
|
"""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())
|
result = interface.createPositionDocument(data.model_dump())
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create link")
|
raise HTTPException(status_code=400, detail="Failed to create link")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/position-documents/{linkId}")
|
@router.delete("/{instanceId}/position-documents/{linkId}")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def deletePositionDocument(
|
async def deletePositionDocument(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
linkId: str = Path(...),
|
linkId: str = Path(...),
|
||||||
context: RequestContext = Depends(getRequestContext)
|
context: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Delete a position-document link."""
|
"""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)
|
existing = interface.getPositionDocument(linkId)
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
raise HTTPException(status_code=404, detail=f"Link {linkId} not found")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from fastapi import status
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Import interfaces and models
|
# Import interfaces and models
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
from modules.auth import limiter, getRequestContext, requireSysAdmin, RequestContext
|
from modules.auth import limiter, getRequestContext, requireSysAdmin, RequestContext
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
|
|
@ -75,7 +75,7 @@ async def sync_all_automation_events(
|
||||||
This will register/remove events based on active flags.
|
This will register/remove events based on active flags.
|
||||||
"""
|
"""
|
||||||
try:
|
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.interfaces.interfaceDbAppObjects import getRootInterface
|
||||||
from modules.features.workflow import syncAutomationEvents
|
from modules.features.workflow import syncAutomationEvents
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ async def remove_event(
|
||||||
# Update automation's eventId if it exists
|
# Update automation's eventId if it exists
|
||||||
if eventId.startswith("automation."):
|
if eventId.startswith("automation."):
|
||||||
automation_id = eventId.replace("automation.", "")
|
automation_id = eventId.replace("automation.", "")
|
||||||
chatInterface = interfaceDbChatObjects.getInterface(context.user)
|
chatInterface = interfaceDbChatbot.getInterface(context.user)
|
||||||
automation = chatInterface.getAutomationDefinition(automation_id)
|
automation = chatInterface.getAutomationDefinition(automation_id)
|
||||||
if automation and getattr(automation, "eventId", None) == eventId:
|
if automation and getattr(automation, "eventId", None) == eventId:
|
||||||
chatInterface.updateAutomationDefinition(automation_id, {"eventId": None})
|
chatInterface.updateAutomationDefinition(automation_id, {"eventId": None})
|
||||||
|
|
@ -204,12 +204,13 @@ async def exportUserData(
|
||||||
for inv in invitationsUsed
|
for inv in invitationsUsed
|
||||||
]
|
]
|
||||||
|
|
||||||
# Audit log
|
# Audit log - GDPR Article 15 data export
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logGdprEvent(
|
||||||
userId=str(currentUser.id),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="gdpr_data_export",
|
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)")
|
logger.info(f"User {currentUser.id} exported personal data (GDPR Art. 15)")
|
||||||
|
|
@ -304,12 +305,13 @@ async def exportPortableData(
|
||||||
"about": portableData
|
"about": portableData
|
||||||
}
|
}
|
||||||
|
|
||||||
# Audit log
|
# Audit log - GDPR Article 20 data portability
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logGdprEvent(
|
||||||
userId=str(currentUser.id),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="gdpr_data_portability",
|
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)")
|
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))
|
rootInterface.db.recordDelete(User, str(currentUser.id))
|
||||||
deletedData.append("User account deleted")
|
deletedData.append("User account deleted")
|
||||||
|
|
||||||
# Audit log (before user is deleted)
|
# Audit log (before user is deleted) - GDPR Article 17 account deletion
|
||||||
audit_logger.logSecurityEvent(
|
audit_logger.logGdprEvent(
|
||||||
userId=str(currentUser.id),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="gdpr_account_deletion",
|
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)")
|
logger.info(f"User {currentUser.id} deleted own account (GDPR Art. 17)")
|
||||||
|
|
|
||||||
|
|
@ -624,7 +624,10 @@ async def logout(
|
||||||
userId=str(currentUser.id),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="logout",
|
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:
|
except Exception:
|
||||||
# Don't fail if audit logging fails
|
# Don't fail if audit logging fails
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,10 @@ async def login(
|
||||||
userId=str(user.id),
|
userId=str(user.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="login",
|
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:
|
except Exception:
|
||||||
# Don't fail if audit logging fails
|
# Don't fail if audit logging fails
|
||||||
|
|
@ -171,10 +174,13 @@ async def login(
|
||||||
try:
|
try:
|
||||||
from modules.shared.auditLogger import audit_logger
|
from modules.shared.auditLogger import audit_logger
|
||||||
audit_logger.logUserAccess(
|
audit_logger.logUserAccess(
|
||||||
userId="unknown",
|
userId=formData.username or "unknown",
|
||||||
mandateId="unknown",
|
mandateId="system",
|
||||||
action="login",
|
action="login_failed",
|
||||||
successInfo=f"failed: {error_msg}"
|
successInfo=f"failed: {error_msg}",
|
||||||
|
ipAddress=request.client.host if request.client else None,
|
||||||
|
userAgent=request.headers.get("user-agent"),
|
||||||
|
success=False
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Don't fail if audit logging fails
|
# 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),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="logout",
|
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:
|
except Exception:
|
||||||
# Don't fail if audit logging fails
|
# Don't fail if audit logging fails
|
||||||
|
|
|
||||||
|
|
@ -634,7 +634,10 @@ async def logout(
|
||||||
userId=str(currentUser.id),
|
userId=str(currentUser.id),
|
||||||
mandateId="system",
|
mandateId="system",
|
||||||
action="logout",
|
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:
|
except Exception:
|
||||||
# Don't fail if audit logging fails
|
# 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
|
from modules.auth import limiter, getCurrentUser
|
||||||
|
|
||||||
# Import interfaces
|
# Import interfaces
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
from modules.interfaces.interfaceDbChatObjects import getInterface
|
from modules.interfaces.interfaceDbChatbot import getInterface
|
||||||
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
|
||||||
|
|
||||||
# Import models
|
# Import models
|
||||||
from modules.datamodels.datamodelChat import (
|
from modules.datamodels.datamodelChatbot import (
|
||||||
ChatWorkflow,
|
ChatWorkflow,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatLog,
|
ChatLog,
|
||||||
|
|
@ -45,7 +45,7 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
def getServiceChat(currentUser: User):
|
def getServiceChat(currentUser: User):
|
||||||
return interfaceDbChatObjects.getInterface(currentUser)
|
return interfaceDbChatbot.getInterface(currentUser)
|
||||||
|
|
||||||
# Consolidated endpoint for getting all workflows
|
# Consolidated endpoint for getting all workflows
|
||||||
@router.get("/", response_model=PaginatedResponse[ChatWorkflow])
|
@router.get("/", response_model=PaginatedResponse[ChatWorkflow])
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||||
|
|
||||||
class PublicService:
|
class PublicService:
|
||||||
"""Lightweight proxy exposing only public callable attributes of a target.
|
"""Lightweight proxy exposing only public callable attributes of a target.
|
||||||
|
|
@ -49,7 +49,7 @@ class Services:
|
||||||
|
|
||||||
# Initialize interfaces with explicit mandateId
|
# 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)
|
self.interfaceDbChat = getChatInterface(user, mandateId=mandateId)
|
||||||
|
|
||||||
from modules.interfaces.interfaceDbAppObjects import getInterface as getAppInterface
|
from modules.interfaces.interfaceDbAppObjects import getInterface as getAppInterface
|
||||||
|
|
@ -58,7 +58,7 @@ class Services:
|
||||||
from modules.interfaces.interfaceDbComponentObjects import getInterface as getComponentInterface
|
from modules.interfaces.interfaceDbComponentObjects import getInterface as getComponentInterface
|
||||||
self.interfaceDbComponent = getComponentInterface(user, mandateId=mandateId)
|
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)
|
self.interfaceDbTrustee = getTrusteeInterface(user, mandateId=mandateId)
|
||||||
|
|
||||||
# Expose RBAC directly on services for convenience
|
# Expose RBAC directly on services for convenience
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import re
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
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.services.serviceExtraction.mainServiceExtraction import ExtractionService
|
||||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||||
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
|
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import logging
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, Any, List, Optional
|
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.datamodels.datamodelExtraction import ContentPart, DocumentIntent
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List, Optional
|
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.datamodels.datamodelExtraction import DocumentIntent
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from modules.datamodels.datamodelUam import User, UserConnection
|
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.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||||
from modules.shared.progressLogger import ProgressLogger
|
from modules.shared.progressLogger import ProgressLogger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import json
|
||||||
from .subRegistry import ExtractorRegistry, ChunkerRegistry
|
from .subRegistry import ExtractorRegistry, ChunkerRegistry
|
||||||
from .subPipeline import runExtraction
|
from .subPipeline import runExtraction
|
||||||
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart, MergeStrategy, ExtractionOptions, PartResult, DocumentIntent
|
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.datamodels.datamodelAi import AiCallResponse, AiCallRequest, AiCallOptions, OperationTypeEnum, AiModelCall
|
||||||
from modules.aicore.aicoreModelRegistry import modelRegistry
|
from modules.aicore.aicoreModelRegistry import modelRegistry
|
||||||
from modules.aicore.aicoreModelSelector import modelSelector
|
from modules.aicore.aicoreModelSelector import modelSelector
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import base64
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Dict, List, Optional, Callable
|
from typing import Any, Dict, List, Optional, Callable
|
||||||
from modules.datamodels.datamodelDocument import RenderedDocument
|
from modules.datamodels.datamodelDocument import RenderedDocument
|
||||||
from modules.datamodels.datamodelChat import ChatDocument
|
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||||
from modules.services.serviceGeneration.subDocumentUtility import (
|
from modules.services.serviceGeneration.subDocumentUtility import (
|
||||||
getFileExtension,
|
getFileExtension,
|
||||||
getMimeTypeFromExtension,
|
getMimeTypeFromExtension,
|
||||||
|
|
|
||||||
|
|
@ -157,11 +157,11 @@ class UtilsService:
|
||||||
|
|
||||||
def storeDebugMessageAndDocuments(self, message, currentUser):
|
def storeDebugMessageAndDocuments(self, message, currentUser):
|
||||||
"""
|
"""
|
||||||
Wrapper to store debug messages and documents via interfaceDbChatObjects.
|
Wrapper to store debug messages and documents via interfaceDbChatbot.
|
||||||
Mirrors storeDebugMessageAndDocuments() in modules.interfaces.interfaceDbChatObjects.
|
Mirrors storeDebugMessageAndDocuments() in modules.interfaces.interfaceDbChatbot.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbChatObjects import storeDebugMessageAndDocuments as _storeDebugMessageAndDocuments
|
from modules.interfaces.interfaceDbChatbot import storeDebugMessageAndDocuments as _storeDebugMessageAndDocuments
|
||||||
_storeDebugMessageAndDocuments(message, currentUser)
|
_storeDebugMessageAndDocuments(message, currentUser)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Silent fail to never break main flow
|
# Silent fail to never break main flow
|
||||||
|
|
|
||||||
|
|
@ -4,201 +4,471 @@
|
||||||
Audit Logging System for PowerOn Gateway
|
Audit Logging System for PowerOn Gateway
|
||||||
|
|
||||||
This module provides centralized audit logging functionality for security events,
|
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 logging
|
||||||
import os
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class AuditLogger:
|
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):
|
def __init__(self):
|
||||||
self.logger = None
|
self._db = None
|
||||||
self._setupAuditLogger()
|
self._modelClass = None
|
||||||
|
self._initialized = False
|
||||||
def _setupAuditLogger(self):
|
self._fallbackToStdout = True
|
||||||
"""Setup the audit logger with daily file rotation"""
|
|
||||||
|
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:
|
try:
|
||||||
# Get log directory from config
|
from modules.datamodels.datamodelAudit import AuditLogEntry
|
||||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||||
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)
|
|
||||||
|
|
||||||
# Ensure log directory exists
|
self._modelClass = AuditLogEntry
|
||||||
os.makedirs(logDir, exist_ok=True)
|
|
||||||
|
|
||||||
# Create audit logger
|
# Get database configuration
|
||||||
self.logger = logging.getLogger('audit')
|
dbHost = APP_CONFIG.get("DB_HOST", "_no_config_default_data")
|
||||||
self.logger.setLevel(logging.INFO)
|
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
|
# Create database connector with system user context
|
||||||
for handler in self.logger.handlers[:]:
|
self._db = DatabaseConnector(
|
||||||
self.logger.removeHandler(handler)
|
dbHost=dbHost,
|
||||||
|
dbDatabase=dbDatabase,
|
||||||
# Create daily rotating file handler for audit log
|
dbUser=dbUser,
|
||||||
rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB
|
dbPassword=dbPassword,
|
||||||
backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5))
|
dbPort=dbPort,
|
||||||
|
userId="system" # Audit logs are created by system
|
||||||
fileHandler = DailyRotatingFileHandler(
|
|
||||||
logDir=logDir,
|
|
||||||
filenamePrefix="log_audit",
|
|
||||||
maxBytes=rotationSize,
|
|
||||||
backupCount=backupCount
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create formatter for audit log
|
# Initialize database and ensure table exists
|
||||||
auditFormatter = logging.Formatter(
|
self._db.initDbSystem()
|
||||||
fmt="%(asctime)s | %(message)s",
|
self._db._ensureTableExists(AuditLogEntry)
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
fileHandler.setFormatter(auditFormatter)
|
|
||||||
|
|
||||||
# Add handler to logger
|
logger.info("AuditLogger database connection initialized successfully")
|
||||||
self.logger.addHandler(fileHandler)
|
return True
|
||||||
|
|
||||||
# Prevent propagation to root logger
|
|
||||||
self.logger.propagate = False
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Fallback to standard logger if audit setup fails
|
logger.warning(f"AuditLogger database initialization failed, using fallback logging: {e}")
|
||||||
self.logger = logging.getLogger(__name__)
|
self._db = None
|
||||||
self.logger.error(f"Failed to setup audit logger: {str(e)}")
|
return False
|
||||||
|
|
||||||
def logEvent(self,
|
def _logToFallback(self, entry: Dict[str, Any]) -> None:
|
||||||
userId: str,
|
"""Log to standard logger as fallback when database is unavailable."""
|
||||||
mandateId: str,
|
if self._fallbackToStdout:
|
||||||
category: str,
|
fallbackMsg = (
|
||||||
action: str,
|
f"AUDIT | {entry.get('timestamp', '')} | "
|
||||||
details: str = "",
|
f"{entry.get('userId', '')} | {entry.get('mandateId', '')} | "
|
||||||
timestamp: Optional[datetime] = None) -> None:
|
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:
|
Args:
|
||||||
userId: User identifier
|
userId: User identifier (or 'system' for system events)
|
||||||
mandateId: Mandate identifier (can be empty if not applicable)
|
mandateId: Mandate context (can be None for system-level events)
|
||||||
category: Event category (e.g., 'key', 'access', 'data')
|
category: Event category (access, key, data, security, gdpr, permission, system)
|
||||||
action: Specific action (e.g., 'decode', 'login', 'logout')
|
action: Specific action performed
|
||||||
details: Additional details about the event
|
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)
|
timestamp: Optional custom timestamp (defaults to current time)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ID of the created audit log entry, or None if logging failed
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not self.logger:
|
# Prepare the entry data
|
||||||
return
|
entryData = {
|
||||||
|
"timestamp": timestamp if timestamp else getUtcTimestamp(),
|
||||||
# Use provided timestamp or current time
|
"userId": userId or "unknown",
|
||||||
if timestamp is None:
|
"username": username,
|
||||||
timestamp = datetime.now()
|
"mandateId": mandateId,
|
||||||
|
"featureInstanceId": featureInstanceId,
|
||||||
# Format the audit log entry
|
"category": category,
|
||||||
# Format: timestamp | userid | mandateid | category | action | details
|
"action": action,
|
||||||
auditEntry = f"{userId} | {mandateId} | {category} | {action} | {details}"
|
"resourceType": resourceType,
|
||||||
|
"resourceId": resourceId,
|
||||||
# Log the event
|
"details": details if details else None,
|
||||||
self.logger.info(auditEntry)
|
"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:
|
except Exception as e:
|
||||||
# Use standard logger as fallback
|
logger.error(f"Failed to log audit event: {e}")
|
||||||
logging.getLogger(__name__).error(f"Failed to log audit event: {str(e)}")
|
# Try fallback
|
||||||
|
try:
|
||||||
|
self._logToFallback(entryData)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def logKeyAccess(self, userId: str, mandateId: str, keyName: str, action: str) -> None:
|
# ===== Convenience Methods for Common Event Types =====
|
||||||
"""Log key access events (decode/encode)"""
|
|
||||||
self.logEvent(
|
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,
|
userId=userId,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
category="key",
|
category="key",
|
||||||
action=action,
|
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:
|
def logUserAccess(
|
||||||
"""Log user access events (login/logout)"""
|
self,
|
||||||
self.logEvent(
|
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,
|
userId=userId,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
category="access",
|
category="access",
|
||||||
action=action,
|
action=action,
|
||||||
details=successInfo
|
details=successInfo,
|
||||||
|
ipAddress=ipAddress,
|
||||||
|
userAgent=userAgent,
|
||||||
|
success=success
|
||||||
)
|
)
|
||||||
|
|
||||||
def logDataAccess(self, userId: str, mandateId: str, action: str, details: str = "") -> None:
|
def logDataAccess(
|
||||||
"""Log data access events"""
|
self,
|
||||||
self.logEvent(
|
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,
|
userId=userId,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
category="data",
|
category="data",
|
||||||
action=action,
|
action=action,
|
||||||
details=details
|
details=details,
|
||||||
|
resourceType=resourceType,
|
||||||
|
resourceId=resourceId,
|
||||||
|
featureInstanceId=featureInstanceId
|
||||||
)
|
)
|
||||||
|
|
||||||
def logSecurityEvent(self, userId: str, mandateId: str, action: str, details: str = "") -> None:
|
def logSecurityEvent(
|
||||||
"""Log security-related events"""
|
self,
|
||||||
self.logEvent(
|
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,
|
userId=userId,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
category="security",
|
category="security",
|
||||||
action=action,
|
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
|
# Global audit logger instance
|
||||||
audit_logger = AuditLogger()
|
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
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult
|
from modules.datamodels.datamodelChatbot import ActionResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, Optional, List
|
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.datamodelExtraction import ContentPart
|
||||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any, Optional, List
|
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.datamodelExtraction import ContentPart
|
||||||
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||||
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
from modules.datamodels.datamodelWorkflow import AiResponse, DocumentData
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, List, Optional
|
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.datamodelAi import AiCallOptions
|
||||||
from modules.datamodels.datamodelExtraction import ContentPart
|
from modules.datamodels.datamodelExtraction import ContentPart
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult
|
from modules.datamodels.datamodelChatbot import ActionResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult
|
from modules.datamodels.datamodelChatbot import ActionResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import json
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.workflows.methods.methodBase import action
|
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
|
from modules.connectors.connectorPreprocessor import PreprocessorConnector
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any
|
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.datamodelDocref import DocumentReferenceList
|
||||||
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy, ContentExtracted, ContentPart
|
from modules.datamodels.datamodelExtraction import ExtractionOptions, MergeStrategy, ContentExtracted, ContentPart
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Dict, Any
|
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.datamodelDocref import DocumentReferenceList
|
||||||
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart
|
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from typing import Dict, Any
|
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
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Any
|
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
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import csv as csv_module
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import csv as csv_module
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import json
|
||||||
import io
|
import io
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import json
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import json
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import json
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import logging
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionDocument
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionItem, TaskStep
|
from modules.datamodels.datamodelChatbot import ActionResult, ActionItem, TaskStep
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||||
from modules.workflows.processing.shared.methodDiscovery import methods
|
from modules.workflows.processing.shared.methodDiscovery import methods
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
from modules.datamodels.datamodelChat import TaskPlan, TaskStep, ActionResult, ReviewResult
|
from modules.datamodels.datamodelChatbot import TaskPlan, TaskStep, ActionResult, ReviewResult
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
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.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
|
||||||
from modules.workflows.processing.shared.promptGenerationTaskplan import (
|
from modules.workflows.processing.shared.promptGenerationTaskplan import (
|
||||||
generateTaskPlanningPrompt
|
generateTaskPlanningPrompt
|
||||||
|
|
@ -51,7 +51,7 @@ class TaskPlanner:
|
||||||
|
|
||||||
# Analyze user intent to obtain cleaned user objective for planning
|
# Analyze user intent to obtain cleaned user objective for planning
|
||||||
# SKIP intent analysis for AUTOMATION mode - it uses predefined JSON plans
|
# 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)
|
workflowMode = getattr(workflow, 'workflowMode', None)
|
||||||
skipIntentionAnalysis = (workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION)
|
skipIntentionAnalysis = (workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from modules.datamodels.datamodelChat import (
|
from modules.datamodels.datamodelChatbot import (
|
||||||
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
||||||
TaskPlan, ActionResult
|
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.modes.modeBase import BaseMode
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
from modules.shared.timeUtils import parseTimestamp
|
from modules.shared.timeUtils import parseTimestamp
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskResult, ActionItem
|
from modules.datamodels.datamodelChatbot import TaskStep, TaskContext, TaskResult, ActionItem
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow
|
from modules.datamodels.datamodelChatbot import ChatWorkflow
|
||||||
from modules.workflows.processing.core.taskPlanner import TaskPlanner
|
from modules.workflows.processing.core.taskPlanner import TaskPlanner
|
||||||
from modules.workflows.processing.core.actionExecutor import ActionExecutor
|
from modules.workflows.processing.core.actionExecutor import ActionExecutor
|
||||||
from modules.workflows.processing.core.messageCreator import MessageCreator
|
from modules.workflows.processing.core.messageCreator import MessageCreator
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
from modules.datamodels.datamodelChat import (
|
from modules.datamodels.datamodelChatbot import (
|
||||||
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
|
||||||
ActionResult, Observation, ObservationPreview, ReviewResult
|
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.modes.modeBase import BaseMode
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
from modules.shared.timeUtils import parseTimestamp
|
from modules.shared.timeUtils import parseTimestamp
|
||||||
|
|
@ -893,7 +893,7 @@ class DynamicMode(BaseMode):
|
||||||
async def _refineDecide(self, context: TaskContext, observation: Observation) -> ReviewResult:
|
async def _refineDecide(self, context: TaskContext, observation: Observation) -> ReviewResult:
|
||||||
"""Refine: decide continue or stop, with reason"""
|
"""Refine: decide continue or stop, with reason"""
|
||||||
# Create proper ReviewContext for extractReviewContent
|
# 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)
|
# Convert observation to dict for extractReviewContent (temporary compatibility)
|
||||||
observationDict = {
|
observationDict = {
|
||||||
'success': observation.success,
|
'success': observation.success,
|
||||||
|
|
@ -1042,7 +1042,7 @@ class DynamicMode(BaseMode):
|
||||||
|
|
||||||
# Parse response using structured parsing with ReviewResult model
|
# Parse response using structured parsing with ReviewResult model
|
||||||
from modules.shared.jsonUtils import parseJsonWithModel
|
from modules.shared.jsonUtils import parseJsonWithModel
|
||||||
from modules.datamodels.datamodelChat import ReviewResult
|
from modules.datamodels.datamodelChatbot import ReviewResult
|
||||||
|
|
||||||
if not resp:
|
if not resp:
|
||||||
return ReviewResult(
|
return ReviewResult(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,7 +348,7 @@ def extractReviewContent(context: Any) -> str:
|
||||||
elif hasattr(context, 'observation') and context.observation:
|
elif hasattr(context, 'observation') and context.observation:
|
||||||
# For observation data, show full content but handle documents specially
|
# For observation data, show full content but handle documents specially
|
||||||
# Handle both Pydantic Observation model and dict format
|
# 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):
|
if isinstance(context.observation, Observation):
|
||||||
# Convert Pydantic model to dict
|
# 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
|
# For observation data in stepResult, show full content but handle documents specially
|
||||||
observation = context.stepResult['observation']
|
observation = context.stepResult['observation']
|
||||||
# Handle both Pydantic Observation model and dict format
|
# Handle both Pydantic Observation model and dict format
|
||||||
from modules.datamodels.datamodelChat import Observation
|
from modules.datamodels.datamodelChatbot import Observation
|
||||||
|
|
||||||
if isinstance(observation, Observation):
|
if isinstance(observation, Observation):
|
||||||
# Convert Pydantic model to dict
|
# Convert Pydantic model to dict
|
||||||
|
|
@ -452,10 +452,10 @@ def extractLatestRefinementFeedback(context: Any) -> str:
|
||||||
# First check for ERROR level logs in workflow
|
# First check for ERROR level logs in workflow
|
||||||
if hasattr(context, 'workflow') and context.workflow:
|
if hasattr(context, 'workflow') and context.workflow:
|
||||||
try:
|
try:
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
from modules.interfaces.interfaceDbAppObjects import getRootInterface
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(rootInterface.currentUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(rootInterface.currentUser)
|
||||||
|
|
||||||
# Get workflow logs
|
# Get workflow logs
|
||||||
chatData = interfaceDbChat.getUnifiedChatData(context.workflow.id, None)
|
chatData = interfaceDbChat.getUnifiedChatData(context.workflow.id, None)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Handles prompt templates for dynamic mode action handling.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, List
|
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 (
|
from modules.workflows.processing.shared.placeholderFactory import (
|
||||||
extractUserPrompt,
|
extractUserPrompt,
|
||||||
extractUserLanguage,
|
extractUserLanguage,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Handles prompt templates and extraction functions for task planning phase.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List
|
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 (
|
from modules.workflows.processing.shared.placeholderFactory import (
|
||||||
extractUserPrompt,
|
extractUserPrompt,
|
||||||
extractAvailableDocumentsSummary,
|
extractAvailableDocumentsSummary,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
||||||
from modules.datamodels import datamodelChat
|
from modules.datamodels import datamodelChatbot
|
||||||
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan, ActionResult, ActionDocument, ChatDocument, ChatMessage
|
from modules.datamodels.datamodelChatbot import TaskStep, TaskContext, TaskPlan, ActionResult, ActionDocument, ChatDocument, ChatMessage
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow, WorkflowModeEnum
|
from modules.datamodels.datamodelChatbot import ChatWorkflow, WorkflowModeEnum
|
||||||
from modules.workflows.processing.modes.modeBase import BaseMode
|
from modules.workflows.processing.modes.modeBase import BaseMode
|
||||||
from modules.workflows.processing.modes.modeDynamic import DynamicMode
|
from modules.workflows.processing.modes.modeDynamic import DynamicMode
|
||||||
from modules.workflows.processing.modes.modeAutomation import AutomationMode
|
from modules.workflows.processing.modes.modeAutomation import AutomationMode
|
||||||
|
|
@ -102,7 +102,7 @@ class WorkflowProcessor:
|
||||||
self.services.chat.progressLogFinish(operationId, False)
|
self.services.chat.progressLogFinish(operationId, False)
|
||||||
raise
|
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"""
|
"""Execute a task step using the appropriate mode"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -494,7 +494,7 @@ class WorkflowProcessor:
|
||||||
|
|
||||||
# Create ActionResult with response
|
# Create ActionResult with response
|
||||||
# For fast path, we create a simple text document with the 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(
|
responseDoc = ActionDocument(
|
||||||
documentName="fast_path_response.txt",
|
documentName="fast_path_response.txt",
|
||||||
|
|
@ -626,7 +626,7 @@ class WorkflowProcessor:
|
||||||
ChatMessage with persisted documents
|
ChatMessage with persisted documents
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
# Check workflow status
|
# Check workflow status
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ import uuid
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from modules.datamodels.datamodelChat import (
|
from modules.datamodels.datamodelChatbot import (
|
||||||
UserInputRequest,
|
UserInputRequest,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatWorkflow,
|
ChatWorkflow,
|
||||||
ChatDocument,
|
ChatDocument,
|
||||||
WorkflowModeEnum
|
WorkflowModeEnum
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelChat import TaskContext
|
from modules.datamodels.datamodelChatbot import TaskContext
|
||||||
from modules.workflows.processing.workflowProcessor import WorkflowProcessor
|
from modules.workflows.processing.workflowProcessor import WorkflowProcessor
|
||||||
from modules.workflows.processing.shared.stateTools import WorkflowStoppedException, checkWorkflowStopped
|
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
|
# Collect file info
|
||||||
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
||||||
from modules.datamodels.datamodelChat import ChatDocument
|
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||||
doc = ChatDocument(
|
doc = ChatDocument(
|
||||||
fileId=fileItem.id,
|
fileId=fileItem.id,
|
||||||
fileName=fileInfo.get("fileName", fileName) if fileInfo else fileName,
|
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
|
# Collect file info
|
||||||
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
fileInfo = self.services.chat.getFileInfo(fileItem.id)
|
||||||
from modules.datamodels.datamodelChat import ChatDocument
|
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||||
doc = ChatDocument(
|
doc = ChatDocument(
|
||||||
fileId=fileItem.id,
|
fileId=fileItem.id,
|
||||||
fileName=fileInfo.get("fileName", fileName) if fileInfo else fileName,
|
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
|
# Persist task result for cross-task/round document references
|
||||||
# Convert ChatTaskResult to WorkflowTaskResult for persistence
|
# Convert ChatTaskResult to WorkflowTaskResult for persistence
|
||||||
from modules.datamodels.datamodelWorkflow import TaskResult as WorkflowTaskResult
|
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)
|
# Get final ActionResult from task execution (last action result)
|
||||||
finalActionResult = None
|
finalActionResult = None
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class AIModelsTester:
|
||||||
self.services.extraction = ExtractionService(self.services)
|
self.services.extraction = ExtractionService(self.services)
|
||||||
|
|
||||||
# Create a minimal workflow context
|
# Create a minimal workflow context
|
||||||
from modules.datamodels.datamodelChat import ChatWorkflow, WorkflowModeEnum
|
from modules.datamodels.datamodelChatbot import ChatWorkflow, WorkflowModeEnum
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
self.services.currentWorkflow = ChatWorkflow(
|
self.services.currentWorkflow = ChatWorkflow(
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ if _gateway_path not in sys.path:
|
||||||
sys.path.insert(0, _gateway_path)
|
sys.path.insert(0, _gateway_path)
|
||||||
|
|
||||||
from modules.datamodels.datamodelAi import OperationTypeEnum
|
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
|
from modules.datamodels.datamodelUam import User
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,8 +94,8 @@ class MethodAiOperationsTester:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Import and initialize services - use the same approach as routeChatPlayground
|
# Import and initialize services - use the same approach as routeChatPlayground
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
|
|
||||||
# Import and initialize services
|
# Import and initialize services
|
||||||
from modules.services import getInterface as getServices
|
from modules.services import getInterface as getServices
|
||||||
|
|
@ -174,7 +174,7 @@ class MethodAiOperationsTester:
|
||||||
imageData = f.read()
|
imageData = f.read()
|
||||||
|
|
||||||
# Create a ChatDocument
|
# Create a ChatDocument
|
||||||
from modules.datamodels.datamodelChat import ChatDocument
|
from modules.datamodels.datamodelChatbot import ChatDocument
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
testImageDoc = ChatDocument(
|
testImageDoc = ChatDocument(
|
||||||
|
|
@ -186,7 +186,7 @@ class MethodAiOperationsTester:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a message with this document
|
# Create a message with this document
|
||||||
from modules.datamodels.datamodelChat import ChatMessage
|
from modules.datamodels.datamodelChatbot import ChatMessage
|
||||||
import time
|
import time
|
||||||
|
|
||||||
testMessage = ChatMessage(
|
testMessage = ChatMessage(
|
||||||
|
|
@ -201,8 +201,8 @@ class MethodAiOperationsTester:
|
||||||
|
|
||||||
# Save message to database
|
# Save message to database
|
||||||
if self.services.workflow:
|
if self.services.workflow:
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
messageDict = testMessage.model_dump()
|
messageDict = testMessage.model_dump()
|
||||||
interfaceDbChat.createMessage(messageDict)
|
interfaceDbChat.createMessage(messageDict)
|
||||||
|
|
||||||
|
|
@ -283,8 +283,8 @@ class MethodAiOperationsTester:
|
||||||
maxSteps=5
|
maxSteps=5
|
||||||
)
|
)
|
||||||
# Save workflow to database
|
# Save workflow to database
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflowDict = testWorkflow.model_dump()
|
workflowDict = testWorkflow.model_dump()
|
||||||
interfaceDbChat.createWorkflow(workflowDict)
|
interfaceDbChat.createWorkflow(workflowDict)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ class AIBehaviorTester:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create and save workflow in database using the interface
|
# 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 uuid
|
||||||
import time
|
import time
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
currentTimestamp = time.time()
|
currentTimestamp = time.time()
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class AIBehaviorTester:
|
||||||
)
|
)
|
||||||
|
|
||||||
# SAVE workflow to database so it exists for access control
|
# SAVE workflow to database so it exists for access control
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflowDict = testWorkflow.model_dump()
|
workflowDict = testWorkflow.model_dump()
|
||||||
interfaceDbChat.createWorkflow(workflowDict)
|
interfaceDbChat.createWorkflow(workflowDict)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ if _gateway_path not in sys.path:
|
||||||
|
|
||||||
# Import the service initialization
|
# Import the service initialization
|
||||||
from modules.services import getInterface as getServices
|
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.datamodels.datamodelUam import User
|
||||||
from modules.features.workflow import chatStart
|
from modules.features.workflow import chatStart
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
|
|
||||||
class WorkflowWithDocumentsTester:
|
class WorkflowWithDocumentsTester:
|
||||||
|
|
@ -192,7 +192,7 @@ class WorkflowWithDocumentsTester:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get current workflow status
|
# Get current workflow status
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
currentWorkflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
currentWorkflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||||
|
|
||||||
if not currentWorkflow:
|
if not currentWorkflow:
|
||||||
|
|
@ -225,7 +225,7 @@ class WorkflowWithDocumentsTester:
|
||||||
if not self.workflow:
|
if not self.workflow:
|
||||||
return {"error": "No workflow to analyze"}
|
return {"error": "No workflow to analyze"}
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ if _gateway_path not in sys.path:
|
||||||
|
|
||||||
# Import the service initialization
|
# Import the service initialization
|
||||||
from modules.services import getInterface as getServices
|
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.datamodels.datamodelUam import User
|
||||||
from modules.features.workflow import chatStart
|
from modules.features.workflow import chatStart
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
|
|
||||||
class WorkflowPromptVariationsTester:
|
class WorkflowPromptVariationsTester:
|
||||||
|
|
@ -115,7 +115,7 @@ class WorkflowPromptVariationsTester:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get current workflow status
|
# Get current workflow status
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
currentWorkflow = interfaceDbChat.getWorkflow(workflow.id)
|
currentWorkflow = interfaceDbChat.getWorkflow(workflow.id)
|
||||||
|
|
||||||
if not currentWorkflow:
|
if not currentWorkflow:
|
||||||
|
|
@ -140,7 +140,7 @@ class WorkflowPromptVariationsTester:
|
||||||
|
|
||||||
def _analyzeWorkflowResults(self, workflow: Any) -> Dict[str, Any]:
|
def _analyzeWorkflowResults(self, workflow: Any) -> Dict[str, Any]:
|
||||||
"""Analyze workflow results and extract information."""
|
"""Analyze workflow results and extract information."""
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflow = interfaceDbChat.getWorkflow(workflow.id)
|
workflow = interfaceDbChat.getWorkflow(workflow.id)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ if _gateway_path not in sys.path:
|
||||||
|
|
||||||
# Import the service initialization
|
# Import the service initialization
|
||||||
from modules.services import getInterface as getServices
|
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.datamodels.datamodelUam import User
|
||||||
from modules.features.workflow import chatStart
|
from modules.features.workflow import chatStart
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
|
|
||||||
class DocumentGenerationFormatsTester:
|
class DocumentGenerationFormatsTester:
|
||||||
|
|
@ -251,7 +251,7 @@ class DocumentGenerationFormatsTester:
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
lastStatus = None
|
lastStatus = None
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
print("Waiting indefinitely (no timeout)")
|
print("Waiting indefinitely (no timeout)")
|
||||||
|
|
@ -296,7 +296,7 @@ class DocumentGenerationFormatsTester:
|
||||||
if not self.workflow:
|
if not self.workflow:
|
||||||
return {"error": "No workflow to analyze"}
|
return {"error": "No workflow to analyze"}
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ if _gateway_path not in sys.path:
|
||||||
|
|
||||||
# Import the service initialization
|
# Import the service initialization
|
||||||
from modules.services import getInterface as getServices
|
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.datamodels.datamodelUam import User
|
||||||
from modules.features.workflow import chatStart
|
from modules.features.workflow import chatStart
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
|
|
||||||
class DocumentGenerationFormatsTester10:
|
class DocumentGenerationFormatsTester10:
|
||||||
|
|
@ -248,7 +248,7 @@ class DocumentGenerationFormatsTester10:
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
lastStatus = None
|
lastStatus = None
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
print("Waiting indefinitely (no timeout)")
|
print("Waiting indefinitely (no timeout)")
|
||||||
|
|
@ -293,7 +293,7 @@ class DocumentGenerationFormatsTester10:
|
||||||
if not self.workflow:
|
if not self.workflow:
|
||||||
return {"error": "No workflow to analyze"}
|
return {"error": "No workflow to analyze"}
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ if _gateway_path not in sys.path:
|
||||||
|
|
||||||
# Import the service initialization
|
# Import the service initialization
|
||||||
from modules.services import getInterface as getServices
|
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.datamodels.datamodelUam import User
|
||||||
from modules.features.workflow import chatStart
|
from modules.features.workflow import chatStart
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatbot as interfaceDbChatbot
|
||||||
|
|
||||||
|
|
||||||
class CodeGenerationFormatsTester11:
|
class CodeGenerationFormatsTester11:
|
||||||
|
|
@ -190,7 +190,7 @@ class CodeGenerationFormatsTester11:
|
||||||
startTime = time.time()
|
startTime = time.time()
|
||||||
lastStatus = None
|
lastStatus = None
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
print("Waiting indefinitely (no timeout)")
|
print("Waiting indefinitely (no timeout)")
|
||||||
|
|
@ -235,7 +235,7 @@ class CodeGenerationFormatsTester11:
|
||||||
if not self.workflow:
|
if not self.workflow:
|
||||||
return {"error": "No workflow to analyze"}
|
return {"error": "No workflow to analyze"}
|
||||||
|
|
||||||
interfaceDbChat = interfaceDbChatObjects.getInterface(self.testUser)
|
interfaceDbChat = interfaceDbChatbot.getInterface(self.testUser)
|
||||||
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
workflow = interfaceDbChat.getWorkflow(self.workflow.id)
|
||||||
|
|
||||||
if not workflow:
|
if not workflow:
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import pytest
|
||||||
import uuid
|
import uuid
|
||||||
from unittest.mock import Mock, AsyncMock, patch
|
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.datamodelWorkflow import ActionDefinition
|
||||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference, DocumentItemReference
|
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference, DocumentItemReference
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Tests state increment methods, helper methods, and updateFromSelection.
|
||||||
import pytest
|
import pytest
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
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.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.datamodelWorkflow import ActionDefinition, AiResponse
|
||||||
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference
|
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
|
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