phase 2 i18n clean
This commit is contained in:
parent
259fd25d9b
commit
be9e47caad
111 changed files with 4819 additions and 4371 deletions
19
app.py
19
app.py
|
|
@ -317,6 +317,15 @@ async def lifespan(app: FastAPI):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Feature catalog registration failed: {e}")
|
logger.error(f"Feature catalog registration failed: {e}")
|
||||||
|
|
||||||
|
# Sync gateway i18n registry to DB and load translation cache
|
||||||
|
try:
|
||||||
|
from modules.shared.i18nRegistry import _syncRegistryToDb, _loadCache
|
||||||
|
await _syncRegistryToDb()
|
||||||
|
await _loadCache()
|
||||||
|
logger.info("i18n registry sync + cache load completed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"i18n registry sync failed (non-critical): {e}")
|
||||||
|
|
||||||
# Pre-warm service center modules (avoids first-request import latency)
|
# Pre-warm service center modules (avoids first-request import latency)
|
||||||
try:
|
try:
|
||||||
from modules.serviceCenter import preWarm
|
from modules.serviceCenter import preWarm
|
||||||
|
|
@ -481,6 +490,16 @@ from modules.auth import (
|
||||||
ProactiveTokenRefreshMiddleware,
|
ProactiveTokenRefreshMiddleware,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# i18n language detection middleware (sets per-request language from Accept-Language header)
|
||||||
|
from modules.shared.i18nRegistry import _setLanguage
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def _i18nMiddleware(request: Request, call_next):
|
||||||
|
acceptLang = request.headers.get("Accept-Language", "")
|
||||||
|
lang = acceptLang[:2].lower() if len(acceptLang) >= 2 and acceptLang[:2].isalpha() else "de"
|
||||||
|
_setLanguage(lang)
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
app.add_middleware(CSRFMiddleware)
|
app.add_middleware(CSRFMiddleware)
|
||||||
|
|
||||||
# Token refresh middleware (silent refresh for expired OAuth tokens)
|
# Token refresh middleware (silent refresh for expired OAuth tokens)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from enum import Enum
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
class AuditCategory(str, Enum):
|
class AuditCategory(str, Enum):
|
||||||
|
|
@ -82,6 +82,7 @@ class AuditAction(str, Enum):
|
||||||
CONFIG_CHANGE = "config_change"
|
CONFIG_CHANGE = "config_change"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Audit-Log-Eintrag")
|
||||||
class AuditLogEntry(BaseModel):
|
class AuditLogEntry(BaseModel):
|
||||||
"""
|
"""
|
||||||
Audit log entry for database storage.
|
Audit log entry for database storage.
|
||||||
|
|
@ -92,117 +93,94 @@ class AuditLogEntry(BaseModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique identifier for the audit entry",
|
description="Unique identifier for the audit entry",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Timestamp
|
# Timestamp
|
||||||
timestamp: float = Field(
|
timestamp: float = Field(
|
||||||
default_factory=getUtcTimestamp,
|
default_factory=getUtcTimestamp,
|
||||||
description="UTC timestamp when the event occurred",
|
description="UTC timestamp when the event occurred",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Zeitstempel", "frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Actor identification
|
# Actor identification
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="ID of the user who performed the action (or 'system' for system events)",
|
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}
|
json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
username: Optional[str] = Field(
|
username: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Username at the time of the event (for historical reference)",
|
description="Username at the time of the event (for historical reference)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Context
|
# Context
|
||||||
mandateId: Optional[str] = Field(
|
mandateId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate context (if applicable)",
|
description="Mandate context (if applicable)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
featureInstanceId: Optional[str] = Field(
|
featureInstanceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature instance context (if applicable)",
|
description="Feature instance context (if applicable)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Event classification
|
# Event classification
|
||||||
category: str = Field(
|
category: str = Field(
|
||||||
description="Event category (access, key, data, security, gdpr, permission, system)",
|
description="Event category (access, key, data, security, gdpr, permission, system)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Kategorie", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
action: str = Field(
|
action: str = Field(
|
||||||
description="Specific action performed",
|
description="Specific action performed",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Aktion", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Event details
|
# Event details
|
||||||
resourceType: Optional[str] = Field(
|
resourceType: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Type of resource affected (e.g., 'User', 'ChatWorkflow', 'TrusteeContract')",
|
description="Type of resource affected (e.g., 'User', 'ChatWorkflow', 'TrusteeContract')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Ressourcentyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
resourceId: Optional[str] = Field(
|
resourceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of the affected resource",
|
description="ID of the affected resource",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Ressourcen-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
details: Optional[str] = Field(
|
details: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Additional details about the event",
|
description="Additional details about the event",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Details", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Request metadata
|
# Request metadata
|
||||||
ipAddress: Optional[str] = Field(
|
ipAddress: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="IP address of the client",
|
description="IP address of the client",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "IP-Adresse", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
userAgent: Optional[str] = Field(
|
userAgent: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="User agent string from the request",
|
description="User agent string from the request",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "User-Agent", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Outcome
|
# Outcome
|
||||||
success: bool = Field(
|
success: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether the action was successful",
|
description="Whether the action was successful",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Erfolgreich", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
errorMessage: Optional[str] = Field(
|
errorMessage: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if the action failed",
|
description="Error message if the action failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Fehlermeldung", "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"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,17 @@ from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Basisdatensatz")
|
||||||
class PowerOnModel(BaseModel):
|
class PowerOnModel(BaseModel):
|
||||||
|
"""Basis-Datenmodell mit System-Audit-Feldern fuer alle DB-Tabellen."""
|
||||||
sysCreatedAt: Optional[float] = Field(
|
sysCreatedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Record creation timestamp (UTC, set by system)",
|
description="Record creation timestamp (UTC, set by system)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Erstellt am",
|
||||||
"frontend_type": "timestamp",
|
"frontend_type": "timestamp",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -25,6 +28,7 @@ class PowerOnModel(BaseModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID who created this record (set by system)",
|
description="User ID who created this record (set by system)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Erstellt von",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -36,6 +40,7 @@ class PowerOnModel(BaseModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Record last modification timestamp (UTC, set by system)",
|
description="Record last modification timestamp (UTC, set by system)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Geaendert am",
|
||||||
"frontend_type": "timestamp",
|
"frontend_type": "timestamp",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -47,6 +52,7 @@ class PowerOnModel(BaseModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID who last modified this record (set by system)",
|
description="User ID who last modified this record (set by system)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Geaendert von",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -54,15 +60,3 @@ class PowerOnModel(BaseModel):
|
||||||
"system": True,
|
"system": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"PowerOnModel",
|
|
||||||
{"en": "Base Record", "de": "Basisdatensatz"},
|
|
||||||
{
|
|
||||||
"sysCreatedAt": {"en": "Created At", "de": "Erstellt am", "fr": "Cree le"},
|
|
||||||
"sysCreatedBy": {"en": "Created By", "de": "Erstellt von", "fr": "Cree par"},
|
|
||||||
"sysModifiedAt": {"en": "Modified At", "de": "Geaendert am", "fr": "Modifie le"},
|
|
||||||
"sysModifiedBy": {"en": "Modified By", "de": "Geaendert von", "fr": "Modifie par"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from enum import Enum
|
||||||
from datetime import date, datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# End-customer price for storage above plan-included volume (CHF per GB per month).
|
# End-customer price for storage above plan-included volume (CHF per GB per month).
|
||||||
|
|
@ -38,203 +38,170 @@ class PeriodTypeEnum(str, Enum):
|
||||||
YEAR = "YEAR"
|
YEAR = "YEAR"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Abrechnungskonto")
|
||||||
class BillingAccount(PowerOnModel):
|
class BillingAccount(PowerOnModel):
|
||||||
"""Billing account for mandate or user-mandate combination."""
|
"""Billing account for mandate or user-mandate combination."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
description="Primary key",
|
||||||
|
json_schema_extra={"label": "ID"},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(..., description="Foreign key to Mandate")
|
mandateId: str = Field(..., description="Foreign key to Mandate", json_schema_extra={"label": "Mandanten-ID"})
|
||||||
userId: Optional[str] = Field(None, description="Foreign key to User (None = mandate pool account, set = user audit account)")
|
userId: Optional[str] = Field(
|
||||||
balance: float = Field(default=0.0, description="Current balance in CHF")
|
None,
|
||||||
warningThreshold: float = Field(default=0.0, description="Warning threshold in CHF")
|
description="Foreign key to User (None = mandate pool account, set = user audit account)",
|
||||||
lastWarningAt: Optional[datetime] = Field(None, description="Last warning sent timestamp")
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
enabled: bool = Field(default=True, description="Account is active")
|
)
|
||||||
|
balance: float = Field(default=0.0, description="Current balance in CHF", json_schema_extra={"label": "Guthaben (CHF)"})
|
||||||
|
warningThreshold: float = Field(
|
||||||
registerModelLabels(
|
default=0.0,
|
||||||
"BillingAccount",
|
description="Warning threshold in CHF",
|
||||||
{"en": "Billing Account", "de": "Abrechnungskonto"},
|
json_schema_extra={"label": "Warnschwelle (CHF)"},
|
||||||
{
|
)
|
||||||
"id": {"en": "ID", "de": "ID"},
|
lastWarningAt: Optional[datetime] = Field(
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
|
None,
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID"},
|
description="Last warning sent timestamp",
|
||||||
"balance": {"en": "Balance (CHF)", "de": "Guthaben (CHF)"},
|
json_schema_extra={"label": "Letzte Warnung"},
|
||||||
"warningThreshold": {"en": "Warning Threshold (CHF)", "de": "Warnschwelle (CHF)"},
|
)
|
||||||
"lastWarningAt": {"en": "Last Warning", "de": "Letzte Warnung"},
|
enabled: bool = Field(default=True, description="Account is active", json_schema_extra={"label": "Aktiv"})
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiv"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Transaktion")
|
||||||
class BillingTransaction(PowerOnModel):
|
class BillingTransaction(PowerOnModel):
|
||||||
"""Single billing transaction (credit, debit, adjustment)."""
|
"""Single billing transaction (credit, debit, adjustment)."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
description="Primary key",
|
||||||
|
json_schema_extra={"label": "ID"},
|
||||||
)
|
)
|
||||||
accountId: str = Field(..., description="Foreign key to BillingAccount")
|
accountId: str = Field(..., description="Foreign key to BillingAccount", json_schema_extra={"label": "Konto-ID"})
|
||||||
transactionType: TransactionTypeEnum = Field(..., description="Transaction type")
|
transactionType: TransactionTypeEnum = Field(..., description="Transaction type", json_schema_extra={"label": "Typ"})
|
||||||
amount: float = Field(..., description="Amount in CHF (always positive)")
|
amount: float = Field(..., description="Amount in CHF (always positive)", json_schema_extra={"label": "Betrag (CHF)"})
|
||||||
description: str = Field(..., description="Transaction description")
|
description: str = Field(..., description="Transaction description", json_schema_extra={"label": "Beschreibung"})
|
||||||
|
|
||||||
# Reference to source
|
# Reference to source
|
||||||
referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type")
|
referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type", json_schema_extra={"label": "Referenztyp"})
|
||||||
referenceId: Optional[str] = Field(None, description="Reference ID")
|
referenceId: Optional[str] = Field(None, description="Reference ID", json_schema_extra={"label": "Referenz-ID"})
|
||||||
|
|
||||||
# Context for workflow transactions
|
# Context for workflow transactions
|
||||||
workflowId: Optional[str] = Field(None, description="Workflow ID (for WORKFLOW transactions)")
|
workflowId: Optional[str] = Field(None, description="Workflow ID (for WORKFLOW transactions)", json_schema_extra={"label": "Workflow-ID"})
|
||||||
featureInstanceId: Optional[str] = Field(None, description="Feature instance ID")
|
featureInstanceId: Optional[str] = Field(None, description="Feature instance ID", json_schema_extra={"label": "Feature-Instanz-ID"})
|
||||||
featureCode: Optional[str] = Field(None, description="Feature code (e.g., automation)")
|
featureCode: Optional[str] = Field(None, description="Feature code (e.g., automation)", json_schema_extra={"label": "Feature-Code"})
|
||||||
aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)")
|
aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)", json_schema_extra={"label": "AI-Anbieter"})
|
||||||
aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)")
|
aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)", json_schema_extra={"label": "AI-Modell"})
|
||||||
createdByUserId: Optional[str] = Field(None, description="User who created/caused this transaction")
|
createdByUserId: Optional[str] = Field(None, description="User who created/caused this transaction", json_schema_extra={"label": "Erstellt von Benutzer"})
|
||||||
|
|
||||||
# AI call metadata (for per-call analytics)
|
# AI call metadata (for per-call analytics)
|
||||||
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
|
processingTime: Optional[float] = Field(None, description="Processing time in seconds", json_schema_extra={"label": "Verarbeitungszeit (s)"})
|
||||||
bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model")
|
bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model", json_schema_extra={"label": "Gesendete Bytes"})
|
||||||
bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model")
|
bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model", json_schema_extra={"label": "Empfangene Bytes"})
|
||||||
errorCount: Optional[int] = Field(None, description="Number of errors in this call")
|
errorCount: Optional[int] = Field(None, description="Number of errors in this call", json_schema_extra={"label": "Fehleranzahl"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"BillingTransaction",
|
|
||||||
{"en": "Billing Transaction", "de": "Transaktion"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID"},
|
|
||||||
"accountId": {"en": "Account ID", "de": "Konto-ID"},
|
|
||||||
"transactionType": {"en": "Type", "de": "Typ"},
|
|
||||||
"amount": {"en": "Amount (CHF)", "de": "Betrag (CHF)"},
|
|
||||||
"description": {"en": "Description", "de": "Beschreibung"},
|
|
||||||
"referenceType": {"en": "Reference Type", "de": "Referenztyp"},
|
|
||||||
"referenceId": {"en": "Reference ID", "de": "Referenz-ID"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "de": "Workflow-ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID"},
|
|
||||||
"featureCode": {"en": "Feature Code", "de": "Feature-Code"},
|
|
||||||
"aicoreProvider": {"en": "AI Provider", "de": "AI-Anbieter"},
|
|
||||||
"aicoreModel": {"en": "AI Model", "de": "AI-Modell"},
|
|
||||||
"createdByUserId": {"en": "Created By User", "de": "Erstellt von Benutzer"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Abrechnungseinstellungen")
|
||||||
class BillingSettings(BaseModel):
|
class BillingSettings(BaseModel):
|
||||||
"""Billing settings per mandate. Only PREPAY_MANDATE model."""
|
"""Billing settings per mandate. Only PREPAY_MANDATE model."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
description="Primary key",
|
||||||
|
json_schema_extra={"label": "ID"},
|
||||||
|
)
|
||||||
|
mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)", json_schema_extra={"label": "Mandanten-ID"})
|
||||||
|
|
||||||
|
warningThresholdPercent: float = Field(
|
||||||
|
default=10.0,
|
||||||
|
description="Warning threshold as percentage",
|
||||||
|
json_schema_extra={"label": "Warnschwelle (%)"},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)")
|
|
||||||
|
|
||||||
warningThresholdPercent: float = Field(default=10.0, description="Warning threshold as percentage")
|
|
||||||
|
|
||||||
# Stripe
|
# Stripe
|
||||||
stripeCustomerId: Optional[str] = Field(None, description="Stripe Customer ID (cus_xxx) — one per mandate")
|
stripeCustomerId: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Stripe Customer ID (cus_xxx) — one per mandate",
|
||||||
|
json_schema_extra={"label": "Stripe-Kunden-ID"},
|
||||||
|
)
|
||||||
|
|
||||||
# Auto-Recharge for AI budget
|
# Auto-Recharge for AI budget
|
||||||
autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low")
|
autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low", json_schema_extra={"label": "Auto-Nachladung"})
|
||||||
rechargeAmountCHF: float = Field(default=10.0, description="Amount per auto-recharge (CHF, prepaid via Stripe)")
|
rechargeAmountCHF: float = Field(
|
||||||
rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month")
|
default=10.0,
|
||||||
rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month")
|
description="Amount per auto-recharge (CHF, prepaid via Stripe)",
|
||||||
monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset")
|
json_schema_extra={"label": "Nachladebetrag (CHF)"},
|
||||||
|
)
|
||||||
|
rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month", json_schema_extra={"label": "Max. Nachladungen/Monat"})
|
||||||
|
rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month", json_schema_extra={"label": "Nachladungen diesen Monat"})
|
||||||
|
monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset", json_schema_extra={"label": "Monats-Reset"})
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
notifyEmails: List[str] = Field(
|
notifyEmails: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Email addresses for billing alerts (pool exhausted, warnings, etc.)",
|
description="Email addresses for billing alerts (pool exhausted, warnings, etc.)",
|
||||||
|
json_schema_extra={"label": "E-Mails fuer Billing-Alerts (Inhaber/Admin)"},
|
||||||
)
|
)
|
||||||
notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached")
|
notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached", json_schema_extra={"label": "Bei Warnung benachrichtigen"})
|
||||||
|
|
||||||
# Storage overage (high-watermark within subscription period; resets on new period)
|
# Storage overage (high-watermark within subscription period; resets on new period)
|
||||||
storageHighWatermarkMB: float = Field(
|
storageHighWatermarkMB: float = Field(
|
||||||
default=0.0, description="Peak indexed data volume MB this billing period"
|
default=0.0,
|
||||||
|
description="Peak indexed data volume MB this billing period",
|
||||||
|
json_schema_extra={"label": "Speicher-Peak (MB)"},
|
||||||
)
|
)
|
||||||
storagePeriodStartAt: Optional[datetime] = Field(
|
storagePeriodStartAt: Optional[datetime] = Field(
|
||||||
None, description="Subscription billing period start used for storage reset"
|
None,
|
||||||
|
description="Subscription billing period start used for storage reset",
|
||||||
|
json_schema_extra={"label": "Speicher-Periodenbeginn"},
|
||||||
)
|
)
|
||||||
storageBilledUpToMB: float = Field(
|
storageBilledUpToMB: float = Field(
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="Overage MB already debited this period (above plan-included volume)",
|
description="Overage MB already debited this period (above plan-included volume)",
|
||||||
|
json_schema_extra={"label": "Speicher abgerechneter Überhang (MB)"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"BillingSettings",
|
|
||||||
{"en": "Billing Settings", "de": "Abrechnungseinstellungen"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
|
|
||||||
"warningThresholdPercent": {"en": "Warning Threshold (%)", "de": "Warnschwelle (%)"},
|
|
||||||
"stripeCustomerId": {"en": "Stripe Customer ID", "de": "Stripe-Kunden-ID"},
|
|
||||||
"autoRechargeEnabled": {"en": "Auto-Recharge", "de": "Auto-Nachladung"},
|
|
||||||
"rechargeAmountCHF": {"en": "Recharge Amount (CHF)", "de": "Nachladebetrag (CHF)"},
|
|
||||||
"rechargeMaxPerMonth": {"en": "Max Recharges/Month", "de": "Max. Nachladungen/Monat"},
|
|
||||||
"notifyEmails": {
|
|
||||||
"en": "Billing notification emails (owner / admin)",
|
|
||||||
"de": "E-Mails fuer Billing-Alerts (Inhaber/Admin)",
|
|
||||||
},
|
|
||||||
"notifyOnWarning": {"en": "Notify on Warning", "de": "Bei Warnung benachrichtigen"},
|
|
||||||
"storageHighWatermarkMB": {"en": "Storage peak (MB)", "de": "Speicher-Peak (MB)"},
|
|
||||||
"storagePeriodStartAt": {"en": "Storage period start", "de": "Speicher-Periodenbeginn"},
|
|
||||||
"storageBilledUpToMB": {
|
|
||||||
"en": "Storage billed overage (MB)",
|
|
||||||
"de": "Speicher abgerechneter Überhang (MB)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StripeWebhookEvent(BaseModel):
|
class StripeWebhookEvent(BaseModel):
|
||||||
"""Stores processed Stripe webhook event IDs for idempotency."""
|
"""Stores processed Stripe webhook event IDs for idempotency."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
description="Primary key",
|
||||||
)
|
)
|
||||||
event_id: str = Field(..., description="Stripe event ID (evt_xxx)")
|
event_id: str = Field(..., description="Stripe event ID (evt_xxx)")
|
||||||
processed_at: datetime = Field(
|
processed_at: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(timezone.utc),
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
description="When the event was processed"
|
description="When the event was processed",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Nutzungsstatistik")
|
||||||
class UsageStatistics(BaseModel):
|
class UsageStatistics(BaseModel):
|
||||||
"""Aggregated usage statistics for quick retrieval."""
|
"""Aggregated usage statistics for quick retrieval."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()), description="Primary key"
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
description="Primary key",
|
||||||
|
json_schema_extra={"label": "ID"},
|
||||||
)
|
)
|
||||||
accountId: str = Field(..., description="Foreign key to BillingAccount")
|
accountId: str = Field(..., description="Foreign key to BillingAccount", json_schema_extra={"label": "Konto-ID"})
|
||||||
periodType: PeriodTypeEnum = Field(..., description="Period type")
|
periodType: PeriodTypeEnum = Field(..., description="Period type", json_schema_extra={"label": "Periodentyp"})
|
||||||
periodStart: date = Field(..., description="Period start date")
|
periodStart: date = Field(..., description="Period start date", json_schema_extra={"label": "Periodenbeginn"})
|
||||||
|
|
||||||
# Aggregated values
|
# Aggregated values
|
||||||
totalCostCHF: float = Field(default=0.0, description="Total cost in CHF")
|
totalCostCHF: float = Field(default=0.0, description="Total cost in CHF", json_schema_extra={"label": "Gesamtkosten (CHF)"})
|
||||||
transactionCount: int = Field(default=0, description="Number of transactions")
|
transactionCount: int = Field(default=0, description="Number of transactions", json_schema_extra={"label": "Anzahl Transaktionen"})
|
||||||
|
|
||||||
# Breakdown by provider
|
# Breakdown by provider
|
||||||
costByProvider: Dict[str, float] = Field(
|
costByProvider: Dict[str, float] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})"
|
description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})",
|
||||||
|
json_schema_extra={"label": "Kosten nach Anbieter"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Breakdown by feature
|
# Breakdown by feature
|
||||||
costByFeature: Dict[str, float] = Field(
|
costByFeature: Dict[str, float] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})"
|
description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})",
|
||||||
|
json_schema_extra={"label": "Kosten nach Feature"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UsageStatistics",
|
|
||||||
{"en": "Usage Statistics", "de": "Nutzungsstatistik"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID"},
|
|
||||||
"accountId": {"en": "Account ID", "de": "Konto-ID"},
|
|
||||||
"periodType": {"en": "Period Type", "de": "Periodentyp"},
|
|
||||||
"periodStart": {"en": "Period Start", "de": "Periodenbeginn"},
|
|
||||||
"totalCostCHF": {"en": "Total Cost (CHF)", "de": "Gesamtkosten (CHF)"},
|
|
||||||
"transactionCount": {"en": "Transaction Count", "de": "Anzahl Transaktionen"},
|
|
||||||
"costByProvider": {"en": "Cost by Provider", "de": "Kosten nach Anbieter"},
|
|
||||||
"costByFeature": {"en": "Cost by Feature", "de": "Kosten nach Feature"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Response Models for API
|
# Response Models for API
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -277,4 +244,3 @@ class BillingCheckResult(BaseModel):
|
||||||
subscriptionUiPath: Optional[str] = None
|
subscriptionUiPath: Optional[str] = None
|
||||||
userAction: Optional[str] = None
|
userAction: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,66 +9,81 @@ Google Drive folder, FTP directory, etc.) for agent-accessible data containers.
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Datenquelle")
|
||||||
class DataSource(PowerOnModel):
|
class DataSource(PowerOnModel):
|
||||||
"""Configured external data source linked to a UserConnection."""
|
"""Konfigurierte externe Datenquelle verknuepft mit einer UserConnection."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
id: str = Field(
|
||||||
connectionId: str = Field(description="FK to UserConnection")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
sourceType: str = Field(
|
description="Primary key",
|
||||||
description="sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder, clickupList (path under /team/...)"
|
json_schema_extra={"label": "ID"},
|
||||||
|
)
|
||||||
|
connectionId: str = Field(
|
||||||
|
description="FK to UserConnection",
|
||||||
|
json_schema_extra={"label": "Verbindungs-ID"},
|
||||||
|
)
|
||||||
|
sourceType: str = Field(
|
||||||
|
description="sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder, clickupList (path under /team/...)",
|
||||||
|
json_schema_extra={"label": "Quellentyp"},
|
||||||
|
)
|
||||||
|
path: str = Field(
|
||||||
|
description="External path (e.g. '/sites/MySite/Documents/Reports')",
|
||||||
|
json_schema_extra={"label": "Pfad"},
|
||||||
|
)
|
||||||
|
label: str = Field(
|
||||||
|
description="User-visible label (often the last path segment)",
|
||||||
|
json_schema_extra={"label": "Bezeichnung"},
|
||||||
)
|
)
|
||||||
path: str = Field(description="External path (e.g. '/sites/MySite/Documents/Reports')")
|
|
||||||
label: str = Field(description="User-visible label (often the last path segment)")
|
|
||||||
displayPath: Optional[str] = Field(
|
displayPath: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Human-readable full path for UI (connection-relative, slash-separated)",
|
description="Human-readable full path for UI (connection-relative, slash-separated)",
|
||||||
|
json_schema_extra={"label": "Anzeigepfad"},
|
||||||
|
)
|
||||||
|
featureInstanceId: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Scoped to feature instance",
|
||||||
|
json_schema_extra={"label": "Feature-Instanz"},
|
||||||
|
)
|
||||||
|
mandateId: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Mandate scope",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID"},
|
||||||
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Owner user ID",
|
||||||
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
|
)
|
||||||
|
autoSync: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Automatically sync on schedule",
|
||||||
|
json_schema_extra={"label": "Auto-Sync"},
|
||||||
|
)
|
||||||
|
lastSynced: Optional[float] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Last sync timestamp",
|
||||||
|
json_schema_extra={"label": "Letzter Sync"},
|
||||||
)
|
)
|
||||||
featureInstanceId: Optional[str] = Field(default=None, description="Scoped to feature instance")
|
|
||||||
mandateId: Optional[str] = Field(default=None, description="Mandate scope")
|
|
||||||
userId: str = Field(default="", description="Owner user ID")
|
|
||||||
autoSync: bool = Field(default=False, description="Automatically sync on schedule")
|
|
||||||
lastSynced: Optional[float] = Field(default=None, description="Last sync timestamp")
|
|
||||||
scope: str = Field(
|
scope: str = Field(
|
||||||
default="personal",
|
default="personal",
|
||||||
description="Data visibility scope: personal, featureInstance, mandate, global",
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
||||||
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
||||||
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
||||||
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
||||||
]}
|
]},
|
||||||
)
|
)
|
||||||
neutralize: bool = Field(
|
neutralize: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this data source should be neutralized before AI processing",
|
description="Whether this data source should be neutralized before AI processing",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"DataSource",
|
|
||||||
{"en": "Data Source", "de": "Datenquelle", "fr": "Source de données"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"connectionId": {"en": "Connection ID", "de": "Verbindungs-ID", "fr": "ID de connexion"},
|
|
||||||
"sourceType": {"en": "Source Type", "de": "Quellentyp", "fr": "Type de source"},
|
|
||||||
"path": {"en": "Path", "de": "Pfad", "fr": "Chemin"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"displayPath": {"en": "Display path", "de": "Anzeigepfad", "fr": "Chemin affiché"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance de fonctionnalité"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
|
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
|
||||||
"autoSync": {"en": "Auto Sync", "de": "Auto-Sync", "fr": "Synchro auto"},
|
|
||||||
"lastSynced": {"en": "Last Synced", "de": "Letzter Sync", "fr": "Dernier sync"},
|
|
||||||
"scope": {"en": "Scope", "de": "Sichtbarkeit"},
|
|
||||||
"neutralize": {"en": "Neutralize", "de": "Neutralisieren"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalEntry(BaseModel):
|
class ExternalEntry(BaseModel):
|
||||||
"""An item (file or folder) from an external data source."""
|
"""An item (file or folder) from an external data source."""
|
||||||
name: str = Field(description="Item name")
|
name: str = Field(description="Item name")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Document reference models for typed document references in workflows.
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
class DocumentReference(BaseModel):
|
class DocumentReference(BaseModel):
|
||||||
|
|
@ -14,11 +14,19 @@ class DocumentReference(BaseModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dokumentlisten-Referenz")
|
||||||
class DocumentListReference(DocumentReference):
|
class DocumentListReference(DocumentReference):
|
||||||
"""Reference to a document list via message label"""
|
"""Reference to a document list via message label"""
|
||||||
messageId: Optional[str] = Field(None, description="Optional message ID for cross-round references")
|
messageId: Optional[str] = Field(
|
||||||
label: str = Field(description="Document list label")
|
None,
|
||||||
|
description="Optional message ID for cross-round references",
|
||||||
|
json_schema_extra={"label": "Nachrichten-ID"},
|
||||||
|
)
|
||||||
|
label: str = Field(
|
||||||
|
description="Document list label",
|
||||||
|
json_schema_extra={"label": "Bezeichnung"},
|
||||||
|
)
|
||||||
|
|
||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
"""Convert to string format: docList:messageId:label or docList:label"""
|
"""Convert to string format: docList:messageId:label or docList:label"""
|
||||||
if self.messageId:
|
if self.messageId:
|
||||||
|
|
@ -26,11 +34,19 @@ class DocumentListReference(DocumentReference):
|
||||||
return f"docList:{self.label}"
|
return f"docList:{self.label}"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dokumentelement-Referenz")
|
||||||
class DocumentItemReference(DocumentReference):
|
class DocumentItemReference(DocumentReference):
|
||||||
"""Reference to a specific document item"""
|
"""Reference to a specific document item"""
|
||||||
documentId: str = Field(description="Document ID")
|
documentId: str = Field(
|
||||||
fileName: Optional[str] = Field(None, description="Optional file name")
|
description="Document ID",
|
||||||
|
json_schema_extra={"label": "Dokument-ID"},
|
||||||
|
)
|
||||||
|
fileName: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional file name",
|
||||||
|
json_schema_extra={"label": "Dateiname"},
|
||||||
|
)
|
||||||
|
|
||||||
def to_string(self) -> str:
|
def to_string(self) -> str:
|
||||||
"""Convert to string format: docItem:documentId:fileName or docItem:documentId"""
|
"""Convert to string format: docItem:documentId:fileName or docItem:documentId"""
|
||||||
if self.fileName:
|
if self.fileName:
|
||||||
|
|
@ -38,21 +54,23 @@ class DocumentItemReference(DocumentReference):
|
||||||
return f"docItem:{self.documentId}"
|
return f"docItem:{self.documentId}"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dokumentreferenz-Liste")
|
||||||
class DocumentReferenceList(BaseModel):
|
class DocumentReferenceList(BaseModel):
|
||||||
"""List of document references with conversion methods"""
|
"""List of document references with conversion methods"""
|
||||||
references: List[DocumentReference] = Field(
|
references: List[DocumentReference] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of document references"
|
description="List of document references",
|
||||||
|
json_schema_extra={"label": "Referenzen"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_string_list(self) -> List[str]:
|
def to_string_list(self) -> List[str]:
|
||||||
"""Convert all references to string list"""
|
"""Convert all references to string list"""
|
||||||
return [ref.to_string() for ref in self.references]
|
return [ref.to_string() for ref in self.references]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string_list(cls, stringList: List[str]) -> "DocumentReferenceList":
|
def from_string_list(cls, stringList: List[str]) -> "DocumentReferenceList":
|
||||||
"""Parse string list to typed references
|
"""Parse string list to typed references
|
||||||
|
|
||||||
Supports formats:
|
Supports formats:
|
||||||
- docList:label
|
- docList:label
|
||||||
- docList:messageId:label
|
- docList:messageId:label
|
||||||
|
|
@ -60,13 +78,13 @@ class DocumentReferenceList(BaseModel):
|
||||||
- docItem:documentId:fileName
|
- docItem:documentId:fileName
|
||||||
"""
|
"""
|
||||||
references = []
|
references = []
|
||||||
|
|
||||||
for refStr in stringList:
|
for refStr in stringList:
|
||||||
if not refStr or not isinstance(refStr, str):
|
if not refStr or not isinstance(refStr, str):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
refStr = refStr.strip()
|
refStr = refStr.strip()
|
||||||
|
|
||||||
# Parse docList: references
|
# Parse docList: references
|
||||||
if refStr.startswith("docList:"):
|
if refStr.startswith("docList:"):
|
||||||
parts = refStr[8:].split(":", 1) # Remove "docList:" prefix
|
parts = refStr[8:].split(":", 1) # Remove "docList:" prefix
|
||||||
|
|
@ -77,7 +95,7 @@ class DocumentReferenceList(BaseModel):
|
||||||
elif len(parts) == 1 and parts[0]:
|
elif len(parts) == 1 and parts[0]:
|
||||||
# docList:label
|
# docList:label
|
||||||
references.append(DocumentListReference(label=parts[0]))
|
references.append(DocumentListReference(label=parts[0]))
|
||||||
|
|
||||||
# Parse docItem: references
|
# Parse docItem: references
|
||||||
elif refStr.startswith("docItem:"):
|
elif refStr.startswith("docItem:"):
|
||||||
parts = refStr[8:].split(":", 1) # Remove "docItem:" prefix
|
parts = refStr[8:].split(":", 1) # Remove "docItem:" prefix
|
||||||
|
|
@ -88,33 +106,12 @@ class DocumentReferenceList(BaseModel):
|
||||||
elif len(parts) == 1 and parts[0]:
|
elif len(parts) == 1 and parts[0]:
|
||||||
# docItem:documentId
|
# docItem:documentId
|
||||||
references.append(DocumentItemReference(documentId=parts[0]))
|
references.append(DocumentItemReference(documentId=parts[0]))
|
||||||
|
|
||||||
# Unknown format - skip or log warning
|
# Unknown format - skip or log warning
|
||||||
else:
|
else:
|
||||||
# Try to parse as simple string (backward compatibility)
|
# Try to parse as simple string (backward compatibility)
|
||||||
# Assume it's a label if it doesn't match known patterns
|
# Assume it's a label if it doesn't match known patterns
|
||||||
if refStr:
|
if refStr:
|
||||||
references.append(DocumentListReference(label=refStr))
|
references.append(DocumentListReference(label=refStr))
|
||||||
|
|
||||||
return cls(references=references)
|
return cls(references=references)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"DocumentReference",
|
|
||||||
{"en": "Document Reference", "fr": "Référence de document"},
|
|
||||||
{
|
|
||||||
"messageId": {"en": "Message ID", "fr": "ID du message"},
|
|
||||||
"label": {"en": "Label", "fr": "Étiquette"},
|
|
||||||
"documentId": {"en": "Document ID", "fr": "ID du document"},
|
|
||||||
"fileName": {"en": "File Name", "fr": "Nom du fichier"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"DocumentReferenceList",
|
|
||||||
{"en": "Document Reference List", "fr": "Liste de références de documents"},
|
|
||||||
{
|
|
||||||
"references": {"en": "References", "fr": "Références"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,54 +9,69 @@ so the agent can query structured feature data (e.g. TrusteePosition rows).
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Feature-Datenquelle")
|
||||||
class FeatureDataSource(PowerOnModel):
|
class FeatureDataSource(PowerOnModel):
|
||||||
"""A feature-instance table attached as data source in the AI workspace."""
|
"""Feature-Instanz-Tabelle als Datenquelle im AI-Workspace."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
id: str = Field(
|
||||||
featureInstanceId: str = Field(description="FK to FeatureInstance")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
featureCode: str = Field(description="Feature code (e.g. trustee, commcoach)")
|
description="Primary key",
|
||||||
tableName: str = Field(description="Table name from DATA_OBJECTS meta (e.g. TrusteePosition)")
|
json_schema_extra={"label": "ID"},
|
||||||
objectKey: str = Field(description="RBAC object key (e.g. data.feature.trustee.TrusteePosition)")
|
)
|
||||||
label: str = Field(description="User-visible label")
|
featureInstanceId: str = Field(
|
||||||
mandateId: str = Field(default="", description="Mandate scope")
|
description="FK to FeatureInstance",
|
||||||
userId: str = Field(default="", description="Owner user ID")
|
json_schema_extra={"label": "Feature-Instanz"},
|
||||||
workspaceInstanceId: str = Field(description="Workspace instance where this source is used")
|
)
|
||||||
|
featureCode: str = Field(
|
||||||
|
description="Feature code (e.g. trustee, commcoach)",
|
||||||
|
json_schema_extra={"label": "Feature"},
|
||||||
|
)
|
||||||
|
tableName: str = Field(
|
||||||
|
description="Table name from DATA_OBJECTS meta (e.g. TrusteePosition)",
|
||||||
|
json_schema_extra={"label": "Tabelle"},
|
||||||
|
)
|
||||||
|
objectKey: str = Field(
|
||||||
|
description="RBAC object key (e.g. data.feature.trustee.TrusteePosition)",
|
||||||
|
json_schema_extra={"label": "Objekt-Schluessel"},
|
||||||
|
)
|
||||||
|
label: str = Field(
|
||||||
|
description="User-visible label",
|
||||||
|
json_schema_extra={"label": "Bezeichnung"},
|
||||||
|
)
|
||||||
|
mandateId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Mandate scope",
|
||||||
|
json_schema_extra={"label": "Mandant"},
|
||||||
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Owner user ID",
|
||||||
|
json_schema_extra={"label": "Benutzer"},
|
||||||
|
)
|
||||||
|
workspaceInstanceId: str = Field(
|
||||||
|
description="Workspace instance where this source is used",
|
||||||
|
json_schema_extra={"label": "Workspace"},
|
||||||
|
)
|
||||||
scope: str = Field(
|
scope: str = Field(
|
||||||
default="personal",
|
default="personal",
|
||||||
description="Data visibility scope: personal, featureInstance, mandate, global",
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
||||||
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
||||||
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
||||||
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
||||||
]}
|
]},
|
||||||
)
|
)
|
||||||
neutralize: bool = Field(
|
neutralize: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this data source should be neutralized before AI processing",
|
description="Whether this data source should be neutralized before AI processing",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
|
||||||
)
|
)
|
||||||
recordFilter: Optional[Dict[str, str]] = Field(
|
recordFilter: Optional[Dict[str, str]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Record-level filter applied when querying this table, e.g. {'sessionId': 'abc-123'}",
|
description="Record-level filter applied when querying this table, e.g. {'sessionId': 'abc-123'}",
|
||||||
|
json_schema_extra={"label": "Datensatzfilter"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"FeatureDataSource",
|
|
||||||
{"en": "Feature Data Source", "de": "Feature-Datenquelle", "fr": "Source de données fonctionnalité"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
"featureCode": {"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
|
|
||||||
"tableName": {"en": "Table", "de": "Tabelle", "fr": "Table"},
|
|
||||||
"objectKey": {"en": "Object Key", "de": "Objekt-Schlüssel", "fr": "Clé objet"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
"userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
|
|
||||||
"workspaceInstanceId": {"en": "Workspace", "de": "Workspace", "fr": "Espace de travail"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -6,85 +6,56 @@ import uuid
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.datamodels.datamodelUtils import TextMultilingual
|
from modules.datamodels.datamodelUtils import TextMultilingual
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Feature")
|
||||||
class Feature(PowerOnModel):
|
class Feature(PowerOnModel):
|
||||||
"""
|
"""Feature-Definition (global, z.B. 'trustee', 'chatbot'). Verfuegbare Funktionalitaeten der Plattform."""
|
||||||
Feature-Definition (global, z.B. 'trustee', 'chatbot').
|
|
||||||
Features sind die verfügbaren Funktionalitäten der Plattform.
|
|
||||||
"""
|
|
||||||
code: str = Field(
|
code: str = Field(
|
||||||
description="Unique feature code (Primary Key), z.B. 'trustee', 'chatbot'",
|
description="Unique feature code (Primary Key), z.B. 'trustee', 'chatbot'",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Code", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
label: TextMultilingual = Field(
|
label: TextMultilingual = Field(
|
||||||
description="Feature label in multiple languages (I18n)",
|
description="Feature label in multiple languages (I18n)",
|
||||||
json_schema_extra={"frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Bezeichnung", "frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
icon: str = Field(
|
icon: str = Field(
|
||||||
default="",
|
default="",
|
||||||
description="Icon identifier for the feature",
|
description="Icon identifier for the feature",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Symbol", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Feature-Instanz")
|
||||||
"Feature",
|
|
||||||
{"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
|
|
||||||
{
|
|
||||||
"code": {"en": "Code", "de": "Code", "fr": "Code"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"icon": {"en": "Icon", "de": "Symbol", "fr": "Icône"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureInstance(PowerOnModel):
|
class FeatureInstance(PowerOnModel):
|
||||||
"""
|
"""Instanz eines Features in einem Mandanten. Ein Mandant kann mehrere Instanzen desselben Features haben."""
|
||||||
Instanz eines Features in einem Mandanten.
|
|
||||||
Ein Mandant kann mehrere Instanzen desselben Features haben.
|
|
||||||
"""
|
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the feature instance",
|
description="Unique ID of the feature instance",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
featureCode: str = Field(
|
featureCode: str = Field(
|
||||||
description="FK → Feature.code",
|
description="FK -> Feature.code",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Feature", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="FK → Mandate.id (CASCADE DELETE)",
|
description="FK -> Mandate.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
default="",
|
default="",
|
||||||
description="Instance label, z.B. 'Buchhaltung 2025'",
|
description="Instance label, z.B. 'Buchhaltung 2025'",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Bezeichnung", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether this feature instance is enabled",
|
description="Whether this feature instance is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
config: Optional[Dict[str, Any]] = Field(
|
config: Optional[Dict[str, Any]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Instance-specific configuration (JSONB). Structure depends on featureCode.",
|
description="Instance-specific configuration (JSONB). Structure depends on featureCode.",
|
||||||
json_schema_extra={"frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Konfiguration", "frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"FeatureInstance",
|
|
||||||
{"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance de fonctionnalité"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"featureCode": {"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
|
||||||
"config": {"en": "Configuration", "de": "Konfiguration", "fr": "Configuration"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,34 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dateiordner")
|
||||||
class FileFolder(PowerOnModel):
|
class FileFolder(PowerOnModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
"""Hierarchischer Ordner fuer die Dateiverwaltung."""
|
||||||
name: str = Field(description="Folder name", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
id: str = Field(
|
||||||
parentId: Optional[str] = Field(default=None, description="Parent folder ID (null = root)", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
mandateId: Optional[str] = Field(default=None, description="Mandate context", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
description="Primary key",
|
||||||
featureInstanceId: Optional[str] = Field(default=None, description="Feature instance context", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
name: str = Field(
|
||||||
registerModelLabels(
|
description="Folder name",
|
||||||
"FileFolder",
|
json_schema_extra={"label": "Name", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
|
||||||
{"en": "File Folder", "fr": "Dossier de fichiers"},
|
)
|
||||||
{
|
parentId: Optional[str] = Field(
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
default=None,
|
||||||
"name": {"en": "Name", "fr": "Nom"},
|
description="Parent folder ID (null = root)",
|
||||||
"parentId": {"en": "Parent Folder", "fr": "Dossier parent"},
|
json_schema_extra={"label": "Uebergeordneter Ordner", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
)
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
|
mandateId: Optional[str] = Field(
|
||||||
},
|
default=None,
|
||||||
)
|
description="Mandate context",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
featureInstanceId: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Feature instance context",
|
||||||
|
json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,66 +5,110 @@
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Datei")
|
||||||
class FileItem(PowerOnModel):
|
class FileItem(PowerOnModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
"""Metadaten einer gespeicherten Datei."""
|
||||||
mandateId: Optional[str] = Field(default="", description="ID of the mandate this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
id: str = Field(
|
||||||
featureInstanceId: Optional[str] = Field(default="", description="ID of the feature instance this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
fileName: str = Field(description="Name of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
description="Primary key",
|
||||||
mimeType: str = Field(description="MIME type of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
json_schema_extra={"label": "ID", "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})
|
)
|
||||||
fileSize: int = Field(description="Size of the file in bytes", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
|
mandateId: Optional[str] = Field(
|
||||||
tags: Optional[List[str]] = Field(default=None, description="Tags for categorization and search", json_schema_extra={"frontend_type": "tags", "frontend_readonly": False, "frontend_required": False})
|
default="",
|
||||||
folderId: Optional[str] = Field(default=None, description="ID of the parent folder", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
description="ID of the mandate this file belongs to",
|
||||||
description: Optional[str] = Field(default=None, description="User-provided description of the file", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False})
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
status: Optional[str] = Field(default=None, description="Processing status: pending, extracted, embedding, indexed, failed", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
)
|
||||||
|
featureInstanceId: Optional[str] = Field(
|
||||||
|
default="",
|
||||||
|
description="ID of the feature instance this file belongs to",
|
||||||
|
json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"},
|
||||||
|
)
|
||||||
|
fileName: str = Field(
|
||||||
|
description="Name of the file",
|
||||||
|
json_schema_extra={"label": "Dateiname", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
|
||||||
|
)
|
||||||
|
mimeType: str = Field(
|
||||||
|
description="MIME type of the file",
|
||||||
|
json_schema_extra={"label": "MIME-Typ", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
fileHash: str = Field(
|
||||||
|
description="Hash of the file",
|
||||||
|
json_schema_extra={"label": "Datei-Hash", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
fileSize: int = Field(
|
||||||
|
description="Size of the file in bytes",
|
||||||
|
json_schema_extra={"label": "Dateigroesse", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
tags: Optional[List[str]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Tags for categorization and search",
|
||||||
|
json_schema_extra={"label": "Tags", "frontend_type": "tags", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
folderId: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="ID of the parent folder",
|
||||||
|
json_schema_extra={"label": "Ordner-ID", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
description: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="User-provided description of the file",
|
||||||
|
json_schema_extra={"label": "Beschreibung", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
status: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Processing status: pending, extracted, embedding, indexed, failed",
|
||||||
|
json_schema_extra={"label": "Status", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
scope: str = Field(
|
scope: str = Field(
|
||||||
default="personal",
|
default="personal",
|
||||||
description="Data visibility scope: personal, featureInstance, mandate, global",
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
||||||
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
||||||
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
||||||
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
||||||
]}
|
]},
|
||||||
)
|
)
|
||||||
neutralize: bool = Field(
|
neutralize: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this file should be neutralized before AI processing",
|
description="Whether this file should be neutralized before AI processing",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
|
||||||
)
|
)
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"FileItem",
|
|
||||||
{"en": "File Item", "fr": "Élément de fichier"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité"},
|
|
||||||
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
|
||||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
||||||
"fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
|
|
||||||
"fileSize": {"en": "File Size", "fr": "Taille du fichier"},
|
|
||||||
"tags": {"en": "Tags", "fr": "Tags"},
|
|
||||||
"folderId": {"en": "Folder ID", "fr": "ID du dossier"},
|
|
||||||
"description": {"en": "Description", "fr": "Description"},
|
|
||||||
"status": {"en": "Status", "fr": "Statut"},
|
|
||||||
"scope": {"en": "Scope", "de": "Sichtbarkeit"},
|
|
||||||
"neutralize": {"en": "Neutralize", "de": "Neutralisieren"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@i18nModel("Datei-Vorschau")
|
||||||
class FilePreview(BaseModel):
|
class FilePreview(BaseModel):
|
||||||
content: Union[str, bytes] = Field(description="File content (text or binary)")
|
"""Vorschau-Inhalt einer Datei fuer die Anzeige."""
|
||||||
mimeType: str = Field(description="MIME type of the file")
|
content: Union[str, bytes] = Field(
|
||||||
fileName: str = Field(description="Original fileName")
|
description="File content (text or binary)",
|
||||||
isText: bool = Field(description="Whether the content is text (True) or binary (False)")
|
json_schema_extra={"label": "Inhalt"},
|
||||||
encoding: Optional[str] = Field(None, description="Text encoding if content is text")
|
)
|
||||||
size: int = Field(description="Size of the content in bytes")
|
mimeType: str = Field(
|
||||||
|
description="MIME type of the file",
|
||||||
|
json_schema_extra={"label": "MIME-Typ"},
|
||||||
|
)
|
||||||
|
fileName: str = Field(
|
||||||
|
description="Original fileName",
|
||||||
|
json_schema_extra={"label": "Dateiname"},
|
||||||
|
)
|
||||||
|
isText: bool = Field(
|
||||||
|
description="Whether the content is text (True) or binary (False)",
|
||||||
|
json_schema_extra={"label": "Ist Text"},
|
||||||
|
)
|
||||||
|
encoding: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Text encoding if content is text",
|
||||||
|
json_schema_extra={"label": "Kodierung"},
|
||||||
|
)
|
||||||
|
size: int = Field(
|
||||||
|
description="Size of the content in bytes",
|
||||||
|
json_schema_extra={"label": "Groesse"},
|
||||||
|
)
|
||||||
|
|
||||||
def toDictWithBase64Encoding(self) -> Dict[str, Any]:
|
def toDictWithBase64Encoding(self) -> Dict[str, Any]:
|
||||||
"""Convert to dictionary with base64 encoding for binary content."""
|
"""Convert to dictionary with base64 encoding for binary content."""
|
||||||
|
|
@ -72,29 +116,21 @@ class FilePreview(BaseModel):
|
||||||
if isinstance(data.get("content"), bytes):
|
if isinstance(data.get("content"), bytes):
|
||||||
data["content"] = base64.b64encode(data["content"]).decode("utf-8")
|
data["content"] = base64.b64encode(data["content"]).decode("utf-8")
|
||||||
return data
|
return data
|
||||||
registerModelLabels(
|
|
||||||
"FilePreview",
|
|
||||||
{"en": "File Preview", "fr": "Aperçu du fichier"},
|
|
||||||
{
|
|
||||||
"content": {"en": "Content", "fr": "Contenu"},
|
|
||||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
||||||
"fileName": {"en": "fileName", "fr": "Nom de fichier"},
|
|
||||||
"isText": {"en": "Is Text", "fr": "Est du texte"},
|
|
||||||
"encoding": {"en": "Encoding", "fr": "Encodage"},
|
|
||||||
"size": {"en": "Size", "fr": "Taille"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dateidaten")
|
||||||
class FileData(PowerOnModel):
|
class FileData(PowerOnModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
"""Rohdaten einer Datei (z.B. Base64)."""
|
||||||
data: str = Field(description="File data content")
|
id: str = Field(
|
||||||
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
registerModelLabels(
|
description="Primary key",
|
||||||
"FileData",
|
json_schema_extra={"label": "ID"},
|
||||||
{"en": "File Data", "fr": "Données de fichier"},
|
)
|
||||||
{
|
data: str = Field(
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
description="File data content",
|
||||||
"data": {"en": "Data", "fr": "Données"},
|
json_schema_extra={"label": "Daten"},
|
||||||
"base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
|
)
|
||||||
},
|
base64Encoded: bool = Field(
|
||||||
)
|
description="Whether the data is base64 encoded",
|
||||||
|
json_schema_extra={"label": "Base64-kodiert"},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ import secrets
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Einladung")
|
||||||
class Invitation(PowerOnModel):
|
class Invitation(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Einladungs-Token für neue User.
|
Einladungs-Token für neue User.
|
||||||
|
|
@ -21,103 +22,76 @@ class Invitation(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the invitation",
|
description="Unique ID of the invitation",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
token: str = Field(
|
token: str = Field(
|
||||||
default_factory=lambda: secrets.token_urlsafe(32),
|
default_factory=lambda: secrets.token_urlsafe(32),
|
||||||
description="Secure invitation token",
|
description="Secure invitation token",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Token", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ziel der Einladung
|
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="FK → Mandate.id - Target mandate for the invitation",
|
description="FK → Mandate.id - Target mandate for the invitation",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
featureInstanceId: Optional[str] = Field(
|
featureInstanceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional FK → FeatureInstance.id - Direct access to specific feature",
|
description="Optional FK → FeatureInstance.id - Direct access to specific feature",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
roleIds: List[str] = Field(
|
roleIds: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of Role IDs to assign to the invited user",
|
description="List of Role IDs to assign to the invited user",
|
||||||
json_schema_extra={"frontend_type": "multiselect", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Rollen", "frontend_type": "multiselect", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Einladungs-Details
|
|
||||||
targetUsername: Optional[str] = Field(
|
targetUsername: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Username of the invited user (must match on acceptance)",
|
description="Username of the invited user (must match on acceptance)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Ziel-Benutzername", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
email: Optional[str] = Field(
|
email: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Email address to send invitation link (optional)",
|
description="Email address to send invitation link (optional)",
|
||||||
json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "E-Mail (optional)", "frontend_type": "email", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
expiresAt: float = Field(
|
expiresAt: float = Field(
|
||||||
description="When the invitation expires (UTC timestamp)",
|
description="When the invitation expires (UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Gueltig bis", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Status
|
|
||||||
usedBy: Optional[str] = Field(
|
usedBy: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID of the person who used the invitation",
|
description="User ID of the person who used the invitation",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Verwendet von", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
usedAt: Optional[float] = Field(
|
usedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When the invitation was used (UTC timestamp)",
|
description="When the invitation was used (UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Verwendet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
revokedAt: Optional[float] = Field(
|
revokedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When the invitation was revoked (UTC timestamp)",
|
description="When the invitation was revoked (UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Widerrufen am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Email-Status
|
|
||||||
emailSent: Optional[bool] = Field(
|
emailSent: Optional[bool] = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether the invitation email was successfully sent",
|
description="Whether the invitation email was successfully sent",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "E-Mail gesendet", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Einschränkungen
|
|
||||||
maxUses: int = Field(
|
maxUses: int = Field(
|
||||||
default=1,
|
default=1,
|
||||||
ge=1,
|
ge=1,
|
||||||
le=100,
|
le=100,
|
||||||
description="Maximum number of times this invitation can be used",
|
description="Maximum number of times this invitation can be used",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Max. Verwendungen", "frontend_type": "number", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
currentUses: int = Field(
|
currentUses: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
ge=0,
|
ge=0,
|
||||||
description="Current number of times this invitation has been used",
|
description="Current number of times this invitation has been used",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Aktuelle Verwendungen", "frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"Invitation",
|
|
||||||
{"en": "Invitation", "de": "Einladung", "fr": "Invitation"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"token": {"en": "Token", "de": "Token", "fr": "Jeton"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
"roleIds": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
|
|
||||||
"targetUsername": {"en": "Target Username", "de": "Ziel-Benutzername", "fr": "Nom d'utilisateur cible"},
|
|
||||||
"email": {"en": "Email (optional)", "de": "E-Mail (optional)", "fr": "Email (optionnel)"},
|
|
||||||
"expiresAt": {"en": "Expires At", "de": "Gültig bis", "fr": "Expire le"},
|
|
||||||
"usedBy": {"en": "Used By", "de": "Verwendet von", "fr": "Utilisé par"},
|
|
||||||
"usedAt": {"en": "Used At", "de": "Verwendet am", "fr": "Utilisé le"},
|
|
||||||
"revokedAt": {"en": "Revoked At", "de": "Widerrufen am", "fr": "Révoqué le"},
|
|
||||||
"emailSent": {"en": "Email Sent", "de": "E-Mail gesendet", "fr": "Email envoyé"},
|
|
||||||
"maxUses": {"en": "Max Uses", "de": "Max. Verwendungen", "fr": "Utilisations max"},
|
|
||||||
"currentUses": {"en": "Current Uses", "de": "Aktuelle Verwendungen", "fr": "Utilisations actuelles"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -15,173 +15,231 @@ Vector fields use json_schema_extra={"db_type": "vector(1536)"} for pgvector.
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Datei-Inhaltsindex")
|
||||||
class FileContentIndex(PowerOnModel):
|
class FileContentIndex(PowerOnModel):
|
||||||
"""Structural index of a file's content objects. Created without AI.
|
"""Struktureller Index der Inhaltsobjekte einer Datei."""
|
||||||
Scope is mirrored from FileItem (poweron_management) at indexing time."""
|
id: str = Field(
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key (typically = fileId)")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
userId: str = Field(description="Owner user ID")
|
description="Primary key (typically = fileId)",
|
||||||
featureInstanceId: str = Field(default="", description="Feature instance scope")
|
json_schema_extra={"label": "ID"},
|
||||||
mandateId: str = Field(default="", description="Mandate scope")
|
)
|
||||||
fileName: str = Field(description="Original file name")
|
userId: str = Field(
|
||||||
mimeType: str = Field(description="MIME type of the file")
|
description="Owner user ID",
|
||||||
containerPath: Optional[str] = Field(default=None, description="Path within a container (e.g. 'archive.zip/folder/report.pdf')")
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
totalObjects: int = Field(default=0, description="Total number of content objects extracted")
|
)
|
||||||
totalSize: int = Field(default=0, description="Total size of all content objects in bytes")
|
featureInstanceId: str = Field(
|
||||||
structure: Dict[str, Any] = Field(default_factory=dict, description="Structural overview (pages, sections, hierarchy)")
|
default="",
|
||||||
objectSummary: List[Dict[str, Any]] = Field(default_factory=list, description="Compact summary per content object")
|
description="Feature instance scope",
|
||||||
extractedAt: float = Field(default_factory=getUtcTimestamp, description="Extraction timestamp")
|
json_schema_extra={"label": "Feature-Instanz-ID"},
|
||||||
status: str = Field(default="pending", description="Processing status: pending, extracted, embedding, indexed, failed")
|
)
|
||||||
|
mandateId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Mandate scope",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID"},
|
||||||
|
)
|
||||||
|
fileName: str = Field(
|
||||||
|
description="Original file name",
|
||||||
|
json_schema_extra={"label": "Dateiname"},
|
||||||
|
)
|
||||||
|
mimeType: str = Field(
|
||||||
|
description="MIME type of the file",
|
||||||
|
json_schema_extra={"label": "MIME-Typ"},
|
||||||
|
)
|
||||||
|
containerPath: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Path within a container (e.g. 'archive.zip/folder/report.pdf')",
|
||||||
|
json_schema_extra={"label": "Container-Pfad"},
|
||||||
|
)
|
||||||
|
totalObjects: int = Field(
|
||||||
|
default=0,
|
||||||
|
description="Total number of content objects extracted",
|
||||||
|
json_schema_extra={"label": "Anzahl Objekte"},
|
||||||
|
)
|
||||||
|
totalSize: int = Field(
|
||||||
|
default=0,
|
||||||
|
description="Total size of all content objects in bytes",
|
||||||
|
json_schema_extra={"label": "Gesamtgroesse"},
|
||||||
|
)
|
||||||
|
structure: Dict[str, Any] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="Structural overview (pages, sections, hierarchy)",
|
||||||
|
json_schema_extra={"label": "Struktur"},
|
||||||
|
)
|
||||||
|
objectSummary: List[Dict[str, Any]] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Compact summary per content object",
|
||||||
|
json_schema_extra={"label": "Objekt-Zusammenfassung"},
|
||||||
|
)
|
||||||
|
extractedAt: float = Field(
|
||||||
|
default_factory=getUtcTimestamp,
|
||||||
|
description="Extraction timestamp",
|
||||||
|
json_schema_extra={"label": "Extrahiert am"},
|
||||||
|
)
|
||||||
|
status: str = Field(
|
||||||
|
default="pending",
|
||||||
|
description="Processing status: pending, extracted, embedding, indexed, failed",
|
||||||
|
json_schema_extra={"label": "Status"},
|
||||||
|
)
|
||||||
scope: str = Field(
|
scope: str = Field(
|
||||||
default="personal",
|
default="personal",
|
||||||
description="Data visibility scope: personal, featureInstance, mandate, global",
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
||||||
|
json_schema_extra={"label": "Sichtbarkeit"},
|
||||||
)
|
)
|
||||||
neutralizationStatus: Optional[str] = Field(
|
neutralizationStatus: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Neutralization status: completed, failed, skipped, None = not required",
|
description="Neutralization status: completed, failed, skipped, None = not required",
|
||||||
|
json_schema_extra={"label": "Neutralisierungsstatus"},
|
||||||
)
|
)
|
||||||
isNeutralized: bool = Field(
|
isNeutralized: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="True if content was neutralized before indexing",
|
description="True if content was neutralized before indexing",
|
||||||
|
json_schema_extra={"label": "Neutralisiert"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Inhalts-Chunk")
|
||||||
"FileContentIndex",
|
|
||||||
{"en": "File Content Index", "fr": "Index du contenu de fichier"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"fileName": {"en": "File Name", "fr": "Nom de fichier"},
|
|
||||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
||||||
"containerPath": {"en": "Container Path", "fr": "Chemin du conteneur"},
|
|
||||||
"totalObjects": {"en": "Total Objects", "fr": "Nombre total d'objets"},
|
|
||||||
"totalSize": {"en": "Total Size", "fr": "Taille totale"},
|
|
||||||
"structure": {"en": "Structure", "fr": "Structure"},
|
|
||||||
"objectSummary": {"en": "Object Summary", "fr": "Résumé des objets"},
|
|
||||||
"extractedAt": {"en": "Extracted At", "fr": "Extrait le"},
|
|
||||||
"status": {"en": "Status", "fr": "Statut"},
|
|
||||||
"scope": {"en": "Scope", "de": "Sichtbarkeit"},
|
|
||||||
"neutralizationStatus": {"en": "Neutralization Status", "de": "Neutralisierungsstatus"},
|
|
||||||
"isNeutralized": {"en": "Is Neutralized", "de": "Neutralisiert"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContentChunk(PowerOnModel):
|
class ContentChunk(PowerOnModel):
|
||||||
"""Persisted content chunk with embedding vector. Reusable across workflows.
|
"""Persistierter Inhalts-Chunk mit Embedding-Vektor."""
|
||||||
Scalar content object (or chunk thereof) with pgvector embedding."""
|
id: str = Field(
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
contentObjectId: str = Field(description="Reference to the content object within FileContentIndex")
|
description="Primary key",
|
||||||
fileId: str = Field(description="FK to the source file")
|
json_schema_extra={"label": "ID"},
|
||||||
userId: str = Field(description="Owner user ID")
|
)
|
||||||
featureInstanceId: str = Field(default="", description="Feature instance scope")
|
contentObjectId: str = Field(
|
||||||
contentType: str = Field(description="Content type: text, image, videostream, audiostream, other")
|
description="Reference to the content object within FileContentIndex",
|
||||||
data: str = Field(description="Content data (text, base64, URL)")
|
json_schema_extra={"label": "Inhaltsobjekt-ID"},
|
||||||
contextRef: Dict[str, Any] = Field(default_factory=dict, description="Context reference (page, position, label)")
|
)
|
||||||
summary: Optional[str] = Field(default=None, description="AI-generated summary (on demand)")
|
fileId: str = Field(
|
||||||
chunkMetadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
description="FK to the source file",
|
||||||
|
json_schema_extra={"label": "Datei-ID"},
|
||||||
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
description="Owner user ID",
|
||||||
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Feature instance scope",
|
||||||
|
json_schema_extra={"label": "Feature-Instanz-ID"},
|
||||||
|
)
|
||||||
|
contentType: str = Field(
|
||||||
|
description="Content type: text, image, videostream, audiostream, other",
|
||||||
|
json_schema_extra={"label": "Inhaltstyp"},
|
||||||
|
)
|
||||||
|
data: str = Field(
|
||||||
|
description="Content data (text, base64, URL)",
|
||||||
|
json_schema_extra={"label": "Daten"},
|
||||||
|
)
|
||||||
|
contextRef: Dict[str, Any] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="Context reference (page, position, label)",
|
||||||
|
json_schema_extra={"label": "Kontext-Referenz"},
|
||||||
|
)
|
||||||
|
summary: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="AI-generated summary (on demand)",
|
||||||
|
json_schema_extra={"label": "Zusammenfassung"},
|
||||||
|
)
|
||||||
|
chunkMetadata: Dict[str, Any] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="Additional metadata",
|
||||||
|
json_schema_extra={"label": "Metadaten"},
|
||||||
|
)
|
||||||
embedding: Optional[List[float]] = Field(
|
embedding: Optional[List[float]] = Field(
|
||||||
default=None, description="pgvector embedding (NOT NULL for text chunks)",
|
default=None,
|
||||||
json_schema_extra={"db_type": "vector(1536)"}
|
description="pgvector embedding (NOT NULL for text chunks)",
|
||||||
|
json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Runden-Speicher")
|
||||||
"ContentChunk",
|
|
||||||
{"en": "Content Chunk", "fr": "Fragment de contenu"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"contentObjectId": {"en": "Content Object ID", "fr": "ID de l'objet de contenu"},
|
|
||||||
"fileId": {"en": "File ID", "fr": "ID du fichier"},
|
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
|
|
||||||
"contentType": {"en": "Content Type", "fr": "Type de contenu"},
|
|
||||||
"data": {"en": "Data", "fr": "Données"},
|
|
||||||
"contextRef": {"en": "Context Reference", "fr": "Référence contextuelle"},
|
|
||||||
"summary": {"en": "Summary", "fr": "Résumé"},
|
|
||||||
"chunkMetadata": {"en": "Metadata", "fr": "Métadonnées"},
|
|
||||||
"embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RoundMemory(PowerOnModel):
|
class RoundMemory(PowerOnModel):
|
||||||
"""Persistent per-round memory for agent tool results, file refs, and decisions.
|
"""Persistenter Speicher pro Agenten-Runde."""
|
||||||
|
id: str = Field(
|
||||||
Stored after each agent round so that RAG can retrieve relevant context
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
even after the ConversationManager summarises older messages away.
|
description="Primary key",
|
||||||
"""
|
json_schema_extra={"label": "ID"},
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
)
|
||||||
workflowId: str = Field(description="FK to the workflow")
|
workflowId: str = Field(
|
||||||
roundNumber: int = Field(default=0, description="Agent round that produced this memory")
|
description="FK to the workflow",
|
||||||
memoryType: str = Field(
|
json_schema_extra={"label": "Workflow-ID"},
|
||||||
description="Category: file_ref, tool_result, decision, data_source_ref"
|
)
|
||||||
|
roundNumber: int = Field(
|
||||||
|
default=0,
|
||||||
|
description="Agent round that produced this memory",
|
||||||
|
json_schema_extra={"label": "Rundennummer"},
|
||||||
|
)
|
||||||
|
memoryType: str = Field(
|
||||||
|
description="Category: file_ref, tool_result, decision, data_source_ref",
|
||||||
|
json_schema_extra={"label": "Speichertyp"},
|
||||||
|
)
|
||||||
|
key: str = Field(
|
||||||
|
description="Dedup key, e.g. 'readFile:<fileId>' or 'plan'",
|
||||||
|
json_schema_extra={"label": "Schluessel"},
|
||||||
|
)
|
||||||
|
summary: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Compact summary (max ~2000 chars)",
|
||||||
|
json_schema_extra={"label": "Zusammenfassung"},
|
||||||
)
|
)
|
||||||
key: str = Field(description="Dedup key, e.g. 'readFile:<fileId>' or 'plan'")
|
|
||||||
summary: str = Field(default="", description="Compact summary (max ~2000 chars)")
|
|
||||||
fullData: Optional[str] = Field(
|
fullData: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Full tool output when small enough (max ~8000 chars)",
|
description="Full tool output when small enough (max ~8000 chars)",
|
||||||
|
json_schema_extra={"label": "Volldaten"},
|
||||||
|
)
|
||||||
|
fileIds: List[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Referenced file IDs",
|
||||||
|
json_schema_extra={"label": "Datei-IDs"},
|
||||||
)
|
)
|
||||||
fileIds: List[str] = Field(default_factory=list, description="Referenced file IDs")
|
|
||||||
embedding: Optional[List[float]] = Field(
|
embedding: Optional[List[float]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Embedding of summary for semantic retrieval",
|
description="Embedding of summary for semantic retrieval",
|
||||||
json_schema_extra={"db_type": "vector(1536)"},
|
json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Workflow-Speicher")
|
||||||
"RoundMemory",
|
|
||||||
{"en": "Round Memory", "fr": "Mémoire de tour"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
|
|
||||||
"roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
|
|
||||||
"memoryType": {"en": "Memory Type", "fr": "Type de mémoire"},
|
|
||||||
"key": {"en": "Key", "fr": "Clé"},
|
|
||||||
"summary": {"en": "Summary", "fr": "Résumé"},
|
|
||||||
"fullData": {"en": "Full Data", "fr": "Données complètes"},
|
|
||||||
"fileIds": {"en": "File IDs", "fr": "IDs de fichier"},
|
|
||||||
"embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowMemory(PowerOnModel):
|
class WorkflowMemory(PowerOnModel):
|
||||||
"""Workflow-scoped key-value cache for entities and facts.
|
"""Workflow-spezifischer Key-Value-Cache fuer Entitaeten und Fakten."""
|
||||||
Extracted during agent rounds, persisted for cross-round and cross-workflow reuse."""
|
id: str = Field(
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
workflowId: str = Field(description="FK to the workflow")
|
description="Primary key",
|
||||||
userId: str = Field(description="Owner user ID")
|
json_schema_extra={"label": "ID"},
|
||||||
featureInstanceId: str = Field(default="", description="Feature instance scope")
|
)
|
||||||
key: str = Field(description="Key identifier (e.g. 'entity:companyName')")
|
workflowId: str = Field(
|
||||||
value: str = Field(description="Extracted value")
|
description="FK to the workflow",
|
||||||
source: str = Field(default="extraction", description="Origin: extraction, tool, conversation, summary")
|
json_schema_extra={"label": "Workflow-ID"},
|
||||||
embedding: Optional[List[float]] = Field(
|
)
|
||||||
default=None, description="Optional embedding for semantic lookup",
|
userId: str = Field(
|
||||||
json_schema_extra={"db_type": "vector(1536)"}
|
description="Owner user ID",
|
||||||
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
|
)
|
||||||
|
featureInstanceId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Feature instance scope",
|
||||||
|
json_schema_extra={"label": "Feature-Instanz-ID"},
|
||||||
|
)
|
||||||
|
key: str = Field(
|
||||||
|
description="Key identifier (e.g. 'entity:companyName')",
|
||||||
|
json_schema_extra={"label": "Schluessel"},
|
||||||
|
)
|
||||||
|
value: str = Field(
|
||||||
|
description="Extracted value",
|
||||||
|
json_schema_extra={"label": "Wert"},
|
||||||
|
)
|
||||||
|
source: str = Field(
|
||||||
|
default="extraction",
|
||||||
|
description="Origin: extraction, tool, conversation, summary",
|
||||||
|
json_schema_extra={"label": "Quelle"},
|
||||||
|
)
|
||||||
|
embedding: Optional[List[float]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Optional embedding for semantic lookup",
|
||||||
|
json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"WorkflowMemory",
|
|
||||||
{"en": "Workflow Memory", "fr": "Mémoire de workflow"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
|
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
|
|
||||||
"key": {"en": "Key", "fr": "Clé"},
|
|
||||||
"value": {"en": "Value", "fr": "Valeur"},
|
|
||||||
"source": {"en": "Source", "fr": "Source"},
|
|
||||||
"embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ Rollen werden über Junction Tables verknüpft für saubere CASCADE DELETE.
|
||||||
import uuid
|
import uuid
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Benutzer-Mandant")
|
||||||
class UserMandate(PowerOnModel):
|
class UserMandate(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
User-Mitgliedschaft in einem Mandanten.
|
User-Mitgliedschaft in einem Mandanten.
|
||||||
|
|
@ -21,36 +22,24 @@ class UserMandate(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the user-mandate membership",
|
description="Unique ID of the user-mandate membership",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="FK → User.id (CASCADE DELETE)",
|
description="FK → User.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
|
json_schema_extra={"label": "Benutzer", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="FK → Mandate.id (CASCADE DELETE)",
|
description="FK → Mandate.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
|
json_schema_extra={"label": "Mandant", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether this membership is enabled",
|
description="Whether this membership is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
# Rollen werden via Junction Table UserMandateRole verknüpft
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UserMandate",
|
|
||||||
{"en": "User Mandate", "de": "Benutzer-Mandant", "fr": "Mandat utilisateur"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Feature-Zugang")
|
||||||
class FeatureAccess(PowerOnModel):
|
class FeatureAccess(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
User-Zugriff auf eine Feature-Instanz.
|
User-Zugriff auf eine Feature-Instanz.
|
||||||
|
|
@ -59,36 +48,24 @@ class FeatureAccess(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the feature access",
|
description="Unique ID of the feature access",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="FK → User.id (CASCADE DELETE)",
|
description="FK → User.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
|
json_schema_extra={"label": "Benutzer", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="FK → FeatureInstance.id (CASCADE DELETE)",
|
description="FK → FeatureInstance.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
|
json_schema_extra={"label": "Feature-Instanz", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether this feature access is enabled",
|
description="Whether this feature access is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
# Rollen werden via Junction Table FeatureAccessRole verknüpft
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"FeatureAccess",
|
|
||||||
{"en": "Feature Access", "de": "Feature-Zugang", "fr": "Accès fonctionnalité"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Benutzer-Mandant-Rolle")
|
||||||
class UserMandateRole(PowerOnModel):
|
class UserMandateRole(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Junction Table: UserMandate zu Role.
|
Junction Table: UserMandate zu Role.
|
||||||
|
|
@ -97,29 +74,19 @@ class UserMandateRole(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the junction record",
|
description="Unique ID of the junction record",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
userMandateId: str = Field(
|
userMandateId: str = Field(
|
||||||
description="FK → UserMandate.id (CASCADE DELETE)",
|
description="FK → UserMandate.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/user-mandates/", "frontend_fk_display_field": "userId"}
|
json_schema_extra={"label": "Benutzer-Mandant", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/user-mandates/", "frontend_fk_display_field": "userId"}
|
||||||
)
|
)
|
||||||
roleId: str = Field(
|
roleId: str = Field(
|
||||||
description="FK → Role.id (CASCADE DELETE)",
|
description="FK → Role.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Feature-Zugang-Rolle")
|
||||||
"UserMandateRole",
|
|
||||||
{"en": "User Mandate Role", "de": "Benutzer-Mandant-Rolle", "fr": "Rôle mandat utilisateur"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userMandateId": {"en": "User Mandate", "de": "Benutzer-Mandant", "fr": "Mandat utilisateur"},
|
|
||||||
"roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureAccessRole(PowerOnModel):
|
class FeatureAccessRole(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Junction Table: FeatureAccess zu Role.
|
Junction Table: FeatureAccess zu Role.
|
||||||
|
|
@ -128,24 +95,13 @@ class FeatureAccessRole(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the junction record",
|
description="Unique ID of the junction record",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
featureAccessId: str = Field(
|
featureAccessId: str = Field(
|
||||||
description="FK → FeatureAccess.id (CASCADE DELETE)",
|
description="FK → FeatureAccess.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-access/", "frontend_fk_display_field": "userId"}
|
json_schema_extra={"label": "Feature-Zugang", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-access/", "frontend_fk_display_field": "userId"}
|
||||||
)
|
)
|
||||||
roleId: str = Field(
|
roleId: str = Field(
|
||||||
description="FK → Role.id (CASCADE DELETE)",
|
description="FK → Role.id (CASCADE DELETE)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"FeatureAccessRole",
|
|
||||||
{"en": "Feature Access Role", "de": "Feature-Zugang-Rolle", "fr": "Rôle accès fonctionnalité"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"featureAccessId": {"en": "Feature Access", "de": "Feature-Zugang", "fr": "Accès fonctionnalité"},
|
|
||||||
"roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import Optional
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
class MessagingChannel(str, Enum):
|
class MessagingChannel(str, Enum):
|
||||||
|
|
@ -26,86 +26,137 @@ class DeliveryStatus(str, Enum):
|
||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Messaging-Abonnement")
|
||||||
class MessagingSubscription(PowerOnModel):
|
class MessagingSubscription(PowerOnModel):
|
||||||
"""Data model for messaging subscriptions"""
|
"""Data model for messaging subscriptions"""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the subscription",
|
description="Unique ID of the subscription",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
subscriptionId: str = Field(
|
subscriptionId: str = Field(
|
||||||
description="Unique subscription identifier (e.g., 'system_errors', 'audit_login')",
|
description="Unique subscription identifier (e.g., 'system_errors', 'audit_login')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": True,
|
||||||
|
"label": "Abonnement-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
subscriptionLabel: str = Field(
|
subscriptionLabel: str = Field(
|
||||||
description="Display name of the subscription",
|
description="Display name of the subscription",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": True,
|
||||||
|
"label": "Bezeichnung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
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,
|
||||||
|
"label": "Mandanten-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance this subscription belongs to",
|
description="ID of the feature instance 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,
|
||||||
|
"label": "Feature-Instanz-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
description: Optional[str] = Field(
|
description: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Description of the subscription",
|
description="Description of the subscription",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "textarea",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Beschreibung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
isSystemSubscription: bool = Field(
|
isSystemSubscription: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this is a system subscription (only admin can create)",
|
description="Whether this is a system subscription (only admin can create)",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "checkbox",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "System-Abonnement",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether the subscription is enabled",
|
description="Whether the subscription is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "checkbox",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Aktiviert",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Messaging-Registrierung")
|
||||||
"MessagingSubscription",
|
|
||||||
{"en": "Messaging Subscription", "fr": "Abonnement de messagerie"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
|
||||||
"subscriptionLabel": {"en": "Subscription Label", "fr": "Label d'abonnement"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
|
||||||
"description": {"en": "Description", "fr": "Description"},
|
|
||||||
"isSystemSubscription": {"en": "System Subscription", "fr": "Abonnement système"},
|
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MessagingSubscriptionRegistration(BaseModel):
|
class MessagingSubscriptionRegistration(BaseModel):
|
||||||
"""Data model for user registrations to messaging subscriptions"""
|
"""Data model for user registrations to messaging subscriptions"""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
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,
|
||||||
|
"label": "ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="ID of the mandate this registration belongs to",
|
description="ID of the mandate this registration 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,
|
||||||
|
"label": "Mandanten-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance this registration belongs to",
|
description="ID of the feature instance this registration 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,
|
||||||
|
"label": "Feature-Instanz-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
"label": "Abonnement-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="ID of the user registered to this subscription",
|
description="ID of the user registered to this subscription",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Benutzer-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
channel: MessagingChannel = Field(
|
channel: MessagingChannel = Field(
|
||||||
description="Channel type for this registration",
|
description="Channel type for this registration",
|
||||||
|
|
@ -117,62 +168,83 @@ class MessagingSubscriptionRegistration(BaseModel):
|
||||||
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
|
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
|
||||||
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
|
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
|
||||||
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
|
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
|
||||||
{"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}}
|
{"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}},
|
||||||
]
|
],
|
||||||
}
|
"label": "Kanal",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
channelConfig: str = Field(
|
channelConfig: str = Field(
|
||||||
default="",
|
default="",
|
||||||
description="Channel-specific configuration (e.g., email address, phone number, Teams user ID)",
|
description="Channel-specific configuration (e.g., email address, phone number, Teams user ID)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Kanal-Konfiguration",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether this registration is enabled",
|
description="Whether this registration is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "checkbox",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Aktiviert",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Messaging-Zustellung")
|
||||||
"MessagingSubscriptionRegistration",
|
|
||||||
{"en": "Messaging Registration", "fr": "Inscription à la messagerie"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
|
||||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
|
||||||
"channel": {"en": "Channel", "fr": "Canal"},
|
|
||||||
"channelConfig": {"en": "Channel Config", "fr": "Configuration du canal"},
|
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MessagingDelivery(BaseModel):
|
class MessagingDelivery(BaseModel):
|
||||||
"""Data model for individual message deliveries"""
|
"""Data model for individual message deliveries"""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
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,
|
||||||
|
"label": "ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="ID of the mandate this delivery belongs to",
|
description="ID of the mandate 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,
|
||||||
|
"label": "Mandanten-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance this delivery belongs to",
|
description="ID of the feature instance 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,
|
||||||
|
"label": "Feature-Instanz-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
"label": "Abonnement-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="ID of the user receiving this delivery",
|
description="ID of the user receiving this 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,
|
||||||
|
"label": "Benutzer-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
channel: MessagingChannel = Field(
|
channel: MessagingChannel = Field(
|
||||||
description="Channel used for this delivery",
|
description="Channel used for this delivery",
|
||||||
|
|
@ -184,9 +256,10 @@ class MessagingDelivery(BaseModel):
|
||||||
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
|
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
|
||||||
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
|
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
|
||||||
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
|
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
|
||||||
{"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}}
|
{"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}},
|
||||||
]
|
],
|
||||||
}
|
"label": "Kanal",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
status: DeliveryStatus = Field(
|
status: DeliveryStatus = Field(
|
||||||
default=DeliveryStatus.PENDING,
|
default=DeliveryStatus.PENDING,
|
||||||
|
|
@ -198,112 +271,113 @@ class MessagingDelivery(BaseModel):
|
||||||
"frontend_options": [
|
"frontend_options": [
|
||||||
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}},
|
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}},
|
||||||
{"value": "sent", "label": {"en": "Sent", "fr": "Envoyé"}},
|
{"value": "sent", "label": {"en": "Sent", "fr": "Envoyé"}},
|
||||||
{"value": "failed", "label": {"en": "Failed", "fr": "Échoué"}}
|
{"value": "failed", "label": {"en": "Failed", "fr": "Échoué"}},
|
||||||
]
|
],
|
||||||
}
|
"label": "Status",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
errorMessage: Optional[str] = Field(
|
errorMessage: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if delivery failed",
|
description="Error message if delivery failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "textarea",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Fehlermeldung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
sentAt: Optional[float] = Field(
|
sentAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When the delivery was sent (UTC timestamp in seconds)",
|
description="When the delivery was sent (UTC timestamp in seconds)",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "datetime",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Gesendet am",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Messaging-Ereignisparameter")
|
||||||
"MessagingDelivery",
|
|
||||||
{"en": "Messaging Delivery", "fr": "Livraison de messagerie"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
|
||||||
"subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
|
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
|
||||||
"channel": {"en": "Channel", "fr": "Canal"},
|
|
||||||
"status": {"en": "Status", "fr": "Statut"},
|
|
||||||
"errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
|
|
||||||
"sentAt": {"en": "Sent At", "fr": "Envoyé le"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MessagingEventParameters(BaseModel):
|
class MessagingEventParameters(BaseModel):
|
||||||
"""Data model for event parameters passed to subscription functions"""
|
"""Data model for event parameters passed to subscription functions"""
|
||||||
triggerData: dict = Field(
|
triggerData: dict = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Event data from trigger as dictionary/JSON",
|
description="Event data from trigger as dictionary/JSON",
|
||||||
json_schema_extra={"frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "json",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Trigger-Daten",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Messaging-Sendeergebnis")
|
||||||
"MessagingEventParameters",
|
|
||||||
{"en": "Messaging Event Parameters", "fr": "Paramètres d'événement de messagerie"},
|
|
||||||
{
|
|
||||||
"triggerData": {"en": "Trigger Data", "fr": "Données de déclenchement"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"MessagingSendResult",
|
|
||||||
{"en": "Messaging Send Result", "fr": "Résultat d'envoi de messagerie"},
|
|
||||||
{
|
|
||||||
"success": {"en": "Success", "fr": "Succès"},
|
|
||||||
"deliveryId": {"en": "Delivery ID", "fr": "ID de livraison"},
|
|
||||||
"errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"MessagingSubscriptionExecutionResult",
|
|
||||||
{"en": "Messaging Subscription Execution Result", "fr": "Résultat d'exécution d'abonnement"},
|
|
||||||
{
|
|
||||||
"success": {"en": "Success", "fr": "Succès"},
|
|
||||||
"messagesSent": {"en": "Messages Sent", "fr": "Messages envoyés"},
|
|
||||||
"errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MessagingSendResult(BaseModel):
|
class MessagingSendResult(BaseModel):
|
||||||
"""Data model for sendMessage result"""
|
"""Data model for sendMessage result"""
|
||||||
success: bool = Field(
|
success: bool = Field(
|
||||||
description="Whether the message was sent successfully",
|
description="Whether the message was sent successfully",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={
|
||||||
|
"frontend_type": "checkbox",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": True,
|
||||||
|
"label": "Erfolg",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
deliveryId: Optional[str] = Field(
|
deliveryId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of the created MessagingDelivery record",
|
description="ID of the created MessagingDelivery record",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "text",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Zustellungs-ID",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
errorMessage: Optional[str] = Field(
|
errorMessage: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if sending failed",
|
description="Error message if sending failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "textarea",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Fehlermeldung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Messaging-Abonnement-Ausführung")
|
||||||
class MessagingSubscriptionExecutionResult(BaseModel):
|
class MessagingSubscriptionExecutionResult(BaseModel):
|
||||||
"""Data model for subscription function execution result"""
|
"""Data model for subscription function execution result"""
|
||||||
success: bool = Field(
|
success: bool = Field(
|
||||||
description="Whether the subscription execution was successful",
|
description="Whether the subscription execution was successful",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={
|
||||||
|
"frontend_type": "checkbox",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": True,
|
||||||
|
"label": "Erfolg",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
messagesSent: int = Field(
|
messagesSent: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Number of messages sent",
|
description="Number of messages sent",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "number",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Gesendete Nachrichten",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
errorMessage: Optional[str] = Field(
|
errorMessage: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if execution failed",
|
description="Error message if execution failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={
|
||||||
|
"frontend_type": "textarea",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Fehlermeldung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from typing import Optional, List
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field, ConfigDict
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(str, Enum):
|
class NotificationType(str, Enum):
|
||||||
|
|
@ -29,20 +29,25 @@ class NotificationStatus(str, Enum):
|
||||||
DISMISSED = "dismissed" # Verworfen/Geschlossen
|
DISMISSED = "dismissed" # Verworfen/Geschlossen
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Benachrichtigungs-Aktion")
|
||||||
class NotificationAction(BaseModel):
|
class NotificationAction(BaseModel):
|
||||||
"""Possible action for a notification"""
|
"""Possible action for a notification"""
|
||||||
actionId: str = Field(
|
actionId: str = Field(
|
||||||
description="Unique identifier for the action (e.g., 'accept', 'decline')"
|
description="Unique identifier for the action (e.g., 'accept', 'decline')",
|
||||||
|
json_schema_extra={"label": "Aktions-ID"},
|
||||||
)
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Display label for the action button"
|
description="Display label for the action button",
|
||||||
|
json_schema_extra={"label": "Bezeichnung"},
|
||||||
)
|
)
|
||||||
style: str = Field(
|
style: str = Field(
|
||||||
default="default",
|
default="default",
|
||||||
description="Button style: 'primary', 'danger', 'default'"
|
description="Button style: 'primary', 'danger', 'default'",
|
||||||
|
json_schema_extra={"label": "Stil"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Benachrichtigung")
|
||||||
class UserNotification(PowerOnModel):
|
class UserNotification(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
In-app notification for a user.
|
In-app notification for a user.
|
||||||
|
|
@ -51,18 +56,18 @@ class UserNotification(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the notification",
|
description="Unique ID of the notification",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="Target user ID for this notification",
|
description="Target user ID for this notification",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Notification type and status
|
|
||||||
type: NotificationType = Field(
|
type: NotificationType = Field(
|
||||||
default=NotificationType.SYSTEM,
|
default=NotificationType.SYSTEM,
|
||||||
description="Type of notification",
|
description="Type of notification",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Typ",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -78,6 +83,7 @@ class UserNotification(PowerOnModel):
|
||||||
default=NotificationStatus.UNREAD,
|
default=NotificationStatus.UNREAD,
|
||||||
description="Current status of the notification",
|
description="Current status of the notification",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Status",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -89,115 +95,63 @@ class UserNotification(PowerOnModel):
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Content
|
|
||||||
title: str = Field(
|
title: str = Field(
|
||||||
description="Notification title",
|
description="Notification title",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Titel", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
message: str = Field(
|
message: str = Field(
|
||||||
description="Notification message/body",
|
description="Notification message/body",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"label": "Nachricht", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": True}
|
||||||
)
|
)
|
||||||
icon: Optional[str] = Field(
|
icon: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional icon identifier (e.g., 'mail', 'warning', 'info')",
|
description="Optional icon identifier (e.g., 'mail', 'warning', 'info')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Symbol", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reference to triggering object (for actionable notifications)
|
|
||||||
referenceType: Optional[str] = Field(
|
referenceType: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Type of referenced object (e.g., 'Invitation', 'Workflow')",
|
description="Type of referenced object (e.g., 'Invitation', 'Workflow')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Referenz-Typ", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
referenceId: Optional[str] = Field(
|
referenceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of referenced object",
|
description="ID of referenced object",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Referenz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Actions (for actionable notifications like invitations)
|
|
||||||
actions: Optional[List[NotificationAction]] = Field(
|
actions: Optional[List[NotificationAction]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="List of possible actions for this notification",
|
description="List of possible actions for this notification",
|
||||||
json_schema_extra={"frontend_type": "json", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Aktionen", "frontend_type": "json", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Action result (when user takes action)
|
|
||||||
actionTaken: Optional[str] = Field(
|
actionTaken: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Which action was taken (actionId)",
|
description="Which action was taken (actionId)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Durchgefuehrte Aktion", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
actionResult: Optional[str] = Field(
|
actionResult: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Result message from the action",
|
description="Result message from the action",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Aktions-Ergebnis", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Timestamps
|
|
||||||
readAt: Optional[float] = Field(
|
readAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When the notification was read (UTC timestamp)",
|
description="When the notification was read (UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Gelesen am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
actionedAt: Optional[float] = Field(
|
actionedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When action was taken (UTC timestamp)",
|
description="When action was taken (UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Bearbeitet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
expiresAt: Optional[float] = Field(
|
expiresAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="When the notification expires (optional, UTC timestamp)",
|
description="When the notification expires (optional, UTC timestamp)",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "Gueltig bis", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UserNotification",
|
|
||||||
{"en": "Notification", "de": "Benachrichtigung", "fr": "Notification"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
|
|
||||||
"type": {"en": "Type", "de": "Typ", "fr": "Type"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"title": {"en": "Title", "de": "Titel", "fr": "Titre"},
|
|
||||||
"message": {"en": "Message", "de": "Nachricht", "fr": "Message"},
|
|
||||||
"icon": {"en": "Icon", "de": "Symbol", "fr": "Icône"},
|
|
||||||
"referenceType": {"en": "Reference Type", "de": "Referenz-Typ", "fr": "Type de référence"},
|
|
||||||
"referenceId": {"en": "Reference ID", "de": "Referenz-ID", "fr": "ID de référence"},
|
|
||||||
"actions": {"en": "Actions", "de": "Aktionen", "fr": "Actions"},
|
|
||||||
"actionTaken": {"en": "Action Taken", "de": "Durchgeführte Aktion", "fr": "Action effectuée"},
|
|
||||||
"actionResult": {"en": "Action Result", "de": "Aktions-Ergebnis", "fr": "Résultat de l'action"},
|
|
||||||
"readAt": {"en": "Read At", "de": "Gelesen am", "fr": "Lu le"},
|
|
||||||
"actionedAt": {"en": "Actioned At", "de": "Bearbeitet am", "fr": "Traité le"},
|
|
||||||
"expiresAt": {"en": "Expires At", "de": "Gültig bis", "fr": "Expire le"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"NotificationType",
|
|
||||||
{"en": "Notification Type", "de": "Benachrichtigungs-Typ", "fr": "Type de notification"},
|
|
||||||
{
|
|
||||||
"invitation": {"en": "Invitation", "de": "Einladung", "fr": "Invitation"},
|
|
||||||
"system": {"en": "System", "de": "System", "fr": "Système"},
|
|
||||||
"workflow": {"en": "Workflow", "de": "Workflow", "fr": "Workflow"},
|
|
||||||
"mention": {"en": "Mention", "de": "Erwähnung", "fr": "Mention"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"NotificationStatus",
|
|
||||||
{"en": "Notification Status", "de": "Benachrichtigungs-Status", "fr": "Statut de notification"},
|
|
||||||
{
|
|
||||||
"unread": {"en": "Unread", "de": "Ungelesen", "fr": "Non lu"},
|
|
||||||
"read": {"en": "Read", "de": "Gelesen", "fr": "Lu"},
|
|
||||||
"actioned": {"en": "Actioned", "de": "Bearbeitet", "fr": "Traité"},
|
|
||||||
"dismissed": {"en": "Dismissed", "de": "Verworfen", "fr": "Rejeté"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from typing import Optional
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.datamodels.datamodelUtils import TextMultilingual
|
from modules.datamodels.datamodelUtils import TextMultilingual
|
||||||
from modules.datamodels.datamodelUam import AccessLevel
|
from modules.datamodels.datamodelUam import AccessLevel
|
||||||
|
|
||||||
|
|
@ -26,6 +26,7 @@ class AccessRuleContext(str, Enum):
|
||||||
RESOURCE = "RESOURCE" # System resources (AI models, actions, etc.)
|
RESOURCE = "RESOURCE" # System resources (AI models, actions, etc.)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Rolle")
|
||||||
class Role(PowerOnModel):
|
class Role(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Data model for RBAC roles.
|
Data model for RBAC roles.
|
||||||
|
|
@ -41,56 +42,42 @@ class Role(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the role",
|
description="Unique ID of the role",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
roleLabel: str = Field(
|
roleLabel: str = Field(
|
||||||
description="Unique role label identifier (e.g., 'admin', 'user', 'viewer')",
|
description="Unique role label identifier (e.g., 'admin', 'user', 'viewer')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Rollen-Label", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
description: TextMultilingual = Field(
|
description: TextMultilingual = Field(
|
||||||
description="Role description in multiple languages",
|
description="Role description in multiple languages",
|
||||||
json_schema_extra={"frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Beschreibung", "frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
# KONTEXT - IMMUTABLE nach Create (nur Create/Delete, kein Update!)
|
# KONTEXT - IMMUTABLE nach Create (nur Create/Delete, kein Update!)
|
||||||
mandateId: Optional[str] = Field(
|
mandateId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="FK → Mandate.id (CASCADE DELETE). Null = Global/Template role.",
|
description="FK → Mandate.id (CASCADE DELETE). Null = Global/Template role.",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
|
json_schema_extra={"label": "Mandant", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
|
||||||
)
|
)
|
||||||
featureInstanceId: Optional[str] = Field(
|
featureInstanceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="FK → FeatureInstance.id (CASCADE DELETE). Null = Mandate-level or Global role.",
|
description="FK → FeatureInstance.id (CASCADE DELETE). Null = Mandate-level or Global role.",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
|
json_schema_extra={"label": "Feature-Instanz", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
|
||||||
)
|
)
|
||||||
featureCode: Optional[str] = Field(
|
featureCode: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature code (z.B. 'trustee') - für Template-Rollen",
|
description="Feature code (z.B. 'trustee') - für Template-Rollen",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "Feature-Code", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
isSystemRole: bool = Field(
|
isSystemRole: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this is a system role that cannot be deleted",
|
description="Whether this is a system role that cannot be deleted",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"label": "System-Rolle", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Zugriffsregel")
|
||||||
"Role",
|
|
||||||
{"en": "Role", "de": "Rolle", "fr": "Rôle"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"roleLabel": {"en": "Role Label", "de": "Rollen-Label", "fr": "Label du rôle"},
|
|
||||||
"description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
"featureCode": {"en": "Feature Code", "de": "Feature-Code", "fr": "Code fonctionnalité"},
|
|
||||||
"isSystemRole": {"en": "System Role", "de": "System-Rolle", "fr": "Rôle système"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AccessRule(PowerOnModel):
|
class AccessRule(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Data model for access control rules.
|
Data model for access control rules.
|
||||||
|
|
@ -101,15 +88,15 @@ class AccessRule(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the access rule",
|
description="Unique ID of the access rule",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
roleId: str = Field(
|
roleId: str = Field(
|
||||||
description="FK → Role.id (CASCADE DELETE!)",
|
description="FK → Role.id (CASCADE DELETE!)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
|
||||||
)
|
)
|
||||||
context: AccessRuleContext = Field(
|
context: AccessRuleContext = Field(
|
||||||
description="Context type: DATA (database), UI (interface), RESOURCE (system resources). IMMUTABLE!",
|
description="Context type: DATA (database), UI (interface), RESOURCE (system resources). IMMUTABLE!",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_options": [
|
json_schema_extra={"label": "Kontext", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_options": [
|
||||||
{"value": "DATA", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
|
{"value": "DATA", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
|
||||||
{"value": "UI", "label": {"en": "UI", "de": "Oberfläche", "fr": "Interface"}},
|
{"value": "UI", "label": {"en": "UI", "de": "Oberfläche", "fr": "Interface"}},
|
||||||
{"value": "RESOURCE", "label": {"en": "Resource", "de": "Ressource", "fr": "Ressource"}}
|
{"value": "RESOURCE", "label": {"en": "Resource", "de": "Ressource", "fr": "Ressource"}}
|
||||||
|
|
@ -118,17 +105,17 @@ class AccessRule(PowerOnModel):
|
||||||
item: Optional[str] = Field(
|
item: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Item identifier (null = all items in context). Format: DATA: '<table>' or '<table>.<field>', UI: cascading string (e.g., 'playground.voice.settings'), RESOURCE: cascading string (e.g., 'ai.model.anthropic')",
|
description="Item identifier (null = all items in context). Format: DATA: '<table>' or '<table>.<field>', UI: cascading string (e.g., 'playground.voice.settings'), RESOURCE: cascading string (e.g., 'ai.model.anthropic')",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"label": "Element", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
||||||
)
|
)
|
||||||
view: bool = Field(
|
view: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="View permission: if true, item is visible/enabled. Only objects with view=true are shown.",
|
description="View permission: if true, item is visible/enabled. Only objects with view=true are shown.",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"label": "Anzeigen", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": True}
|
||||||
)
|
)
|
||||||
read: Optional[AccessLevel] = Field(
|
read: Optional[AccessLevel] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Read permission level (only for DATA context)",
|
description="Read permission level (only for DATA context)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Lesen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
||||||
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
||||||
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
||||||
|
|
@ -138,7 +125,7 @@ class AccessRule(PowerOnModel):
|
||||||
create: Optional[AccessLevel] = Field(
|
create: Optional[AccessLevel] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Create permission level (only for DATA context)",
|
description="Create permission level (only for DATA context)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Erstellen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
||||||
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
||||||
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
||||||
|
|
@ -148,7 +135,7 @@ class AccessRule(PowerOnModel):
|
||||||
update: Optional[AccessLevel] = Field(
|
update: Optional[AccessLevel] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Update permission level (only for DATA context)",
|
description="Update permission level (only for DATA context)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Aktualisieren", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
||||||
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
||||||
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
||||||
|
|
@ -158,7 +145,7 @@ class AccessRule(PowerOnModel):
|
||||||
delete: Optional[AccessLevel] = Field(
|
delete: Optional[AccessLevel] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Delete permission level (only for DATA context)",
|
description="Delete permission level (only for DATA context)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
json_schema_extra={"label": "Loeschen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
|
||||||
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
|
||||||
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
|
||||||
|
|
@ -167,23 +154,6 @@ class AccessRule(PowerOnModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AccessRule",
|
|
||||||
{"en": "Access Rule", "de": "Zugriffsregel", "fr": "Règle d'accès"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
|
|
||||||
"context": {"en": "Context", "de": "Kontext", "fr": "Contexte"},
|
|
||||||
"item": {"en": "Item", "de": "Element", "fr": "Élément"},
|
|
||||||
"view": {"en": "View", "de": "Anzeigen", "fr": "Vue"},
|
|
||||||
"read": {"en": "Read", "de": "Lesen", "fr": "Lecture"},
|
|
||||||
"create": {"en": "Create", "de": "Erstellen", "fr": "Créer"},
|
|
||||||
"update": {"en": "Update", "de": "Aktualisieren", "fr": "Mettre à jour"},
|
|
||||||
"delete": {"en": "Delete", "de": "Löschen", "fr": "Supprimer"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# IMMUTABLE Fields Definition - für Enforcement auf Application-Level
|
# IMMUTABLE Fields Definition - für Enforcement auf Application-Level
|
||||||
IMMUTABLE_FIELDS = {
|
IMMUTABLE_FIELDS = {
|
||||||
"Role": ["mandateId", "featureInstanceId", "featureCode"],
|
"Role": ["mandateId", "featureInstanceId", "featureCode"],
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Multi-Tenant Design:
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
from .datamodelUam import AuthAuthority
|
from .datamodelUam import AuthAuthority
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
@ -31,46 +31,79 @@ class TokenPurpose(str, Enum):
|
||||||
DATA_CONNECTION = "dataConnection"
|
DATA_CONNECTION = "dataConnection"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Token")
|
||||||
class Token(PowerOnModel):
|
class Token(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Authentication Token model.
|
Authentication Token model.
|
||||||
|
|
||||||
Multi-Tenant Design:
|
Multi-Tenant Design:
|
||||||
- Token ist User-gebunden, NICHT Mandant-gebunden
|
- Token ist User-gebunden, NICHT Mandant-gebunden
|
||||||
- Ermöglicht parallele Arbeit in mehreren Mandanten
|
- Ermöglicht parallele Arbeit in mehreren Mandanten
|
||||||
- Mandant-Kontext wird per Request-Header bestimmt
|
- Mandant-Kontext wird per Request-Header bestimmt
|
||||||
"""
|
"""
|
||||||
id: Optional[str] = None
|
id: Optional[str] = Field(
|
||||||
userId: str
|
default=None,
|
||||||
authority: AuthAuthority
|
json_schema_extra={"label": "ID"},
|
||||||
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
...,
|
||||||
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
|
)
|
||||||
|
authority: AuthAuthority = Field(
|
||||||
|
...,
|
||||||
|
json_schema_extra={"label": "Autoritaet"},
|
||||||
|
)
|
||||||
connectionId: Optional[str] = Field(
|
connectionId: Optional[str] = Field(
|
||||||
None, description="ID of the connection this token belongs to"
|
None,
|
||||||
|
description="ID of the connection this token belongs to",
|
||||||
|
json_schema_extra={"label": "Verbindungs-ID"},
|
||||||
)
|
)
|
||||||
tokenPurpose: Optional[TokenPurpose] = Field(
|
tokenPurpose: Optional[TokenPurpose] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="authSession = gateway login JWT; dataConnection = provider OAuth for a connection",
|
description="authSession = gateway login JWT; dataConnection = provider OAuth for a connection",
|
||||||
|
json_schema_extra={"label": "Token-Verwendung"},
|
||||||
|
)
|
||||||
|
tokenAccess: str = Field(
|
||||||
|
...,
|
||||||
|
json_schema_extra={"label": "Zugriffstoken"},
|
||||||
|
)
|
||||||
|
tokenType: str = Field(
|
||||||
|
default="bearer",
|
||||||
|
json_schema_extra={"label": "Token-Typ"},
|
||||||
)
|
)
|
||||||
tokenAccess: str
|
|
||||||
tokenType: str = "bearer"
|
|
||||||
expiresAt: float = Field(
|
expiresAt: float = Field(
|
||||||
description="When the token expires (UTC timestamp in seconds)"
|
description="When the token expires (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"label": "Laeuft ab am"},
|
||||||
|
)
|
||||||
|
tokenRefresh: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
json_schema_extra={"label": "Refresh-Token"},
|
||||||
)
|
)
|
||||||
tokenRefresh: Optional[str] = None
|
|
||||||
status: TokenStatus = Field(
|
status: TokenStatus = Field(
|
||||||
default=TokenStatus.ACTIVE, description="Token status: active/revoked"
|
default=TokenStatus.ACTIVE,
|
||||||
|
description="Token status: active/revoked",
|
||||||
|
json_schema_extra={"label": "Status"},
|
||||||
)
|
)
|
||||||
revokedAt: Optional[float] = Field(
|
revokedAt: Optional[float] = Field(
|
||||||
None, description="When the token was revoked (UTC timestamp in seconds)"
|
None,
|
||||||
|
description="When the token was revoked (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"label": "Widerrufen am"},
|
||||||
)
|
)
|
||||||
revokedBy: Optional[str] = Field(
|
revokedBy: Optional[str] = Field(
|
||||||
None, description="User ID who revoked the token (admin/self)"
|
None,
|
||||||
|
description="User ID who revoked the token (admin/self)",
|
||||||
|
json_schema_extra={"label": "Widerrufen von"},
|
||||||
|
)
|
||||||
|
reason: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional revocation reason",
|
||||||
|
json_schema_extra={"label": "Grund"},
|
||||||
)
|
)
|
||||||
reason: Optional[str] = Field(None, description="Optional revocation reason")
|
|
||||||
sessionId: Optional[str] = Field(
|
sessionId: Optional[str] = Field(
|
||||||
None, description="Logical session grouping for logout revocation"
|
None,
|
||||||
|
description="Logical session grouping for logout revocation",
|
||||||
|
json_schema_extra={"label": "Sitzungs-ID"},
|
||||||
)
|
)
|
||||||
# ENTFERNT: mandateId - Token ist nicht mehr Mandant-spezifisch
|
|
||||||
# Mandant-Kontext wird per Request-Header (X-Mandate-Id) bestimmt
|
|
||||||
|
|
||||||
model_config = ConfigDict(use_enum_values=True)
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
|
|
||||||
|
|
@ -91,51 +124,44 @@ class Token(PowerOnModel):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Authentifizierungsereignis")
|
||||||
"Token",
|
|
||||||
{"en": "Token", "de": "Token", "fr": "Jeton"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
|
||||||
"authority": {"en": "Authority", "de": "Autorität", "fr": "Autorité"},
|
|
||||||
"connectionId": {"en": "Connection ID", "de": "Verbindungs-ID", "fr": "ID de connexion"},
|
|
||||||
"tokenPurpose": {"en": "Token purpose", "de": "Token-Verwendung", "fr": "Usage du jeton"},
|
|
||||||
"tokenAccess": {"en": "Access Token", "de": "Zugriffstoken", "fr": "Jeton d'accès"},
|
|
||||||
"tokenType": {"en": "Token Type", "de": "Token-Typ", "fr": "Type de jeton"},
|
|
||||||
"expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
|
|
||||||
"tokenRefresh": {"en": "Refresh Token", "de": "Refresh-Token", "fr": "Jeton de rafraîchissement"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"revokedAt": {"en": "Revoked At", "de": "Widerrufen am", "fr": "Révoqué le"},
|
|
||||||
"revokedBy": {"en": "Revoked By", "de": "Widerrufen von", "fr": "Révoqué par"},
|
|
||||||
"reason": {"en": "Reason", "de": "Grund", "fr": "Raison"},
|
|
||||||
"sessionId": {"en": "Session ID", "de": "Sitzungs-ID", "fr": "ID de session"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthEvent(PowerOnModel):
|
class AuthEvent(PowerOnModel):
|
||||||
"""Authentication event for audit logging."""
|
"""Authentication event for audit logging."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the auth event", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
id: str = Field(
|
||||||
userId: str = Field(description="ID of the user this event belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
eventType: str = Field(description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
description="Unique ID of the auth event",
|
||||||
timestamp: float = Field(default_factory=getUtcTimestamp, description="Unix timestamp when the event occurred", json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True})
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
ipAddress: Optional[str] = Field(default=None, description="IP address from which the event originated", 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})
|
userId: str = Field(
|
||||||
success: bool = Field(default=True, description="Whether the authentication event was successful", json_schema_extra={"frontend_type": "boolean", "frontend_readonly": True, "frontend_required": True})
|
description="ID of the user this event belongs to",
|
||||||
details: Optional[str] = Field(default=None, description="Additional details about the event", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
|
)
|
||||||
|
eventType: str = Field(
|
||||||
registerModelLabels(
|
description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')",
|
||||||
"AuthEvent",
|
json_schema_extra={"label": "Ereignistyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
{"en": "Authentication Event", "de": "Authentifizierungsereignis", "fr": "Événement d'authentification"},
|
)
|
||||||
{
|
timestamp: float = Field(
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
default_factory=getUtcTimestamp,
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
description="Unix timestamp when the event occurred",
|
||||||
"eventType": {"en": "Event Type", "de": "Ereignistyp", "fr": "Type d'événement"},
|
json_schema_extra={"label": "Zeitstempel", "frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True},
|
||||||
"timestamp": {"en": "Timestamp", "de": "Zeitstempel", "fr": "Horodatage"},
|
)
|
||||||
"ipAddress": {"en": "IP Address", "de": "IP-Adresse", "fr": "Adresse IP"},
|
ipAddress: Optional[str] = Field(
|
||||||
"userAgent": {"en": "User Agent", "de": "User-Agent", "fr": "Agent utilisateur"},
|
default=None,
|
||||||
"success": {"en": "Success", "de": "Erfolgreich", "fr": "Succès"},
|
description="IP address from which the event originated",
|
||||||
"details": {"en": "Details", "de": "Details", "fr": "Détails"},
|
json_schema_extra={"label": "IP-Adresse", "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={"label": "User-Agent", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
success: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether the authentication event was successful",
|
||||||
|
json_schema_extra={"label": "Erfolgreich", "frontend_type": "boolean", "frontend_readonly": True, "frontend_required": True},
|
||||||
|
)
|
||||||
|
details: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Additional details about the event",
|
||||||
|
json_schema_extra={"label": "Details", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from enum import Enum
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -55,123 +55,224 @@ class BillingPeriodEnum(str, Enum):
|
||||||
# Catalog: SubscriptionPlan (static, in-memory)
|
# Catalog: SubscriptionPlan (static, in-memory)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
@i18nModel("Abonnement-Plan")
|
||||||
class SubscriptionPlan(BaseModel):
|
class SubscriptionPlan(BaseModel):
|
||||||
"""Plan definition (catalog entry). Not stored per mandate — static."""
|
"""Plan-Definition (Katalog). Nicht pro Mandat gespeichert — statisch."""
|
||||||
planKey: str = Field(..., description="Unique plan identifier")
|
planKey: str = Field(
|
||||||
selectableByUser: bool = Field(default=True, description="Whether users can choose this plan in the UI")
|
...,
|
||||||
|
description="Unique plan identifier",
|
||||||
|
json_schema_extra={"label": "Plan"},
|
||||||
|
)
|
||||||
|
selectableByUser: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether users can choose this plan in the UI",
|
||||||
|
json_schema_extra={"label": "Waehlbar"},
|
||||||
|
)
|
||||||
|
|
||||||
title: Dict[str, str] = Field(default_factory=dict, description="Multilingual title (en/de/fr)")
|
title: Dict[str, str] = Field(
|
||||||
description: Dict[str, str] = Field(default_factory=dict, description="Multilingual description")
|
default_factory=dict,
|
||||||
|
description="Multilingual title (en/de/fr)",
|
||||||
|
json_schema_extra={"label": "Titel"},
|
||||||
|
)
|
||||||
|
description: Dict[str, str] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="Multilingual description",
|
||||||
|
json_schema_extra={"label": "Beschreibung"},
|
||||||
|
)
|
||||||
|
|
||||||
currency: str = Field(default="CHF", description="Billing currency")
|
currency: str = Field(
|
||||||
billingPeriod: BillingPeriodEnum = Field(default=BillingPeriodEnum.MONTHLY, description="Recurring interval")
|
default="CHF",
|
||||||
pricePerUserCHF: float = Field(default=0.0, description="Price per active user per period")
|
description="Billing currency",
|
||||||
pricePerFeatureInstanceCHF: float = Field(default=0.0, description="Price per active feature instance per period")
|
json_schema_extra={"label": "Waehrung"},
|
||||||
autoRenew: bool = Field(default=True, description="Stripe renews automatically at period end")
|
)
|
||||||
|
billingPeriod: BillingPeriodEnum = Field(
|
||||||
|
default=BillingPeriodEnum.MONTHLY,
|
||||||
|
description="Recurring interval",
|
||||||
|
json_schema_extra={"label": "Abrechnungszeitraum"},
|
||||||
|
)
|
||||||
|
pricePerUserCHF: float = Field(
|
||||||
|
default=0.0,
|
||||||
|
description="Price per active user per period",
|
||||||
|
json_schema_extra={"label": "Preis pro User (CHF)"},
|
||||||
|
)
|
||||||
|
pricePerFeatureInstanceCHF: float = Field(
|
||||||
|
default=0.0,
|
||||||
|
description="Price per active feature instance per period",
|
||||||
|
json_schema_extra={"label": "Preis pro Instanz (CHF)"},
|
||||||
|
)
|
||||||
|
autoRenew: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Stripe renews automatically at period end",
|
||||||
|
json_schema_extra={"label": "Auto-Verlaengerung"},
|
||||||
|
)
|
||||||
|
|
||||||
maxUsers: Optional[int] = Field(None, description="Hard cap on active users (None = unlimited)")
|
maxUsers: Optional[int] = Field(
|
||||||
maxFeatureInstances: Optional[int] = Field(None, description="Hard cap on active feature instances (None = unlimited)")
|
None,
|
||||||
trialDays: Optional[int] = Field(None, description="Trial duration in days (only for trial plans)")
|
description="Hard cap on active users (None = unlimited)",
|
||||||
maxDataVolumeMB: Optional[int] = Field(None, description="Soft-limit for data volume in MB per mandate (None = unlimited)")
|
json_schema_extra={"label": "Max. Benutzer"},
|
||||||
budgetAiCHF: float = Field(default=0.0, description="AI budget (CHF) included in subscription price per billing period")
|
)
|
||||||
successorPlanKey: Optional[str] = Field(None, description="Plan to transition to when trial ends")
|
maxFeatureInstances: Optional[int] = Field(
|
||||||
|
None,
|
||||||
|
description="Hard cap on active feature instances (None = unlimited)",
|
||||||
registerModelLabels(
|
json_schema_extra={"label": "Max. Instanzen"},
|
||||||
"SubscriptionPlan",
|
)
|
||||||
{"en": "Subscription Plan", "de": "Abonnement-Plan", "fr": "Plan d'abonnement"},
|
trialDays: Optional[int] = Field(
|
||||||
{
|
None,
|
||||||
"planKey": {"en": "Plan", "de": "Plan", "fr": "Plan"},
|
description="Trial duration in days (only for trial plans)",
|
||||||
"selectableByUser": {"en": "Selectable", "de": "Wählbar", "fr": "Sélectionnable"},
|
json_schema_extra={"label": "Probentage"},
|
||||||
"billingPeriod": {"en": "Billing Period", "de": "Abrechnungszeitraum", "fr": "Période de facturation"},
|
)
|
||||||
"pricePerUserCHF": {"en": "Price per User (CHF)", "de": "Preis pro User (CHF)"},
|
maxDataVolumeMB: Optional[int] = Field(
|
||||||
"pricePerFeatureInstanceCHF": {"en": "Price per Instance (CHF)", "de": "Preis pro Instanz (CHF)"},
|
None,
|
||||||
"maxUsers": {"en": "Max Users", "de": "Max. Benutzer", "fr": "Max. utilisateurs"},
|
description="Soft-limit for data volume in MB per mandate (None = unlimited)",
|
||||||
"maxFeatureInstances": {"en": "Max Instances", "de": "Max. Instanzen", "fr": "Max. instances"},
|
json_schema_extra={"label": "Datenvolumen (MB)"},
|
||||||
"maxDataVolumeMB": {"en": "Data Volume (MB)", "de": "Datenvolumen (MB)"},
|
)
|
||||||
"budgetAiCHF": {"en": "AI Budget (CHF)", "de": "AI-Budget (CHF)"},
|
budgetAiCHF: float = Field(
|
||||||
},
|
default=0.0,
|
||||||
)
|
description="AI budget (CHF) included in subscription price per billing period",
|
||||||
|
json_schema_extra={"label": "AI-Budget (CHF)"},
|
||||||
|
)
|
||||||
|
successorPlanKey: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Plan to transition to when trial ends",
|
||||||
|
json_schema_extra={"label": "Nachfolge-Plan"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Stripe Price mapping (persisted in DB, auto-created at bootstrap)
|
# Stripe Price mapping (persisted in DB, auto-created at bootstrap)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
@i18nModel("Stripe-Planpreise")
|
||||||
class StripePlanPrice(BaseModel):
|
class StripePlanPrice(BaseModel):
|
||||||
"""Persisted mapping from planKey to Stripe Product/Price IDs.
|
"""Persistierte Zuordnung planKey zu Stripe Product/Price IDs."""
|
||||||
Auto-created at startup — no manual configuration needed.
|
id: str = Field(
|
||||||
Uses separate Stripe Products for users and instances for clear invoice labels."""
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
description="Primary key",
|
||||||
planKey: str = Field(..., description="Reference to SubscriptionPlan.planKey")
|
json_schema_extra={"label": "ID"},
|
||||||
stripeProductId: str = Field("", description="Legacy single-product ID (unused)")
|
)
|
||||||
stripeProductIdUsers: Optional[str] = Field(None, description="Stripe Product ID for user licenses")
|
planKey: str = Field(
|
||||||
stripeProductIdInstances: Optional[str] = Field(None, description="Stripe Product ID for feature instances")
|
...,
|
||||||
stripePriceIdUsers: Optional[str] = Field(None, description="Stripe Price ID for user-seat line item")
|
description="Reference to SubscriptionPlan.planKey",
|
||||||
stripePriceIdInstances: Optional[str] = Field(None, description="Stripe Price ID for instance line item")
|
json_schema_extra={"label": "Plan"},
|
||||||
|
)
|
||||||
|
stripeProductId: str = Field(
|
||||||
registerModelLabels(
|
"",
|
||||||
"StripePlanPrice",
|
description="Legacy single-product ID (unused)",
|
||||||
{"en": "Stripe Plan Prices", "de": "Stripe-Planpreise"},
|
json_schema_extra={"label": "Stripe-Produkt-ID (Legacy)"},
|
||||||
{
|
)
|
||||||
"planKey": {"en": "Plan", "de": "Plan"},
|
stripeProductIdUsers: Optional[str] = Field(
|
||||||
"stripeProductIdUsers": {"en": "Product (Users)", "de": "Produkt (User)"},
|
None,
|
||||||
"stripeProductIdInstances": {"en": "Product (Instances)", "de": "Produkt (Instanzen)"},
|
description="Stripe Product ID for user licenses",
|
||||||
"stripePriceIdUsers": {"en": "Price ID (Users)", "de": "Preis-ID (User)"},
|
json_schema_extra={"label": "Produkt (User)"},
|
||||||
"stripePriceIdInstances": {"en": "Price ID (Instances)", "de": "Preis-ID (Instanzen)"},
|
)
|
||||||
},
|
stripeProductIdInstances: Optional[str] = Field(
|
||||||
)
|
None,
|
||||||
|
description="Stripe Product ID for feature instances",
|
||||||
|
json_schema_extra={"label": "Produkt (Instanzen)"},
|
||||||
|
)
|
||||||
|
stripePriceIdUsers: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Stripe Price ID for user-seat line item",
|
||||||
|
json_schema_extra={"label": "Preis-ID (User)"},
|
||||||
|
)
|
||||||
|
stripePriceIdInstances: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Stripe Price ID for instance line item",
|
||||||
|
json_schema_extra={"label": "Preis-ID (Instanzen)"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Instance: MandateSubscription
|
# Instance: MandateSubscription
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
@i18nModel("Mandanten-Abonnement")
|
||||||
class MandateSubscription(PowerOnModel):
|
class MandateSubscription(PowerOnModel):
|
||||||
"""A subscription instance bound to a specific mandate.
|
"""Abonnement-Instanz gebunden an einen Mandanten."""
|
||||||
See wiki/concepts/Subscription-State-Machine.md for state transitions."""
|
id: str = Field(
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
mandateId: str = Field(..., description="Foreign key to Mandate")
|
description="Primary key",
|
||||||
planKey: str = Field(..., description="Reference to SubscriptionPlan.planKey")
|
json_schema_extra={"label": "ID"},
|
||||||
|
)
|
||||||
|
mandateId: str = Field(
|
||||||
|
...,
|
||||||
|
description="Foreign key to Mandate",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID"},
|
||||||
|
)
|
||||||
|
planKey: str = Field(
|
||||||
|
...,
|
||||||
|
description="Reference to SubscriptionPlan.planKey",
|
||||||
|
json_schema_extra={"label": "Plan"},
|
||||||
|
)
|
||||||
|
|
||||||
status: SubscriptionStatusEnum = Field(default=SubscriptionStatusEnum.PENDING, description="Current lifecycle status")
|
status: SubscriptionStatusEnum = Field(
|
||||||
recurring: bool = Field(default=True, description="True: auto-renews at period end. False: expires at period end (gekuendigt).")
|
default=SubscriptionStatusEnum.PENDING,
|
||||||
|
description="Current lifecycle status",
|
||||||
|
json_schema_extra={"label": "Status"},
|
||||||
|
)
|
||||||
|
recurring: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="True: auto-renews at period end. False: expires at period end (gekuendigt).",
|
||||||
|
json_schema_extra={"label": "Wiederkehrend"},
|
||||||
|
)
|
||||||
|
|
||||||
startedAt: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Record creation timestamp")
|
startedAt: datetime = Field(
|
||||||
effectiveFrom: Optional[datetime] = Field(None, description="When this subscription becomes operative. None = immediate. Set for SCHEDULED subs.")
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
endedAt: Optional[datetime] = Field(None, description="When subscription ended (terminal)")
|
description="Record creation timestamp",
|
||||||
currentPeriodStart: Optional[datetime] = Field(None, description="Current billing period start (synced from Stripe)")
|
json_schema_extra={"label": "Gestartet"},
|
||||||
currentPeriodEnd: Optional[datetime] = Field(None, description="Current billing period end (synced from Stripe)")
|
)
|
||||||
trialEndsAt: Optional[datetime] = Field(None, description="Trial expiry timestamp")
|
effectiveFrom: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="When this subscription becomes operative. None = immediate. Set for SCHEDULED subs.",
|
||||||
|
json_schema_extra={"label": "Wirksam ab"},
|
||||||
|
)
|
||||||
|
endedAt: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="When subscription ended (terminal)",
|
||||||
|
json_schema_extra={"label": "Beendet"},
|
||||||
|
)
|
||||||
|
currentPeriodStart: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="Current billing period start (synced from Stripe)",
|
||||||
|
json_schema_extra={"label": "Periodenbeginn"},
|
||||||
|
)
|
||||||
|
currentPeriodEnd: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="Current billing period end (synced from Stripe)",
|
||||||
|
json_schema_extra={"label": "Periodenende"},
|
||||||
|
)
|
||||||
|
trialEndsAt: Optional[datetime] = Field(
|
||||||
|
None,
|
||||||
|
description="Trial expiry timestamp",
|
||||||
|
json_schema_extra={"label": "Trial endet"},
|
||||||
|
)
|
||||||
|
|
||||||
snapshotPricePerUserCHF: float = Field(default=0.0, description="Price snapshot at activation (for invoice history)")
|
snapshotPricePerUserCHF: float = Field(
|
||||||
snapshotPricePerInstanceCHF: float = Field(default=0.0, description="Price snapshot at activation")
|
default=0.0,
|
||||||
|
description="Price snapshot at activation (for invoice history)",
|
||||||
|
json_schema_extra={"label": "Preis/User (CHF)"},
|
||||||
|
)
|
||||||
|
snapshotPricePerInstanceCHF: float = Field(
|
||||||
|
default=0.0,
|
||||||
|
description="Price snapshot at activation",
|
||||||
|
json_schema_extra={"label": "Preis/Instanz (CHF)"},
|
||||||
|
)
|
||||||
|
|
||||||
stripeSubscriptionId: Optional[str] = Field(None, description="Stripe Subscription ID (sub_xxx)")
|
stripeSubscriptionId: Optional[str] = Field(
|
||||||
stripeItemIdUsers: Optional[str] = Field(None, description="Stripe Subscription Item ID for user seats")
|
None,
|
||||||
stripeItemIdInstances: Optional[str] = Field(None, description="Stripe Subscription Item ID for feature instances")
|
description="Stripe Subscription ID (sub_xxx)",
|
||||||
|
json_schema_extra={"label": "Stripe-Abonnement-ID"},
|
||||||
|
)
|
||||||
registerModelLabels(
|
stripeItemIdUsers: Optional[str] = Field(
|
||||||
"MandateSubscription",
|
None,
|
||||||
{"en": "Mandate Subscription", "de": "Mandanten-Abonnement", "fr": "Abonnement du mandat"},
|
description="Stripe Subscription Item ID for user seats",
|
||||||
{
|
json_schema_extra={"label": "Stripe-Item (User)"},
|
||||||
"id": {"en": "ID", "de": "ID"},
|
)
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
|
stripeItemIdInstances: Optional[str] = Field(
|
||||||
"planKey": {"en": "Plan", "de": "Plan"},
|
None,
|
||||||
"status": {"en": "Status", "de": "Status"},
|
description="Stripe Subscription Item ID for feature instances",
|
||||||
"recurring": {"en": "Recurring", "de": "Wiederkehrend"},
|
json_schema_extra={"label": "Stripe-Item (Instanzen)"},
|
||||||
"startedAt": {"en": "Started", "de": "Gestartet"},
|
)
|
||||||
"effectiveFrom": {"en": "Effective From", "de": "Wirksam ab"},
|
|
||||||
"endedAt": {"en": "Ended", "de": "Beendet"},
|
|
||||||
"currentPeriodStart": {"en": "Period Start", "de": "Periodenbeginn"},
|
|
||||||
"currentPeriodEnd": {"en": "Period End", "de": "Periodenende"},
|
|
||||||
"trialEndsAt": {"en": "Trial Ends", "de": "Trial endet"},
|
|
||||||
"snapshotPricePerUserCHF": {"en": "Price/User (CHF)", "de": "Preis/User (CHF)"},
|
|
||||||
"snapshotPricePerInstanceCHF": {"en": "Price/Instance (CHF)", "de": "Preis/Instanz (CHF)"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -225,10 +326,10 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
|
||||||
"STANDARD_YEARLY": SubscriptionPlan(
|
"STANDARD_YEARLY": SubscriptionPlan(
|
||||||
planKey="STANDARD_YEARLY",
|
planKey="STANDARD_YEARLY",
|
||||||
selectableByUser=True,
|
selectableByUser=True,
|
||||||
title={"en": "Standard (Yearly)", "de": "Standard (Jährlich)", "fr": "Standard (Annuel)"},
|
title={"en": "Standard (Yearly)", "de": "Standard (Jaehrlich)", "fr": "Standard (Annuel)"},
|
||||||
description={
|
description={
|
||||||
"en": "Usage-based billing per active user and feature instance, billed yearly. Includes 120 CHF AI budget.",
|
"en": "Usage-based billing per active user and feature instance, billed yearly. Includes 120 CHF AI budget.",
|
||||||
"de": "Nutzungsbasierte Abrechnung pro aktivem User und Feature-Instanz, jährlich. Inkl. 120 CHF AI-Budget.",
|
"de": "Nutzungsbasierte Abrechnung pro aktivem User und Feature-Instanz, jaehrlich. Inkl. 120 CHF AI-Budget.",
|
||||||
},
|
},
|
||||||
billingPeriod=BillingPeriodEnum.YEARLY,
|
billingPeriod=BillingPeriodEnum.YEARLY,
|
||||||
pricePerUserCHF=948.0,
|
pricePerUserCHF=948.0,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from typing import Optional, List, Dict, Any
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field, EmailStr, field_validator, computed_field
|
from pydantic import BaseModel, Field, EmailStr, field_validator, computed_field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,6 +61,7 @@ class UserPermissions(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Mandant")
|
||||||
class Mandate(PowerOnModel):
|
class Mandate(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
Mandate (Mandant/Tenant) model.
|
Mandate (Mandant/Tenant) model.
|
||||||
|
|
@ -69,31 +70,31 @@ class Mandate(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the mandate",
|
description="Unique ID of the mandate",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
name: str = Field(
|
name: str = Field(
|
||||||
description="Name of the mandate",
|
description="Name of the mandate",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True, "label": "Name"},
|
||||||
)
|
)
|
||||||
label: Optional[str] = Field(
|
label: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Display label of the mandate",
|
description="Display label of the mandate",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Label"},
|
||||||
)
|
)
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Indicates whether the mandate is enabled",
|
description="Indicates whether the mandate is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "Aktiviert"},
|
||||||
)
|
)
|
||||||
isSystem: bool = Field(
|
isSystem: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this is a system mandate (e.g. root mandate). Cannot be deleted.",
|
description="Whether this is a system mandate (e.g. root mandate). Cannot be deleted.",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False, "label": "System-Mandant"},
|
||||||
)
|
)
|
||||||
deletedAt: Optional[float] = Field(
|
deletedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Timestamp when the mandate was soft-deleted. After 30 days, hard-delete is triggered.",
|
description="Timestamp when the mandate was soft-deleted. After 30 days, hard-delete is triggered.",
|
||||||
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
|
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Gelöscht am"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator('isSystem', mode='before')
|
@field_validator('isSystem', mode='before')
|
||||||
|
|
@ -104,38 +105,91 @@ class Mandate(PowerOnModel):
|
||||||
return False
|
return False
|
||||||
return v
|
return v
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Benutzerverbindung")
|
||||||
"Mandate",
|
|
||||||
{"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"name": {"en": "Name", "de": "Name", "fr": "Nom"},
|
|
||||||
"label": {"en": "Label", "de": "Label", "fr": "Libellé"},
|
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
|
||||||
"isSystem": {"en": "System Mandate", "de": "System-Mandant", "fr": "Mandat système"},
|
|
||||||
"deletedAt": {"en": "Deleted at", "de": "Gelöscht am", "fr": "Supprimé le"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserConnection(PowerOnModel):
|
class UserConnection(PowerOnModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
id: str = Field(
|
||||||
userId: str = Field(description="ID of the user this connection belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
authority: AuthAuthority = Field(description="Authentication authority", json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": "/api/connections/authorities/options"})
|
description="Unique ID of the connection",
|
||||||
externalId: str = Field(description="User ID in the external system", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
externalUsername: str = Field(description="Username in the external system", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
)
|
||||||
externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system", json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False})
|
userId: str = Field(
|
||||||
status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "/api/connections/statuses/options"})
|
description="ID of the user this connection belongs to",
|
||||||
connectedAt: float = Field(default_factory=getUtcTimestamp, description="When the connection was established (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Benutzer-ID"},
|
||||||
lastChecked: float = Field(default_factory=getUtcTimestamp, description="When the connection was last verified (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
|
)
|
||||||
expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
|
authority: AuthAuthority = Field(
|
||||||
tokenStatus: Optional[str] = Field(None, description="Current token status: active, expired, none", json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": [
|
description="Authentication authority",
|
||||||
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
|
json_schema_extra={
|
||||||
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
|
"frontend_type": "select",
|
||||||
{"value": "none", "label": {"en": "None", "fr": "Aucun"}},
|
"frontend_readonly": True,
|
||||||
]})
|
"frontend_required": False,
|
||||||
tokenExpiresAt: Optional[float] = Field(None, description="When the current token expires (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
|
"frontend_options": "/api/connections/authorities/options",
|
||||||
grantedScopes: Optional[List[str]] = Field(None, description="OAuth scopes granted for this connection", json_schema_extra={"frontend_type": "list", "frontend_readonly": True, "frontend_required": False})
|
"label": "Autorität",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
externalId: str = Field(
|
||||||
|
description="User ID in the external system",
|
||||||
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Externe ID"},
|
||||||
|
)
|
||||||
|
externalUsername: str = Field(
|
||||||
|
description="Username in the external system",
|
||||||
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Externer Benutzername"},
|
||||||
|
)
|
||||||
|
externalEmail: Optional[EmailStr] = Field(
|
||||||
|
None,
|
||||||
|
description="Email in the external system",
|
||||||
|
json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False, "label": "Externe E-Mail"},
|
||||||
|
)
|
||||||
|
status: ConnectionStatus = Field(
|
||||||
|
default=ConnectionStatus.ACTIVE,
|
||||||
|
description="Connection status",
|
||||||
|
json_schema_extra={
|
||||||
|
"frontend_type": "select",
|
||||||
|
"frontend_readonly": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"frontend_options": "/api/connections/statuses/options",
|
||||||
|
"label": "Status",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
connectedAt: float = Field(
|
||||||
|
default_factory=getUtcTimestamp,
|
||||||
|
description="When the connection was established (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Verbunden am"},
|
||||||
|
)
|
||||||
|
lastChecked: float = Field(
|
||||||
|
default_factory=getUtcTimestamp,
|
||||||
|
description="When the connection was last verified (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Zuletzt geprüft"},
|
||||||
|
)
|
||||||
|
expiresAt: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
description="When the connection expires (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Läuft ab am"},
|
||||||
|
)
|
||||||
|
tokenStatus: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Current token status: active, expired, none",
|
||||||
|
json_schema_extra={
|
||||||
|
"frontend_type": "select",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"frontend_options": [
|
||||||
|
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
|
||||||
|
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
|
||||||
|
{"value": "none", "label": {"en": "None", "fr": "Aucun"}},
|
||||||
|
],
|
||||||
|
"label": "Verbindungsstatus",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
tokenExpiresAt: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
description="When the current token expires (UTC timestamp in seconds)",
|
||||||
|
json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Token läuft ab am"},
|
||||||
|
)
|
||||||
|
grantedScopes: Optional[List[str]] = Field(
|
||||||
|
None,
|
||||||
|
description="OAuth scopes granted for this connection",
|
||||||
|
json_schema_extra={"frontend_type": "list", "frontend_readonly": True, "frontend_required": False, "label": "Gewährte Berechtigungen"},
|
||||||
|
)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@computed_field
|
@computed_field
|
||||||
|
|
@ -157,29 +211,7 @@ class UserConnection(PowerOnModel):
|
||||||
return f"{authorityLabels.get(self.authority.value, self.authority.value)}: {self.externalUsername}"
|
return f"{authorityLabels.get(self.authority.value, self.authority.value)}: {self.externalUsername}"
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Benutzer")
|
||||||
"UserConnection",
|
|
||||||
{"en": "User Connection", "de": "Benutzerverbindung", "fr": "Connexion utilisateur"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
|
||||||
"authority": {"en": "Authority", "de": "Autorität", "fr": "Autorité"},
|
|
||||||
"externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
|
|
||||||
"externalUsername": {"en": "External Username", "de": "Externer Benutzername", "fr": "Nom d'utilisateur externe"},
|
|
||||||
"externalEmail": {"en": "External Email", "de": "Externe E-Mail", "fr": "Email externe"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"connectedAt": {"en": "Connected At", "de": "Verbunden am", "fr": "Connecté le"},
|
|
||||||
"lastChecked": {"en": "Last Checked", "de": "Zuletzt geprüft", "fr": "Dernière vérification"},
|
|
||||||
"expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
|
|
||||||
"tokenStatus": {"en": "Connection Status", "de": "Verbindungsstatus", "fr": "Statut de connexion"},
|
|
||||||
"tokenExpiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
|
|
||||||
"grantedScopes": {"en": "Granted Scopes", "de": "Gewährte Berechtigungen", "fr": "Autorisations accordées"},
|
|
||||||
"connectionReference": {"en": "Connection Reference", "de": "Verbindungsreferenz", "fr": "Référence de connexion"},
|
|
||||||
"displayLabel": {"en": "Display Label", "de": "Anzeigebezeichnung", "fr": "Libellé d'affichage"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class User(PowerOnModel):
|
class User(PowerOnModel):
|
||||||
"""
|
"""
|
||||||
User model.
|
User model.
|
||||||
|
|
@ -193,31 +225,37 @@ class User(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique ID of the user",
|
description="Unique ID of the user",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
username: str = Field(
|
username: str = Field(
|
||||||
description="Username for login (immutable after creation)",
|
description="Username for login (immutable after creation)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Benutzername"},
|
||||||
)
|
)
|
||||||
email: Optional[EmailStr] = Field(
|
email: Optional[EmailStr] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Email address of the user",
|
description="Email address of the user",
|
||||||
json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": True}
|
json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": True, "label": "E-Mail"},
|
||||||
)
|
)
|
||||||
fullName: Optional[str] = Field(
|
fullName: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Full name of the user",
|
description="Full name of the user",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Vollständiger Name"},
|
||||||
)
|
)
|
||||||
language: str = Field(
|
language: str = Field(
|
||||||
default="de",
|
default="de",
|
||||||
description="Preferred language of the user (ISO 639-1 code: de, en, fr, it)",
|
description="Preferred language of the user (ISO 639-1 code: de, en, fr, it)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [
|
json_schema_extra={
|
||||||
{"value": "de", "label": {"en": "Deutsch", "de": "Deutsch", "fr": "Allemand"}},
|
"frontend_type": "select",
|
||||||
{"value": "en", "label": {"en": "English", "de": "Englisch", "fr": "Anglais"}},
|
"frontend_readonly": False,
|
||||||
{"value": "fr", "label": {"en": "Français", "de": "Französisch", "fr": "Français"}},
|
"frontend_required": True,
|
||||||
{"value": "it", "label": {"en": "Italiano", "de": "Italienisch", "fr": "Italien"}},
|
"frontend_options": [
|
||||||
]}
|
{"value": "de", "label": {"en": "Deutsch", "de": "Deutsch", "fr": "Allemand"}},
|
||||||
|
{"value": "en", "label": {"en": "English", "de": "Englisch", "fr": "Anglais"}},
|
||||||
|
{"value": "fr", "label": {"en": "Français", "de": "Französisch", "fr": "Français"}},
|
||||||
|
{"value": "it", "label": {"en": "Italiano", "de": "Italienisch", "fr": "Italien"}},
|
||||||
|
],
|
||||||
|
"label": "Sprache",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator('language', mode='before')
|
@field_validator('language', mode='before')
|
||||||
|
|
@ -245,13 +283,13 @@ class User(PowerOnModel):
|
||||||
enabled: bool = Field(
|
enabled: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Indicates whether the user is enabled",
|
description="Indicates whether the user is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "Aktiviert"},
|
||||||
)
|
)
|
||||||
|
|
||||||
isSysAdmin: bool = Field(
|
isSysAdmin: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Global SysAdmin flag. SysAdmin = System-Zugriff, KEIN Daten-Zugriff!",
|
description="Global SysAdmin flag. SysAdmin = System-Zugriff, KEIN Daten-Zugriff!",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "System-Admin"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator('isSysAdmin', mode='before')
|
@field_validator('isSysAdmin', mode='before')
|
||||||
|
|
@ -265,48 +303,45 @@ class User(PowerOnModel):
|
||||||
authenticationAuthority: AuthAuthority = Field(
|
authenticationAuthority: AuthAuthority = Field(
|
||||||
default=AuthAuthority.LOCAL,
|
default=AuthAuthority.LOCAL,
|
||||||
description="Primary authentication authority",
|
description="Primary authentication authority",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": "/api/connections/authorities/options"}
|
json_schema_extra={
|
||||||
|
"frontend_type": "select",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_required": False,
|
||||||
|
"frontend_options": "/api/connections/authorities/options",
|
||||||
|
"label": "Authentifizierung",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
roleLabels: List[str] = Field(
|
roleLabels: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Role labels (from DB or enriched when loading users)",
|
description="Role labels (from DB or enriched when loading users)",
|
||||||
json_schema_extra={"frontend_type": "multiselect", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False},
|
json_schema_extra={
|
||||||
|
"frontend_type": "multiselect",
|
||||||
|
"frontend_readonly": True,
|
||||||
|
"frontend_visible": False,
|
||||||
|
"frontend_required": False,
|
||||||
|
"label": "Rollen-Labels",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
@i18nModel("Benutzerzugang")
|
||||||
"User",
|
|
||||||
{"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"username": {"en": "Username", "de": "Benutzername", "fr": "Nom d'utilisateur"},
|
|
||||||
"email": {"en": "Email", "de": "E-Mail", "fr": "Email"},
|
|
||||||
"fullName": {"en": "Full Name", "de": "Vollständiger Name", "fr": "Nom complet"},
|
|
||||||
"language": {"en": "Language", "de": "Sprache", "fr": "Langue"},
|
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
|
||||||
"isSysAdmin": {"en": "System Admin", "de": "System-Admin", "fr": "Admin système"},
|
|
||||||
"authenticationAuthority": {"en": "Auth Authority", "de": "Authentifizierung", "fr": "Autorité d'authentification"},
|
|
||||||
"roleLabels": {"en": "Role Labels", "de": "Rollen-Labels", "fr": "Libellés de rôles"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserInDB(User):
|
class UserInDB(User):
|
||||||
"""User model with password hash for database storage."""
|
"""User model with password hash for database storage."""
|
||||||
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
|
hashedPassword: Optional[str] = Field(
|
||||||
resetToken: Optional[str] = Field(None, description="Password reset token (UUID)")
|
None,
|
||||||
resetTokenExpires: Optional[float] = Field(None, description="Reset token expiration (UTC timestamp in seconds)")
|
description="Hash of the user password",
|
||||||
|
json_schema_extra={"label": "Passwort-Hash"},
|
||||||
|
)
|
||||||
registerModelLabels(
|
resetToken: Optional[str] = Field(
|
||||||
"UserInDB",
|
None,
|
||||||
{"en": "User Access", "de": "Benutzerzugang", "fr": "Accès de l'utilisateur"},
|
description="Password reset token (UUID)",
|
||||||
{
|
json_schema_extra={"label": "Reset-Token"},
|
||||||
"hashedPassword": {"en": "Password hash", "de": "Passwort-Hash", "fr": "Hachage de mot de passe"},
|
)
|
||||||
"resetToken": {"en": "Reset Token", "de": "Reset-Token", "fr": "Jeton de réinitialisation"},
|
resetTokenExpires: Optional[float] = Field(
|
||||||
"resetTokenExpires": {"en": "Reset Token Expires", "de": "Token läuft ab", "fr": "Expiration du jeton"},
|
None,
|
||||||
},
|
description="Reset token expiration (UTC timestamp in seconds)",
|
||||||
)
|
json_schema_extra={"label": "Token läuft ab"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _normalizeTtsVoiceMap(value: Any) -> Optional[Dict[str, str]]:
|
def _normalizeTtsVoiceMap(value: Any) -> Optional[Dict[str, str]]:
|
||||||
|
|
@ -336,17 +371,50 @@ def _normalizeTtsVoiceMap(value: Any) -> Optional[Dict[str, str]]:
|
||||||
return out if out else None
|
return out if out else None
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Spracheinstellungen")
|
||||||
class UserVoicePreferences(PowerOnModel):
|
class UserVoicePreferences(PowerOnModel):
|
||||||
"""User-level voice/language preferences, shared across all features."""
|
"""User-level voice/language preferences, shared across all features."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
|
id: str = Field(
|
||||||
userId: str = Field(description="User ID")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
mandateId: Optional[str] = Field(default=None, description="Mandate scope (None = global for user)")
|
description="Primary key",
|
||||||
sttLanguage: str = Field(default="de-DE", description="Speech-to-text language code")
|
json_schema_extra={"label": "ID"},
|
||||||
ttsLanguage: str = Field(default="de-DE", description="Text-to-speech language code")
|
)
|
||||||
ttsVoice: Optional[str] = Field(default=None, description="Preferred TTS voice identifier")
|
userId: str = Field(description="User ID", json_schema_extra={"label": "Benutzer-ID"})
|
||||||
ttsVoiceMap: Optional[Dict[str, str]] = Field(default=None, description="Language-to-voice mapping")
|
mandateId: Optional[str] = Field(
|
||||||
translationSourceLanguage: Optional[str] = Field(default=None, description="Source language for translations")
|
default=None,
|
||||||
translationTargetLanguage: Optional[str] = Field(default=None, description="Target language for translations")
|
description="Mandate scope (None = global for user)",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID"},
|
||||||
|
)
|
||||||
|
sttLanguage: str = Field(
|
||||||
|
default="de-DE",
|
||||||
|
description="Speech-to-text language code",
|
||||||
|
json_schema_extra={"label": "STT-Sprache"},
|
||||||
|
)
|
||||||
|
ttsLanguage: str = Field(
|
||||||
|
default="de-DE",
|
||||||
|
description="Text-to-speech language code",
|
||||||
|
json_schema_extra={"label": "TTS-Sprache"},
|
||||||
|
)
|
||||||
|
ttsVoice: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Preferred TTS voice identifier",
|
||||||
|
json_schema_extra={"label": "TTS-Stimme"},
|
||||||
|
)
|
||||||
|
ttsVoiceMap: Optional[Dict[str, str]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Language-to-voice mapping",
|
||||||
|
json_schema_extra={"label": "Stimmen-Zuordnung"},
|
||||||
|
)
|
||||||
|
translationSourceLanguage: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Source language for translations",
|
||||||
|
json_schema_extra={"label": "Übersetzung Quelle"},
|
||||||
|
)
|
||||||
|
translationTargetLanguage: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Target language for translations",
|
||||||
|
json_schema_extra={"label": "Übersetzung Ziel"},
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("ttsVoiceMap", mode="before")
|
@field_validator("ttsVoiceMap", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -354,18 +422,3 @@ class UserVoicePreferences(PowerOnModel):
|
||||||
return _normalizeTtsVoiceMap(value)
|
return _normalizeTtsVoiceMap(value)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UserVoicePreferences",
|
|
||||||
{"en": "Voice Preferences", "de": "Spracheinstellungen", "fr": "Préférences vocales"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
|
|
||||||
"sttLanguage": {"en": "STT Language", "de": "STT-Sprache", "fr": "Langue STT"},
|
|
||||||
"ttsLanguage": {"en": "TTS Language", "de": "TTS-Sprache", "fr": "Langue TTS"},
|
|
||||||
"ttsVoice": {"en": "TTS Voice", "de": "TTS-Stimme", "fr": "Voix TTS"},
|
|
||||||
"ttsVoiceMap": {"en": "Voice Map", "de": "Stimmen-Zuordnung", "fr": "Carte des voix"},
|
|
||||||
"translationSourceLanguage": {"en": "Translation Source", "de": "Übersetzung Quelle", "fr": "Langue source"},
|
|
||||||
"translationTargetLanguage": {"en": "Translation Target", "de": "Übersetzung Ziel", "fr": "Langue cible"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import List, Literal
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
UiLanguageStatus = Literal["complete", "incomplete", "generating"]
|
UiLanguageStatus = Literal["complete", "incomplete", "generating"]
|
||||||
|
|
@ -20,7 +20,7 @@ class I18nEntry(BaseModel):
|
||||||
"db.management.files.name" for backend data objects.
|
"db.management.files.name" for backend data objects.
|
||||||
key: German plaintext (the canonical identifier across all sets).
|
key: German plaintext (the canonical identifier across all sets).
|
||||||
value: For xx (base set): UI context description for AI translation.
|
value: For xx (base set): UI context description for AI translation.
|
||||||
For language sets (de, en, …): the translated text.
|
For language sets (de, en, ...): the translated text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
context: str = Field(
|
context: str = Field(
|
||||||
|
|
@ -37,17 +37,15 @@ class I18nEntry(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("UI-Sprachset")
|
||||||
class UiLanguageSet(PowerOnModel):
|
class UiLanguageSet(PowerOnModel):
|
||||||
"""One row per language. id = ISO 639-1 code or 'xx' (base set).
|
"""Ein Sprachset pro Sprache. id = ISO 639-1 Code oder 'xx' (Basisset). Enthaelt alle Uebersetzungen."""
|
||||||
|
|
||||||
The xx set is the master: key = German plaintext, value = UI context for AI.
|
|
||||||
All other sets (incl. de) are AI-generated translations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
...,
|
...,
|
||||||
description="ISO 639-1 language code or 'xx' for the base set",
|
description="ISO 639-1 language code or 'xx' for the base set",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Code",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -57,6 +55,7 @@ class UiLanguageSet(PowerOnModel):
|
||||||
...,
|
...,
|
||||||
description="Human-readable language name",
|
description="Human-readable language name",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Bezeichnung",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -66,6 +65,7 @@ class UiLanguageSet(PowerOnModel):
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Translation entries: list of {context, key, value}",
|
description="Translation entries: list of {context, key, value}",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Eintraege",
|
||||||
"frontend_type": "textarea",
|
"frontend_type": "textarea",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -75,6 +75,7 @@ class UiLanguageSet(PowerOnModel):
|
||||||
default="complete",
|
default="complete",
|
||||||
description="complete | incomplete | generating",
|
description="complete | incomplete | generating",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Status",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -89,21 +90,9 @@ class UiLanguageSet(PowerOnModel):
|
||||||
default=False,
|
default=False,
|
||||||
description="True only for the xx base set",
|
description="True only for the xx base set",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Standard",
|
||||||
"frontend_type": "boolean",
|
"frontend_type": "boolean",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UiLanguageSet",
|
|
||||||
{"en": "UI Language Set", "de": "UI-Sprachset"},
|
|
||||||
{
|
|
||||||
"id": {"en": "Code", "de": "Code"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung"},
|
|
||||||
"entries": {"en": "Entries", "de": "Einträge"},
|
|
||||||
"status": {"en": "Status", "de": "Status"},
|
|
||||||
"isDefault": {"en": "Default", "de": "Standard"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,40 @@
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
"""Utility datamodels: Prompt, TextMultilingual."""
|
"""Utility datamodels: Prompt, TextMultilingual."""
|
||||||
|
|
||||||
from typing import Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Prompt")
|
||||||
class Prompt(PowerOnModel):
|
class Prompt(PowerOnModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
"""Benutzer- oder System-Prompt fuer die KI."""
|
||||||
mandateId: str = Field(default="", description="ID of the mandate this prompt belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
id: str = Field(
|
||||||
isSystem: bool = Field(default=False, description="System prompt visible to all users (read-only for non-SysAdmin)", json_schema_extra={"frontend_type": "boolean", "frontend_readonly": True, "frontend_required": False})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
content: str = Field(description="Content of the prompt", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True})
|
description="Primary key",
|
||||||
name: str = Field(description="Name of the prompt", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
mandateId: str = Field(
|
||||||
|
default="",
|
||||||
|
description="ID of the mandate this prompt belongs to",
|
||||||
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
isSystem: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="System prompt visible to all users (read-only for non-SysAdmin)",
|
||||||
|
json_schema_extra={"label": "System", "frontend_type": "boolean", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
content: str = Field(
|
||||||
|
description="Content of the prompt",
|
||||||
|
json_schema_extra={"label": "Inhalt", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True},
|
||||||
|
)
|
||||||
|
name: str = Field(
|
||||||
|
description="Name of the prompt",
|
||||||
|
json_schema_extra={"label": "Name", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator('isSystem', mode='before')
|
@field_validator('isSystem', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def _coerceIsSystem(cls, v):
|
def _coerceIsSystem(cls, v):
|
||||||
|
|
@ -23,62 +43,64 @@ class Prompt(PowerOnModel):
|
||||||
if v is None:
|
if v is None:
|
||||||
return False
|
return False
|
||||||
return v
|
return v
|
||||||
registerModelLabels(
|
|
||||||
"Prompt",
|
|
||||||
{"en": "Prompt", "fr": "Invite"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
|
|
||||||
"isSystem": {"en": "System", "fr": "Système"},
|
|
||||||
"content": {"en": "Content", "fr": "Contenu"},
|
|
||||||
"name": {"en": "Name", "fr": "Nom"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TextMultilingual(BaseModel):
|
class TextMultilingual(BaseModel):
|
||||||
"""
|
"""Multilingual text field. Language codes follow ISO 639-1 (en, de, fr, it, …)."""
|
||||||
Multilingual text field supporting multiple languages.
|
|
||||||
Default languages: en (English), ge (German), fr (French), it (Italian)
|
|
||||||
English (en) is the default/required language.
|
|
||||||
"""
|
|
||||||
en: str = Field(description="English text (default language, required)")
|
en: str = Field(description="English text (default language, required)")
|
||||||
ge: Optional[str] = Field(None, description="German text")
|
de: Optional[str] = Field(None, description="German text")
|
||||||
fr: Optional[str] = Field(None, description="French text")
|
fr: Optional[str] = Field(None, description="French text")
|
||||||
it: Optional[str] = Field(None, description="Italian text")
|
it: Optional[str] = Field(None, description="Italian text")
|
||||||
|
|
||||||
@field_validator('en')
|
@field_validator('en')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_en_required(cls, v):
|
def _validateEnRequired(cls, v):
|
||||||
"""Ensure English text is not empty"""
|
|
||||||
if not v or not v.strip():
|
if not v or not v.strip():
|
||||||
raise ValueError("English text (en) is required and cannot be empty")
|
raise ValueError("English text (en) is required and cannot be empty")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def model_dump(self, **kwargs) -> Dict[str, str]:
|
def model_dump(self, **kwargs) -> Dict[str, str]:
|
||||||
"""Return as dictionary, filtering out None values"""
|
|
||||||
result = {}
|
result = {}
|
||||||
for lang in ['en', 'ge', 'fr', 'it']:
|
for key in self.model_fields:
|
||||||
value = getattr(self, lang, None)
|
value = getattr(self, key, None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
result[lang] = value
|
result[key] = value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, str]) -> 'TextMultilingual':
|
def from_dict(cls, data: Dict[str, str]) -> 'TextMultilingual':
|
||||||
"""Create TextMultilingual from dictionary"""
|
fields = {k: data[k] for k in cls.model_fields if k in data}
|
||||||
return cls(
|
fields.setdefault('en', '')
|
||||||
en=data.get('en', ''),
|
return cls(**fields)
|
||||||
ge=data.get('ge'),
|
|
||||||
fr=data.get('fr'),
|
|
||||||
it=data.get('it')
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_text(self, lang: str = 'en') -> str:
|
def get_text(self, lang: str = 'en') -> str:
|
||||||
"""Get text for a specific language, fallback to English if not available"""
|
"""Get text for *lang*. Falls back to English."""
|
||||||
value = getattr(self, lang, None)
|
value = getattr(self, lang, None)
|
||||||
if value:
|
if value:
|
||||||
return value
|
return value
|
||||||
return self.en # Fallback to English
|
return self.en
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromUniform(cls, text: str) -> "TextMultilingual":
|
||||||
|
"""Same string in all languages (bootstrap / i18n key until per-language values exist in DB)."""
|
||||||
|
t = text.strip()
|
||||||
|
if not t:
|
||||||
|
raise ValueError("Text must be non-empty")
|
||||||
|
return cls(en=t, de=t, fr=t, it=t)
|
||||||
|
|
||||||
|
|
||||||
|
def coerce_text_multilingual(val: Any) -> TextMultilingual:
|
||||||
|
"""Normalize str, dict, or TextMultilingual for Role.description and similar fields."""
|
||||||
|
if isinstance(val, TextMultilingual):
|
||||||
|
return val
|
||||||
|
if isinstance(val, dict):
|
||||||
|
if not val:
|
||||||
|
return TextMultilingual.fromUniform("—")
|
||||||
|
d = {k: val[k] for k in TextMultilingual.model_fields if k in val and val[k] is not None}
|
||||||
|
if not d.get("en"):
|
||||||
|
d["en"] = (d.get("de") or d.get("fr") or "—").strip() or "—"
|
||||||
|
return TextMultilingual(**{k: d[k] for k in TextMultilingual.model_fields if k in d})
|
||||||
|
if isinstance(val, str) and val.strip():
|
||||||
|
return TextMultilingual.fromUniform(val)
|
||||||
|
return TextMultilingual.fromUniform("—")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,45 +6,52 @@ Workflow execution models for action definitions, AI responses, and workflow-lev
|
||||||
|
|
||||||
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrokenJson
|
from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrokenJson
|
||||||
|
|
||||||
# Import DocumentReferenceList at runtime (needed for ActionDefinition)
|
# Import DocumentReferenceList at runtime (needed for ActionDefinition)
|
||||||
from modules.datamodels.datamodelDocref import DocumentReferenceList
|
from modules.datamodels.datamodelDocref import DocumentReferenceList
|
||||||
|
|
||||||
|
@i18nModel("Aktionsdefinition")
|
||||||
class ActionDefinition(BaseModel):
|
class ActionDefinition(BaseModel):
|
||||||
"""Action definition with selection and parameters from planning phase"""
|
"""Action definition with selection and parameters from planning phase"""
|
||||||
|
|
||||||
# Core action selection (Stage 1)
|
# Core action selection (Stage 1)
|
||||||
action: str = Field(description="Compound action name (method.action)")
|
action: str = Field(description="Compound action name (method.action)", json_schema_extra={"label": "Aktion"})
|
||||||
actionObjective: str = Field(description="Objective for this action")
|
actionObjective: str = Field(description="Objective for this action", json_schema_extra={"label": "Aktionsziel"})
|
||||||
userMessage: Optional[str] = Field(
|
userMessage: Optional[str] = Field(
|
||||||
None,
|
None,
|
||||||
description="User-friendly message in user's language explaining what this action will do (generated by AI in prompts)"
|
description="User-friendly message in user's language explaining what this action will do (generated by AI in prompts)",
|
||||||
|
json_schema_extra={"label": "Benutzernachricht"},
|
||||||
)
|
)
|
||||||
parametersContext: Optional[str] = Field(
|
parametersContext: Optional[str] = Field(
|
||||||
None,
|
None,
|
||||||
description="Context for parameter generation"
|
description="Context for parameter generation",
|
||||||
|
json_schema_extra={"label": "Parameter-Kontext"},
|
||||||
)
|
)
|
||||||
learnings: List[str] = Field(
|
learnings: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Learnings from previous actions"
|
description="Learnings from previous actions",
|
||||||
|
json_schema_extra={"label": "Erkenntnisse"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resources (ALWAYS defined in Stage 1 if action needs them)
|
# Resources (ALWAYS defined in Stage 1 if action needs them)
|
||||||
documentList: Optional[DocumentReferenceList] = Field(
|
documentList: Optional[DocumentReferenceList] = Field(
|
||||||
None,
|
None,
|
||||||
description="Document references (ALWAYS defined in Stage 1 if action needs documents)"
|
description="Document references (ALWAYS defined in Stage 1 if action needs documents)",
|
||||||
|
json_schema_extra={"label": "Dokumentenliste"},
|
||||||
)
|
)
|
||||||
connectionReference: Optional[str] = Field(
|
connectionReference: Optional[str] = Field(
|
||||||
None,
|
None,
|
||||||
description="Connection reference (ALWAYS defined in Stage 1 if action needs connection)"
|
description="Connection reference (ALWAYS defined in Stage 1 if action needs connection)",
|
||||||
|
json_schema_extra={"label": "Verbindungsreferenz"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parameters (may be defined in Stage 1 OR Stage 2, depending on action and actionObjective)
|
# Parameters (may be defined in Stage 1 OR Stage 2, depending on action and actionObjective)
|
||||||
parameters: Optional[Dict[str, Any]] = Field(
|
parameters: Optional[Dict[str, Any]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Action-specific parameters (generated in Stage 2 for complex actions, or inferred from actionObjective for simple actions)"
|
description="Action-specific parameters (generated in Stage 2 for complex actions, or inferred from actionObjective for simple actions)",
|
||||||
|
json_schema_extra={"label": "Parameter"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def hasParameters(self) -> bool:
|
def hasParameters(self) -> bool:
|
||||||
|
|
@ -75,34 +82,47 @@ class ActionDefinition(BaseModel):
|
||||||
self.connectionReference = connectionRef
|
self.connectionReference = connectionRef
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("KI-Antwort-Metadaten")
|
||||||
class AiResponseMetadata(BaseModel):
|
class AiResponseMetadata(BaseModel):
|
||||||
"""Metadata for AI response (varies by operation type)."""
|
"""Metadata for AI response (varies by operation type)."""
|
||||||
|
|
||||||
# Document Generation Metadata
|
# Document Generation Metadata
|
||||||
title: Optional[str] = Field(None, description="Document title")
|
title: Optional[str] = Field(None, description="Document title", json_schema_extra={"label": "Titel"})
|
||||||
filename: Optional[str] = Field(None, description="Document filename")
|
filename: Optional[str] = Field(None, description="Document filename", json_schema_extra={"label": "Dateiname"})
|
||||||
|
|
||||||
# Operation-Specific Metadata
|
# Operation-Specific Metadata
|
||||||
operationType: Optional[str] = Field(None, description="Type of operation performed")
|
operationType: Optional[str] = Field(None, description="Type of operation performed", json_schema_extra={"label": "Vorgangstyp"})
|
||||||
schemaVersion: Optional[str] = Field(None, description="Schema version (e.g., 'parameters_v1')", alias="schema")
|
schemaVersion: Optional[str] = Field(
|
||||||
extractionMethod: Optional[str] = Field(None, description="Method used for extraction")
|
None,
|
||||||
sourceDocuments: Optional[List[str]] = Field(None, description="Source document references")
|
description="Schema version (e.g., 'parameters_v1')",
|
||||||
|
alias="schema",
|
||||||
|
json_schema_extra={"label": "Schema-Version"},
|
||||||
|
)
|
||||||
|
extractionMethod: Optional[str] = Field(None, description="Method used for extraction", json_schema_extra={"label": "Extraktionsmethode"})
|
||||||
|
sourceDocuments: Optional[List[str]] = Field(None, description="Source document references", json_schema_extra={"label": "Quelldokumente"})
|
||||||
|
|
||||||
# Additional metadata (for extensibility)
|
# Additional metadata (for extensibility)
|
||||||
additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
|
additionalData: Optional[Dict[str, Any]] = Field(
|
||||||
|
|
||||||
|
|
||||||
class DocumentData(BaseModel):
|
|
||||||
"""Single document in response"""
|
|
||||||
documentName: str = Field(description="Document name")
|
|
||||||
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
|
|
||||||
mimeType: str = Field(description="MIME type of the document")
|
|
||||||
sourceJson: Optional[Dict[str, Any]] = Field(
|
|
||||||
None,
|
None,
|
||||||
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
|
description="Additional operation-specific metadata",
|
||||||
|
json_schema_extra={"label": "Zusätzliche Daten"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dokumentdaten")
|
||||||
|
class DocumentData(BaseModel):
|
||||||
|
"""Single document in response"""
|
||||||
|
documentName: str = Field(description="Document name", json_schema_extra={"label": "Dokumentname"})
|
||||||
|
documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)", json_schema_extra={"label": "Dokumentdaten"})
|
||||||
|
mimeType: str = Field(description="MIME type of the document", json_schema_extra={"label": "MIME-Typ"})
|
||||||
|
sourceJson: Optional[Dict[str, Any]] = Field(
|
||||||
|
None,
|
||||||
|
description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)",
|
||||||
|
json_schema_extra={"label": "Quell-JSON"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Extraktionsparameter")
|
||||||
class ExtractContentParameters(BaseModel):
|
class ExtractContentParameters(BaseModel):
|
||||||
"""Parameters for extraction action.
|
"""Parameters for extraction action.
|
||||||
|
|
||||||
|
|
@ -110,24 +130,34 @@ class ExtractContentParameters(BaseModel):
|
||||||
All action parameter models follow this pattern: defined in the same module as the action.
|
All action parameter models follow this pattern: defined in the same module as the action.
|
||||||
However, since this is a workflow-level model used across the system, it's defined here.
|
However, since this is a workflow-level model used across the system, it's defined here.
|
||||||
"""
|
"""
|
||||||
documentList: DocumentReferenceList = Field(description="Document references to extract content from")
|
documentList: DocumentReferenceList = Field(
|
||||||
|
description="Document references to extract content from",
|
||||||
|
json_schema_extra={"label": "Dokumentenliste"},
|
||||||
|
)
|
||||||
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
||||||
None,
|
None,
|
||||||
description="Extraction options (determined dynamically based on task and document characteristics)"
|
description="Extraction options (determined dynamically based on task and document characteristics)",
|
||||||
|
json_schema_extra={"label": "Extraktionsoptionen"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("KI-Antwort")
|
||||||
class AiResponse(BaseModel):
|
class AiResponse(BaseModel):
|
||||||
"""Unified response from all AI calls (planning, text, documents)"""
|
"""Unified response from all AI calls (planning, text, documents)"""
|
||||||
|
|
||||||
content: str = Field(description="Response content (JSON string for planning, text for analysis, unified JSON for documents)")
|
content: str = Field(
|
||||||
|
description="Response content (JSON string for planning, text for analysis, unified JSON for documents)",
|
||||||
|
json_schema_extra={"label": "Inhalt"},
|
||||||
|
)
|
||||||
metadata: Optional[AiResponseMetadata] = Field(
|
metadata: Optional[AiResponseMetadata] = Field(
|
||||||
None,
|
None,
|
||||||
description="Response metadata (varies by operation type)"
|
description="Response metadata (varies by operation type)",
|
||||||
|
json_schema_extra={"label": "Metadaten"},
|
||||||
)
|
)
|
||||||
documents: Optional[List[DocumentData]] = Field(
|
documents: Optional[List[DocumentData]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Generated documents (only for document generation operations)"
|
description="Generated documents (only for document generation operations)",
|
||||||
|
json_schema_extra={"label": "Dokumente"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def toJson(self) -> Dict[str, Any]:
|
def toJson(self) -> Dict[str, Any]:
|
||||||
|
|
@ -186,278 +216,88 @@ class AiResponse(BaseModel):
|
||||||
|
|
||||||
# Workflow-level models
|
# Workflow-level models
|
||||||
|
|
||||||
|
@i18nModel("Anfragekontext")
|
||||||
class RequestContext(BaseModel):
|
class RequestContext(BaseModel):
|
||||||
"""Normalized request context from user input"""
|
"""Normalized request context from user input"""
|
||||||
|
|
||||||
originalPrompt: str = Field(description="Original user prompt")
|
originalPrompt: str = Field(description="Original user prompt", json_schema_extra={"label": "Ursprüngliche Eingabe"})
|
||||||
documents: List[Any] = Field( # ChatDocument - forward reference
|
documents: List[Any] = Field( # ChatDocument - forward reference
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Documents provided by user"
|
description="Documents provided by user",
|
||||||
|
json_schema_extra={"label": "Dokumente"},
|
||||||
)
|
)
|
||||||
userLanguage: str = Field(description="User's language")
|
userLanguage: str = Field(description="User's language", json_schema_extra={"label": "Benutzersprache"})
|
||||||
detectedComplexity: str = Field(
|
detectedComplexity: str = Field(
|
||||||
description="Complexity level: simple, moderate, complex"
|
description="Complexity level: simple, moderate, complex",
|
||||||
|
json_schema_extra={"label": "Erkannte Komplexität"},
|
||||||
)
|
)
|
||||||
requiresDocuments: bool = Field(default=False, description="Whether request requires documents")
|
requiresDocuments: bool = Field(default=False, description="Whether request requires documents", json_schema_extra={"label": "Benötigt Dokumente"})
|
||||||
requiresWebResearch: bool = Field(default=False, description="Whether request requires web research")
|
requiresWebResearch: bool = Field(default=False, description="Whether request requires web research", json_schema_extra={"label": "Benötigt Web-Recherche"})
|
||||||
requiresAnalysis: bool = Field(default=False, description="Whether request requires analysis")
|
requiresAnalysis: bool = Field(default=False, description="Whether request requires analysis", json_schema_extra={"label": "Benötigt Analyse"})
|
||||||
expectedOutputFormat: Optional[str] = Field(None, description="Expected output format")
|
expectedOutputFormat: Optional[str] = Field(None, description="Expected output format", json_schema_extra={"label": "Erwartetes Ausgabeformat"})
|
||||||
expectedOutputType: Optional[str] = Field(None, description="Expected output type: answer, document, analysis")
|
expectedOutputType: Optional[str] = Field(None, description="Expected output type: answer, document, analysis", json_schema_extra={"label": "Erwarteter Ausgabetyp"})
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Verständnis-Ergebnis")
|
||||||
class UnderstandingResult(BaseModel):
|
class UnderstandingResult(BaseModel):
|
||||||
"""Result from initial understanding phase (combined AI call)"""
|
"""Result from initial understanding phase (combined AI call)"""
|
||||||
|
|
||||||
parameters: Dict[str, Any] = Field(
|
parameters: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Basic parameters (language, format, detail level)"
|
description="Basic parameters (language, format, detail level)",
|
||||||
|
json_schema_extra={"label": "Parameter"},
|
||||||
)
|
)
|
||||||
intention: Dict[str, Any] = Field(
|
intention: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="User intention (primaryGoal, secondaryGoals, intentionType)"
|
description="User intention (primaryGoal, secondaryGoals, intentionType)",
|
||||||
|
json_schema_extra={"label": "Absicht"},
|
||||||
)
|
)
|
||||||
context: Dict[str, Any] = Field(
|
context: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Extracted context (topics, requirements, constraints)"
|
description="Extracted context (topics, requirements, constraints)",
|
||||||
|
json_schema_extra={"label": "Kontext"},
|
||||||
)
|
)
|
||||||
documentReferences: List[Dict[str, Any]] = Field(
|
documentReferences: List[Dict[str, Any]] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Document references with purpose and relevance"
|
description="Document references with purpose and relevance",
|
||||||
|
json_schema_extra={"label": "Dokumentenreferenzen"},
|
||||||
)
|
)
|
||||||
tasks: List["TaskDefinition"] = Field( # Forward reference
|
tasks: List["TaskDefinition"] = Field( # Forward reference
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Task definitions with deliverables"
|
description="Task definitions with deliverables",
|
||||||
|
json_schema_extra={"label": "Aufgaben"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Aufgabenbeschreibung")
|
||||||
class TaskDefinition(BaseModel):
|
class TaskDefinition(BaseModel):
|
||||||
"""Task definition from understanding phase"""
|
"""Task definition from understanding phase"""
|
||||||
|
|
||||||
id: str = Field(description="Task identifier")
|
id: str = Field(description="Task identifier", json_schema_extra={"label": "Aufgaben-ID"})
|
||||||
objective: str = Field(description="Task objective")
|
objective: str = Field(description="Task objective", json_schema_extra={"label": "Ziel"})
|
||||||
deliverable: Dict[str, Any] = Field(
|
deliverable: Dict[str, Any] = Field(
|
||||||
description="Deliverable specification (type, format, style, detailLevel)"
|
description="Deliverable specification (type, format, style, detailLevel)",
|
||||||
|
json_schema_extra={"label": "Lieferobjekt"},
|
||||||
)
|
)
|
||||||
requiresWebResearch: bool = Field(default=False, description="Whether task requires web research")
|
requiresWebResearch: bool = Field(default=False, description="Whether task requires web research", json_schema_extra={"label": "Benötigt Web-Recherche"})
|
||||||
requiresDocumentAnalysis: bool = Field(default=False, description="Whether task requires document analysis")
|
requiresDocumentAnalysis: bool = Field(default=False, description="Whether task requires document analysis", json_schema_extra={"label": "Benötigt Dokumentenanalyse"})
|
||||||
requiresContentGeneration: bool = Field(default=True, description="Whether task requires content generation")
|
requiresContentGeneration: bool = Field(default=True, description="Whether task requires content generation", json_schema_extra={"label": "Benötigt Inhaltserstellung"})
|
||||||
requiredDocuments: List[str] = Field(
|
requiredDocuments: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Document references needed for this task"
|
description="Document references needed for this task",
|
||||||
|
json_schema_extra={"label": "Benötigte Dokumente"},
|
||||||
)
|
)
|
||||||
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
|
||||||
None,
|
None,
|
||||||
description="Extraction options for document processing (determined dynamically based on task and document characteristics)"
|
description="Extraction options for document processing (determined dynamically based on task and document characteristics)",
|
||||||
|
json_schema_extra={"label": "Extraktionsoptionen"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskResult(BaseModel):
|
@i18nModel("Workflow-Aufgabenergebnis")
|
||||||
|
class WorkflowTaskResult(BaseModel):
|
||||||
"""Result from task execution"""
|
"""Result from task execution"""
|
||||||
|
|
||||||
taskId: str = Field(description="Task identifier")
|
taskId: str = Field(description="Task identifier", json_schema_extra={"label": "Aufgaben-ID"})
|
||||||
actionResult: Any = Field(description="ActionResult from task execution") # ActionResult - forward reference
|
actionResult: Any = Field(description="ActionResult from task execution", json_schema_extra={"label": "Aktionsergebnis"}) # ActionResult - forward reference
|
||||||
|
|
||||||
|
|
||||||
# Register model labels for UI
|
|
||||||
registerModelLabels(
|
|
||||||
"RequestContext",
|
|
||||||
{"en": "Request Context", "fr": "Contexte de la demande"},
|
|
||||||
{
|
|
||||||
"originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
|
|
||||||
"documents": {"en": "Documents", "fr": "Documents"},
|
|
||||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
|
||||||
"detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
|
|
||||||
"requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
|
|
||||||
"expectedOutputFormat": {"en": "Expected Output Format", "fr": "Format de sortie attendu"},
|
|
||||||
"expectedOutputType": {"en": "Expected Output Type", "fr": "Type de sortie attendu"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UnderstandingResult",
|
|
||||||
{"en": "Understanding Result", "fr": "Résultat de compréhension"},
|
|
||||||
{
|
|
||||||
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
||||||
"intention": {"en": "Intention", "fr": "Intention"},
|
|
||||||
"context": {"en": "Context", "fr": "Contexte"},
|
|
||||||
"documentReferences": {"en": "Document References", "fr": "Références de documents"},
|
|
||||||
"tasks": {"en": "Tasks", "fr": "Tâches"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskDefinition",
|
|
||||||
{"en": "Task Definition", "fr": "Définition de tâche"},
|
|
||||||
{
|
|
||||||
"id": {"en": "Task ID", "fr": "ID de la tâche"},
|
|
||||||
"objective": {"en": "Objective", "fr": "Objectif"},
|
|
||||||
"deliverable": {"en": "Deliverable", "fr": "Livrable"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de documents"},
|
|
||||||
"requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
|
|
||||||
"requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
|
|
||||||
"extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskResult",
|
|
||||||
{"en": "Task Result", "fr": "Résultat de tâche"},
|
|
||||||
{
|
|
||||||
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
|
|
||||||
"actionResult": {"en": "Action Result", "fr": "Résultat de l'action"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"RequestContext",
|
|
||||||
{"en": "Request Context", "fr": "Contexte de la demande"},
|
|
||||||
{
|
|
||||||
"originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
|
|
||||||
"documents": {"en": "Documents", "fr": "Documents"},
|
|
||||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
|
||||||
"detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
|
|
||||||
"requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
|
|
||||||
"expectedOutputFormat": {"en": "Expected Output Format", "fr": "Format de sortie attendu"},
|
|
||||||
"expectedOutputType": {"en": "Expected Output Type", "fr": "Type de sortie attendu"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UnderstandingResult",
|
|
||||||
{"en": "Understanding Result", "fr": "Résultat de compréhension"},
|
|
||||||
{
|
|
||||||
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
||||||
"intention": {"en": "Intention", "fr": "Intention"},
|
|
||||||
"context": {"en": "Context", "fr": "Contexte"},
|
|
||||||
"documentReferences": {"en": "Document References", "fr": "Références de documents"},
|
|
||||||
"tasks": {"en": "Tasks", "fr": "Tâches"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskDefinition",
|
|
||||||
{"en": "Task Definition", "fr": "Définition de tâche"},
|
|
||||||
{
|
|
||||||
"id": {"en": "Task ID", "fr": "ID de la tâche"},
|
|
||||||
"objective": {"en": "Objective", "fr": "Objectif"},
|
|
||||||
"deliverable": {"en": "Deliverable", "fr": "Livrable"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de documents"},
|
|
||||||
"requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
|
|
||||||
"requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
|
|
||||||
"extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskResult",
|
|
||||||
{"en": "Task Result", "fr": "Résultat de tâche"},
|
|
||||||
{
|
|
||||||
"taskId": {"en": "Task ID", "fr": "ID de la tâche"},
|
|
||||||
"actionResult": {"en": "Action Result", "fr": "Résultat de l'action"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register model labels for UI
|
|
||||||
registerModelLabels(
|
|
||||||
"ActionDefinition",
|
|
||||||
{"en": "Action Definition", "fr": "Définition d'action"},
|
|
||||||
{
|
|
||||||
"action": {"en": "Action", "fr": "Action"},
|
|
||||||
"actionObjective": {"en": "Action Objective", "fr": "Objectif de l'action"},
|
|
||||||
"parametersContext": {"en": "Parameters Context", "fr": "Contexte des paramètres"},
|
|
||||||
"learnings": {"en": "Learnings", "fr": "Apprentissages"},
|
|
||||||
"documentList": {"en": "Document List", "fr": "Liste de documents"},
|
|
||||||
"connectionReference": {"en": "Connection Reference", "fr": "Référence de connexion"},
|
|
||||||
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AiResponse",
|
|
||||||
{"en": "AI Response", "fr": "Réponse IA"},
|
|
||||||
{
|
|
||||||
"content": {"en": "Content", "fr": "Contenu"},
|
|
||||||
"metadata": {"en": "Metadata", "fr": "Métadonnées"},
|
|
||||||
"documents": {"en": "Documents", "fr": "Documents"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AiResponseMetadata",
|
|
||||||
{"en": "AI Response Metadata", "fr": "Métadonnées de réponse IA"},
|
|
||||||
{
|
|
||||||
"title": {"en": "Title", "fr": "Titre"},
|
|
||||||
"filename": {"en": "Filename", "fr": "Nom de fichier"},
|
|
||||||
"operationType": {"en": "Operation Type", "fr": "Type d'opération"},
|
|
||||||
"schemaVersion": {"en": "Schema Version", "fr": "Version du schéma"},
|
|
||||||
"extractionMethod": {"en": "Extraction Method", "fr": "Méthode d'extraction"},
|
|
||||||
"sourceDocuments": {"en": "Source Documents", "fr": "Documents sources"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"DocumentData",
|
|
||||||
{"en": "Document Data", "fr": "Données de document"},
|
|
||||||
{
|
|
||||||
"documentName": {"en": "Document Name", "fr": "Nom du document"},
|
|
||||||
"documentData": {"en": "Document Data", "fr": "Données du document"},
|
|
||||||
"mimeType": {"en": "MIME Type", "fr": "Type MIME"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"RequestContext",
|
|
||||||
{"en": "Request Context", "fr": "Contexte de requête"},
|
|
||||||
{
|
|
||||||
"originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
|
|
||||||
"documents": {"en": "Documents", "fr": "Documents"},
|
|
||||||
"userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
|
|
||||||
"detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
|
|
||||||
"requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"UnderstandingResult",
|
|
||||||
{"en": "Understanding Result", "fr": "Résultat de compréhension"},
|
|
||||||
{
|
|
||||||
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
||||||
"intention": {"en": "Intention", "fr": "Intention"},
|
|
||||||
"context": {"en": "Context", "fr": "Contexte"},
|
|
||||||
"documentReferences": {"en": "Document References", "fr": "Références de documents"},
|
|
||||||
"tasks": {"en": "Tasks", "fr": "Tâches"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskDefinition",
|
|
||||||
{"en": "Task Definition", "fr": "Définition de tâche"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
|
||||||
"objective": {"en": "Objective", "fr": "Objectif"},
|
|
||||||
"deliverable": {"en": "Deliverable", "fr": "Livrable"},
|
|
||||||
"requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
|
|
||||||
"requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de document"},
|
|
||||||
"requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
|
|
||||||
"requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
|
|
||||||
"extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TaskResult",
|
|
||||||
{"en": "Task Result", "fr": "Résultat de tâche"},
|
|
||||||
{
|
|
||||||
"taskId": {"en": "Task ID", "fr": "ID de tâche"},
|
|
||||||
"actionResult": {"en": "Action Result", "fr": "Résultat d'action"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,85 +6,97 @@ 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.datamodelChat import ActionResult
|
||||||
from modules.shared.frontendTypes import FrontendType
|
from modules.shared.frontendTypes import FrontendType
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Workflow-Aktionsparameter")
|
||||||
class WorkflowActionParameter(BaseModel):
|
class WorkflowActionParameter(BaseModel):
|
||||||
"""
|
"""
|
||||||
Parameter schema definition for a workflow action.
|
Parameter schema definition for a workflow action.
|
||||||
|
|
||||||
This defines the structure and UI rendering for a single action parameter,
|
This defines the structure and UI rendering for a single action parameter,
|
||||||
NOT the actual parameter values (those are in ActionDefinition.parameters).
|
NOT the actual parameter values (those are in ActionDefinition.parameters).
|
||||||
"""
|
"""
|
||||||
name: str = Field(description="Parameter name")
|
name: str = Field(
|
||||||
type: str = Field(description="Python type as string: 'str', 'int', 'bool', 'List[str]', etc.")
|
description="Parameter name",
|
||||||
frontendType: FrontendType = Field(description="UI rendering type (from global FrontendType enum)")
|
json_schema_extra={"label": "Name"},
|
||||||
|
)
|
||||||
|
type: str = Field(
|
||||||
|
description="Python type as string: 'str', 'int', 'bool', 'List[str]', etc.",
|
||||||
|
json_schema_extra={"label": "Typ"},
|
||||||
|
)
|
||||||
|
frontendType: FrontendType = Field(
|
||||||
|
description="UI rendering type (from global FrontendType enum)",
|
||||||
|
json_schema_extra={"label": "Frontend-Typ"},
|
||||||
|
)
|
||||||
frontendOptions: Optional[Union[str, List[str]]] = Field(
|
frontendOptions: Optional[Union[str, List[str]]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or list of strings (e.g., ['txt', 'json']). For custom types, this is automatically set to the API endpoint."
|
description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or list of strings (e.g., ['txt', 'json']). For custom types, this is automatically set to the API endpoint.",
|
||||||
|
json_schema_extra={"label": "Frontend-Optionen"},
|
||||||
|
)
|
||||||
|
required: bool = Field(
|
||||||
|
False,
|
||||||
|
description="Whether parameter is required",
|
||||||
|
json_schema_extra={"label": "Pflichtfeld"},
|
||||||
|
)
|
||||||
|
default: Optional[Any] = Field(
|
||||||
|
None,
|
||||||
|
description="Default value",
|
||||||
|
json_schema_extra={"label": "Standard"},
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
"",
|
||||||
|
description="Parameter description",
|
||||||
|
json_schema_extra={"label": "Beschreibung"},
|
||||||
)
|
)
|
||||||
required: bool = Field(False, description="Whether parameter is required")
|
|
||||||
default: Optional[Any] = Field(None, description="Default value")
|
|
||||||
description: str = Field("", description="Parameter description")
|
|
||||||
validation: Optional[Dict[str, Any]] = Field(
|
validation: Optional[Dict[str, Any]] = Field(
|
||||||
None,
|
None,
|
||||||
description="Validation rules (e.g., {'min': 1, 'max': 100})"
|
description="Validation rules (e.g., {'min': 1, 'max': 100})",
|
||||||
|
json_schema_extra={"label": "Validierung"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Workflow-Aktionsdefinition")
|
||||||
class WorkflowActionDefinition(BaseModel):
|
class WorkflowActionDefinition(BaseModel):
|
||||||
"""
|
"""
|
||||||
Complete schema definition of a workflow action.
|
Complete schema definition of a workflow action.
|
||||||
|
|
||||||
This defines the metadata, parameters, and execution function for an action.
|
This defines the metadata, parameters, and execution function for an action.
|
||||||
This is different from datamodelWorkflow.ActionDefinition which contains
|
This is different from datamodelWorkflow.ActionDefinition which contains
|
||||||
actual execution values (action, actionObjective, parameters with values).
|
actual execution values (action, actionObjective, parameters with values).
|
||||||
|
|
||||||
This class defines the ACTION SCHEMA, not the execution plan.
|
This class defines the ACTION SCHEMA, not the execution plan.
|
||||||
"""
|
"""
|
||||||
actionId: str = Field(
|
actionId: str = Field(
|
||||||
description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')"
|
description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')",
|
||||||
|
json_schema_extra={"label": "Aktions-ID"},
|
||||||
|
)
|
||||||
|
description: str = Field(
|
||||||
|
description="Action description",
|
||||||
|
json_schema_extra={"label": "Beschreibung"},
|
||||||
)
|
)
|
||||||
description: str = Field(description="Action description")
|
|
||||||
parameters: Dict[str, WorkflowActionParameter] = Field(
|
parameters: Dict[str, WorkflowActionParameter] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Parameter schema definitions"
|
description="Parameter schema definitions",
|
||||||
|
json_schema_extra={"label": "Parameter"},
|
||||||
)
|
)
|
||||||
execute: Optional[Callable] = Field(
|
execute: Optional[Callable] = Field(
|
||||||
None,
|
None,
|
||||||
description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically."
|
description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically.",
|
||||||
|
json_schema_extra={"label": "Ausfuehrung"},
|
||||||
|
)
|
||||||
|
category: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Action category for grouping",
|
||||||
|
json_schema_extra={"label": "Kategorie"},
|
||||||
|
)
|
||||||
|
tags: List[str] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Tags for search/filtering",
|
||||||
|
json_schema_extra={"label": "Tags"},
|
||||||
|
)
|
||||||
|
dynamicMode: bool = Field(
|
||||||
|
False,
|
||||||
|
description="Whether this action is available in dynamic workflow mode (only tagged actions are visible in action planning and refinement prompts)",
|
||||||
|
json_schema_extra={"label": "Dynamischer Modus"},
|
||||||
)
|
)
|
||||||
category: Optional[str] = Field(None, description="Action category for grouping")
|
|
||||||
tags: List[str] = Field(default_factory=list, description="Tags for search/filtering")
|
|
||||||
dynamicMode: bool = Field(False, description="Whether this action is available in dynamic workflow mode (only tagged actions are visible in action planning and refinement prompts)")
|
|
||||||
|
|
||||||
|
|
||||||
# Register model labels for UI
|
|
||||||
registerModelLabels(
|
|
||||||
"WorkflowActionDefinition",
|
|
||||||
{"en": "Workflow Action Definition", "fr": "Définition d'action de workflow"},
|
|
||||||
{
|
|
||||||
"actionId": {"en": "Action ID", "fr": "ID d'action"},
|
|
||||||
"description": {"en": "Description", "fr": "Description"},
|
|
||||||
"parameters": {"en": "Parameters", "fr": "Paramètres"},
|
|
||||||
"category": {"en": "Category", "fr": "Catégorie"},
|
|
||||||
"tags": {"en": "Tags", "fr": "Étiquettes"},
|
|
||||||
"dynamicMode": {"en": "Dynamic Mode", "fr": "Mode dynamique"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"WorkflowActionParameter",
|
|
||||||
{"en": "Workflow Action Parameter", "fr": "Paramètre d'action de workflow"},
|
|
||||||
{
|
|
||||||
"name": {"en": "Name", "fr": "Nom"},
|
|
||||||
"type": {"en": "Type", "fr": "Type"},
|
|
||||||
"frontendType": {"en": "Frontend Type", "fr": "Type frontend"},
|
|
||||||
"frontendOptions": {"en": "Frontend Options", "fr": "Options frontend"},
|
|
||||||
"required": {"en": "Required", "fr": "Requis"},
|
|
||||||
"default": {"en": "Default", "fr": "Par défaut"},
|
|
||||||
"description": {"en": "Description", "fr": "Description"},
|
|
||||||
"validation": {"en": "Validation", "fr": "Validation"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Feature metadata
|
# Feature metadata
|
||||||
FEATURE_CODE = "chatbot"
|
FEATURE_CODE = "chatbot"
|
||||||
FEATURE_LABEL = {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"}
|
FEATURE_LABEL = "Chatbot"
|
||||||
FEATURE_ICON = "mdi-robot"
|
FEATURE_ICON = "mdi-robot"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog
|
# UI Objects for RBAC catalog
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.chatbot.conversations",
|
"objectKey": "ui.feature.chatbot.conversations",
|
||||||
"label": {"en": "Conversations", "de": "Konversationen", "fr": "Conversations"},
|
"label": "Konversationen",
|
||||||
"meta": {"area": "conversations"}
|
"meta": {"area": "conversations"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -28,22 +28,22 @@ UI_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.chatbot.startStream",
|
"objectKey": "resource.feature.chatbot.startStream",
|
||||||
"label": {"en": "Start Chat (Stream)", "de": "Chat starten (Stream)", "fr": "Démarrer chat (Stream)"},
|
"label": "Chat starten (Stream)",
|
||||||
"meta": {"endpoint": "/api/chatbot/{instanceId}/start/stream", "method": "POST"}
|
"meta": {"endpoint": "/api/chatbot/{instanceId}/start/stream", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.chatbot.stop",
|
"objectKey": "resource.feature.chatbot.stop",
|
||||||
"label": {"en": "Stop Chat", "de": "Chat stoppen", "fr": "Arrêter chat"},
|
"label": "Chat stoppen",
|
||||||
"meta": {"endpoint": "/api/chatbot/{instanceId}/stop/{workflowId}", "method": "POST"}
|
"meta": {"endpoint": "/api/chatbot/{instanceId}/stop/{workflowId}", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.chatbot.threads",
|
"objectKey": "resource.feature.chatbot.threads",
|
||||||
"label": {"en": "Get Threads", "de": "Threads abrufen", "fr": "Récupérer threads"},
|
"label": "Threads abrufen",
|
||||||
"meta": {"endpoint": "/api/chatbot/{instanceId}/threads", "method": "GET"}
|
"meta": {"endpoint": "/api/chatbot/{instanceId}/threads", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.chatbot.delete",
|
"objectKey": "resource.feature.chatbot.delete",
|
||||||
"label": {"en": "Delete Chat", "de": "Chat löschen", "fr": "Supprimer chat"},
|
"label": "Chat löschen",
|
||||||
"meta": {"endpoint": "/api/chatbot/{instanceId}/{workflowId}", "method": "DELETE"}
|
"meta": {"endpoint": "/api/chatbot/{instanceId}/{workflowId}", "method": "DELETE"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -74,11 +74,7 @@ REQUIRED_SERVICES = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "chatbot-viewer",
|
"roleLabel": "chatbot-viewer",
|
||||||
"description": {
|
"description": "Chatbot Betrachter - Chat-Threads ansehen (nur lesen)",
|
||||||
"en": "Chatbot Viewer - View chat threads (read-only)",
|
|
||||||
"de": "Chatbot Betrachter - Chat-Threads ansehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur Chatbot - Consulter les threads (lecture seule)"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# UI: only threads view, NO active chat
|
# UI: only threads view, NO active chat
|
||||||
{"context": "UI", "item": "ui.feature.chatbot.threads", "view": True},
|
{"context": "UI", "item": "ui.feature.chatbot.threads", "view": True},
|
||||||
|
|
@ -90,11 +86,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "chatbot-user",
|
"roleLabel": "chatbot-user",
|
||||||
"description": {
|
"description": "Chatbot Benutzer - Chatbot nutzen und eigene Threads verwalten",
|
||||||
"en": "Chatbot User - Use the chatbot and manage own threads",
|
|
||||||
"de": "Chatbot Benutzer - Chatbot nutzen und eigene Threads verwalten",
|
|
||||||
"fr": "Utilisateur Chatbot - Utiliser le chatbot et gérer ses threads"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# UI: full access to all views
|
# UI: full access to all views
|
||||||
{"context": "UI", "item": "ui.feature.chatbot.conversations", "view": True},
|
{"context": "UI", "item": "ui.feature.chatbot.conversations", "view": True},
|
||||||
|
|
@ -110,11 +102,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "chatbot-admin",
|
"roleLabel": "chatbot-admin",
|
||||||
"description": {
|
"description": "Chatbot Admin - Vollzugriff auf alle Chatbot-Funktionen",
|
||||||
"en": "Chatbot Admin - Full access to all chatbot features",
|
|
||||||
"de": "Chatbot Admin - Vollzugriff auf alle Chatbot-Funktionen",
|
|
||||||
"fr": "Administrateur Chatbot - Accès complet à toutes les fonctions chatbot"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# Full UI access
|
# Full UI access
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
|
|
@ -391,7 +379,8 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get existing template roles for this feature (Pydantic models)
|
# Get existing template roles for this feature (Pydantic models)
|
||||||
|
|
@ -412,7 +401,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
# Create new template role
|
# Create new template role
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None, # Global template
|
mandateId=None, # Global template
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ from modules.features.chatbot.interfaceFeatureChatbot import ChatbotConversation
|
||||||
# Import chatbot feature
|
# Import chatbot feature
|
||||||
from modules.features.chatbot import chatProcess
|
from modules.features.chatbot import chatProcess
|
||||||
from modules.features.chatbot.mainChatbot import getEventManager
|
from modules.features.chatbot.mainChatbot import getEventManager
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureChatbot")
|
||||||
|
|
||||||
# Pre-warm AI connectors when this router loads (before first request).
|
# Pre-warm AI connectors when this router loads (before first request).
|
||||||
# Ensures connectors are ready; avoids 4–8 s delay on first chatbot message.
|
# Ensures connectors are ready; avoids 4–8 s delay on first chatbot message.
|
||||||
|
|
@ -265,7 +267,7 @@ async def stream_chatbot_start(
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="Failed to create or load workflow"
|
detail=routeApiMsg("Failed to create or load workflow")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get event queue for the workflow
|
# Get event queue for the workflow
|
||||||
|
|
@ -562,7 +564,7 @@ def delete_chatbot(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to delete workflow"
|
detail=routeApiMsg("Failed to delete workflow")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,23 @@ from typing import Dict, List, Any
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
FEATURE_CODE = "commcoach"
|
FEATURE_CODE = "commcoach"
|
||||||
FEATURE_LABEL = {"en": "Communication Coach", "de": "Kommunikations-Coach", "fr": "Coach Communication"}
|
FEATURE_LABEL = "Kommunikations-Coach"
|
||||||
FEATURE_ICON = "mdi-account-voice"
|
FEATURE_ICON = "mdi-account-voice"
|
||||||
|
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.commcoach.dashboard",
|
"objectKey": "ui.feature.commcoach.dashboard",
|
||||||
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
"label": "Dashboard",
|
||||||
"meta": {"area": "dashboard"}
|
"meta": {"area": "dashboard"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.commcoach.coaching",
|
"objectKey": "ui.feature.commcoach.coaching",
|
||||||
"label": {"en": "Coaching & Dossier", "de": "Coaching & Dossier", "fr": "Coaching & Dossier"},
|
"label": "Coaching & Dossier",
|
||||||
"meta": {"area": "coaching"}
|
"meta": {"area": "coaching"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.commcoach.settings",
|
"objectKey": "ui.feature.commcoach.settings",
|
||||||
"label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"},
|
"label": "Einstellungen",
|
||||||
"meta": {"area": "settings"}
|
"meta": {"area": "settings"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -35,7 +35,7 @@ UI_OBJECTS = [
|
||||||
DATA_OBJECTS = [
|
DATA_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingContext",
|
"objectKey": "data.feature.commcoach.CoachingContext",
|
||||||
"label": {"en": "Coaching Context", "de": "Coaching-Kontext", "fr": "Contexte coaching"},
|
"label": "Coaching-Kontext",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "CoachingContext",
|
"table": "CoachingContext",
|
||||||
"fields": ["id", "title", "category", "status"],
|
"fields": ["id", "title", "category", "status"],
|
||||||
|
|
@ -45,7 +45,7 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingSession",
|
"objectKey": "data.feature.commcoach.CoachingSession",
|
||||||
"label": {"en": "Coaching Session", "de": "Coaching-Session", "fr": "Session coaching"},
|
"label": "Coaching-Session",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "CoachingSession",
|
"table": "CoachingSession",
|
||||||
"fields": ["id", "contextId", "status", "summary"],
|
"fields": ["id", "contextId", "status", "summary"],
|
||||||
|
|
@ -55,12 +55,12 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingMessage",
|
"objectKey": "data.feature.commcoach.CoachingMessage",
|
||||||
"label": {"en": "Coaching Message", "de": "Coaching-Nachricht", "fr": "Message coaching"},
|
"label": "Coaching-Nachricht",
|
||||||
"meta": {"table": "CoachingMessage", "fields": ["id", "sessionId", "role", "content"]}
|
"meta": {"table": "CoachingMessage", "fields": ["id", "sessionId", "role", "content"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingTask",
|
"objectKey": "data.feature.commcoach.CoachingTask",
|
||||||
"label": {"en": "Coaching Task", "de": "Coaching-Aufgabe", "fr": "Tache coaching"},
|
"label": "Coaching-Aufgabe",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "CoachingTask",
|
"table": "CoachingTask",
|
||||||
"fields": ["id", "contextId", "title", "status"],
|
"fields": ["id", "contextId", "title", "status"],
|
||||||
|
|
@ -70,27 +70,27 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingScore",
|
"objectKey": "data.feature.commcoach.CoachingScore",
|
||||||
"label": {"en": "Coaching Score", "de": "Coaching-Score", "fr": "Score coaching"},
|
"label": "Coaching-Score",
|
||||||
"meta": {"table": "CoachingScore", "fields": ["id", "dimension", "score", "trend"]}
|
"meta": {"table": "CoachingScore", "fields": ["id", "dimension", "score", "trend"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingUserProfile",
|
"objectKey": "data.feature.commcoach.CoachingUserProfile",
|
||||||
"label": {"en": "User Profile", "de": "Benutzerprofil", "fr": "Profil utilisateur"},
|
"label": "Benutzerprofil",
|
||||||
"meta": {"table": "CoachingUserProfile", "fields": ["id", "userId", "dailyReminderEnabled"]}
|
"meta": {"table": "CoachingUserProfile", "fields": ["id", "userId", "dailyReminderEnabled"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingPersona",
|
"objectKey": "data.feature.commcoach.CoachingPersona",
|
||||||
"label": {"en": "Coaching Persona", "de": "Coaching-Persona", "fr": "Persona coaching"},
|
"label": "Coaching-Persona",
|
||||||
"meta": {"table": "CoachingPersona", "fields": ["id", "key", "label", "gender"]}
|
"meta": {"table": "CoachingPersona", "fields": ["id", "key", "label", "gender"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.CoachingBadge",
|
"objectKey": "data.feature.commcoach.CoachingBadge",
|
||||||
"label": {"en": "Coaching Badge", "de": "Coaching-Auszeichnung", "fr": "Badge coaching"},
|
"label": "Coaching-Auszeichnung",
|
||||||
"meta": {"table": "CoachingBadge", "fields": ["id", "badgeKey", "awardedAt"]}
|
"meta": {"table": "CoachingBadge", "fields": ["id", "badgeKey", "awardedAt"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.commcoach.*",
|
"objectKey": "data.feature.commcoach.*",
|
||||||
"label": {"en": "All CommCoach Data", "de": "Alle CommCoach-Daten", "fr": "Toutes les donnees CommCoach"},
|
"label": "Alle CommCoach-Daten",
|
||||||
"meta": {"wildcard": True}
|
"meta": {"wildcard": True}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -98,27 +98,27 @@ DATA_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.commcoach.context.create",
|
"objectKey": "resource.feature.commcoach.context.create",
|
||||||
"label": {"en": "Create Context", "de": "Kontext erstellen", "fr": "Creer contexte"},
|
"label": "Kontext erstellen",
|
||||||
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts", "method": "POST"}
|
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.commcoach.context.archive",
|
"objectKey": "resource.feature.commcoach.context.archive",
|
||||||
"label": {"en": "Archive Context", "de": "Kontext archivieren", "fr": "Archiver contexte"},
|
"label": "Kontext archivieren",
|
||||||
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/archive", "method": "POST"}
|
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/archive", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.commcoach.session.start",
|
"objectKey": "resource.feature.commcoach.session.start",
|
||||||
"label": {"en": "Start Session", "de": "Session starten", "fr": "Demarrer session"},
|
"label": "Session starten",
|
||||||
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/sessions/start", "method": "POST"}
|
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/sessions/start", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.commcoach.session.complete",
|
"objectKey": "resource.feature.commcoach.session.complete",
|
||||||
"label": {"en": "Complete Session", "de": "Session abschliessen", "fr": "Terminer session"},
|
"label": "Session abschliessen",
|
||||||
"meta": {"endpoint": "/api/commcoach/{instanceId}/sessions/{sessionId}/complete", "method": "POST"}
|
"meta": {"endpoint": "/api/commcoach/{instanceId}/sessions/{sessionId}/complete", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.commcoach.task.manage",
|
"objectKey": "resource.feature.commcoach.task.manage",
|
||||||
"label": {"en": "Manage Tasks", "de": "Aufgaben verwalten", "fr": "Gerer taches"},
|
"label": "Aufgaben verwalten",
|
||||||
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/tasks", "method": "POST"}
|
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/tasks", "method": "POST"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -126,30 +126,22 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "commcoach-viewer",
|
"roleLabel": "commcoach-viewer",
|
||||||
"description": {
|
"description": "Kommunikations-Coach Betrachter - Coaching-Daten ansehen (nur lesen)",
|
||||||
"en": "Communication Coach Viewer - View coaching data (read-only)",
|
|
||||||
"de": "Kommunikations-Coach Betrachter - Coaching-Daten ansehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur Coach Communication - Consulter les donnees coaching (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.dossier", "view": True},
|
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
||||||
|
# Viewer: keine RESOURCE-Endpunkte (Mutationen); Regel explizit fuer konsistente Kontext-Matrix
|
||||||
|
{"context": "RESOURCE", "item": None, "view": False},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "commcoach-user",
|
"roleLabel": "commcoach-user",
|
||||||
"description": {
|
"description": "Kommunikations-Coach Benutzer - Kann eigene Coaching-Kontexte und Sessions verwalten",
|
||||||
"en": "Communication Coach User - Can manage own coaching contexts and sessions",
|
|
||||||
"de": "Kommunikations-Coach Benutzer - Kann eigene Coaching-Kontexte und Sessions verwalten",
|
|
||||||
"fr": "Utilisateur Coach Communication - Peut gerer ses propres contextes et sessions",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.dossier", "view": True},
|
|
||||||
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
|
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
|
||||||
{"context": "DATA", "item": "data.feature.commcoach.CoachingContext", "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
|
{"context": "DATA", "item": "data.feature.commcoach.CoachingContext", "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
|
||||||
{"context": "DATA", "item": "data.feature.commcoach.CoachingSession", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
{"context": "DATA", "item": "data.feature.commcoach.CoachingSession", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
|
|
@ -166,11 +158,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "commcoach-admin",
|
"roleLabel": "commcoach-admin",
|
||||||
"description": {
|
"description": "Kommunikations-Coach Admin - Alle UI- und API-Aktionen; Daten nur eigene Datensaetze",
|
||||||
"en": "Communication Coach Admin - All UI and API actions; data scoped to own records",
|
|
||||||
"de": "Kommunikations-Coach Admin - Alle UI- und API-Aktionen; Daten nur eigene Datensaetze",
|
|
||||||
"fr": "Administrateur Coach Communication - Toute l'UI et les API; donnees propres",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "RESOURCE", "item": None, "view": True},
|
{"context": "RESOURCE", "item": None, "view": True},
|
||||||
|
|
@ -271,6 +259,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
|
|
@ -287,7 +276,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ from .datamodelCommcoach import (
|
||||||
StartSessionRequest, CreatePersonaRequest, UpdatePersonaRequest,
|
StartSessionRequest, CreatePersonaRequest, UpdatePersonaRequest,
|
||||||
)
|
)
|
||||||
from .serviceCommcoach import CommcoachService, emitSessionEvent, getSessionEventQueue, cleanupSessionEvents
|
from .serviceCommcoach import CommcoachService, emitSessionEvent, getSessionEventQueue, cleanupSessionEvents
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureCommcoach")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_activeProcessTasks: dict = {}
|
_activeProcessTasks: dict = {}
|
||||||
|
|
@ -78,14 +80,14 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||||
raise HTTPException(status_code=404, detail=f"Feature instance '{instanceId}' not found")
|
raise HTTPException(status_code=404, detail=f"Feature instance '{instanceId}' not found")
|
||||||
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
|
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=500, detail="Feature instance has no mandateId")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Feature instance has no mandateId"))
|
||||||
return str(mandateId)
|
return str(mandateId)
|
||||||
|
|
||||||
|
|
||||||
def _validateOwnership(record: dict, context: RequestContext, fieldName: str = "userId") -> None:
|
def _validateOwnership(record: dict, context: RequestContext, fieldName: str = "userId") -> None:
|
||||||
"""Strict ownership check. SysAdmin does NOT bypass for content access."""
|
"""Strict ownership check. SysAdmin does NOT bypass for content access."""
|
||||||
if record.get(fieldName) != str(context.user.id):
|
if record.get(fieldName) != str(context.user.id):
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Not found"))
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
@ -158,7 +160,7 @@ async def getContext(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
tasks = interface.getTasks(contextId, userId)
|
tasks = interface.getTasks(contextId, userId)
|
||||||
|
|
@ -187,7 +189,7 @@ async def updateContext(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
updates = body.model_dump(exclude_none=True)
|
updates = body.model_dump(exclude_none=True)
|
||||||
|
|
@ -208,7 +210,7 @@ async def deleteContext(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
interface.deleteContext(contextId)
|
interface.deleteContext(contextId)
|
||||||
|
|
@ -228,7 +230,7 @@ async def archiveContext(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ARCHIVED.value})
|
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ARCHIVED.value})
|
||||||
|
|
@ -249,7 +251,7 @@ async def activateContext(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ACTIVE.value})
|
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ACTIVE.value})
|
||||||
|
|
@ -274,7 +276,7 @@ async def listSessions(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
sessions = interface.getSessions(contextId, userId)
|
sessions = interface.getSessions(contextId, userId)
|
||||||
|
|
@ -297,7 +299,7 @@ async def startSession(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
activeSession = interface.getActiveSession(contextId, userId)
|
activeSession = interface.getActiveSession(contextId, userId)
|
||||||
|
|
@ -420,7 +422,7 @@ async def getSession(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
messages = interface.getMessages(sessionId)
|
messages = interface.getMessages(sessionId)
|
||||||
|
|
@ -441,7 +443,7 @@ async def completeSession(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
||||||
|
|
@ -466,7 +468,7 @@ async def cancelSession(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
from modules.shared.timeUtils import getIsoTimestamp
|
from modules.shared.timeUtils import getIsoTimestamp
|
||||||
|
|
@ -496,11 +498,11 @@ async def sendMessageStream(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
||||||
raise HTTPException(status_code=400, detail="Session is not active")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Session is not active"))
|
||||||
|
|
||||||
contextId = session.get("contextId")
|
contextId = session.get("contextId")
|
||||||
service = CommcoachService(context.user, mandateId, instanceId)
|
service = CommcoachService(context.user, mandateId, instanceId)
|
||||||
|
|
@ -572,15 +574,15 @@ async def sendAudioStream(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
|
||||||
raise HTTPException(status_code=400, detail="Session is not active")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Session is not active"))
|
||||||
|
|
||||||
audioBody = await request.body()
|
audioBody = await request.body()
|
||||||
if not audioBody:
|
if not audioBody:
|
||||||
raise HTTPException(status_code=400, detail="No audio data received")
|
raise HTTPException(status_code=400, detail=routeApiMsg("No audio data received"))
|
||||||
|
|
||||||
from .serviceCommcoach import _getUserVoicePrefs
|
from .serviceCommcoach import _getUserVoicePrefs
|
||||||
language, _ = _getUserVoicePrefs(str(context.user.id), mandateId)
|
language, _ = _getUserVoicePrefs(str(context.user.id), mandateId)
|
||||||
|
|
@ -640,7 +642,7 @@ async def streamSession(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
async def _eventGenerator():
|
async def _eventGenerator():
|
||||||
|
|
@ -708,7 +710,7 @@ async def createTask(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
taskData = CoachingTask(
|
taskData = CoachingTask(
|
||||||
|
|
@ -739,7 +741,7 @@ async def updateTask(
|
||||||
|
|
||||||
task = interface.getTask(taskId)
|
task = interface.getTask(taskId)
|
||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
|
||||||
_validateOwnership(task, context)
|
_validateOwnership(task, context)
|
||||||
|
|
||||||
updates = body.model_dump(exclude_none=True)
|
updates = body.model_dump(exclude_none=True)
|
||||||
|
|
@ -761,7 +763,7 @@ async def updateTaskStatus(
|
||||||
|
|
||||||
task = interface.getTask(taskId)
|
task = interface.getTask(taskId)
|
||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
|
||||||
_validateOwnership(task, context)
|
_validateOwnership(task, context)
|
||||||
|
|
||||||
updates = {"status": body.status.value}
|
updates = {"status": body.status.value}
|
||||||
|
|
@ -786,7 +788,7 @@ async def deleteTask(
|
||||||
|
|
||||||
task = interface.getTask(taskId)
|
task = interface.getTask(taskId)
|
||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
|
||||||
_validateOwnership(task, context)
|
_validateOwnership(task, context)
|
||||||
|
|
||||||
interface.deleteTask(taskId)
|
interface.deleteTask(taskId)
|
||||||
|
|
@ -867,7 +869,7 @@ async def exportDossier(
|
||||||
|
|
||||||
ctx = interface.getContext(contextId)
|
ctx = interface.getContext(contextId)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
raise HTTPException(status_code=404, detail="Context not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
|
||||||
_validateOwnership(ctx, context)
|
_validateOwnership(ctx, context)
|
||||||
|
|
||||||
tasks = interface.getTasks(contextId, userId)
|
tasks = interface.getTasks(contextId, userId)
|
||||||
|
|
@ -902,7 +904,7 @@ async def exportSession(
|
||||||
|
|
||||||
session = interface.getSession(sessionId)
|
session = interface.getSession(sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
|
||||||
_validateOwnership(session, context)
|
_validateOwnership(session, context)
|
||||||
|
|
||||||
contextId = session.get("contextId")
|
contextId = session.get("contextId")
|
||||||
|
|
@ -983,9 +985,9 @@ async def updatePersonaRoute(
|
||||||
|
|
||||||
persona = interface.getPersona(personaId)
|
persona = interface.getPersona(personaId)
|
||||||
if not persona:
|
if not persona:
|
||||||
raise HTTPException(status_code=404, detail="Persona not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Persona not found"))
|
||||||
if persona.get("category") == "builtin":
|
if persona.get("category") == "builtin":
|
||||||
raise HTTPException(status_code=403, detail="Builtin personas cannot be edited")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Builtin personas cannot be edited"))
|
||||||
_validateOwnership(persona, context)
|
_validateOwnership(persona, context)
|
||||||
|
|
||||||
updates = body.model_dump(exclude_none=True)
|
updates = body.model_dump(exclude_none=True)
|
||||||
|
|
@ -1006,9 +1008,9 @@ async def deletePersonaRoute(
|
||||||
|
|
||||||
persona = interface.getPersona(personaId)
|
persona = interface.getPersona(personaId)
|
||||||
if not persona:
|
if not persona:
|
||||||
raise HTTPException(status_code=404, detail="Persona not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Persona not found"))
|
||||||
if persona.get("category") == "builtin":
|
if persona.get("category") == "builtin":
|
||||||
raise HTTPException(status_code=403, detail="Builtin personas cannot be deleted")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Builtin personas cannot be deleted"))
|
||||||
_validateOwnership(persona, context)
|
_validateOwnership(persona, context)
|
||||||
|
|
||||||
interface.deletePersona(personaId)
|
interface.deletePersona(personaId)
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ class TestFeatureMetadata:
|
||||||
assert FEATURE_CODE == "commcoach"
|
assert FEATURE_CODE == "commcoach"
|
||||||
|
|
||||||
def test_featureLabel(self):
|
def test_featureLabel(self):
|
||||||
assert "de" in FEATURE_LABEL
|
assert isinstance(FEATURE_LABEL, str)
|
||||||
assert "en" in FEATURE_LABEL
|
assert "Coach" in FEATURE_LABEL
|
||||||
assert "Coach" in FEATURE_LABEL["de"]
|
|
||||||
|
|
||||||
def test_featureIcon(self):
|
def test_featureIcon(self):
|
||||||
assert FEATURE_ICON.startswith("mdi-")
|
assert FEATURE_ICON.startswith("mdi-")
|
||||||
|
|
@ -37,17 +36,17 @@ class TestFeatureDefinition:
|
||||||
class TestRbacObjects:
|
class TestRbacObjects:
|
||||||
def test_uiObjectsExist(self):
|
def test_uiObjectsExist(self):
|
||||||
objs = getUiObjects()
|
objs = getUiObjects()
|
||||||
assert len(objs) >= 4
|
assert len(objs) >= 3
|
||||||
keys = [o["objectKey"] for o in objs]
|
keys = [o["objectKey"] for o in objs]
|
||||||
assert "ui.feature.commcoach.dashboard" in keys
|
assert "ui.feature.commcoach.dashboard" in keys
|
||||||
assert "ui.feature.commcoach.coaching" in keys
|
assert "ui.feature.commcoach.coaching" in keys
|
||||||
assert "ui.feature.commcoach.dossier" in keys
|
|
||||||
assert "ui.feature.commcoach.settings" in keys
|
assert "ui.feature.commcoach.settings" in keys
|
||||||
|
|
||||||
def test_uiObjectsHaveLabels(self):
|
def test_uiObjectsHaveLabels(self):
|
||||||
for obj in getUiObjects():
|
for obj in getUiObjects():
|
||||||
assert "label" in obj
|
assert "label" in obj
|
||||||
assert "de" in obj["label"]
|
assert isinstance(obj["label"], str)
|
||||||
|
assert len(obj["label"]) > 0
|
||||||
|
|
||||||
def test_dataObjectsExist(self):
|
def test_dataObjectsExist(self):
|
||||||
objs = getDataObjects()
|
objs = getDataObjects()
|
||||||
|
|
@ -94,7 +93,7 @@ class TestTemplateRoles:
|
||||||
def test_roleHasDescription(self):
|
def test_roleHasDescription(self):
|
||||||
for role in getTemplateRoles():
|
for role in getTemplateRoles():
|
||||||
assert "description" in role
|
assert "description" in role
|
||||||
assert "de" in role["description"]
|
assert isinstance(role["description"], str) and len(role["description"].strip()) > 0
|
||||||
|
|
||||||
def test_roleHasAccessRules(self):
|
def test_roleHasAccessRules(self):
|
||||||
for role in getTemplateRoles():
|
for role in getTemplateRoles():
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from enum import Enum
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,437 +54,341 @@ class AutoTemplateScope(str, Enum):
|
||||||
# AutoWorkflow
|
# AutoWorkflow
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@i18nModel("Workflow")
|
||||||
class AutoWorkflow(PowerOnModel):
|
class AutoWorkflow(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Primary key",
|
description="Primary key",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="Mandate ID",
|
description="Mandate ID",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Mandanten-ID"},
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="Feature instance ID",
|
description="Feature instance ID",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Feature-Instanz-ID"},
|
||||||
)
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="User-friendly workflow name",
|
description="User-friendly workflow name",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_required": True, "label": "Bezeichnung"},
|
||||||
)
|
)
|
||||||
description: Optional[str] = Field(
|
description: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Workflow description",
|
description="Workflow description",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Beschreibung"},
|
||||||
)
|
)
|
||||||
tags: List[str] = Field(
|
tags: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Tags for categorization",
|
description="Tags for categorization",
|
||||||
json_schema_extra={"frontend_type": "tags", "frontend_required": False},
|
json_schema_extra={"frontend_type": "tags", "frontend_required": False, "label": "Tags"},
|
||||||
)
|
)
|
||||||
isTemplate: bool = Field(
|
isTemplate: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Whether this workflow is a template",
|
description="Whether this workflow is a template",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
|
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Ist Vorlage"},
|
||||||
)
|
)
|
||||||
templateSourceId: Optional[str] = Field(
|
templateSourceId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of the template this workflow was created from",
|
description="ID of the template this workflow was created from",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Vorlagen-Quelle"},
|
||||||
)
|
)
|
||||||
templateScope: Optional[str] = Field(
|
templateScope: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Template scope: user, instance, mandate, system (AutoTemplateScope)",
|
description="Template scope: user, instance, mandate, system (AutoTemplateScope)",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_required": False},
|
json_schema_extra={"frontend_type": "select", "frontend_required": False, "label": "Vorlagen-Bereich"},
|
||||||
)
|
)
|
||||||
sharedReadOnly: bool = Field(
|
sharedReadOnly: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="If true, shared template is read-only for non-owners",
|
description="If true, shared template is read-only for non-owners",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
|
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Freigabe nur-lesen"},
|
||||||
)
|
)
|
||||||
currentVersionId: Optional[str] = Field(
|
currentVersionId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of the currently published AutoVersion",
|
description="ID of the currently published AutoVersion",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Aktuelle Version"},
|
||||||
)
|
)
|
||||||
active: bool = Field(
|
active: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether workflow is active",
|
description="Whether workflow is active",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
|
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Aktiv"},
|
||||||
)
|
)
|
||||||
eventId: Optional[str] = Field(
|
eventId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Scheduler event ID for incremental sync",
|
description="Scheduler event ID for incremental sync",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Event-ID"},
|
||||||
)
|
)
|
||||||
notifyOnFailure: bool = Field(
|
notifyOnFailure: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Send notification (in-app + email) when a run fails",
|
description="Send notification (in-app + email) when a run fails",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
|
json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Bei Fehler benachrichtigen"},
|
||||||
)
|
)
|
||||||
# Legacy fields kept for backward compatibility during transition
|
# Legacy fields kept for backward compatibility during transition
|
||||||
graph: Dict[str, Any] = Field(
|
graph: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Graph with nodes and connections (legacy; prefer AutoVersion.graph)",
|
description="Graph with nodes and connections (legacy; prefer AutoVersion.graph)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Graph"},
|
||||||
)
|
)
|
||||||
invocations: List[Dict[str, Any]] = Field(
|
invocations: List[Dict[str, Any]] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Entry points / starts (manual, form, schedule, webhook, ...)",
|
description="Entry points / starts (manual, form, schedule, webhook, ...)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Starts / Einstiegspunkte"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AutoWorkflow",
|
|
||||||
{"en": "Workflow", "de": "Workflow", "fr": "Workflow"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID", "fr": "ID instance"},
|
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
|
|
||||||
"tags": {"en": "Tags", "de": "Tags", "fr": "Tags"},
|
|
||||||
"isTemplate": {"en": "Is Template", "de": "Ist Vorlage", "fr": "Est modèle"},
|
|
||||||
"templateSourceId": {"en": "Template Source", "de": "Vorlagen-Quelle", "fr": "Source du modèle"},
|
|
||||||
"templateScope": {"en": "Template Scope", "de": "Vorlagen-Bereich", "fr": "Portée du modèle"},
|
|
||||||
"sharedReadOnly": {"en": "Shared Read-Only", "de": "Freigabe nur-lesen", "fr": "Partage lecture seule"},
|
|
||||||
"currentVersionId": {"en": "Current Version", "de": "Aktuelle Version", "fr": "Version actuelle"},
|
|
||||||
"active": {"en": "Active", "de": "Aktiv", "fr": "Actif"},
|
|
||||||
"eventId": {"en": "Event ID", "de": "Event-ID", "fr": "ID événement"},
|
|
||||||
"graph": {"en": "Graph", "de": "Graph", "fr": "Graphe"},
|
|
||||||
"invocations": {"en": "Starts / Entry points", "de": "Starts / Einstiegspunkte", "fr": "Points d'entrée"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AutoVersion
|
# AutoVersion
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@i18nModel("Workflow-Version")
|
||||||
class AutoVersion(PowerOnModel):
|
class AutoVersion(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Primary key",
|
description="Primary key",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
workflowId: str = Field(
|
workflowId: str = Field(
|
||||||
description="FK -> AutoWorkflow",
|
description="FK -> AutoWorkflow",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
|
||||||
)
|
)
|
||||||
versionNumber: int = Field(
|
versionNumber: int = Field(
|
||||||
default=1,
|
default=1,
|
||||||
description="Incrementing version number",
|
description="Incrementing version number",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Version"},
|
||||||
)
|
)
|
||||||
status: str = Field(
|
status: str = Field(
|
||||||
default=AutoWorkflowStatus.DRAFT.value,
|
default=AutoWorkflowStatus.DRAFT.value,
|
||||||
description="Version status: draft, published, archived",
|
description="Version status: draft, published, archived",
|
||||||
json_schema_extra={"frontend_type": "select", "frontend_required": False},
|
json_schema_extra={"frontend_type": "select", "frontend_required": False, "label": "Status"},
|
||||||
)
|
)
|
||||||
graph: Dict[str, Any] = Field(
|
graph: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Graph with nodes and connections (incl. node parameters)",
|
description="Graph with nodes and connections (incl. node parameters)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": True},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": True, "label": "Graph"},
|
||||||
)
|
)
|
||||||
invocations: List[Dict[str, Any]] = Field(
|
invocations: List[Dict[str, Any]] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="Entry points / starts for this version",
|
description="Entry points / starts for this version",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Einstiegspunkte"},
|
||||||
)
|
)
|
||||||
publishedAt: Optional[float] = Field(
|
publishedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Timestamp when version was published",
|
description="Timestamp when version was published",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Veröffentlicht am"},
|
||||||
)
|
)
|
||||||
publishedBy: Optional[str] = Field(
|
publishedBy: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID who published this version",
|
description="User ID who published this version",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Veröffentlicht von"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AutoVersion",
|
|
||||||
{"en": "Workflow Version", "de": "Workflow-Version", "fr": "Version workflow"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
|
|
||||||
"versionNumber": {"en": "Version", "de": "Version", "fr": "Version"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"graph": {"en": "Graph", "de": "Graph", "fr": "Graphe"},
|
|
||||||
"invocations": {"en": "Entry Points", "de": "Einstiegspunkte", "fr": "Points d'entrée"},
|
|
||||||
"publishedAt": {"en": "Published At", "de": "Veröffentlicht am", "fr": "Publié le"},
|
|
||||||
"publishedBy": {"en": "Published By", "de": "Veröffentlicht von", "fr": "Publié par"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AutoRun
|
# AutoRun
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@i18nModel("Workflow-Ausführung")
|
||||||
class AutoRun(PowerOnModel):
|
class AutoRun(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Primary key",
|
description="Primary key",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
workflowId: str = Field(
|
workflowId: str = Field(
|
||||||
description="Workflow ID",
|
description="Workflow ID",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
|
||||||
)
|
)
|
||||||
mandateId: Optional[str] = Field(
|
mandateId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID for cross-feature querying",
|
description="Mandate ID for cross-feature querying",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Mandanten-ID"},
|
||||||
)
|
)
|
||||||
ownerId: Optional[str] = Field(
|
ownerId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID who triggered this run",
|
description="User ID who triggered this run",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Auslöser"},
|
||||||
)
|
)
|
||||||
versionId: Optional[str] = Field(
|
versionId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="AutoVersion ID used for this run",
|
description="AutoVersion ID used for this run",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Versions-ID"},
|
||||||
)
|
)
|
||||||
status: str = Field(
|
status: str = Field(
|
||||||
default=AutoRunStatus.RUNNING.value,
|
default=AutoRunStatus.RUNNING.value,
|
||||||
description="Status: running, paused, completed, failed, cancelled",
|
description="Status: running, paused, completed, failed, cancelled",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
|
||||||
)
|
)
|
||||||
trigger: Dict[str, Any] = Field(
|
trigger: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Trigger info (type, entryPointId, payload, etc.)",
|
description="Trigger info (type, entryPointId, payload, etc.)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Auslöser"},
|
||||||
)
|
)
|
||||||
startedAt: Optional[float] = Field(
|
startedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Run start timestamp",
|
description="Run start timestamp",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Gestartet am"},
|
||||||
)
|
)
|
||||||
completedAt: Optional[float] = Field(
|
completedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Run completion timestamp",
|
description="Run completion timestamp",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Abgeschlossen am"},
|
||||||
)
|
)
|
||||||
nodeOutputs: Dict[str, Any] = Field(
|
nodeOutputs: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Outputs from executed nodes",
|
description="Outputs from executed nodes",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Node-Ausgaben"},
|
||||||
)
|
)
|
||||||
currentNodeId: Optional[str] = Field(
|
currentNodeId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Node ID when paused (human task / email wait)",
|
description="Node ID when paused (human task / email wait)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Aktueller Knoten"},
|
||||||
)
|
)
|
||||||
resumeContext: Dict[str, Any] = Field(
|
resumeContext: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Context for resume (connectionMap, inputSources, etc.)",
|
description="Context for resume (connectionMap, inputSources, etc.)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Wiederaufnahme-Kontext"},
|
||||||
)
|
)
|
||||||
error: Optional[str] = Field(
|
error: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if failed",
|
description="Error message if failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False, "label": "Fehler"},
|
||||||
)
|
)
|
||||||
costTokens: int = Field(
|
costTokens: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Total tokens consumed by AI nodes",
|
description="Total tokens consumed by AI nodes",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Tokens"},
|
||||||
)
|
)
|
||||||
costCredits: float = Field(
|
costCredits: float = Field(
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="Total credits consumed",
|
description="Total credits consumed",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Credits"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AutoRun",
|
|
||||||
{"en": "Workflow Run", "de": "Workflow-Ausführung", "fr": "Exécution workflow"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
|
|
||||||
"ownerId": {"en": "Owner", "de": "Auslöser", "fr": "Propriétaire"},
|
|
||||||
"versionId": {"en": "Version ID", "de": "Versions-ID", "fr": "ID version"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"trigger": {"en": "Trigger", "de": "Auslöser", "fr": "Déclencheur"},
|
|
||||||
"startedAt": {"en": "Started At", "de": "Gestartet am", "fr": "Démarré le"},
|
|
||||||
"completedAt": {"en": "Completed At", "de": "Abgeschlossen am", "fr": "Terminé le"},
|
|
||||||
"nodeOutputs": {"en": "Node Outputs", "de": "Node-Ausgaben", "fr": "Sorties nœuds"},
|
|
||||||
"currentNodeId": {"en": "Current Node", "de": "Aktueller Knoten", "fr": "Nœud actuel"},
|
|
||||||
"resumeContext": {"en": "Resume Context", "de": "Wiederaufnahme-Kontext", "fr": "Contexte reprise"},
|
|
||||||
"error": {"en": "Error", "de": "Fehler", "fr": "Erreur"},
|
|
||||||
"costTokens": {"en": "Tokens Used", "de": "Verbrauchte Tokens", "fr": "Tokens utilisés"},
|
|
||||||
"costCredits": {"en": "Credits Used", "de": "Verbrauchte Credits", "fr": "Crédits utilisés"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AutoStepLog
|
# AutoStepLog
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@i18nModel("Schritt-Protokoll")
|
||||||
class AutoStepLog(PowerOnModel):
|
class AutoStepLog(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Primary key",
|
description="Primary key",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
runId: str = Field(
|
runId: str = Field(
|
||||||
description="FK -> AutoRun",
|
description="FK -> AutoRun",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Lauf-ID"},
|
||||||
)
|
)
|
||||||
nodeId: str = Field(
|
nodeId: str = Field(
|
||||||
description="Node ID in the graph",
|
description="Node ID in the graph",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knoten-ID"},
|
||||||
)
|
)
|
||||||
nodeType: str = Field(
|
nodeType: str = Field(
|
||||||
description="Node type (e.g. ai.chat, email.send)",
|
description="Node type (e.g. ai.chat, email.send)",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knotentyp"},
|
||||||
)
|
)
|
||||||
status: str = Field(
|
status: str = Field(
|
||||||
default=AutoStepStatus.PENDING.value,
|
default=AutoStepStatus.PENDING.value,
|
||||||
description="Step status: pending, running, completed, failed, skipped",
|
description="Step status: pending, running, completed, failed, skipped",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
|
||||||
)
|
)
|
||||||
inputSnapshot: Dict[str, Any] = Field(
|
inputSnapshot: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Snapshot of inputs at execution time",
|
description="Snapshot of inputs at execution time",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Eingabe-Snapshot"},
|
||||||
)
|
)
|
||||||
output: Dict[str, Any] = Field(
|
output: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Node output",
|
description="Node output",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Ausgabe"},
|
||||||
)
|
)
|
||||||
error: Optional[str] = Field(
|
error: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message if step failed",
|
description="Error message if step failed",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False, "label": "Fehler"},
|
||||||
)
|
)
|
||||||
startedAt: Optional[float] = Field(
|
startedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Step start timestamp",
|
description="Step start timestamp",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Gestartet am"},
|
||||||
)
|
)
|
||||||
completedAt: Optional[float] = Field(
|
completedAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Step completion timestamp",
|
description="Step completion timestamp",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Abgeschlossen am"},
|
||||||
)
|
)
|
||||||
durationMs: Optional[int] = Field(
|
durationMs: Optional[int] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Execution duration in milliseconds",
|
description="Execution duration in milliseconds",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Dauer (ms)"},
|
||||||
)
|
)
|
||||||
tokensUsed: int = Field(
|
tokensUsed: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Tokens consumed by this step",
|
description="Tokens consumed by this step",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Tokens"},
|
||||||
)
|
)
|
||||||
retryCount: int = Field(
|
retryCount: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Number of retries executed",
|
description="Number of retries executed",
|
||||||
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Wiederholungen"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AutoStepLog",
|
|
||||||
{"en": "Step Log", "de": "Schritt-Protokoll", "fr": "Journal d'étape"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"runId": {"en": "Run ID", "de": "Lauf-ID", "fr": "ID exécution"},
|
|
||||||
"nodeId": {"en": "Node ID", "de": "Knoten-ID", "fr": "ID nœud"},
|
|
||||||
"nodeType": {"en": "Node Type", "de": "Knotentyp", "fr": "Type nœud"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"inputSnapshot": {"en": "Input Snapshot", "de": "Eingabe-Snapshot", "fr": "Snapshot entrée"},
|
|
||||||
"output": {"en": "Output", "de": "Ausgabe", "fr": "Sortie"},
|
|
||||||
"error": {"en": "Error", "de": "Fehler", "fr": "Erreur"},
|
|
||||||
"startedAt": {"en": "Started At", "de": "Gestartet am", "fr": "Démarré le"},
|
|
||||||
"completedAt": {"en": "Completed At", "de": "Abgeschlossen am", "fr": "Terminé le"},
|
|
||||||
"durationMs": {"en": "Duration (ms)", "de": "Dauer (ms)", "fr": "Durée (ms)"},
|
|
||||||
"tokensUsed": {"en": "Tokens Used", "de": "Verbrauchte Tokens", "fr": "Tokens utilisés"},
|
|
||||||
"retryCount": {"en": "Retry Count", "de": "Wiederholungen", "fr": "Nombre de tentatives"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AutoTask
|
# AutoTask
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@i18nModel("Aufgabe")
|
||||||
class AutoTask(PowerOnModel):
|
class AutoTask(PowerOnModel):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Primary key",
|
description="Primary key",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
|
||||||
)
|
)
|
||||||
runId: str = Field(
|
runId: str = Field(
|
||||||
description="FK -> AutoRun",
|
description="FK -> AutoRun",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Lauf-ID"},
|
||||||
)
|
)
|
||||||
workflowId: str = Field(
|
workflowId: str = Field(
|
||||||
description="Workflow ID",
|
description="Workflow ID",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
|
||||||
)
|
)
|
||||||
nodeId: str = Field(
|
nodeId: str = Field(
|
||||||
description="Node ID in the graph",
|
description="Node ID in the graph",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knoten-ID"},
|
||||||
)
|
)
|
||||||
nodeType: str = Field(
|
nodeType: str = Field(
|
||||||
description="Node type: form, approval, upload, comment, review, selection, confirmation",
|
description="Node type: form, approval, upload, comment, review, selection, confirmation",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knotentyp"},
|
||||||
)
|
)
|
||||||
config: Dict[str, Any] = Field(
|
config: Dict[str, Any] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Node config (form schema, approval text, etc.)",
|
description="Node config (form schema, approval text, etc.)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Konfiguration"},
|
||||||
)
|
)
|
||||||
assigneeId: Optional[str] = Field(
|
assigneeId: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="User ID assigned to complete the task",
|
description="User ID assigned to complete the task",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Zugewiesen an"},
|
||||||
)
|
)
|
||||||
status: str = Field(
|
status: str = Field(
|
||||||
default=AutoTaskStatus.PENDING.value,
|
default=AutoTaskStatus.PENDING.value,
|
||||||
description="Status: pending, completed, cancelled, expired",
|
description="Status: pending, completed, cancelled, expired",
|
||||||
json_schema_extra={"frontend_type": "text", "frontend_required": False},
|
json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
|
||||||
)
|
)
|
||||||
result: Optional[Dict[str, Any]] = Field(
|
result: Optional[Dict[str, Any]] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Task result (form data, approval decision, etc.)",
|
description="Task result (form data, approval decision, etc.)",
|
||||||
json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
|
json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Ergebnis"},
|
||||||
)
|
)
|
||||||
expiresAt: Optional[float] = Field(
|
expiresAt: Optional[float] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Expiration timestamp for the task",
|
description="Expiration timestamp for the task",
|
||||||
json_schema_extra={"frontend_type": "datetime", "frontend_required": False},
|
json_schema_extra={"frontend_type": "datetime", "frontend_required": False, "label": "Läuft ab am"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"AutoTask",
|
|
||||||
{"en": "Task", "de": "Aufgabe", "fr": "Tâche"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"runId": {"en": "Run ID", "de": "Lauf-ID", "fr": "ID exécution"},
|
|
||||||
"workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
|
|
||||||
"nodeId": {"en": "Node ID", "de": "Knoten-ID", "fr": "ID nœud"},
|
|
||||||
"nodeType": {"en": "Node Type", "de": "Knotentyp", "fr": "Type nœud"},
|
|
||||||
"config": {"en": "Config", "de": "Konfiguration", "fr": "Configuration"},
|
|
||||||
"assigneeId": {"en": "Assignee", "de": "Zugewiesen an", "fr": "Assigné à"},
|
|
||||||
"status": {"en": "Status", "de": "Status", "fr": "Statut"},
|
|
||||||
"result": {"en": "Result", "de": "Ergebnis", "fr": "Résultat"},
|
|
||||||
"expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Backward-compatible aliases for transition period
|
# Backward-compatible aliases for transition period
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,18 @@ def default_manual_entry_point() -> Dict[str, Any]:
|
||||||
"kind": "manual",
|
"kind": "manual",
|
||||||
"category": "on_demand",
|
"category": "on_demand",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"title": {
|
"title": "Jetzt ausführen",
|
||||||
"de": "Jetzt ausführen",
|
|
||||||
"en": "Run now",
|
|
||||||
"fr": "Exécuter",
|
|
||||||
},
|
|
||||||
"description": {},
|
"description": {},
|
||||||
"config": {},
|
"config": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_title(title: Any) -> Dict[str, str]:
|
def _normalize_title(title: Any) -> str:
|
||||||
if isinstance(title, dict):
|
if isinstance(title, dict):
|
||||||
return {k: str(v) for k, v in title.items() if v is not None}
|
return str(title.get("de") or title.get("en") or title.get("fr") or "").strip()
|
||||||
if isinstance(title, str) and title.strip():
|
if isinstance(title, str) and title.strip():
|
||||||
return {"de": title, "en": title, "fr": title}
|
return title.strip()
|
||||||
return {"de": "Start", "en": "Start", "fr": "Départ"}
|
return "Start"
|
||||||
|
|
||||||
|
|
||||||
def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
|
def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
|
|
@ -21,28 +21,28 @@ REQUIRED_SERVICES = [
|
||||||
{"serviceKey": "clickup", "meta": {"usage": "ClickUp actions"}},
|
{"serviceKey": "clickup", "meta": {"usage": "ClickUp actions"}},
|
||||||
{"serviceKey": "generation", "meta": {"usage": "file.create document rendering"}},
|
{"serviceKey": "generation", "meta": {"usage": "file.create document rendering"}},
|
||||||
]
|
]
|
||||||
FEATURE_LABEL = {"en": "Graphical Editor", "de": "Grafischer Editor", "fr": "Éditeur graphique"}
|
FEATURE_LABEL = "Grafischer Editor"
|
||||||
FEATURE_ICON = "mdi-sitemap"
|
FEATURE_ICON = "mdi-sitemap"
|
||||||
|
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.graphicalEditor.editor",
|
"objectKey": "ui.feature.graphicalEditor.editor",
|
||||||
"label": {"en": "Editor", "de": "Editor", "fr": "Éditeur"},
|
"label": "Editor",
|
||||||
"meta": {"area": "editor"}
|
"meta": {"area": "editor"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.graphicalEditor.workflows",
|
"objectKey": "ui.feature.graphicalEditor.workflows",
|
||||||
"label": {"en": "Workflows", "de": "Workflows", "fr": "Workflows"},
|
"label": "Workflows",
|
||||||
"meta": {"area": "workflows"}
|
"meta": {"area": "workflows"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.graphicalEditor.templates",
|
"objectKey": "ui.feature.graphicalEditor.templates",
|
||||||
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
|
"label": "Vorlagen",
|
||||||
"meta": {"area": "templates"}
|
"meta": {"area": "templates"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.graphicalEditor.workflows-tasks",
|
"objectKey": "ui.feature.graphicalEditor.workflows-tasks",
|
||||||
"label": {"en": "Tasks", "de": "Tasks", "fr": "Tâches"},
|
"label": "Tasks",
|
||||||
"meta": {"area": "tasks"}
|
"meta": {"area": "tasks"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -50,17 +50,17 @@ UI_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.graphicalEditor.dashboard",
|
"objectKey": "resource.feature.graphicalEditor.dashboard",
|
||||||
"label": {"en": "Access Dashboard", "de": "Dashboard aufrufen", "fr": "Acceder au tableau de bord"},
|
"label": "Dashboard aufrufen",
|
||||||
"meta": {"endpoint": "/api/workflows/{instanceId}/info", "method": "GET"}
|
"meta": {"endpoint": "/api/workflows/{instanceId}/info", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.graphicalEditor.node-types",
|
"objectKey": "resource.feature.graphicalEditor.node-types",
|
||||||
"label": {"en": "Get Node Types", "de": "Node-Typen abrufen", "fr": "Obtenir types de nœuds"},
|
"label": "Node-Typen abrufen",
|
||||||
"meta": {"endpoint": "/api/workflows/{instanceId}/node-types", "method": "GET"}
|
"meta": {"endpoint": "/api/workflows/{instanceId}/node-types", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.graphicalEditor.execute",
|
"objectKey": "resource.feature.graphicalEditor.execute",
|
||||||
"label": {"en": "Execute Workflow", "de": "Workflow ausführen", "fr": "Exécuter le workflow"},
|
"label": "Workflow ausführen",
|
||||||
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
|
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -68,11 +68,7 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "graphicalEditor-viewer",
|
"roleLabel": "graphicalEditor-viewer",
|
||||||
"description": {
|
"description": "Grafischer Editor Betrachter - Workflows ansehen (nur lesen)",
|
||||||
"en": "GraphicalEditor Viewer - View workflows (read-only)",
|
|
||||||
"de": "Grafischer Editor Betrachter - Workflows ansehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur Éditeur graphique - Consulter les workflows (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
|
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows-tasks", "view": True},
|
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows-tasks", "view": True},
|
||||||
|
|
@ -82,11 +78,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "graphicalEditor-user",
|
"roleLabel": "graphicalEditor-user",
|
||||||
"description": {
|
"description": "Grafischer Editor Benutzer - Flow-Builder nutzen",
|
||||||
"en": "GraphicalEditor User - Use flow builder",
|
|
||||||
"de": "Grafischer Editor Benutzer - Flow-Builder nutzen",
|
|
||||||
"fr": "Utilisateur Éditeur graphique - Utiliser le flow builder",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.graphicalEditor.editor", "view": True},
|
{"context": "UI", "item": "ui.feature.graphicalEditor.editor", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
|
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
|
||||||
|
|
@ -100,11 +92,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "graphicalEditor-admin",
|
"roleLabel": "graphicalEditor-admin",
|
||||||
"description": {
|
"description": "Grafischer Editor Admin - Volle UI und API für die Instanz; Daten weiterhin benutzerspezifisch (MY)",
|
||||||
"en": "GraphicalEditor Admin - Full UI and API for the instance; data remains user-scoped (MY)",
|
|
||||||
"de": "Grafischer Editor Admin - Volle UI und API für die Instanz; Daten weiterhin benutzerspezifisch (MY)",
|
|
||||||
"fr": "Administrateur Éditeur graphique - UI et API complets pour l'instance; donnees limitees a l'utilisateur (MY)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "RESOURCE", "item": None, "view": True},
|
{"context": "RESOURCE", "item": None, "view": True},
|
||||||
|
|
@ -272,6 +260,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role
|
from modules.datamodels.datamodelRbac import Role
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
|
|
@ -285,7 +274,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=template.get("description", {}),
|
description=coerce_text_multilingual(template.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.prompt",
|
"id": "ai.prompt",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Prompt", "de": "Prompt", "fr": "Invite"},
|
"label": "Prompt",
|
||||||
"description": {"en": "Enter a prompt and AI does something", "de": "Prompt eingeben und KI führt aus", "fr": "Entrer une invite et l'IA exécute"},
|
"description": "Prompt eingeben und KI führt aus",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "aiPrompt", "type": "string", "required": True, "frontendType": "textarea",
|
{"name": "aiPrompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||||
"description": {"en": "AI prompt", "de": "KI-Prompt", "fr": "Invite IA"}},
|
"description": "KI-Prompt"},
|
||||||
{"name": "outputFormat", "type": "string", "required": False, "frontendType": "select",
|
{"name": "outputFormat", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["text", "json", "emailDraft"]},
|
"frontendOptions": {"options": ["text", "json", "emailDraft"]},
|
||||||
"description": {"en": "Output format", "de": "Ausgabeformat", "fr": "Format de sortie"}, "default": "text"},
|
"description": "Ausgabeformat", "default": "text"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -25,11 +25,11 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.webResearch",
|
"id": "ai.webResearch",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Web Research", "de": "Web-Recherche", "fr": "Recherche web"},
|
"label": "Web-Recherche",
|
||||||
"description": {"en": "Research on the web", "de": "Recherche im Web", "fr": "Recherche sur le web"},
|
"description": "Recherche im Web",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||||
"description": {"en": "Research query", "de": "Recherche-Anfrage", "fr": "Requête de recherche"}},
|
"description": "Recherche-Anfrage"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -42,12 +42,12 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.summarizeDocument",
|
"id": "ai.summarizeDocument",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Summarize Document", "de": "Dokument zusammenfassen", "fr": "Résumer document"},
|
"label": "Dokument zusammenfassen",
|
||||||
"description": {"en": "Summarize document content", "de": "Dokumentinhalt zusammenfassen", "fr": "Résumer le contenu du document"},
|
"description": "Dokumentinhalt zusammenfassen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "summaryLength", "type": "string", "required": False, "frontendType": "select",
|
{"name": "summaryLength", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["short", "medium", "long"]},
|
"frontendOptions": {"options": ["short", "medium", "long"]},
|
||||||
"description": {"en": "Short, medium, or long", "de": "Kurz, mittel oder lang", "fr": "Court, moyen ou long"}, "default": "medium"},
|
"description": "Kurz, mittel oder lang", "default": "medium"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -60,12 +60,12 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.translateDocument",
|
"id": "ai.translateDocument",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Translate Document", "de": "Dokument übersetzen", "fr": "Traduire document"},
|
"label": "Dokument übersetzen",
|
||||||
"description": {"en": "Translate document to target language", "de": "Dokument in Zielsprache übersetzen", "fr": "Traduire le document"},
|
"description": "Dokument in Zielsprache übersetzen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "targetLanguage", "type": "string", "required": True, "frontendType": "select",
|
{"name": "targetLanguage", "type": "string", "required": True, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["en", "de", "fr", "it", "es", "pt", "nl"]},
|
"frontendOptions": {"options": ["en", "de", "fr", "it", "es", "pt", "nl"]},
|
||||||
"description": {"en": "Target language", "de": "Zielsprache", "fr": "Langue cible"}},
|
"description": "Zielsprache"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -78,12 +78,12 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.convertDocument",
|
"id": "ai.convertDocument",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Convert Document", "de": "Dokument konvertieren", "fr": "Convertir document"},
|
"label": "Dokument konvertieren",
|
||||||
"description": {"en": "Convert document to another format", "de": "Dokument in anderes Format konvertieren", "fr": "Convertir le document"},
|
"description": "Dokument in anderes Format konvertieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "targetFormat", "type": "string", "required": True, "frontendType": "select",
|
{"name": "targetFormat", "type": "string", "required": True, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["pdf", "docx", "txt", "html", "md"]},
|
"frontendOptions": {"options": ["pdf", "docx", "txt", "html", "md"]},
|
||||||
"description": {"en": "Target format", "de": "Zielformat", "fr": "Format cible"}},
|
"description": "Zielformat"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -96,11 +96,11 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.generateDocument",
|
"id": "ai.generateDocument",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Generate Document", "de": "Dokument generieren", "fr": "Générer document"},
|
"label": "Dokument generieren",
|
||||||
"description": {"en": "Generate document from prompt", "de": "Dokument aus Prompt generieren", "fr": "Générer un document"},
|
"description": "Dokument aus Prompt generieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||||
"description": {"en": "Generation prompt", "de": "Generierungs-Prompt", "fr": "Invite de génération"}},
|
"description": "Generierungs-Prompt"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -113,14 +113,14 @@ AI_NODES = [
|
||||||
{
|
{
|
||||||
"id": "ai.generateCode",
|
"id": "ai.generateCode",
|
||||||
"category": "ai",
|
"category": "ai",
|
||||||
"label": {"en": "Generate Code", "de": "Code generieren", "fr": "Générer code"},
|
"label": "Code generieren",
|
||||||
"description": {"en": "Generate code from description", "de": "Code aus Beschreibung generieren", "fr": "Générer du code"},
|
"description": "Code aus Beschreibung generieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
|
||||||
"description": {"en": "Code generation prompt", "de": "Code-Generierungs-Prompt", "fr": "Invite de génération de code"}},
|
"description": "Code-Generierungs-Prompt"},
|
||||||
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["python", "javascript", "typescript", "java", "csharp", "go"]},
|
"frontendOptions": {"options": ["python", "javascript", "typescript", "java", "csharp", "go"]},
|
||||||
"description": {"en": "Programming language", "de": "Programmiersprache", "fr": "Langage de programmation"}, "default": "python"},
|
"description": "Programmiersprache", "default": "python"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,26 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.searchTasks",
|
"id": "clickup.searchTasks",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "Search tasks", "de": "Aufgaben suchen", "fr": "Rechercher tâches"},
|
"label": "Aufgaben suchen",
|
||||||
"description": {"en": "Search tasks in a workspace", "de": "Aufgaben in einem Workspace suchen", "fr": "Rechercher des tâches"},
|
"description": "Aufgaben in einem Workspace suchen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "teamId", "type": "string", "required": True, "frontendType": "text",
|
{"name": "teamId", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Workspace (team) ID", "de": "Team-/Workspace-ID", "fr": "ID équipe"}},
|
"description": "Team-/Workspace-ID"},
|
||||||
{"name": "query", "type": "string", "required": True, "frontendType": "text",
|
{"name": "query", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Search query", "de": "Suchbegriff", "fr": "Requête"}},
|
"description": "Suchbegriff"},
|
||||||
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Page", "de": "Seite", "fr": "Page"}, "default": 0},
|
"description": "Seite", "default": 0},
|
||||||
{"name": "listId", "type": "string", "required": False, "frontendType": "clickupList",
|
{"name": "listId", "type": "string", "required": False, "frontendType": "clickupList",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Search in this list", "de": "In dieser Liste suchen", "fr": "Rechercher dans cette liste"}},
|
"description": "In dieser Liste suchen"},
|
||||||
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Include closed tasks", "de": "Erledigte einbeziehen", "fr": "Inclure terminées"}, "default": False},
|
"description": "Erledigte einbeziehen", "default": False},
|
||||||
{"name": "fullTaskData", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "fullTaskData", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Return full task data", "de": "Vollständige Daten", "fr": "Données complètes"}, "default": False},
|
"description": "Vollständige Daten", "default": False},
|
||||||
{"name": "matchNameOnly", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "matchNameOnly", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Match title only", "de": "Nur Titel", "fr": "Titre uniquement"}, "default": True},
|
"description": "Nur Titel", "default": True},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -38,18 +38,18 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.listTasks",
|
"id": "clickup.listTasks",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "List tasks", "de": "Aufgaben auflisten", "fr": "Lister les tâches"},
|
"label": "Aufgaben auflisten",
|
||||||
"description": {"en": "List tasks in a list", "de": "Aufgaben einer Liste auflisten", "fr": "Lister les tâches"},
|
"description": "Aufgaben einer Liste auflisten",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "clickupList",
|
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "clickupList",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Path to list", "de": "Pfad zur Liste", "fr": "Chemin vers la liste"}},
|
"description": "Pfad zur Liste"},
|
||||||
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
{"name": "page", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Page", "de": "Seite", "fr": "Page"}, "default": 0},
|
"description": "Seite", "default": 0},
|
||||||
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Include closed", "de": "Erledigte einbeziehen", "fr": "Inclure terminées"}, "default": False},
|
"description": "Erledigte einbeziehen", "default": False},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -62,15 +62,15 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.getTask",
|
"id": "clickup.getTask",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "Get task", "de": "Aufgabe abrufen", "fr": "Obtenir la tâche"},
|
"label": "Aufgabe abrufen",
|
||||||
"description": {"en": "Get one task by ID or path", "de": "Eine Aufgabe abrufen", "fr": "Obtenir une tâche"},
|
"description": "Eine Aufgabe abrufen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
|
"description": "Task-ID"},
|
||||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "text",
|
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Or path .../task/{id}", "de": "Oder Pfad", "fr": "Ou chemin"}},
|
"description": "Oder Pfad"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -83,39 +83,39 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.createTask",
|
"id": "clickup.createTask",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "Create task", "de": "Aufgabe erstellen", "fr": "Créer une tâche"},
|
"label": "Aufgabe erstellen",
|
||||||
"description": {"en": "Create a task in a list", "de": "Aufgabe erstellen", "fr": "Créer une tâche"},
|
"description": "Aufgabe erstellen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "teamId", "type": "string", "required": False, "frontendType": "text",
|
{"name": "teamId", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Workspace (team)", "de": "Workspace", "fr": "Équipe"}},
|
"description": "Workspace"},
|
||||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "clickupList",
|
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "clickupList",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Path to list", "de": "Pfad zur Liste", "fr": "Chemin"}},
|
"description": "Pfad zur Liste"},
|
||||||
{"name": "listId", "type": "string", "required": False, "frontendType": "text",
|
{"name": "listId", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "List ID", "de": "Listen-ID", "fr": "ID liste"}},
|
"description": "Listen-ID"},
|
||||||
{"name": "name", "type": "string", "required": True, "frontendType": "text",
|
{"name": "name", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Task name", "de": "Name", "fr": "Nom"}},
|
"description": "Name"},
|
||||||
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
||||||
"description": {"en": "Description", "de": "Beschreibung", "fr": "Description"}},
|
"description": "Beschreibung"},
|
||||||
{"name": "taskStatus", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskStatus", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Status", "de": "Status", "fr": "Statut"}},
|
"description": "Status"},
|
||||||
{"name": "taskPriority", "type": "string", "required": False, "frontendType": "select",
|
{"name": "taskPriority", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["1", "2", "3", "4"]},
|
"frontendOptions": {"options": ["1", "2", "3", "4"]},
|
||||||
"description": {"en": "Priority 1-4", "de": "Priorität 1-4", "fr": "Priorité 1-4"}},
|
"description": "Priorität 1-4"},
|
||||||
{"name": "taskDueDateMs", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskDueDateMs", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Due date (Unix ms)", "de": "Fälligkeit (ms)", "fr": "Échéance (ms)"}},
|
"description": "Fälligkeit (ms)"},
|
||||||
{"name": "taskAssigneeIds", "type": "object", "required": False, "frontendType": "json",
|
{"name": "taskAssigneeIds", "type": "object", "required": False, "frontendType": "json",
|
||||||
"description": {"en": "Assignee user ids", "de": "Zugewiesene", "fr": "Assignés"}},
|
"description": "Zugewiesene"},
|
||||||
{"name": "taskTimeEstimateMs", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskTimeEstimateMs", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Time estimate (ms)", "de": "Zeitschätzung (ms)", "fr": "Estimation (ms)"}},
|
"description": "Zeitschätzung (ms)"},
|
||||||
{"name": "taskTimeEstimateHours", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskTimeEstimateHours", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Time estimate (hours)", "de": "Zeitschätzung (h)", "fr": "Heures"}},
|
"description": "Zeitschätzung (h)"},
|
||||||
{"name": "customFieldValues", "type": "object", "required": False, "frontendType": "json",
|
{"name": "customFieldValues", "type": "object", "required": False, "frontendType": "json",
|
||||||
"description": {"en": "Custom fields", "de": "Benutzerdefinierte Felder", "fr": "Champs personnalisés"}},
|
"description": "Benutzerdefinierte Felder"},
|
||||||
{"name": "taskFields", "type": "string", "required": False, "frontendType": "json",
|
{"name": "taskFields", "type": "string", "required": False, "frontendType": "json",
|
||||||
"description": {"en": "Extra JSON (advanced)", "de": "Zusätzliches JSON", "fr": "JSON avancé"}},
|
"description": "Zusätzliches JSON"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -128,19 +128,19 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.updateTask",
|
"id": "clickup.updateTask",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "Update task", "de": "Aufgabe aktualisieren", "fr": "Mettre à jour la tâche"},
|
"label": "Aufgabe aktualisieren",
|
||||||
"description": {"en": "Update task fields", "de": "Felder der Aufgabe ändern", "fr": "Mettre à jour les champs"},
|
"description": "Felder der Aufgabe ändern",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
|
"description": "Task-ID"},
|
||||||
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Or path to task", "de": "Oder Pfad", "fr": "Ou chemin"}},
|
"description": "Oder Pfad"},
|
||||||
{"name": "taskUpdateEntries", "type": "object", "required": False, "frontendType": "keyValueRows",
|
{"name": "taskUpdateEntries", "type": "object", "required": False, "frontendType": "keyValueRows",
|
||||||
"description": {"en": "Fields to update", "de": "Zu ändernde Felder", "fr": "Champs à mettre à jour"}},
|
"description": "Zu ändernde Felder"},
|
||||||
{"name": "taskUpdate", "type": "string", "required": False, "frontendType": "json",
|
{"name": "taskUpdate", "type": "string", "required": False, "frontendType": "json",
|
||||||
"description": {"en": "JSON body (advanced)", "de": "JSON für API", "fr": "Corps JSON"}},
|
"description": "JSON für API"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -153,17 +153,17 @@ CLICKUP_NODES = [
|
||||||
{
|
{
|
||||||
"id": "clickup.uploadAttachment",
|
"id": "clickup.uploadAttachment",
|
||||||
"category": "clickup",
|
"category": "clickup",
|
||||||
"label": {"en": "Upload attachment", "de": "Anhang hochladen", "fr": "Téléverser pièce jointe"},
|
"label": "Anhang hochladen",
|
||||||
"description": {"en": "Upload file to a task", "de": "Datei an Task anhängen", "fr": "Joindre un fichier"},
|
"description": "Datei an Task anhängen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
|
"description": "ClickUp-Verbindung"},
|
||||||
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
|
"description": "Task-ID"},
|
||||||
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
{"name": "path", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Or path to task", "de": "Oder Pfad", "fr": "Ou chemin"}},
|
"description": "Oder Pfad"},
|
||||||
{"name": "fileName", "type": "string", "required": False, "frontendType": "text",
|
{"name": "fileName", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "File name", "de": "Dateiname", "fr": "Nom du fichier"}},
|
"description": "Dateiname"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ DATA_NODES = [
|
||||||
{
|
{
|
||||||
"id": "data.aggregate",
|
"id": "data.aggregate",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"},
|
"label": "Sammeln",
|
||||||
"description": {"en": "Collect results from loop iterations", "de": "Ergebnisse aus Schleifen-Iterationen sammeln", "fr": "Collecter les résultats des itérations"},
|
"description": "Ergebnisse aus Schleifen-Iterationen sammeln",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "mode", "type": "string", "required": False, "frontendType": "select",
|
{"name": "mode", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["collect", "concat", "sum", "count"]},
|
"frontendOptions": {"options": ["collect", "concat", "sum", "count"]},
|
||||||
"description": {"en": "Aggregation mode", "de": "Aggregationsmodus", "fr": "Mode d'agrégation"}, "default": "collect"},
|
"description": "Aggregationsmodus", "default": "collect"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -22,11 +22,11 @@ DATA_NODES = [
|
||||||
{
|
{
|
||||||
"id": "data.transform",
|
"id": "data.transform",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"label": {"en": "Transform", "de": "Umwandeln", "fr": "Transformer"},
|
"label": "Umwandeln",
|
||||||
"description": {"en": "Map and restructure data", "de": "Daten umstrukturieren", "fr": "Restructurer les données"},
|
"description": "Daten umstrukturieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "mappings", "type": "json", "required": True, "frontendType": "mappingTable",
|
{"name": "mappings", "type": "json", "required": True, "frontendType": "mappingTable",
|
||||||
"description": {"en": "Field mappings", "de": "Feld-Zuordnungen", "fr": "Correspondances"}, "default": []},
|
"description": "Feld-Zuordnungen", "default": []},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -38,11 +38,11 @@ DATA_NODES = [
|
||||||
{
|
{
|
||||||
"id": "data.filter",
|
"id": "data.filter",
|
||||||
"category": "data",
|
"category": "data",
|
||||||
"label": {"en": "Filter", "de": "Filtern", "fr": "Filtrer"},
|
"label": "Filtern",
|
||||||
"description": {"en": "Filter items by condition", "de": "Elemente nach Bedingung filtern", "fr": "Filtrer par condition"},
|
"description": "Elemente nach Bedingung filtern",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "condition", "type": "string", "required": True, "frontendType": "filterExpression",
|
{"name": "condition", "type": "string", "required": True, "frontendType": "filterExpression",
|
||||||
"description": {"en": "Filter condition", "de": "Filterbedingung", "fr": "Condition de filtre"}},
|
"description": "Filterbedingung"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,23 @@ EMAIL_NODES = [
|
||||||
{
|
{
|
||||||
"id": "email.checkEmail",
|
"id": "email.checkEmail",
|
||||||
"category": "email",
|
"category": "email",
|
||||||
"label": {"en": "Check Email", "de": "E-Mail prüfen", "fr": "Vérifier email"},
|
"label": "E-Mail prüfen",
|
||||||
"description": {"en": "Check for new emails", "de": "Neue E-Mails prüfen", "fr": "Vérifier les nouveaux emails"},
|
"description": "Neue E-Mails prüfen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "Email account connection", "de": "E-Mail-Konto Verbindung", "fr": "Connexion compte email"}},
|
"description": "E-Mail-Konto Verbindung"},
|
||||||
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Folder (e.g. Inbox)", "de": "Ordner", "fr": "Dossier"}, "default": "Inbox"},
|
"description": "Ordner", "default": "Inbox"},
|
||||||
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Max emails to fetch", "de": "Max E-Mails", "fr": "Max emails"}, "default": 100},
|
"description": "Max E-Mails", "default": 100},
|
||||||
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Only emails from this address", "de": "Nur von dieser Adresse", "fr": "Seulement de cette adresse"}, "default": ""},
|
"description": "Nur von dieser Adresse", "default": ""},
|
||||||
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Subject must contain", "de": "Betreff muss enthalten", "fr": "Le sujet doit contenir"}, "default": ""},
|
"description": "Betreff muss enthalten", "default": ""},
|
||||||
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Only with attachments", "de": "Nur mit Anhängen", "fr": "Avec pièces jointes"}, "default": False},
|
"description": "Nur mit Anhängen", "default": False},
|
||||||
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Advanced: raw filter", "de": "Erweitert: Filter-Text", "fr": "Avancé: filtre brut"}, "default": ""},
|
"description": "Erweitert: Filter-Text", "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -34,29 +34,29 @@ EMAIL_NODES = [
|
||||||
{
|
{
|
||||||
"id": "email.searchEmail",
|
"id": "email.searchEmail",
|
||||||
"category": "email",
|
"category": "email",
|
||||||
"label": {"en": "Search Email", "de": "E-Mail suchen", "fr": "Rechercher email"},
|
"label": "E-Mail suchen",
|
||||||
"description": {"en": "Search or find emails", "de": "E-Mails suchen", "fr": "Rechercher des emails"},
|
"description": "E-Mails suchen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "Email account connection", "de": "E-Mail-Konto Verbindung", "fr": "Connexion compte email"}},
|
"description": "E-Mail-Konto Verbindung"},
|
||||||
{"name": "query", "type": "string", "required": False, "frontendType": "text",
|
{"name": "query", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Search term", "de": "Suchbegriff", "fr": "Terme de recherche"}, "default": ""},
|
"description": "Suchbegriff", "default": ""},
|
||||||
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Folder to search", "de": "Ordner", "fr": "Dossier"}, "default": "Inbox"},
|
"description": "Ordner", "default": "Inbox"},
|
||||||
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Max emails", "de": "Max E-Mails", "fr": "Max emails"}, "default": 100},
|
"description": "Max E-Mails", "default": 100},
|
||||||
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "From address", "de": "Von Adresse", "fr": "De l'adresse"}, "default": ""},
|
"description": "Von Adresse", "default": ""},
|
||||||
{"name": "toAddress", "type": "string", "required": False, "frontendType": "text",
|
{"name": "toAddress", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "To address", "de": "An Adresse", "fr": "À l'adresse"}, "default": ""},
|
"description": "An Adresse", "default": ""},
|
||||||
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Subject contains", "de": "Betreff enthält", "fr": "Sujet contient"}, "default": ""},
|
"description": "Betreff enthält", "default": ""},
|
||||||
{"name": "bodyContains", "type": "string", "required": False, "frontendType": "text",
|
{"name": "bodyContains", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Body contains", "de": "Inhalt enthält", "fr": "Corps contient"}, "default": ""},
|
"description": "Inhalt enthält", "default": ""},
|
||||||
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "With attachments", "de": "Mit Anhängen", "fr": "Avec pièces jointes"}, "default": False},
|
"description": "Mit Anhängen", "default": False},
|
||||||
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Advanced: raw KQL", "de": "Erweitert: KQL-Filter", "fr": "Avancé: filtre KQL"}, "default": ""},
|
"description": "Erweitert: KQL-Filter", "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -69,17 +69,17 @@ EMAIL_NODES = [
|
||||||
{
|
{
|
||||||
"id": "email.draftEmail",
|
"id": "email.draftEmail",
|
||||||
"category": "email",
|
"category": "email",
|
||||||
"label": {"en": "Draft Email", "de": "E-Mail entwerfen", "fr": "Brouillon email"},
|
"label": "E-Mail entwerfen",
|
||||||
"description": {"en": "Create a draft email", "de": "E-Mail-Entwurf erstellen", "fr": "Créer un brouillon"},
|
"description": "E-Mail-Entwurf erstellen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "Email account", "de": "E-Mail-Konto", "fr": "Compte email"}},
|
"description": "E-Mail-Konto"},
|
||||||
{"name": "subject", "type": "string", "required": True, "frontendType": "text",
|
{"name": "subject", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Subject", "de": "Betreff", "fr": "Sujet"}},
|
"description": "Betreff"},
|
||||||
{"name": "body", "type": "string", "required": True, "frontendType": "textarea",
|
{"name": "body", "type": "string", "required": True, "frontendType": "textarea",
|
||||||
"description": {"en": "Body", "de": "Inhalt", "fr": "Corps"}},
|
"description": "Inhalt"},
|
||||||
{"name": "to", "type": "string", "required": False, "frontendType": "text",
|
{"name": "to", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Recipient(s)", "de": "Empfänger", "fr": "Destinataire(s)"}, "default": ""},
|
"description": "Empfänger", "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,22 @@ FILE_NODES = [
|
||||||
{
|
{
|
||||||
"id": "file.create",
|
"id": "file.create",
|
||||||
"category": "file",
|
"category": "file",
|
||||||
"label": {"en": "Create File", "de": "Datei erstellen", "fr": "Créer fichier"},
|
"label": "Datei erstellen",
|
||||||
"description": {
|
"description": "Erstellt eine Datei aus Kontext (Text/Markdown von KI).",
|
||||||
"en": "Create a file from context (text/markdown from AI).",
|
|
||||||
"de": "Erstellt eine Datei aus Kontext (Text/Markdown von KI).",
|
|
||||||
"fr": "Crée un fichier à partir du contexte.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "contentSources", "type": "json", "required": False, "frontendType": "json",
|
{"name": "contentSources", "type": "json", "required": False, "frontendType": "json",
|
||||||
"description": {"en": "Context source refs", "de": "Kontext-Quellen", "fr": "Sources de contexte"}, "default": []},
|
"description": "Kontext-Quellen", "default": []},
|
||||||
{"name": "outputFormat", "type": "string", "required": True, "frontendType": "select",
|
{"name": "outputFormat", "type": "string", "required": True, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["docx", "pdf", "txt", "html", "md"]},
|
"frontendOptions": {"options": ["docx", "pdf", "txt", "html", "md"]},
|
||||||
"description": {"en": "Output format", "de": "Ausgabeformat", "fr": "Format de sortie"}, "default": "docx"},
|
"description": "Ausgabeformat", "default": "docx"},
|
||||||
{"name": "title", "type": "string", "required": False, "frontendType": "text",
|
{"name": "title", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Document title", "de": "Dokumenttitel", "fr": "Titre du document"}},
|
"description": "Dokumenttitel"},
|
||||||
{"name": "templateName", "type": "string", "required": False, "frontendType": "select",
|
{"name": "templateName", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["default", "corporate", "minimal"]},
|
"frontendOptions": {"options": ["default", "corporate", "minimal"]},
|
||||||
"description": {"en": "Style preset", "de": "Stil-Vorlage", "fr": "Prését style"}},
|
"description": "Stil-Vorlage"},
|
||||||
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
{"name": "language", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["de", "en", "fr"]},
|
"frontendOptions": {"options": ["de", "en", "fr"]},
|
||||||
"description": {"en": "Language", "de": "Sprache", "fr": "Langue"}, "default": "de"},
|
"description": "Sprache", "default": "de"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,20 @@ FLOW_NODES = [
|
||||||
{
|
{
|
||||||
"id": "flow.ifElse",
|
"id": "flow.ifElse",
|
||||||
"category": "flow",
|
"category": "flow",
|
||||||
"label": {"en": "If / Else", "de": "Wenn / Sonst", "fr": "Si / Sinon"},
|
"label": "Wenn / Sonst",
|
||||||
"description": {"en": "Branch based on condition", "de": "Verzweigung nach Bedingung", "fr": "Branche selon condition"},
|
"description": "Verzweigung nach Bedingung",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "condition",
|
"name": "condition",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": True,
|
"required": True,
|
||||||
"frontendType": "condition",
|
"frontendType": "condition",
|
||||||
"description": {"en": "Condition to evaluate", "de": "Bedingung", "fr": "Condition"},
|
"description": "Bedingung",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 2,
|
"outputs": 2,
|
||||||
"outputLabels": {"en": ["Yes", "No"], "de": ["Ja", "Nein"], "fr": ["Oui", "Non"]},
|
"outputLabels": ["Ja", "Nein"],
|
||||||
"inputPorts": {0: {"accepts": ["Transit"]}},
|
"inputPorts": {0: {"accepts": ["Transit"]}},
|
||||||
"outputPorts": {0: {"schema": "Transit"}, 1: {"schema": "Transit"}},
|
"outputPorts": {0: {"schema": "Transit"}, 1: {"schema": "Transit"}},
|
||||||
"executor": "flow",
|
"executor": "flow",
|
||||||
|
|
@ -27,22 +27,22 @@ FLOW_NODES = [
|
||||||
{
|
{
|
||||||
"id": "flow.switch",
|
"id": "flow.switch",
|
||||||
"category": "flow",
|
"category": "flow",
|
||||||
"label": {"en": "Switch", "de": "Switch", "fr": "Switch"},
|
"label": "Switch",
|
||||||
"description": {"en": "Multiple branches based on value", "de": "Mehrere Zweige nach Wert", "fr": "Branches multiples selon valeur"},
|
"description": "Mehrere Zweige nach Wert",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "value",
|
"name": "value",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": True,
|
"required": True,
|
||||||
"frontendType": "text",
|
"frontendType": "text",
|
||||||
"description": {"en": "Value to match", "de": "Zu vergleichender Wert", "fr": "Valeur à comparer"},
|
"description": "Zu vergleichender Wert",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "cases",
|
"name": "cases",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"required": False,
|
"required": False,
|
||||||
"frontendType": "caseList",
|
"frontendType": "caseList",
|
||||||
"description": {"en": "List of cases", "de": "Fälle", "fr": "Cas"},
|
"description": "Fälle",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
|
|
@ -55,15 +55,15 @@ FLOW_NODES = [
|
||||||
{
|
{
|
||||||
"id": "flow.loop",
|
"id": "flow.loop",
|
||||||
"category": "flow",
|
"category": "flow",
|
||||||
"label": {"en": "Loop / For Each", "de": "Schleife / Für Jedes", "fr": "Boucle / Pour Chaque"},
|
"label": "Schleife / Für Jedes",
|
||||||
"description": {"en": "Iterate over array items", "de": "Über Array-Elemente iterieren", "fr": "Itérer sur les éléments"},
|
"description": "Über Array-Elemente iterieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "items",
|
"name": "items",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": True,
|
"required": True,
|
||||||
"frontendType": "text",
|
"frontendType": "text",
|
||||||
"description": {"en": "Path to array (e.g. {{input.items}})", "de": "Pfad zum Array", "fr": "Chemin vers le tableau"},
|
"description": "Pfad zum Array",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
|
|
@ -76,8 +76,8 @@ FLOW_NODES = [
|
||||||
{
|
{
|
||||||
"id": "flow.merge",
|
"id": "flow.merge",
|
||||||
"category": "flow",
|
"category": "flow",
|
||||||
"label": {"en": "Merge", "de": "Zusammenführen", "fr": "Fusionner"},
|
"label": "Zusammenführen",
|
||||||
"description": {"en": "Merge multiple branches", "de": "Mehrere Zweige zusammenführen", "fr": "Fusionner plusieurs branches"},
|
"description": "Mehrere Zweige zusammenführen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "mode",
|
"name": "mode",
|
||||||
|
|
@ -85,7 +85,7 @@ FLOW_NODES = [
|
||||||
"required": False,
|
"required": False,
|
||||||
"frontendType": "select",
|
"frontendType": "select",
|
||||||
"frontendOptions": {"options": ["first", "all", "append"]},
|
"frontendOptions": {"options": ["first", "all", "append"]},
|
||||||
"description": {"en": "Merge mode", "de": "Zusammenführungsmodus", "fr": "Mode de fusion"},
|
"description": "Zusammenführungsmodus",
|
||||||
"default": "first",
|
"default": "first",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,15 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.form",
|
"id": "input.form",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Form", "de": "Formular", "fr": "Formulaire"},
|
"label": "Formular",
|
||||||
"description": {"en": "User fills out a form", "de": "Benutzer füllt ein Formular aus", "fr": "L'utilisateur remplit un formulaire"},
|
"description": "Benutzer füllt ein Formular aus",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "fields",
|
"name": "fields",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
"required": True,
|
"required": True,
|
||||||
"frontendType": "fieldBuilder",
|
"frontendType": "fieldBuilder",
|
||||||
"description": {
|
"description": "Formularfelder",
|
||||||
"en": "Form fields: [{name, type, label, required, options?}]",
|
|
||||||
"de": "Formularfelder",
|
|
||||||
"fr": "Champs du formulaire",
|
|
||||||
},
|
|
||||||
"default": [],
|
"default": [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -31,16 +27,16 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.approval",
|
"id": "input.approval",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Approval", "de": "Genehmigung", "fr": "Approbation"},
|
"label": "Genehmigung",
|
||||||
"description": {"en": "User approves or rejects", "de": "Benutzer genehmigt oder lehnt ab", "fr": "L'utilisateur approuve ou rejette"},
|
"description": "Benutzer genehmigt oder lehnt ab",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "title", "type": "string", "required": True, "frontendType": "text",
|
{"name": "title", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Approval title", "de": "Genehmigungstitel", "fr": "Titre"}},
|
"description": "Genehmigungstitel"},
|
||||||
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
|
||||||
"description": {"en": "What to approve", "de": "Was genehmigt werden soll", "fr": "Ce qu'il faut approuver"}},
|
"description": "Was genehmigt werden soll"},
|
||||||
{"name": "approvalType", "type": "string", "required": False, "frontendType": "select",
|
{"name": "approvalType", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["generic", "document"]},
|
"frontendOptions": {"options": ["generic", "document"]},
|
||||||
"description": {"en": "Type: document or generic", "de": "Typ: document oder generic", "fr": "Type"}, "default": "generic"},
|
"description": "Typ: document oder generic", "default": "generic"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -52,18 +48,18 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.upload",
|
"id": "input.upload",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Upload", "de": "Upload", "fr": "Téléversement"},
|
"label": "Upload",
|
||||||
"description": {"en": "User uploads file(s)", "de": "Benutzer lädt Datei(en) hoch", "fr": "L'utilisateur téléverse des fichiers"},
|
"description": "Benutzer lädt Datei(en) hoch",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "accept", "type": "string", "required": False, "frontendType": "text",
|
{"name": "accept", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Accept string for file input (e.g. .pdf,image/*)", "de": "Accept-String", "fr": "Chaîne accept"}, "default": ""},
|
"description": "Accept-String", "default": ""},
|
||||||
{"name": "allowedTypes", "type": "json", "required": False, "frontendType": "multiselect",
|
{"name": "allowedTypes", "type": "json", "required": False, "frontendType": "multiselect",
|
||||||
"frontendOptions": {"options": ["pdf", "docx", "xlsx", "pptx", "txt", "csv", "jpg", "png", "gif"]},
|
"frontendOptions": {"options": ["pdf", "docx", "xlsx", "pptx", "txt", "csv", "jpg", "png", "gif"]},
|
||||||
"description": {"en": "Selected file types", "de": "Ausgewählte Dateitypen", "fr": "Types sélectionnés"}, "default": []},
|
"description": "Ausgewählte Dateitypen", "default": []},
|
||||||
{"name": "maxSize", "type": "number", "required": False, "frontendType": "number",
|
{"name": "maxSize", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Max file size in MB", "de": "Max. Dateigröße in MB", "fr": "Taille max en Mo"}, "default": 10},
|
"description": "Max. Dateigröße in MB", "default": 10},
|
||||||
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Allow multiple files", "de": "Mehrere Dateien erlauben", "fr": "Autoriser plusieurs fichiers"}, "default": False},
|
"description": "Mehrere Dateien erlauben", "default": False},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -75,13 +71,13 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.comment",
|
"id": "input.comment",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Comment", "de": "Kommentar", "fr": "Commentaire"},
|
"label": "Kommentar",
|
||||||
"description": {"en": "User adds a comment", "de": "Benutzer fügt einen Kommentar hinzu", "fr": "L'utilisateur ajoute un commentaire"},
|
"description": "Benutzer fügt einen Kommentar hinzu",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "placeholder", "type": "string", "required": False, "frontendType": "text",
|
{"name": "placeholder", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Placeholder text", "de": "Platzhalter", "fr": "Texte indicatif"}, "default": ""},
|
"description": "Platzhalter", "default": ""},
|
||||||
{"name": "required", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "required", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Comment required", "de": "Kommentar erforderlich", "fr": "Commentaire requis"}, "default": True},
|
"description": "Kommentar erforderlich", "default": True},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -93,14 +89,14 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.review",
|
"id": "input.review",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Review", "de": "Prüfung", "fr": "Revue"},
|
"label": "Prüfung",
|
||||||
"description": {"en": "User reviews content", "de": "Benutzer prüft Inhalt", "fr": "L'utilisateur révise le contenu"},
|
"description": "Benutzer prüft Inhalt",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "contentRef", "type": "string", "required": True, "frontendType": "text",
|
{"name": "contentRef", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Reference to content", "de": "Referenz auf Inhalt", "fr": "Référence au contenu"}},
|
"description": "Referenz auf Inhalt"},
|
||||||
{"name": "reviewType", "type": "string", "required": False, "frontendType": "select",
|
{"name": "reviewType", "type": "string", "required": False, "frontendType": "select",
|
||||||
"frontendOptions": {"options": ["generic", "document"]},
|
"frontendOptions": {"options": ["generic", "document"]},
|
||||||
"description": {"en": "Type of review", "de": "Art der Prüfung", "fr": "Type de revue"}, "default": "generic"},
|
"description": "Art der Prüfung", "default": "generic"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -112,13 +108,13 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.selection",
|
"id": "input.selection",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Selection", "de": "Auswahl", "fr": "Sélection"},
|
"label": "Auswahl",
|
||||||
"description": {"en": "User selects from options", "de": "Benutzer wählt aus Optionen", "fr": "L'utilisateur choisit parmi les options"},
|
"description": "Benutzer wählt aus Optionen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "options", "type": "json", "required": True, "frontendType": "keyValueRows",
|
{"name": "options", "type": "json", "required": True, "frontendType": "keyValueRows",
|
||||||
"description": {"en": "Options: [{value, label}]", "de": "Optionen", "fr": "Options"}, "default": []},
|
"description": "Optionen", "default": []},
|
||||||
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Allow multiple selection", "de": "Mehrfachauswahl erlauben", "fr": "Sélection multiple"}, "default": False},
|
"description": "Mehrfachauswahl erlauben", "default": False},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -130,15 +126,15 @@ INPUT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "input.confirmation",
|
"id": "input.confirmation",
|
||||||
"category": "input",
|
"category": "input",
|
||||||
"label": {"en": "Confirmation", "de": "Bestätigung", "fr": "Confirmation"},
|
"label": "Bestätigung",
|
||||||
"description": {"en": "User confirms yes/no", "de": "Benutzer bestätigt Ja/Nein", "fr": "L'utilisateur confirme oui/non"},
|
"description": "Benutzer bestätigt Ja/Nein",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "question", "type": "string", "required": True, "frontendType": "text",
|
{"name": "question", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Question to confirm", "de": "Zu bestätigende Frage", "fr": "Question à confirmer"}},
|
"description": "Zu bestätigende Frage"},
|
||||||
{"name": "confirmLabel", "type": "string", "required": False, "frontendType": "text",
|
{"name": "confirmLabel", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Label for confirm button", "de": "Label für Bestätigen-Button", "fr": "Libellé confirmer"}, "default": "Confirm"},
|
"description": "Label für Bestätigen-Button", "default": "Confirm"},
|
||||||
{"name": "rejectLabel", "type": "string", "required": False, "frontendType": "text",
|
{"name": "rejectLabel", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Label for reject button", "de": "Label für Ablehnen-Button", "fr": "Libellé refuser"}, "default": "Reject"},
|
"description": "Label für Ablehnen-Button", "default": "Reject"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.findFile",
|
"id": "sharepoint.findFile",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "Find File", "de": "Datei finden", "fr": "Trouver fichier"},
|
"label": "Datei finden",
|
||||||
"description": {"en": "Find file by path or search", "de": "Datei nach Pfad oder Suche finden", "fr": "Trouver fichier par chemin ou recherche"},
|
"description": "Datei nach Pfad oder Suche finden",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "searchQuery", "type": "string", "required": True, "frontendType": "text",
|
{"name": "searchQuery", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Search query or path", "de": "Suchanfrage oder Pfad", "fr": "Requête ou chemin"}},
|
"description": "Suchanfrage oder Pfad"},
|
||||||
{"name": "site", "type": "string", "required": False, "frontendType": "text",
|
{"name": "site", "type": "string", "required": False, "frontendType": "text",
|
||||||
"description": {"en": "Optional site hint", "de": "Optionaler Site-Hinweis", "fr": "Indication de site"}, "default": ""},
|
"description": "Optionaler Site-Hinweis", "default": ""},
|
||||||
{"name": "maxResults", "type": "number", "required": False, "frontendType": "number",
|
{"name": "maxResults", "type": "number", "required": False, "frontendType": "number",
|
||||||
"description": {"en": "Max results", "de": "Max Ergebnisse", "fr": "Max résultats"}, "default": 1000},
|
"description": "Max Ergebnisse", "default": 1000},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -28,14 +28,14 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.readFile",
|
"id": "sharepoint.readFile",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "Read File", "de": "Datei lesen", "fr": "Lire fichier"},
|
"label": "Datei lesen",
|
||||||
"description": {"en": "Extract content from file", "de": "Inhalt aus Datei extrahieren", "fr": "Extraire le contenu du fichier"},
|
"description": "Inhalt aus Datei extrahieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "File path", "de": "Dateipfad", "fr": "Chemin"}},
|
"description": "Dateipfad"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -48,14 +48,14 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.uploadFile",
|
"id": "sharepoint.uploadFile",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "Upload File", "de": "Datei hochladen", "fr": "Téléverser fichier"},
|
"label": "Datei hochladen",
|
||||||
"description": {"en": "Upload file to SharePoint", "de": "Datei zu SharePoint hochladen", "fr": "Téléverser fichier vers SharePoint"},
|
"description": "Datei zu SharePoint hochladen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Target folder path", "de": "Zielordner-Pfad", "fr": "Chemin du dossier cible"}},
|
"description": "Zielordner-Pfad"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -68,14 +68,14 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.listFiles",
|
"id": "sharepoint.listFiles",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "List Files", "de": "Dateien auflisten", "fr": "Lister fichiers"},
|
"label": "Dateien auflisten",
|
||||||
"description": {"en": "List files in folder", "de": "Dateien in Ordner auflisten", "fr": "Lister les fichiers"},
|
"description": "Dateien in Ordner auflisten",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Folder path", "de": "Ordnerpfad", "fr": "Chemin du dossier"}, "default": "/"},
|
"description": "Ordnerpfad", "default": "/"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -88,14 +88,14 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.downloadFile",
|
"id": "sharepoint.downloadFile",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "Download File", "de": "Datei herunterladen", "fr": "Télécharger fichier"},
|
"label": "Datei herunterladen",
|
||||||
"description": {"en": "Download file from path", "de": "Datei vom Pfad herunterladen", "fr": "Télécharger le fichier"},
|
"description": "Datei vom Pfad herunterladen",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Full file path", "de": "Vollständiger Dateipfad", "fr": "Chemin complet du fichier"}},
|
"description": "Vollständiger Dateipfad"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -108,17 +108,17 @@ SHAREPOINT_NODES = [
|
||||||
{
|
{
|
||||||
"id": "sharepoint.copyFile",
|
"id": "sharepoint.copyFile",
|
||||||
"category": "sharepoint",
|
"category": "sharepoint",
|
||||||
"label": {"en": "Copy File", "de": "Datei kopieren", "fr": "Copier fichier"},
|
"label": "Datei kopieren",
|
||||||
"description": {"en": "Copy file to destination", "de": "Datei an Ziel kopieren", "fr": "Copier le fichier"},
|
"description": "Datei an Ziel kopieren",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
|
"description": "SharePoint-Verbindung"},
|
||||||
{"name": "sourcePath", "type": "string", "required": True, "frontendType": "sharepointFile",
|
{"name": "sourcePath", "type": "string", "required": True, "frontendType": "sharepointFile",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Source file path", "de": "Quelldatei-Pfad", "fr": "Chemin fichier source"}},
|
"description": "Quelldatei-Pfad"},
|
||||||
{"name": "destPath", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
{"name": "destPath", "type": "string", "required": True, "frontendType": "sharepointFolder",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "Destination folder", "de": "Zielordner", "fr": "Dossier cible"}},
|
"description": "Zielordner"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,8 @@ TRIGGER_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trigger.manual",
|
"id": "trigger.manual",
|
||||||
"category": "trigger",
|
"category": "trigger",
|
||||||
"label": {"en": "Start", "de": "Start", "fr": "Départ"},
|
"label": "Start",
|
||||||
"description": {
|
"description": "Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …).",
|
||||||
"en": "Manual, API, or background triggers (webhook, email, …).",
|
|
||||||
"de": "Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …).",
|
|
||||||
"fr": "Manuel, API ou déclencheurs en arrière-plan.",
|
|
||||||
},
|
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"inputs": 0,
|
"inputs": 0,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -22,19 +18,15 @@ TRIGGER_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trigger.form",
|
"id": "trigger.form",
|
||||||
"category": "trigger",
|
"category": "trigger",
|
||||||
"label": {"en": "Start (form)", "de": "Start (Formular)", "fr": "Départ (formulaire)"},
|
"label": "Start (Formular)",
|
||||||
"description": {
|
"description": "Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node.",
|
||||||
"en": "Form fields are filled at run time; configure fields on this node.",
|
|
||||||
"de": "Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node.",
|
|
||||||
"fr": "Les champs sont remplis au démarrage.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "formFields",
|
"name": "formFields",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
"required": False,
|
"required": False,
|
||||||
"frontendType": "fieldBuilder",
|
"frontendType": "fieldBuilder",
|
||||||
"description": {"en": "Field definitions", "de": "Felddefinitionen", "fr": "Définitions"},
|
"description": "Felddefinitionen",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"inputs": 0,
|
"inputs": 0,
|
||||||
|
|
@ -47,19 +39,15 @@ TRIGGER_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trigger.schedule",
|
"id": "trigger.schedule",
|
||||||
"category": "trigger",
|
"category": "trigger",
|
||||||
"label": {"en": "Start (schedule)", "de": "Start (Zeitplan)", "fr": "Départ (planification)"},
|
"label": "Start (Zeitplan)",
|
||||||
"description": {
|
"description": "Cron-Ausdruck für geplante Läufe.",
|
||||||
"en": "Cron expression for scheduled runs (configure on this node).",
|
|
||||||
"de": "Cron-Ausdruck für geplante Läufe.",
|
|
||||||
"fr": "Expression cron pour les exécutions planifiées.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "cron",
|
"name": "cron",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": False,
|
"required": False,
|
||||||
"frontendType": "cron",
|
"frontendType": "cron",
|
||||||
"description": {"en": "Cron expression", "de": "Cron-Ausdruck", "fr": "Expression cron"},
|
"description": "Cron-Ausdruck",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"inputs": 0,
|
"inputs": 0,
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,17 @@ TRUSTEE_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trustee.refreshAccountingData",
|
"id": "trustee.refreshAccountingData",
|
||||||
"category": "trustee",
|
"category": "trustee",
|
||||||
"label": {"en": "Refresh Accounting Data", "de": "Buchhaltungsdaten aktualisieren", "fr": "Actualiser données comptables"},
|
"label": "Buchhaltungsdaten aktualisieren",
|
||||||
"description": {
|
"description": "Buchhaltungsdaten aus externem System importieren/aktualisieren.",
|
||||||
"en": "Import/refresh accounting data from external system (e.g. Abacus).",
|
|
||||||
"de": "Buchhaltungsdaten aus externem System importieren/aktualisieren.",
|
|
||||||
"fr": "Importer/actualiser les données comptables.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||||
"description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
"description": "Trustee Feature-Instanz-ID"},
|
||||||
{"name": "forceRefresh", "type": "boolean", "required": False, "frontendType": "checkbox",
|
{"name": "forceRefresh", "type": "boolean", "required": False, "frontendType": "checkbox",
|
||||||
"description": {"en": "Force re-import", "de": "Import erzwingen", "fr": "Forcer la réimportation"}, "default": False},
|
"description": "Import erzwingen", "default": False},
|
||||||
{"name": "dateFrom", "type": "string", "required": False, "frontendType": "date",
|
{"name": "dateFrom", "type": "string", "required": False, "frontendType": "date",
|
||||||
"description": {"en": "Start date (YYYY-MM-DD)", "de": "Startdatum", "fr": "Date début"}, "default": ""},
|
"description": "Startdatum", "default": ""},
|
||||||
{"name": "dateTo", "type": "string", "required": False, "frontendType": "date",
|
{"name": "dateTo", "type": "string", "required": False, "frontendType": "date",
|
||||||
"description": {"en": "End date (YYYY-MM-DD)", "de": "Enddatum", "fr": "Date fin"}, "default": ""},
|
"description": "Enddatum", "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -32,22 +28,18 @@ TRUSTEE_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trustee.extractFromFiles",
|
"id": "trustee.extractFromFiles",
|
||||||
"category": "trustee",
|
"category": "trustee",
|
||||||
"label": {"en": "Extract Documents", "de": "Dokumente extrahieren", "fr": "Extraire documents"},
|
"label": "Dokumente extrahieren",
|
||||||
"description": {
|
"description": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren.",
|
||||||
"en": "Extract document type and data from PDF/JPG via AI.",
|
|
||||||
"de": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren.",
|
|
||||||
"fr": "Extraire type et données de PDF/JPG par IA.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "connectionReference", "type": "string", "required": False, "frontendType": "userConnection",
|
{"name": "connectionReference", "type": "string", "required": False, "frontendType": "userConnection",
|
||||||
"description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}, "default": ""},
|
"description": "SharePoint-Verbindung", "default": ""},
|
||||||
{"name": "sharepointFolder", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
{"name": "sharepointFolder", "type": "string", "required": False, "frontendType": "sharepointFolder",
|
||||||
"frontendOptions": {"dependsOn": "connectionReference"},
|
"frontendOptions": {"dependsOn": "connectionReference"},
|
||||||
"description": {"en": "SharePoint folder path", "de": "SharePoint-Ordnerpfad", "fr": "Chemin dossier SharePoint"}, "default": ""},
|
"description": "SharePoint-Ordnerpfad", "default": ""},
|
||||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||||
"description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
"description": "Trustee Feature-Instanz-ID"},
|
||||||
{"name": "prompt", "type": "string", "required": False, "frontendType": "textarea",
|
{"name": "prompt", "type": "string", "required": False, "frontendType": "textarea",
|
||||||
"description": {"en": "AI prompt for extraction", "de": "AI-Prompt für Extraktion", "fr": "Prompt IA"}, "default": ""},
|
"description": "AI-Prompt für Extraktion", "default": ""},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -60,17 +52,13 @@ TRUSTEE_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trustee.processDocuments",
|
"id": "trustee.processDocuments",
|
||||||
"category": "trustee",
|
"category": "trustee",
|
||||||
"label": {"en": "Process Documents", "de": "Dokumente verarbeiten", "fr": "Traiter documents"},
|
"label": "Dokumente verarbeiten",
|
||||||
"description": {
|
"description": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen.",
|
||||||
"en": "Create TrusteeDocument + TrusteePosition from extraction result.",
|
|
||||||
"de": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen.",
|
|
||||||
"fr": "Créer TrusteeDocument + TrusteePosition.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
|
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Reference to extraction result", "de": "Referenz auf Ergebnis", "fr": "Référence au résultat"}},
|
"description": "Referenz auf Ergebnis"},
|
||||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||||
"description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
"description": "Trustee Feature-Instanz-ID"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
@ -83,17 +71,13 @@ TRUSTEE_NODES = [
|
||||||
{
|
{
|
||||||
"id": "trustee.syncToAccounting",
|
"id": "trustee.syncToAccounting",
|
||||||
"category": "trustee",
|
"category": "trustee",
|
||||||
"label": {"en": "Sync to Accounting", "de": "In Buchhaltung synchronisieren", "fr": "Synchroniser comptabilité"},
|
"label": "In Buchhaltung synchronisieren",
|
||||||
"description": {
|
"description": "Trustee-Positionen in Buchhaltungssystem übertragen.",
|
||||||
"en": "Push trustee positions to accounting system.",
|
|
||||||
"de": "Trustee-Positionen in Buchhaltungssystem übertragen.",
|
|
||||||
"fr": "Transférer les positions vers la comptabilité.",
|
|
||||||
},
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
|
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
|
||||||
"description": {"en": "Reference to processed documents", "de": "Referenz auf Ergebnis", "fr": "Référence au résultat"}},
|
"description": "Referenz auf Ergebnis"},
|
||||||
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
|
||||||
"description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
|
"description": "Trustee Feature-Instanz-ID"},
|
||||||
],
|
],
|
||||||
"inputs": 1,
|
"inputs": 1,
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,16 @@ def getNodeTypesForApi(
|
||||||
nodes = getNodeTypes(services, language)
|
nodes = getNodeTypes(services, language)
|
||||||
localized = [_localizeNode(n, language) for n in nodes]
|
localized = [_localizeNode(n, language) for n in nodes]
|
||||||
categories = [
|
categories = [
|
||||||
{"id": "trigger", "label": {"en": "Trigger", "de": "Trigger", "fr": "Déclencheur"}},
|
{"id": "trigger", "label": "Trigger"},
|
||||||
{"id": "input", "label": {"en": "Input/Human", "de": "Eingabe/Mensch", "fr": "Entrée/Humain"}},
|
{"id": "input", "label": "Eingabe/Mensch"},
|
||||||
{"id": "flow", "label": {"en": "Flow", "de": "Ablauf", "fr": "Flux"}},
|
{"id": "flow", "label": "Ablauf"},
|
||||||
{"id": "data", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
|
{"id": "data", "label": "Daten"},
|
||||||
{"id": "ai", "label": {"en": "AI", "de": "KI", "fr": "IA"}},
|
{"id": "ai", "label": "KI"},
|
||||||
{"id": "file", "label": {"en": "File", "de": "Datei", "fr": "Fichier"}},
|
{"id": "file", "label": "Datei"},
|
||||||
{"id": "email", "label": {"en": "Email", "de": "E-Mail", "fr": "Email"}},
|
{"id": "email", "label": "E-Mail"},
|
||||||
{"id": "sharepoint", "label": {"en": "SharePoint", "de": "SharePoint", "fr": "SharePoint"}},
|
{"id": "sharepoint", "label": "SharePoint"},
|
||||||
{"id": "clickup", "label": {"en": "ClickUp", "de": "ClickUp", "fr": "ClickUp"}},
|
{"id": "clickup", "label": "ClickUp"},
|
||||||
{"id": "trustee", "label": {"en": "Trustee", "de": "Treuhand", "fr": "Fiduciaire"}},
|
{"id": "trustee", "label": "Treuhand"},
|
||||||
]
|
]
|
||||||
|
|
||||||
catalogSerialized = {}
|
catalogSerialized = {}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||||
class PortField(BaseModel):
|
class PortField(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
type: str # str, int, bool, List[str], List[Document], Dict[str,Any]
|
type: str # str, int, bool, List[str], List[Document], Dict[str,Any]
|
||||||
description: Dict[str, str] = {} # {en, de, fr}
|
description: str = ""
|
||||||
required: bool = True
|
required: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,97 +57,97 @@ class OutputPortDef(BaseModel):
|
||||||
PORT_TYPE_CATALOG: Dict[str, PortSchema] = {
|
PORT_TYPE_CATALOG: Dict[str, PortSchema] = {
|
||||||
"DocumentList": PortSchema(name="DocumentList", fields=[
|
"DocumentList": PortSchema(name="DocumentList", fields=[
|
||||||
PortField(name="documents", type="List[Document]",
|
PortField(name="documents", type="List[Document]",
|
||||||
description={"en": "List of documents", "de": "Dokumentenliste", "fr": "Liste de documents"}),
|
description="Dokumentenliste"),
|
||||||
]),
|
]),
|
||||||
"FileList": PortSchema(name="FileList", fields=[
|
"FileList": PortSchema(name="FileList", fields=[
|
||||||
PortField(name="files", type="List[File]",
|
PortField(name="files", type="List[File]",
|
||||||
description={"en": "List of files", "de": "Dateiliste", "fr": "Liste de fichiers"}),
|
description="Dateiliste"),
|
||||||
]),
|
]),
|
||||||
"EmailDraft": PortSchema(name="EmailDraft", fields=[
|
"EmailDraft": PortSchema(name="EmailDraft", fields=[
|
||||||
PortField(name="subject", type="str",
|
PortField(name="subject", type="str",
|
||||||
description={"en": "Subject", "de": "Betreff", "fr": "Sujet"}),
|
description="Betreff"),
|
||||||
PortField(name="body", type="str",
|
PortField(name="body", type="str",
|
||||||
description={"en": "Body", "de": "Inhalt", "fr": "Corps"}),
|
description="Inhalt"),
|
||||||
PortField(name="to", type="List[str]",
|
PortField(name="to", type="List[str]",
|
||||||
description={"en": "Recipients", "de": "Empfänger", "fr": "Destinataires"}),
|
description="Empfänger"),
|
||||||
PortField(name="cc", type="List[str]", required=False,
|
PortField(name="cc", type="List[str]", required=False,
|
||||||
description={"en": "CC", "de": "CC", "fr": "CC"}),
|
description="CC"),
|
||||||
PortField(name="attachments", type="List[Document]", required=False,
|
PortField(name="attachments", type="List[Document]", required=False,
|
||||||
description={"en": "Attachments", "de": "Anhänge", "fr": "Pièces jointes"}),
|
description="Anhänge"),
|
||||||
]),
|
]),
|
||||||
"EmailList": PortSchema(name="EmailList", fields=[
|
"EmailList": PortSchema(name="EmailList", fields=[
|
||||||
PortField(name="emails", type="List[Email]",
|
PortField(name="emails", type="List[Email]",
|
||||||
description={"en": "Emails", "de": "E-Mails", "fr": "Emails"}),
|
description="E-Mails"),
|
||||||
]),
|
]),
|
||||||
"TaskList": PortSchema(name="TaskList", fields=[
|
"TaskList": PortSchema(name="TaskList", fields=[
|
||||||
PortField(name="tasks", type="List[Task]",
|
PortField(name="tasks", type="List[Task]",
|
||||||
description={"en": "Tasks", "de": "Aufgaben", "fr": "Tâches"}),
|
description="Aufgaben"),
|
||||||
]),
|
]),
|
||||||
"TaskResult": PortSchema(name="TaskResult", fields=[
|
"TaskResult": PortSchema(name="TaskResult", fields=[
|
||||||
PortField(name="success", type="bool",
|
PortField(name="success", type="bool",
|
||||||
description={"en": "Success", "de": "Erfolg", "fr": "Succès"}),
|
description="Erfolg"),
|
||||||
PortField(name="taskId", type="str",
|
PortField(name="taskId", type="str",
|
||||||
description={"en": "Task ID", "de": "Aufgaben-ID", "fr": "ID tâche"}),
|
description="Aufgaben-ID"),
|
||||||
PortField(name="task", type="Dict",
|
PortField(name="task", type="Dict",
|
||||||
description={"en": "Task data", "de": "Aufgabendaten", "fr": "Données tâche"}),
|
description="Aufgabendaten"),
|
||||||
]),
|
]),
|
||||||
"FormPayload": PortSchema(name="FormPayload", fields=[
|
"FormPayload": PortSchema(name="FormPayload", fields=[
|
||||||
PortField(name="payload", type="Dict[str,Any]",
|
PortField(name="payload", type="Dict[str,Any]",
|
||||||
description={"en": "Form data", "de": "Formulardaten", "fr": "Données formulaire"}),
|
description="Formulardaten"),
|
||||||
]),
|
]),
|
||||||
"AiResult": PortSchema(name="AiResult", fields=[
|
"AiResult": PortSchema(name="AiResult", fields=[
|
||||||
PortField(name="prompt", type="str",
|
PortField(name="prompt", type="str",
|
||||||
description={"en": "Prompt", "de": "Prompt", "fr": "Invite"}),
|
description="Prompt"),
|
||||||
PortField(name="response", type="str",
|
PortField(name="response", type="str",
|
||||||
description={"en": "Response text", "de": "Antworttext", "fr": "Texte réponse"}),
|
description="Antworttext"),
|
||||||
PortField(name="responseData", type="Dict", required=False,
|
PortField(name="responseData", type="Dict", required=False,
|
||||||
description={"en": "Structured response", "de": "Strukturierte Antwort", "fr": "Réponse structurée"}),
|
description="Strukturierte Antwort"),
|
||||||
PortField(name="context", type="str",
|
PortField(name="context", type="str",
|
||||||
description={"en": "Context", "de": "Kontext", "fr": "Contexte"}),
|
description="Kontext"),
|
||||||
PortField(name="documents", type="List[Document]",
|
PortField(name="documents", type="List[Document]",
|
||||||
description={"en": "Documents", "de": "Dokumente", "fr": "Documents"}),
|
description="Dokumente"),
|
||||||
]),
|
]),
|
||||||
"BoolResult": PortSchema(name="BoolResult", fields=[
|
"BoolResult": PortSchema(name="BoolResult", fields=[
|
||||||
PortField(name="result", type="bool",
|
PortField(name="result", type="bool",
|
||||||
description={"en": "Result", "de": "Ergebnis", "fr": "Résultat"}),
|
description="Ergebnis"),
|
||||||
PortField(name="reason", type="str", required=False,
|
PortField(name="reason", type="str", required=False,
|
||||||
description={"en": "Reason", "de": "Begründung", "fr": "Raison"}),
|
description="Begründung"),
|
||||||
]),
|
]),
|
||||||
"TextResult": PortSchema(name="TextResult", fields=[
|
"TextResult": PortSchema(name="TextResult", fields=[
|
||||||
PortField(name="text", type="str",
|
PortField(name="text", type="str",
|
||||||
description={"en": "Text", "de": "Text", "fr": "Texte"}),
|
description="Text"),
|
||||||
]),
|
]),
|
||||||
"LoopItem": PortSchema(name="LoopItem", fields=[
|
"LoopItem": PortSchema(name="LoopItem", fields=[
|
||||||
PortField(name="currentItem", type="Any",
|
PortField(name="currentItem", type="Any",
|
||||||
description={"en": "Current item", "de": "Aktuelles Element", "fr": "Élément courant"}),
|
description="Aktuelles Element"),
|
||||||
PortField(name="currentIndex", type="int",
|
PortField(name="currentIndex", type="int",
|
||||||
description={"en": "Current index", "de": "Aktueller Index", "fr": "Index courant"}),
|
description="Aktueller Index"),
|
||||||
PortField(name="items", type="List[Any]",
|
PortField(name="items", type="List[Any]",
|
||||||
description={"en": "All items", "de": "Alle Elemente", "fr": "Tous les éléments"}),
|
description="Alle Elemente"),
|
||||||
PortField(name="count", type="int",
|
PortField(name="count", type="int",
|
||||||
description={"en": "Total count", "de": "Gesamtanzahl", "fr": "Nombre total"}),
|
description="Gesamtanzahl"),
|
||||||
]),
|
]),
|
||||||
"AggregateResult": PortSchema(name="AggregateResult", fields=[
|
"AggregateResult": PortSchema(name="AggregateResult", fields=[
|
||||||
PortField(name="items", type="List[Any]",
|
PortField(name="items", type="List[Any]",
|
||||||
description={"en": "Collected items", "de": "Gesammelte Elemente", "fr": "Éléments collectés"}),
|
description="Gesammelte Elemente"),
|
||||||
PortField(name="count", type="int",
|
PortField(name="count", type="int",
|
||||||
description={"en": "Count", "de": "Anzahl", "fr": "Nombre"}),
|
description="Anzahl"),
|
||||||
]),
|
]),
|
||||||
"MergeResult": PortSchema(name="MergeResult", fields=[
|
"MergeResult": PortSchema(name="MergeResult", fields=[
|
||||||
PortField(name="inputs", type="Dict[int,Any]",
|
PortField(name="inputs", type="Dict[int,Any]",
|
||||||
description={"en": "Inputs by port", "de": "Eingaben nach Port", "fr": "Entrées par port"}),
|
description="Eingaben nach Port"),
|
||||||
PortField(name="first", type="Any",
|
PortField(name="first", type="Any",
|
||||||
description={"en": "First available", "de": "Erstes verfügbares", "fr": "Premier disponible"}),
|
description="Erstes verfügbares"),
|
||||||
PortField(name="merged", type="Dict",
|
PortField(name="merged", type="Dict",
|
||||||
description={"en": "Merged data", "de": "Zusammengeführte Daten", "fr": "Données fusionnées"}),
|
description="Zusammengeführte Daten"),
|
||||||
]),
|
]),
|
||||||
"ActionResult": PortSchema(name="ActionResult", fields=[
|
"ActionResult": PortSchema(name="ActionResult", fields=[
|
||||||
PortField(name="success", type="bool",
|
PortField(name="success", type="bool",
|
||||||
description={"en": "Success", "de": "Erfolg", "fr": "Succès"}),
|
description="Erfolg"),
|
||||||
PortField(name="error", type="str", required=False,
|
PortField(name="error", type="str", required=False,
|
||||||
description={"en": "Error", "de": "Fehler", "fr": "Erreur"}),
|
description="Fehler"),
|
||||||
PortField(name="data", type="Dict", required=False,
|
PortField(name="data", type="Dict", required=False,
|
||||||
description={"en": "Result data", "de": "Ergebnisdaten", "fr": "Données résultat"}),
|
description="Ergebnisdaten"),
|
||||||
]),
|
]),
|
||||||
"Transit": PortSchema(name="Transit", fields=[]),
|
"Transit": PortSchema(name="Transit", fields=[]),
|
||||||
}
|
}
|
||||||
|
|
@ -479,10 +479,16 @@ def _deriveFormPayloadSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
|
||||||
portFields = []
|
portFields = []
|
||||||
for f in fields_param:
|
for f in fields_param:
|
||||||
if isinstance(f, dict) and f.get("name"):
|
if isinstance(f, dict) and f.get("name"):
|
||||||
|
_lab = f.get("label")
|
||||||
|
_desc = (
|
||||||
|
str(_lab.get("de") or _lab.get("en") or f["name"])
|
||||||
|
if isinstance(_lab, dict)
|
||||||
|
else str(_lab if _lab is not None else f["name"])
|
||||||
|
)
|
||||||
portFields.append(PortField(
|
portFields.append(PortField(
|
||||||
name=f["name"],
|
name=f["name"],
|
||||||
type=f.get("type", "str"),
|
type=f.get("type", "str"),
|
||||||
description=f.get("label", {}) if isinstance(f.get("label"), dict) else {"en": str(f.get("label", f["name"]))},
|
description=_desc,
|
||||||
required=f.get("required", False),
|
required=f.get("required", False),
|
||||||
))
|
))
|
||||||
return PortSchema(name="FormPayload_dynamic", fields=portFields) if portFields else None
|
return PortSchema(name="FormPayload_dynamic", fields=portFields) if portFields else None
|
||||||
|
|
@ -499,6 +505,6 @@ def _deriveTransformSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
|
||||||
portFields.append(PortField(
|
portFields.append(PortField(
|
||||||
name=m["outputField"],
|
name=m["outputField"],
|
||||||
type=m.get("type", "str"),
|
type=m.get("type", "str"),
|
||||||
description={"en": m.get("label", m["outputField"])},
|
description=str(m.get("label", m["outputField"])),
|
||||||
))
|
))
|
||||||
return PortSchema(name="Transform_dynamic", fields=portFields) if portFields else None
|
return PortSchema(name="Transform_dynamic", fields=portFields) if portFields else None
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ from modules.workflows.automation2.runEnvelope import (
|
||||||
normalize_run_envelope,
|
normalize_run_envelope,
|
||||||
)
|
)
|
||||||
from modules.features.graphicalEditor.entryPoints import find_invocation
|
from modules.features.graphicalEditor.entryPoints import find_invocation
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureGraphicalEditor")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -48,13 +50,13 @@ def _build_execute_run_envelope(
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="entryPointId requires a saved workflow (workflowId must refer to a stored workflow)",
|
detail=routeApiMsg("entryPointId requires a saved workflow (workflowId must refer to a stored workflow)"),
|
||||||
)
|
)
|
||||||
inv = find_invocation(workflow, entry_point_id)
|
inv = find_invocation(workflow, entry_point_id)
|
||||||
if not inv:
|
if not inv:
|
||||||
raise HTTPException(status_code=400, detail="entryPointId not found on workflow")
|
raise HTTPException(status_code=400, detail=routeApiMsg("entryPointId not found on workflow"))
|
||||||
if not inv.get("enabled", True):
|
if not inv.get("enabled", True):
|
||||||
raise HTTPException(status_code=400, detail="entry point is disabled")
|
raise HTTPException(status_code=400, detail=routeApiMsg("entry point is disabled"))
|
||||||
kind = inv.get("kind", "manual")
|
kind = inv.get("kind", "manual")
|
||||||
trig_map = {
|
trig_map = {
|
||||||
"manual": "manual",
|
"manual": "manual",
|
||||||
|
|
@ -107,7 +109,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||||
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
|
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
|
||||||
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
|
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
|
||||||
if not featureAccess or not featureAccess.enabled:
|
if not featureAccess or not featureAccess.enabled:
|
||||||
raise HTTPException(status_code=403, detail="Access denied to this feature instance")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied to this feature instance"))
|
||||||
return str(instance.mandateId) if instance.mandateId else ""
|
return str(instance.mandateId) if instance.mandateId else ""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -327,7 +329,7 @@ def create_draft_version(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
version = iface.createDraftVersion(workflowId)
|
version = iface.createDraftVersion(workflowId)
|
||||||
if not version:
|
if not version:
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -345,7 +347,7 @@ def publish_version(
|
||||||
userId = str(context.user.id) if context.user else None
|
userId = str(context.user.id) if context.user else None
|
||||||
version = iface.publishVersion(versionId, userId=userId)
|
version = iface.publishVersion(versionId, userId=userId)
|
||||||
if not version:
|
if not version:
|
||||||
raise HTTPException(status_code=400, detail="Version not found or not in draft status")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Version not found or not in draft status"))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -362,7 +364,7 @@ def unpublish_version(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
version = iface.unpublishVersion(versionId)
|
version = iface.unpublishVersion(versionId)
|
||||||
if not version:
|
if not version:
|
||||||
raise HTTPException(status_code=400, detail="Version not found or not published")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Version not found or not published"))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -379,7 +381,7 @@ def archive_version(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
version = iface.archiveVersion(versionId)
|
version = iface.archiveVersion(versionId)
|
||||||
if not version:
|
if not version:
|
||||||
raise HTTPException(status_code=404, detail="Version not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Version not found"))
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -442,11 +444,11 @@ def create_template_from_workflow(
|
||||||
workflowId = body.get("workflowId")
|
workflowId = body.get("workflowId")
|
||||||
scope = body.get("scope", "user")
|
scope = body.get("scope", "user")
|
||||||
if not workflowId:
|
if not workflowId:
|
||||||
raise HTTPException(status_code=400, detail="workflowId required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("workflowId required"))
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
template = iface.createTemplateFromWorkflow(workflowId, scope=scope)
|
template = iface.createTemplateFromWorkflow(workflowId, scope=scope)
|
||||||
if not template:
|
if not template:
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -463,7 +465,7 @@ def copy_template(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
workflow = iface.copyTemplateToUser(templateId)
|
workflow = iface.copyTemplateToUser(templateId)
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise HTTPException(status_code=404, detail="Template not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Template not found"))
|
||||||
return workflow
|
return workflow
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -480,11 +482,11 @@ def share_template(
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
scope = body.get("scope")
|
scope = body.get("scope")
|
||||||
if not scope or scope not in ("user", "instance", "mandate", "system"):
|
if not scope or scope not in ("user", "instance", "mandate", "system"):
|
||||||
raise HTTPException(status_code=400, detail="scope must be user, instance, mandate, or system")
|
raise HTTPException(status_code=400, detail=routeApiMsg("scope must be user, instance, mandate, or system"))
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
template = iface.shareTemplate(templateId, scope=scope)
|
template = iface.shareTemplate(templateId, scope=scope)
|
||||||
if not template:
|
if not template:
|
||||||
raise HTTPException(status_code=404, detail="Template not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Template not found"))
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -506,12 +508,12 @@ async def post_editor_chat(
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
message = body.get("message", "")
|
message = body.get("message", "")
|
||||||
if not message:
|
if not message:
|
||||||
raise HTTPException(status_code=400, detail="message required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("message required"))
|
||||||
|
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
wf = iface.getWorkflow(workflowId)
|
wf = iface.getWorkflow(workflowId)
|
||||||
if not wf:
|
if not wf:
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
|
|
||||||
userLanguage = body.get("userLanguage", "de")
|
userLanguage = body.get("userLanguage", "de")
|
||||||
conversationHistory = body.get("conversationHistory") or []
|
conversationHistory = body.get("conversationHistory") or []
|
||||||
|
|
@ -946,7 +948,7 @@ def get_workflow(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
wf = iface.getWorkflow(workflowId)
|
wf = iface.getWorkflow(workflowId)
|
||||||
if not wf:
|
if not wf:
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
return wf
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -979,7 +981,7 @@ def update_workflow(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
updated = iface.updateWorkflow(workflowId, body)
|
updated = iface.updateWorkflow(workflowId, body)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -995,7 +997,7 @@ def delete_workflow(
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
if not iface.deleteWorkflow(workflowId):
|
if not iface.deleteWorkflow(workflowId):
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1015,20 +1017,20 @@ async def post_workflow_webhook(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
wf = iface.getWorkflow(workflowId)
|
wf = iface.getWorkflow(workflowId)
|
||||||
if not wf or not wf.get("graph"):
|
if not wf or not wf.get("graph"):
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
inv = find_invocation(wf, entryPointId)
|
inv = find_invocation(wf, entryPointId)
|
||||||
if not inv:
|
if not inv:
|
||||||
raise HTTPException(status_code=404, detail="Entry point not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Entry point not found"))
|
||||||
if inv.get("kind") != "webhook":
|
if inv.get("kind") != "webhook":
|
||||||
raise HTTPException(status_code=400, detail="Entry point is not a webhook")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is not a webhook"))
|
||||||
if not inv.get("enabled", True):
|
if not inv.get("enabled", True):
|
||||||
raise HTTPException(status_code=400, detail="Entry point is disabled")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is disabled"))
|
||||||
cfg = inv.get("config") or {}
|
cfg = inv.get("config") or {}
|
||||||
secret = cfg.get("webhookSecret")
|
secret = cfg.get("webhookSecret")
|
||||||
if secret:
|
if secret:
|
||||||
hdr = request.headers.get("X-Webhook-Secret")
|
hdr = request.headers.get("X-Webhook-Secret")
|
||||||
if hdr != str(secret):
|
if hdr != str(secret):
|
||||||
raise HTTPException(status_code=403, detail="Invalid webhook secret")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Invalid webhook secret"))
|
||||||
|
|
||||||
services = getGraphicalEditorServices(
|
services = getGraphicalEditorServices(
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -1083,14 +1085,14 @@ async def post_workflow_form_submit(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
wf = iface.getWorkflow(workflowId)
|
wf = iface.getWorkflow(workflowId)
|
||||||
if not wf or not wf.get("graph"):
|
if not wf or not wf.get("graph"):
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
inv = find_invocation(wf, entryPointId)
|
inv = find_invocation(wf, entryPointId)
|
||||||
if not inv:
|
if not inv:
|
||||||
raise HTTPException(status_code=404, detail="Entry point not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Entry point not found"))
|
||||||
if inv.get("kind") != "form":
|
if inv.get("kind") != "form":
|
||||||
raise HTTPException(status_code=400, detail="Entry point is not a form")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is not a form"))
|
||||||
if not inv.get("enabled", True):
|
if not inv.get("enabled", True):
|
||||||
raise HTTPException(status_code=400, detail="Entry point is disabled")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is disabled"))
|
||||||
|
|
||||||
services = getGraphicalEditorServices(
|
services = getGraphicalEditorServices(
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -1161,7 +1163,7 @@ def get_workflow_runs(
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
if not iface.getWorkflow(workflowId):
|
if not iface.getWorkflow(workflowId):
|
||||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
|
||||||
runs = iface.getRunsByWorkflow(workflowId)
|
runs = iface.getRunsByWorkflow(workflowId)
|
||||||
return {"runs": runs}
|
return {"runs": runs}
|
||||||
|
|
||||||
|
|
@ -1200,16 +1202,16 @@ async def resume_run(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
run = iface.getRun(runId)
|
run = iface.getRun(runId)
|
||||||
if not run:
|
if not run:
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||||
taskId = body.get("taskId")
|
taskId = body.get("taskId")
|
||||||
result = body.get("result")
|
result = body.get("result")
|
||||||
if not taskId or result is None:
|
if not taskId or result is None:
|
||||||
raise HTTPException(status_code=400, detail="taskId and result required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("taskId and result required"))
|
||||||
task = iface.getTask(taskId)
|
task = iface.getTask(taskId)
|
||||||
if not task or task.get("runId") != runId:
|
if not task or task.get("runId") != runId:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
|
||||||
if task.get("status") != "pending":
|
if task.get("status") != "pending":
|
||||||
raise HTTPException(status_code=400, detail="Task already completed")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Task already completed"))
|
||||||
iface.updateTask(taskId, status="completed", result=result)
|
iface.updateTask(taskId, status="completed", result=result)
|
||||||
nodeId = task.get("nodeId")
|
nodeId = task.get("nodeId")
|
||||||
nodeOutputs = dict(run.get("nodeOutputs") or {})
|
nodeOutputs = dict(run.get("nodeOutputs") or {})
|
||||||
|
|
@ -1217,7 +1219,7 @@ async def resume_run(
|
||||||
workflowId = run.get("workflowId")
|
workflowId = run.get("workflowId")
|
||||||
wf = iface.getWorkflow(workflowId) if workflowId else None
|
wf = iface.getWorkflow(workflowId) if workflowId else None
|
||||||
if not wf or not wf.get("graph"):
|
if not wf or not wf.get("graph"):
|
||||||
raise HTTPException(status_code=400, detail="Workflow graph not found")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Workflow graph not found"))
|
||||||
graph = wf["graph"]
|
graph = wf["graph"]
|
||||||
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
resume_result = await executeGraph(
|
resume_result = await executeGraph(
|
||||||
|
|
@ -1280,16 +1282,16 @@ async def complete_task(
|
||||||
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
|
||||||
task = iface.getTask(taskId)
|
task = iface.getTask(taskId)
|
||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
|
||||||
runId = task.get("runId")
|
runId = task.get("runId")
|
||||||
result = body.get("result")
|
result = body.get("result")
|
||||||
if result is None:
|
if result is None:
|
||||||
raise HTTPException(status_code=400, detail="result required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("result required"))
|
||||||
run = iface.getRun(runId)
|
run = iface.getRun(runId)
|
||||||
if not run:
|
if not run:
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||||
if task.get("status") != "pending":
|
if task.get("status") != "pending":
|
||||||
raise HTTPException(status_code=400, detail="Task already completed")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Task already completed"))
|
||||||
iface.updateTask(taskId, status="completed", result=result)
|
iface.updateTask(taskId, status="completed", result=result)
|
||||||
nodeId = task.get("nodeId")
|
nodeId = task.get("nodeId")
|
||||||
nodeOutputs = dict(run.get("nodeOutputs") or {})
|
nodeOutputs = dict(run.get("nodeOutputs") or {})
|
||||||
|
|
@ -1297,7 +1299,7 @@ async def complete_task(
|
||||||
workflowId = run.get("workflowId")
|
workflowId = run.get("workflowId")
|
||||||
wf = iface.getWorkflow(workflowId) if workflowId else None
|
wf = iface.getWorkflow(workflowId) if workflowId else None
|
||||||
if not wf or not wf.get("graph"):
|
if not wf or not wf.get("graph"):
|
||||||
raise HTTPException(status_code=400, detail="Workflow graph not found")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Workflow graph not found"))
|
||||||
graph = wf["graph"]
|
graph = wf["graph"]
|
||||||
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
return await executeGraph(
|
return await executeGraph(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
|
|
||||||
|
|
||||||
class DataScope(str, Enum):
|
class DataScope(str, Enum):
|
||||||
|
|
@ -17,83 +17,128 @@ class DataScope(str, Enum):
|
||||||
GLOBAL = "global"
|
GLOBAL = "global"
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Daten-Neutralisierung Konfiguration")
|
||||||
class DataNeutraliserConfig(PowerOnModel):
|
class DataNeutraliserConfig(PowerOnModel):
|
||||||
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})
|
"""Konfiguration fuer die Daten-Neutralisierung."""
|
||||||
mandateId: str = Field(description="ID of the mandate this configuration belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
id: str = Field(
|
||||||
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})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
userId: str = Field(description="ID of the user who created this configuration", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
description="Unique ID of the configuration",
|
||||||
enabled: bool = Field(default=True, description="Whether data neutralization is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False})
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
scope: str = Field(default="personal", description="Data visibility scope: personal, featureInstance, mandate, global", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
)
|
||||||
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
mandateId: str = Field(
|
||||||
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
description="ID of the mandate this configuration belongs to",
|
||||||
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
)
|
||||||
]})
|
featureInstanceId: str = Field(
|
||||||
neutralizationStatus: str = Field(default="not_required", description="Status of neutralization: pending, completed, failed, not_required", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
|
description="ID of the feature instance this configuration belongs to",
|
||||||
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})
|
json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
sharepointSourcePath: str = Field(default="", description="SharePoint path to read files for neutralization", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
)
|
||||||
sharepointTargetPath: str = Field(default="", description="SharePoint path to store neutralized files", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
|
userId: str = Field(
|
||||||
registerModelLabels(
|
description="ID of the user who created this configuration",
|
||||||
"DataNeutraliserConfig",
|
json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
{"en": "Data Neutralization Config", "fr": "Configuration de neutralisation des données"},
|
)
|
||||||
{
|
enabled: bool = Field(
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
default=True,
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
description="Whether data neutralization is enabled",
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
)
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
scope: str = Field(
|
||||||
"scope": {"en": "Scope", "fr": "Portée"},
|
default="personal",
|
||||||
"neutralizationStatus": {"en": "Neutralization Status", "fr": "Statut de neutralisation"},
|
description="Data visibility scope: personal, featureInstance, mandate, global",
|
||||||
"namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
|
json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
|
||||||
"sharepointSourcePath": {"en": "Source Path", "fr": "Chemin source"},
|
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
|
||||||
"sharepointTargetPath": {"en": "Target Path", "fr": "Chemin cible"},
|
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
|
||||||
},
|
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
|
||||||
)
|
{"value": "global", "label": {"en": "Global", "de": "Global"}},
|
||||||
|
]},
|
||||||
|
)
|
||||||
|
neutralizationStatus: str = Field(
|
||||||
|
default="not_required",
|
||||||
|
description="Status of neutralization: pending, completed, failed, not_required",
|
||||||
|
json_schema_extra={"label": "Neutralisierungsstatus", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
namesToParse: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Multiline list of names to parse for neutralization",
|
||||||
|
json_schema_extra={"label": "Zu parsende Namen", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
sharepointSourcePath: str = Field(
|
||||||
|
default="",
|
||||||
|
description="SharePoint path to read files for neutralization",
|
||||||
|
json_schema_extra={"label": "SharePoint Quellpfad", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
sharepointTargetPath: str = Field(
|
||||||
|
default="",
|
||||||
|
description="SharePoint path to store neutralized files",
|
||||||
|
json_schema_extra={"label": "SharePoint Zielpfad", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Neutralisiertes Datenattribut")
|
||||||
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})
|
"""Zuordnung Originaltext zu Platzhalter fuer neutralisierte Daten."""
|
||||||
mandateId: str = Field(description="ID of the mandate this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
id: str = Field(
|
||||||
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})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
userId: str = Field(description="ID of the user who created this attribute", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
description="Unique ID of the attribute mapping (used as UID in neutralized files)",
|
||||||
originalText: str = Field(description="Original text that was neutralized", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
json_schema_extra={"label": "ID", "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})
|
)
|
||||||
patternType: str = Field(description="Type of pattern that matched (email, phone, name, etc.)", 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={"label": "Mandanten-ID", "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={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
description="ID of the user who created this attribute",
|
||||||
|
json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
|
)
|
||||||
|
originalText: str = Field(
|
||||||
|
description="Original text that was neutralized",
|
||||||
|
json_schema_extra={"label": "Originaltext", "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={"label": "Datei-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
|
)
|
||||||
|
patternType: str = Field(
|
||||||
|
description="Type of pattern that matched (email, phone, name, etc.)",
|
||||||
|
json_schema_extra={"label": "Mustertyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Neutralisierungs-Snapshot")
|
||||||
class DataNeutralizationSnapshot(BaseModel):
|
class DataNeutralizationSnapshot(BaseModel):
|
||||||
"""Stores the full neutralized text (with embedded placeholders) per source."""
|
"""Speichert den vollstaendigen neutralisierten Text (mit Platzhaltern) pro Quelle."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(
|
||||||
mandateId: str = Field(description="Mandate scope")
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
featureInstanceId: str = Field(default="", description="Feature instance scope")
|
json_schema_extra={"label": "ID"},
|
||||||
userId: str = Field(description="User who triggered neutralization")
|
)
|
||||||
sourceLabel: str = Field(description="Human label, e.g. 'Prompt', 'Kontext', 'Nachricht 3'")
|
mandateId: str = Field(
|
||||||
neutralizedText: str = Field(description="Full text with [type.uuid] placeholders embedded")
|
description="Mandate scope",
|
||||||
placeholderCount: int = Field(default=0, description="Number of placeholders in the text")
|
json_schema_extra={"label": "Mandanten-ID"},
|
||||||
registerModelLabels(
|
)
|
||||||
"DataNeutralizerAttributes",
|
featureInstanceId: str = Field(
|
||||||
{"en": "Neutralized Data Attribute", "fr": "Attribut de données neutralisées"},
|
default="",
|
||||||
{
|
description="Feature instance scope",
|
||||||
"id": {"en": "ID", "fr": "ID"},
|
json_schema_extra={"label": "Feature-Instanz-ID"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
)
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
|
userId: str = Field(
|
||||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
description="User who triggered neutralization",
|
||||||
"originalText": {"en": "Original Text", "fr": "Texte original"},
|
json_schema_extra={"label": "Benutzer-ID"},
|
||||||
"fileId": {"en": "File ID", "fr": "ID de fichier"},
|
)
|
||||||
"patternType": {"en": "Pattern Type", "fr": "Type de modèle"},
|
sourceLabel: str = Field(
|
||||||
},
|
description="Human label, e.g. 'Prompt', 'Kontext', 'Nachricht 3'",
|
||||||
)
|
json_schema_extra={"label": "Quelle"},
|
||||||
registerModelLabels(
|
)
|
||||||
"DataNeutralizationSnapshot",
|
neutralizedText: str = Field(
|
||||||
{"en": "Neutralization Snapshot", "de": "Neutralisierungs-Snapshot"},
|
description="Full text with [type.uuid] placeholders embedded",
|
||||||
{
|
json_schema_extra={"label": "Neutralisierter Text"},
|
||||||
"id": {"en": "ID"},
|
)
|
||||||
"mandateId": {"en": "Mandate ID"},
|
placeholderCount: int = Field(
|
||||||
"featureInstanceId": {"en": "Feature Instance ID"},
|
default=0,
|
||||||
"userId": {"en": "User ID"},
|
description="Number of placeholders in the text",
|
||||||
"sourceLabel": {"en": "Source", "de": "Quelle"},
|
json_schema_extra={"label": "Platzhalter"},
|
||||||
"neutralizedText": {"en": "Neutralized Text", "de": "Neutralisierter Text"},
|
)
|
||||||
"placeholderCount": {"en": "Placeholders", "de": "Platzhalter"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Feature metadata
|
# Feature metadata
|
||||||
FEATURE_CODE = "neutralization"
|
FEATURE_CODE = "neutralization"
|
||||||
FEATURE_LABEL = {"en": "Neutralization", "de": "Neutralisierung", "fr": "Neutralisation"}
|
FEATURE_LABEL = "Neutralisierung"
|
||||||
FEATURE_ICON = "mdi-shield-check"
|
FEATURE_ICON = "mdi-shield-check"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog
|
# UI Objects for RBAC catalog
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.neutralization.playground",
|
"objectKey": "ui.feature.neutralization.playground",
|
||||||
"label": {"en": "Playground", "de": "Spielwiese", "fr": "Bac à sable"},
|
"label": "Spielwiese",
|
||||||
"meta": {"area": "playground"}
|
"meta": {"area": "playground"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -28,17 +28,17 @@ UI_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.neutralization.process.text",
|
"objectKey": "resource.feature.neutralization.process.text",
|
||||||
"label": {"en": "Process Text", "de": "Text verarbeiten", "fr": "Traiter texte"},
|
"label": "Text verarbeiten",
|
||||||
"meta": {"endpoint": "/api/neutralization/process/text", "method": "POST"}
|
"meta": {"endpoint": "/api/neutralization/process/text", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.neutralization.process.files",
|
"objectKey": "resource.feature.neutralization.process.files",
|
||||||
"label": {"en": "Process Files", "de": "Dateien verarbeiten", "fr": "Traiter fichiers"},
|
"label": "Dateien verarbeiten",
|
||||||
"meta": {"endpoint": "/api/neutralization/process/files", "method": "POST"}
|
"meta": {"endpoint": "/api/neutralization/process/files", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.neutralization.config.update",
|
"objectKey": "resource.feature.neutralization.config.update",
|
||||||
"label": {"en": "Update Config", "de": "Konfiguration aktualisieren", "fr": "Mettre à jour config"},
|
"label": "Konfiguration aktualisieren",
|
||||||
"meta": {"endpoint": "/api/neutralization/config", "method": "PUT"}
|
"meta": {"endpoint": "/api/neutralization/config", "method": "PUT"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -47,11 +47,7 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "neutralization-viewer",
|
"roleLabel": "neutralization-viewer",
|
||||||
"description": {
|
"description": "Neutralisierungs-Betrachter - Neutralisierungsdaten einsehen (nur lesen)",
|
||||||
"en": "Neutralization Viewer - View neutralization data (read-only)",
|
|
||||||
"de": "Neutralisierungs-Betrachter - Neutralisierungsdaten einsehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur neutralisation - Consulter les données de neutralisation (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
||||||
|
|
@ -59,11 +55,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "neutralization-user",
|
"roleLabel": "neutralization-user",
|
||||||
"description": {
|
"description": "Neutralisierungs-Benutzer - Neutralisierungstools nutzen und eigene Daten verwalten",
|
||||||
"en": "Neutralization User - Use neutralization tools and manage own data",
|
|
||||||
"de": "Neutralisierungs-Benutzer - Neutralisierungstools nutzen und eigene Daten verwalten",
|
|
||||||
"fr": "Utilisateur neutralisation - Utiliser les outils et gérer ses propres données",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
|
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
|
||||||
|
|
@ -72,11 +64,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "neutralization-admin",
|
"roleLabel": "neutralization-admin",
|
||||||
"description": {
|
"description": "Neutralisierungs-Administrator - Vollzugriff auf Neutralisierungs-Einstellungen und Daten",
|
||||||
"en": "Neutralization Administrator - Full access to neutralization settings and data",
|
|
||||||
"de": "Neutralisierungs-Administrator - Vollzugriff auf Neutralisierungs-Einstellungen und Daten",
|
|
||||||
"fr": "Administrateur neutralisation - Accès complet aux paramètres et données",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
||||||
|
|
@ -84,11 +72,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "neutralization-analyst",
|
"roleLabel": "neutralization-analyst",
|
||||||
"description": {
|
"description": "Neutralisierungs-Analyst - Neutralisierungsdaten analysieren und verarbeiten",
|
||||||
"en": "Neutralization Analyst - Analyze and process neutralization data",
|
|
||||||
"de": "Neutralisierungs-Analyst - Neutralisierungsdaten analysieren und verarbeiten",
|
|
||||||
"fr": "Analyste neutralisation - Analyser et traiter les données de neutralisation",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
|
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
|
||||||
|
|
@ -163,7 +147,8 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
|
|
@ -180,7 +165,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
# Import interfaces
|
# Import interfaces
|
||||||
from .datamodelFeatureNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes, DataNeutralizationSnapshot
|
from .datamodelFeatureNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes, DataNeutralizationSnapshot
|
||||||
from .neutralizePlayground import NeutralizationPlayground
|
from .neutralizePlayground import NeutralizationPlayground
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureNeutralizer")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -22,7 +24,7 @@ def _assertFeatureInstancePathMatchesContext(featureInstanceIdFromPath: str, con
|
||||||
if ctxId and pathId and pathId != ctxId:
|
if ctxId and pathId and pathId != ctxId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Feature instance id in URL does not match request context (X-Instance-Id)",
|
detail=routeApiMsg("Feature instance id in URL does not match request context (X-Instance-Id)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,13 +125,13 @@ async def neutralize_file(
|
||||||
if not file.filename or not file.filename.strip():
|
if not file.filename or not file.filename.strip():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="File name is required"
|
detail=routeApiMsg("File name is required")
|
||||||
)
|
)
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
if not content:
|
if not content:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="File is empty"
|
detail=routeApiMsg("File is empty")
|
||||||
)
|
)
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -164,7 +166,7 @@ def neutralize_text(
|
||||||
if not text:
|
if not text:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Text content is required"
|
detail=routeApiMsg("Text content is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
|
|
@ -199,7 +201,7 @@ def resolve_text(
|
||||||
if not text:
|
if not text:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Text content is required"
|
detail=routeApiMsg("Text content is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
|
|
@ -320,7 +322,7 @@ async def process_sharepoint_files(
|
||||||
if not source_path or not target_path:
|
if not source_path or not target_path:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Both source and target paths are required"
|
detail=routeApiMsg("Both source and target paths are required")
|
||||||
)
|
)
|
||||||
|
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
|
|
@ -353,7 +355,7 @@ def batch_process_files(
|
||||||
if not files_data:
|
if not files_data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Files data is required"
|
detail=routeApiMsg("Files data is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
|
|
@ -453,7 +455,7 @@ def _retriggerNeutralizationBody(context: RequestContext, fileId: str) -> Dict[s
|
||||||
if not fileId:
|
if not fileId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="fileId is required",
|
detail=routeApiMsg("fileId is required"),
|
||||||
)
|
)
|
||||||
service = NeutralizationPlayground(
|
service = NeutralizationPlayground(
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -521,7 +523,7 @@ def cleanup_file_attributes(
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to cleanup file attributes"
|
detail=routeApiMsg("Failed to cleanup file attributes")
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from modules.features.neutralization.interfaceFeatureNeutralizer import Interfac
|
||||||
# Import all necessary classes and functions for neutralization
|
# Import all necessary classes and functions for neutralization
|
||||||
from .subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute
|
from .subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute
|
||||||
from .subProcessText import TextProcessor, PlainText
|
from .subProcessText import TextProcessor, PlainText
|
||||||
from .subProcessList import ListProcessor, TableData
|
from .subProcessList import ListProcessor, NeutralizationTableData
|
||||||
from .subProcessBinary import BinaryProcessor
|
from .subProcessBinary import BinaryProcessor
|
||||||
from .subProcessPdfInPlace import neutralize_pdf_in_place
|
from .subProcessPdfInPlace import neutralize_pdf_in_place
|
||||||
from .subPatterns import HeaderPatterns, DataPatterns, TextTablePatterns
|
from .subPatterns import HeaderPatterns, DataPatterns, TextTablePatterns
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from .subParseString import StringParser
|
||||||
from .subPatterns import getPatternForHeader, HeaderPatterns
|
from .subPatterns import getPatternForHeader, HeaderPatterns
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TableData:
|
class NeutralizationTableData:
|
||||||
"""Repräsentiert Tabellendaten"""
|
"""Repräsentiert Tabellendaten"""
|
||||||
headers: List[str]
|
headers: List[str]
|
||||||
rows: List[List[str]]
|
rows: List[List[str]]
|
||||||
|
|
@ -34,17 +34,17 @@ class ListProcessor:
|
||||||
self.string_parser = StringParser(NamesToParse)
|
self.string_parser = StringParser(NamesToParse)
|
||||||
self.header_patterns = HeaderPatterns.patterns
|
self.header_patterns = HeaderPatterns.patterns
|
||||||
|
|
||||||
def _anonymizeTable(self, table: TableData) -> TableData:
|
def _anonymizeTable(self, table: NeutralizationTableData) -> NeutralizationTableData:
|
||||||
"""
|
"""
|
||||||
Anonymize table data based on headers
|
Anonymize table data based on headers
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
table: TableData object to anonymize
|
table: NeutralizationTableData object to anonymize
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TableData: Anonymized table
|
NeutralizationTableData: Anonymized table
|
||||||
"""
|
"""
|
||||||
anonymizedTable = TableData(
|
anonymizedTable = NeutralizationTableData(
|
||||||
headers=table.headers.copy(),
|
headers=table.headers.copy(),
|
||||||
rows=[row.copy() for row in table.rows],
|
rows=[row.copy() for row in table.rows],
|
||||||
source_type=table.source_type
|
source_type=table.source_type
|
||||||
|
|
@ -76,7 +76,7 @@ class ListProcessor:
|
||||||
Tuple of (processed_data, mapping, replaced_fields, processed_info)
|
Tuple of (processed_data, mapping, replaced_fields, processed_info)
|
||||||
"""
|
"""
|
||||||
df = pd.read_csv(StringIO(content), encoding='utf-8')
|
df = pd.read_csv(StringIO(content), encoding='utf-8')
|
||||||
table = TableData(
|
table = NeutralizationTableData(
|
||||||
headers=df.columns.tolist(),
|
headers=df.columns.tolist(),
|
||||||
rows=df.values.tolist(),
|
rows=df.values.tolist(),
|
||||||
source_type='csv'
|
source_type='csv'
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from typing import List, Dict, Any, Optional, ForwardRef
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
@ -109,6 +109,7 @@ class GeoPolylinie(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Dokument")
|
||||||
class Dokument(BaseModel):
|
class Dokument(BaseModel):
|
||||||
"""Supporting data object for file and URL management with versioning."""
|
"""Supporting data object for file and URL management with versioning."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
|
|
@ -117,24 +118,28 @@ class Dokument(BaseModel):
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="ID",
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="ID of the mandate this document belongs to",
|
description="ID of the mandate this document belongs to",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Mandats-ID",
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance this document belongs to",
|
description="ID of the feature instance this document belongs to",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Feature-Instanz-ID",
|
||||||
)
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Document label",
|
description="Document label",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=False,
|
frontend_readonly=False,
|
||||||
frontend_required=True,
|
frontend_required=True,
|
||||||
|
label="Bezeichnung",
|
||||||
)
|
)
|
||||||
versionsbezeichnung: Optional[str] = Field(
|
versionsbezeichnung: Optional[str] = Field(
|
||||||
None,
|
None,
|
||||||
|
|
@ -369,6 +374,7 @@ class Gemeinde(BaseModel):
|
||||||
ParzelleRef = ForwardRef('Parzelle')
|
ParzelleRef = ForwardRef('Parzelle')
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Parzelle")
|
||||||
class Parzelle(PowerOnModel):
|
class Parzelle(PowerOnModel):
|
||||||
"""Represents a plot with all building law properties."""
|
"""Represents a plot with all building law properties."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
|
|
@ -377,18 +383,21 @@ class Parzelle(PowerOnModel):
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="ID",
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="ID of the mandate",
|
description="ID of the mandate",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Mandats-ID",
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance",
|
description="ID of the feature instance",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Feature-Instanz-ID",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Grunddaten
|
# Grunddaten
|
||||||
|
|
@ -397,6 +406,7 @@ class Parzelle(PowerOnModel):
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=False,
|
frontend_readonly=False,
|
||||||
frontend_required=True,
|
frontend_required=True,
|
||||||
|
label="Bezeichnung",
|
||||||
)
|
)
|
||||||
parzellenAliasTags: List[str] = Field(
|
parzellenAliasTags: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
|
|
@ -595,6 +605,7 @@ class Parzelle(PowerOnModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Projekt")
|
||||||
class Projekt(PowerOnModel):
|
class Projekt(PowerOnModel):
|
||||||
"""Core object representing a construction project."""
|
"""Core object representing a construction project."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
|
|
@ -603,24 +614,28 @@ class Projekt(PowerOnModel):
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="ID",
|
||||||
)
|
)
|
||||||
mandateId: str = Field(
|
mandateId: str = Field(
|
||||||
description="ID of the mandate",
|
description="ID of the mandate",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Mandats-ID",
|
||||||
)
|
)
|
||||||
featureInstanceId: str = Field(
|
featureInstanceId: str = Field(
|
||||||
description="ID of the feature instance",
|
description="ID of the feature instance",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=True,
|
frontend_readonly=True,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Feature-Instanz-ID",
|
||||||
)
|
)
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Project designation",
|
description="Project designation",
|
||||||
frontend_type="text",
|
frontend_type="text",
|
||||||
frontend_readonly=False,
|
frontend_readonly=False,
|
||||||
frontend_required=True,
|
frontend_required=True,
|
||||||
|
label="Bezeichnung",
|
||||||
)
|
)
|
||||||
statusProzess: Optional[StatusProzess] = Field(
|
statusProzess: Optional[StatusProzess] = Field(
|
||||||
None,
|
None,
|
||||||
|
|
@ -628,6 +643,7 @@ class Projekt(PowerOnModel):
|
||||||
frontend_type="select",
|
frontend_type="select",
|
||||||
frontend_readonly=False,
|
frontend_readonly=False,
|
||||||
frontend_required=False,
|
frontend_required=False,
|
||||||
|
label="Prozessstatus",
|
||||||
)
|
)
|
||||||
perimeter: Optional[GeoPolylinie] = Field(
|
perimeter: Optional[GeoPolylinie] = Field(
|
||||||
None,
|
None,
|
||||||
|
|
@ -670,39 +686,3 @@ class Projekt(PowerOnModel):
|
||||||
Parzelle.model_rebuild()
|
Parzelle.model_rebuild()
|
||||||
Projekt.model_rebuild()
|
Projekt.model_rebuild()
|
||||||
|
|
||||||
|
|
||||||
# Register labels for frontend
|
|
||||||
registerModelLabels(
|
|
||||||
"Projekt",
|
|
||||||
{"en": "Project", "fr": "Projet", "de": "Projekt"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"Parzelle",
|
|
||||||
{"en": "Plot", "fr": "Parcelle", "de": "Parzelle"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"Dokument",
|
|
||||||
{"en": "Document", "fr": "Document", "de": "Dokument"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ import logging
|
||||||
|
|
||||||
# Feature metadata for RBAC catalog
|
# Feature metadata for RBAC catalog
|
||||||
FEATURE_CODE = "realestate"
|
FEATURE_CODE = "realestate"
|
||||||
FEATURE_LABEL = {"en": "Real Estate", "de": "Immobilien", "fr": "Immobilier"}
|
FEATURE_LABEL = "Immobilien"
|
||||||
FEATURE_ICON = "mdi-home-city"
|
FEATURE_ICON = "mdi-home-city"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog (only map view)
|
# UI Objects for RBAC catalog (only map view)
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.realestate.dashboard",
|
"objectKey": "ui.feature.realestate.dashboard",
|
||||||
"label": {"en": "Map", "de": "Karte", "fr": "Carte"},
|
"label": "Karte",
|
||||||
"meta": {"area": "dashboard"}
|
"meta": {"area": "dashboard"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -26,12 +26,12 @@ UI_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.realestate.project.create",
|
"objectKey": "resource.feature.realestate.project.create",
|
||||||
"label": {"en": "Create Project", "de": "Projekt erstellen", "fr": "Créer projet"},
|
"label": "Projekt erstellen",
|
||||||
"meta": {"endpoint": "/api/realestate/project", "method": "POST"}
|
"meta": {"endpoint": "/api/realestate/project", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.realestate.project.delete",
|
"objectKey": "resource.feature.realestate.project.delete",
|
||||||
"label": {"en": "Delete Project", "de": "Projekt löschen", "fr": "Supprimer projet"},
|
"label": "Projekt löschen",
|
||||||
"meta": {"endpoint": "/api/realestate/project/{projectId}", "method": "DELETE"}
|
"meta": {"endpoint": "/api/realestate/project/{projectId}", "method": "DELETE"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -41,11 +41,7 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "realestate-viewer",
|
"roleLabel": "realestate-viewer",
|
||||||
"description": {
|
"description": "Immobilien-Betrachter - Immobilien-Informationen einsehen (nur lesen)",
|
||||||
"en": "Real Estate Viewer - View property information (read-only)",
|
|
||||||
"de": "Immobilien-Betrachter - Immobilien-Informationen einsehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur immobilier - Consulter les informations immobilières (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
||||||
|
|
@ -53,11 +49,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "realestate-user",
|
"roleLabel": "realestate-user",
|
||||||
"description": {
|
"description": "Immobilien-Benutzer - Eigene Immobilien-Daten erstellen und verwalten",
|
||||||
"en": "Real Estate User - Create and manage own property records",
|
|
||||||
"de": "Immobilien-Benutzer - Eigene Immobilien-Daten erstellen und verwalten",
|
|
||||||
"fr": "Utilisateur immobilier - Créer et gérer ses propres données immobilières",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
|
|
@ -66,11 +58,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "realestate-admin",
|
"roleLabel": "realestate-admin",
|
||||||
"description": {
|
"description": "Immobilien-Administrator - Vollzugriff auf alle Immobiliendaten und Einstellungen",
|
||||||
"en": "Real Estate Administrator - Full access to all property data and settings",
|
|
||||||
"de": "Immobilien-Administrator - Vollzugriff auf alle Immobiliendaten und Einstellungen",
|
|
||||||
"fr": "Administrateur immobilier - Accès complet aux données et paramètres",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
||||||
|
|
@ -80,11 +68,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "realestate-manager",
|
"roleLabel": "realestate-manager",
|
||||||
"description": {
|
"description": "Immobilien-Verwalter - Immobilien und Mieter verwalten",
|
||||||
"en": "Real Estate Manager - Manage properties and tenants",
|
|
||||||
"de": "Immobilien-Verwalter - Immobilien und Mieter verwalten",
|
|
||||||
"fr": "Gestionnaire immobilier - Gérer les propriétés et locataires",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||||
|
|
@ -154,6 +138,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
db = rootInterface.db
|
db = rootInterface.db
|
||||||
|
|
@ -174,7 +159,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ from modules.aicore.aicorePluginTavily import AiTavily
|
||||||
|
|
||||||
# Import attribute utilities for model schema
|
# Import attribute utilities for model schema
|
||||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureRealEstate")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -339,7 +341,7 @@ def update_project(
|
||||||
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
updated = interface.updateProjekt(projectId, data)
|
updated = interface.updateProjekt(projectId, data)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail="Update failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -360,7 +362,7 @@ def delete_project(
|
||||||
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
if not interface.deleteProjekt(projectId):
|
if not interface.deleteProjekt(projectId):
|
||||||
raise HTTPException(status_code=500, detail="Delete failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
|
||||||
|
|
||||||
|
|
||||||
# ----- Parcels CRUD -----
|
# ----- Parcels CRUD -----
|
||||||
|
|
@ -496,7 +498,7 @@ def update_parcel(
|
||||||
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
updated = interface.updateParzelle(parcelId, data)
|
updated = interface.updateParzelle(parcelId, data)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail="Update failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -517,7 +519,7 @@ def delete_parcel(
|
||||||
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
if not interface.deleteParzelle(parcelId):
|
if not interface.deleteParzelle(parcelId):
|
||||||
raise HTTPException(status_code=500, detail="Delete failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
|
||||||
|
|
||||||
|
|
||||||
# ===== Helpers for Gemeinde/BZO routes =====
|
# ===== Helpers for Gemeinde/BZO routes =====
|
||||||
|
|
@ -885,7 +887,7 @@ async def process_command(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {context.user.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -893,7 +895,7 @@ async def process_command(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {context.user.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -903,7 +905,7 @@ async def process_command(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {context.user.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Processing command request from user {context.user.id} (mandate: {context.mandateId})")
|
logger.info(f"Processing command request from user {context.user.id} (mandate: {context.mandateId})")
|
||||||
|
|
@ -957,7 +959,7 @@ def get_available_tables(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {context.user.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -965,7 +967,7 @@ def get_available_tables(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {context.user.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -975,7 +977,7 @@ def get_available_tables(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {context.user.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Getting available tables for user {context.user.id} (mandate: {context.mandateId})")
|
logger.info(f"Getting available tables for user {context.user.id} (mandate: {context.mandateId})")
|
||||||
|
|
@ -1066,7 +1068,7 @@ def get_table_data(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -1074,7 +1076,7 @@ def get_table_data(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -1084,7 +1086,7 @@ def get_table_data(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Getting table data for '{table}' from user {context.user.id} (mandate: {context.mandateId})")
|
logger.info(f"Getting table data for '{table}' from user {context.user.id} (mandate: {context.mandateId})")
|
||||||
|
|
@ -1235,7 +1237,7 @@ async def create_table_record(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -1243,7 +1245,7 @@ async def create_table_record(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -1253,7 +1255,7 @@ async def create_table_record(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {context.user.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Special handling for Projekt with parcel data
|
# Special handling for Projekt with parcel data
|
||||||
|
|
@ -1265,7 +1267,7 @@ async def create_table_record(
|
||||||
if not label:
|
if not label:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="label is required"
|
detail=routeApiMsg("label is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
status_prozess = data.get("statusProzess", "Eingang")
|
status_prozess = data.get("statusProzess", "Eingang")
|
||||||
|
|
@ -1278,7 +1280,7 @@ async def create_table_record(
|
||||||
if not isinstance(parzellen_data, list):
|
if not isinstance(parzellen_data, list):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="parzellen must be an array"
|
detail=routeApiMsg("parzellen must be an array")
|
||||||
)
|
)
|
||||||
elif "parzelle" in data:
|
elif "parzelle" in data:
|
||||||
# Single parcel
|
# Single parcel
|
||||||
|
|
@ -1289,7 +1291,7 @@ async def create_table_record(
|
||||||
if not parzellen_data:
|
if not parzellen_data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="parzelle or parzellen data is required"
|
detail=routeApiMsg("parzelle or parzellen data is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use helper function to create project with parcel data
|
# Use helper function to create project with parcel data
|
||||||
|
|
@ -1402,7 +1404,7 @@ def get_parcels_wfs(
|
||||||
logger.error(f"Error fetching WFS parcels: {e}", exc_info=True)
|
logger.error(f"Error fetching WFS parcels: {e}", exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
detail="Failed to fetch parcel data from WFS"
|
detail=routeApiMsg("Failed to fetch parcel data from WFS")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1441,7 +1443,7 @@ async def search_parcel(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {context.user.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Searching parcel for user {context.user.id} (mandate: {context.mandateId}) with location: {location}")
|
logger.info(f"Searching parcel for user {context.user.id} (mandate: {context.mandateId}) with location: {location}")
|
||||||
|
|
@ -1817,7 +1819,7 @@ async def parcel_selection_summary(
|
||||||
if not csrf_token:
|
if not csrf_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
parcels = body.get("parcels", [])
|
parcels = body.get("parcels", [])
|
||||||
if not parcels:
|
if not parcels:
|
||||||
|
|
@ -1868,19 +1870,19 @@ async def add_adjacent_parcel(
|
||||||
if not csrf_token:
|
if not csrf_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
location = body.get("location")
|
location = body.get("location")
|
||||||
selected_parcels = body.get("selected_parcels", [])
|
selected_parcels = body.get("selected_parcels", [])
|
||||||
if not location or "x" not in location or "y" not in location:
|
if not location or "x" not in location or "y" not in location:
|
||||||
raise HTTPException(status_code=400, detail="location with x,y required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("location with x,y required"))
|
||||||
loc_str = f"{location['x']},{location['y']}"
|
loc_str = f"{location['x']},{location['y']}"
|
||||||
connector = SwissTopoMapServerConnector()
|
connector = SwissTopoMapServerConnector()
|
||||||
parcel_data = await connector.search_parcel(loc_str)
|
parcel_data = await connector.search_parcel(loc_str)
|
||||||
if not parcel_data:
|
if not parcel_data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No parcel found at this location"
|
detail=routeApiMsg("No parcel found at this location")
|
||||||
)
|
)
|
||||||
extracted = connector.extract_parcel_attributes(parcel_data)
|
extracted = connector.extract_parcel_attributes(parcel_data)
|
||||||
attributes = parcel_data.get("attributes", {})
|
attributes = parcel_data.get("attributes", {})
|
||||||
|
|
@ -1932,7 +1934,7 @@ async def add_adjacent_parcel(
|
||||||
if not is_parcel_adjacent_to_selection(new_parcel_response, selected_parcels):
|
if not is_parcel_adjacent_to_selection(new_parcel_response, selected_parcels):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Nur angrenzende Parzellen können hinzugefügt werden"
|
detail=routeApiMsg("Nur angrenzende Parzellen können hinzugefügt werden")
|
||||||
)
|
)
|
||||||
bbox = parcel_data.get("bbox", [])
|
bbox = parcel_data.get("bbox", [])
|
||||||
map_view["zoom_bounds"] = {
|
map_view["zoom_bounds"] = {
|
||||||
|
|
@ -2020,21 +2022,21 @@ async def add_parcel_to_project(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {context.user.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {context.user.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate CSRF token format
|
# Validate CSRF token format
|
||||||
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
int(csrf_token, 16)
|
int(csrf_token, 16)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Adding parcel to project {projekt_id} for user {context.user.id} (mandate: {context.mandateId})")
|
logger.info(f"Adding parcel to project {projekt_id} for user {context.user.id} (mandate: {context.mandateId})")
|
||||||
|
|
|
||||||
|
|
@ -12,24 +12,24 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Feature metadata
|
# Feature metadata
|
||||||
FEATURE_CODE = "teamsbot"
|
FEATURE_CODE = "teamsbot"
|
||||||
FEATURE_LABEL = {"en": "Teams Bot", "de": "Teams Bot", "fr": "Teams Bot"}
|
FEATURE_LABEL = "Teams Bot"
|
||||||
FEATURE_ICON = "mdi-headset"
|
FEATURE_ICON = "mdi-headset"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog
|
# UI Objects for RBAC catalog
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.teamsbot.dashboard",
|
"objectKey": "ui.feature.teamsbot.dashboard",
|
||||||
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
"label": "Dashboard",
|
||||||
"meta": {"area": "dashboard"}
|
"meta": {"area": "dashboard"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.teamsbot.sessions",
|
"objectKey": "ui.feature.teamsbot.sessions",
|
||||||
"label": {"en": "Sessions", "de": "Sitzungen", "fr": "Sessions"},
|
"label": "Sitzungen",
|
||||||
"meta": {"area": "sessions"}
|
"meta": {"area": "sessions"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.teamsbot.settings",
|
"objectKey": "ui.feature.teamsbot.settings",
|
||||||
"label": {"en": "Settings", "de": "Einstellungen", "fr": "Paramètres"},
|
"label": "Einstellungen",
|
||||||
"meta": {"area": "settings", "admin_only": True}
|
"meta": {"area": "settings", "admin_only": True}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -38,7 +38,7 @@ UI_OBJECTS = [
|
||||||
DATA_OBJECTS = [
|
DATA_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.teamsbot.TeamsbotSession",
|
"objectKey": "data.feature.teamsbot.TeamsbotSession",
|
||||||
"label": {"en": "Session", "de": "Sitzung", "fr": "Session"},
|
"label": "Sitzung",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TeamsbotSession",
|
"table": "TeamsbotSession",
|
||||||
"fields": ["id", "meetingLink", "botName", "status", "startedAt", "endedAt"],
|
"fields": ["id", "meetingLink", "botName", "status", "startedAt", "endedAt"],
|
||||||
|
|
@ -48,7 +48,7 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.teamsbot.TeamsbotTranscript",
|
"objectKey": "data.feature.teamsbot.TeamsbotTranscript",
|
||||||
"label": {"en": "Transcript", "de": "Transkript", "fr": "Transcription"},
|
"label": "Transkript",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TeamsbotTranscript",
|
"table": "TeamsbotTranscript",
|
||||||
"fields": ["id", "sessionId", "speaker", "text", "timestamp"],
|
"fields": ["id", "sessionId", "speaker", "text", "timestamp"],
|
||||||
|
|
@ -58,7 +58,7 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.teamsbot.TeamsbotBotResponse",
|
"objectKey": "data.feature.teamsbot.TeamsbotBotResponse",
|
||||||
"label": {"en": "Bot Response", "de": "Bot-Antwort", "fr": "Réponse du bot"},
|
"label": "Bot-Antwort",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TeamsbotBotResponse",
|
"table": "TeamsbotBotResponse",
|
||||||
"fields": ["id", "sessionId", "responseText", "detectedIntent"],
|
"fields": ["id", "sessionId", "responseText", "detectedIntent"],
|
||||||
|
|
@ -68,7 +68,7 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.teamsbot.*",
|
"objectKey": "data.feature.teamsbot.*",
|
||||||
"label": {"en": "All Teams Bot Data", "de": "Alle Teams Bot Daten", "fr": "Toutes les données Teams Bot"},
|
"label": "Alle Teams Bot Daten",
|
||||||
"meta": {"wildcard": True, "description": "Wildcard for all teamsbot data tables"}
|
"meta": {"wildcard": True, "description": "Wildcard for all teamsbot data tables"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -77,22 +77,22 @@ DATA_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.teamsbot.session.start",
|
"objectKey": "resource.feature.teamsbot.session.start",
|
||||||
"label": {"en": "Start Session", "de": "Sitzung starten", "fr": "Démarrer session"},
|
"label": "Sitzung starten",
|
||||||
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions", "method": "POST"}
|
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.teamsbot.session.stop",
|
"objectKey": "resource.feature.teamsbot.session.stop",
|
||||||
"label": {"en": "Stop Session", "de": "Sitzung beenden", "fr": "Arrêter session"},
|
"label": "Sitzung beenden",
|
||||||
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}/stop", "method": "POST"}
|
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}/stop", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.teamsbot.session.delete",
|
"objectKey": "resource.feature.teamsbot.session.delete",
|
||||||
"label": {"en": "Delete Session", "de": "Sitzung löschen", "fr": "Supprimer session"},
|
"label": "Sitzung löschen",
|
||||||
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}", "method": "DELETE"}
|
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}", "method": "DELETE"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.teamsbot.config.edit",
|
"objectKey": "resource.feature.teamsbot.config.edit",
|
||||||
"label": {"en": "Edit Configuration", "de": "Konfiguration bearbeiten", "fr": "Modifier configuration"},
|
"label": "Konfiguration bearbeiten",
|
||||||
"meta": {"endpoint": "/api/teamsbot/{instanceId}/config", "method": "PUT", "admin_only": True}
|
"meta": {"endpoint": "/api/teamsbot/{instanceId}/config", "method": "PUT", "admin_only": True}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -101,11 +101,7 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "teamsbot-admin",
|
"roleLabel": "teamsbot-admin",
|
||||||
"description": {
|
"description": "Teams Bot Administrator - Vollzugriff auf alle Sitzungen und Einstellungen",
|
||||||
"en": "Teams Bot Administrator - Full access to all sessions and settings",
|
|
||||||
"de": "Teams Bot Administrator - Vollzugriff auf alle Sitzungen und Einstellungen",
|
|
||||||
"fr": "Administrateur Teams Bot - Accès complet aux sessions et paramètres"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
# Full UI access (all views including settings)
|
# Full UI access (all views including settings)
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
|
|
@ -120,11 +116,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "teamsbot-viewer",
|
"roleLabel": "teamsbot-viewer",
|
||||||
"description": {
|
"description": "Teams Bot Betrachter - Sitzungen und Transkripte ansehen (nur lesen)",
|
||||||
"en": "Teams Bot Viewer - View sessions and transcripts (read-only)",
|
|
||||||
"de": "Teams Bot Betrachter - Sitzungen und Transkripte ansehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur Teams Bot - Consulter les sessions et transcriptions (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
|
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
|
||||||
|
|
@ -133,11 +125,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "teamsbot-user",
|
"roleLabel": "teamsbot-user",
|
||||||
"description": {
|
"description": "Teams Bot Benutzer - Kann Sitzungen starten/stoppen und Transkripte einsehen",
|
||||||
"en": "Teams Bot User - Can start/stop sessions and view transcripts",
|
|
||||||
"de": "Teams Bot Benutzer - Kann Sitzungen starten/stoppen und Transkripte einsehen",
|
|
||||||
"fr": "Utilisateur Teams Bot - Peut démarrer/arrêter des sessions et voir les transcriptions",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
|
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
|
||||||
|
|
@ -223,7 +211,8 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
templateRoles = [r for r in existingRoles if r.mandateId is None]
|
templateRoles = [r for r in existingRoles if r.mandateId is None]
|
||||||
|
|
@ -239,7 +228,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ from .datamodelTeamsbot import (
|
||||||
|
|
||||||
# Import service
|
# Import service
|
||||||
from .service import TeamsbotService
|
from .service import TeamsbotService
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureTeamsbot")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -71,7 +73,7 @@ def _extractTeamsMeetingUrl(rawInput: str) -> str:
|
||||||
urls = re.findall(urlPattern, rawInput)
|
urls = re.findall(urlPattern, rawInput)
|
||||||
|
|
||||||
if not urls:
|
if not urls:
|
||||||
raise HTTPException(status_code=400, detail="Kein gültiger Meeting-Link gefunden. Bitte einen Teams-Link eingeben.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Kein gültiger Meeting-Link gefunden. Bitte einen Teams-Link eingeben."))
|
||||||
|
|
||||||
# Step 2: Find the Teams URL (prefer direct teams.microsoft.com, then SafeLinks)
|
# Step 2: Find the Teams URL (prefer direct teams.microsoft.com, then SafeLinks)
|
||||||
teamsUrl = None
|
teamsUrl = None
|
||||||
|
|
@ -101,7 +103,7 @@ def _extractTeamsMeetingUrl(rawInput: str) -> str:
|
||||||
if not teamsUrl or "teams.microsoft.com" not in teamsUrl:
|
if not teamsUrl or "teams.microsoft.com" not in teamsUrl:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Kein gültiger Teams-Meeting-Link gefunden. Der Link muss 'teams.microsoft.com' enthalten."
|
detail=routeApiMsg("Kein gültiger Teams-Meeting-Link gefunden. Der Link muss 'teams.microsoft.com' enthalten.")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Extracted meeting URL: {teamsUrl[:80]}... (from input length {len(rawInput)})")
|
logger.info(f"Extracted meeting URL: {teamsUrl[:80]}... (from input length {len(rawInput)})")
|
||||||
|
|
@ -129,7 +131,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||||
|
|
||||||
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
|
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=500, detail="Feature instance has no mandateId")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Feature instance has no mandateId"))
|
||||||
|
|
||||||
return str(mandateId)
|
return str(mandateId)
|
||||||
|
|
||||||
|
|
@ -463,7 +465,7 @@ async def deleteSession(
|
||||||
# Don't delete active sessions
|
# Don't delete active sessions
|
||||||
currentStatus = session.get("status")
|
currentStatus = session.get("status")
|
||||||
if currentStatus in [TeamsbotSessionStatus.ACTIVE.value, TeamsbotSessionStatus.JOINING.value]:
|
if currentStatus in [TeamsbotSessionStatus.ACTIVE.value, TeamsbotSessionStatus.JOINING.value]:
|
||||||
raise HTTPException(status_code=400, detail="Cannot delete an active session. Stop it first.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Cannot delete an active session. Stop it first."))
|
||||||
|
|
||||||
interface.deleteSession(sessionId)
|
interface.deleteSession(sessionId)
|
||||||
logger.info(f"Teamsbot session {sessionId} deleted")
|
logger.info(f"Teamsbot session {sessionId} deleted")
|
||||||
|
|
@ -639,7 +641,7 @@ async def listSystemBots(
|
||||||
):
|
):
|
||||||
"""List all system bot accounts for this mandate. Passwords are never returned."""
|
"""List all system bot accounts for this mandate. Passwords are never returned."""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
interface = _getInterface(context, instanceId)
|
interface = _getInterface(context, instanceId)
|
||||||
bots = interface.getSystemBots(mandateId)
|
bots = interface.getSystemBots(mandateId)
|
||||||
|
|
@ -655,7 +657,7 @@ async def createSystemBot(
|
||||||
):
|
):
|
||||||
"""Create a new system bot account. Password is encrypted before storage."""
|
"""Create a new system bot account. Password is encrypted before storage."""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
interface = _getInterface(context, instanceId)
|
interface = _getInterface(context, instanceId)
|
||||||
|
|
||||||
|
|
@ -666,7 +668,7 @@ async def createSystemBot(
|
||||||
|
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
raise HTTPException(status_code=400, detail="Email and password are required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Email and password are required"))
|
||||||
|
|
||||||
# Encrypt the password
|
# Encrypt the password
|
||||||
from modules.shared.configuration import encryptValue
|
from modules.shared.configuration import encryptValue
|
||||||
|
|
@ -698,7 +700,7 @@ async def deleteSystemBot(
|
||||||
):
|
):
|
||||||
"""Delete a system bot account."""
|
"""Delete a system bot account."""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
|
||||||
_validateInstanceAccess(instanceId, context)
|
_validateInstanceAccess(instanceId, context)
|
||||||
interface = _getInterface(context, instanceId)
|
interface = _getInterface(context, instanceId)
|
||||||
|
|
||||||
|
|
@ -750,7 +752,7 @@ async def saveUserAccount(
|
||||||
displayName = body.get("displayName")
|
displayName = body.get("displayName")
|
||||||
|
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
raise HTTPException(status_code=400, detail="Email and password are required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Email and password are required"))
|
||||||
|
|
||||||
from modules.shared.configuration import encryptValue
|
from modules.shared.configuration import encryptValue
|
||||||
encryptedPassword = encryptValue(password, userId=userId, keyName="userAccountPassword")
|
encryptedPassword = encryptValue(password, userId=userId, keyName="userAccountPassword")
|
||||||
|
|
@ -827,7 +829,7 @@ async def submitMfaCode(
|
||||||
await queue.put({"action": mfaAction, "code": mfaCode})
|
await queue.put({"action": mfaAction, "code": mfaCode})
|
||||||
return {"submitted": True}
|
return {"submitted": True}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=404, detail="No active MFA challenge for this session")
|
raise HTTPException(status_code=404, detail=routeApiMsg("No active MFA challenge for this session"))
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
@ -925,7 +927,7 @@ async def testAuth(
|
||||||
Does NOT join the meeting — only checks which page Teams serves.
|
Does NOT join the meeting — only checks which page Teams serves.
|
||||||
"""
|
"""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing (uses system bot credentials)")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing (uses system bot credentials)"))
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
|
|
@ -935,7 +937,7 @@ async def testAuth(
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
meetingUrl = body.get("meetingUrl")
|
meetingUrl = body.get("meetingUrl")
|
||||||
if not meetingUrl:
|
if not meetingUrl:
|
||||||
raise HTTPException(status_code=400, detail="meetingUrl is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("meetingUrl is required"))
|
||||||
|
|
||||||
# Load system bot credentials:
|
# Load system bot credentials:
|
||||||
# 1. Use email/password from request body (direct override)
|
# 1. Use email/password from request body (direct override)
|
||||||
|
|
@ -1000,7 +1002,7 @@ async def testAuth(
|
||||||
# Forward to browser bot service (single all-in-one call — may timeout with many variants)
|
# Forward to browser bot service (single all-in-one call — may timeout with many variants)
|
||||||
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
||||||
if not browserBotUrl:
|
if not browserBotUrl:
|
||||||
raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
|
||||||
|
|
||||||
browserBotUrl = browserBotUrl.rstrip("/")
|
browserBotUrl = browserBotUrl.rstrip("/")
|
||||||
payload = {
|
payload = {
|
||||||
|
|
@ -1037,14 +1039,14 @@ async def getTestAuthVariants(
|
||||||
Frontend calls this once, then runs each variant individually.
|
Frontend calls this once, then runs each variant individually.
|
||||||
"""
|
"""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing"))
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
_validateInstanceAccess(instanceId, context)
|
_validateInstanceAccess(instanceId, context)
|
||||||
effectiveConfig = _getInstanceConfig(instanceId)
|
effectiveConfig = _getInstanceConfig(instanceId)
|
||||||
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
||||||
if not browserBotUrl:
|
if not browserBotUrl:
|
||||||
raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
|
||||||
|
|
||||||
browserBotUrl = browserBotUrl.rstrip("/")
|
browserBotUrl = browserBotUrl.rstrip("/")
|
||||||
try:
|
try:
|
||||||
|
|
@ -1073,7 +1075,7 @@ async def testAuthSingleVariant(
|
||||||
Each call stays within Azure's 240s timeout.
|
Each call stays within Azure's 240s timeout.
|
||||||
"""
|
"""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing (uses system bot credentials)")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing (uses system bot credentials)"))
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
|
|
@ -1084,7 +1086,7 @@ async def testAuthSingleVariant(
|
||||||
variantId = body.get("variantId")
|
variantId = body.get("variantId")
|
||||||
meetingUrl = body.get("meetingUrl")
|
meetingUrl = body.get("meetingUrl")
|
||||||
if not variantId or not meetingUrl:
|
if not variantId or not meetingUrl:
|
||||||
raise HTTPException(status_code=400, detail="variantId and meetingUrl are required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("variantId and meetingUrl are required"))
|
||||||
|
|
||||||
# Load credentials (same logic as testAuth)
|
# Load credentials (same logic as testAuth)
|
||||||
email = body.get("botEmail")
|
email = body.get("botEmail")
|
||||||
|
|
@ -1116,7 +1118,7 @@ async def testAuthSingleVariant(
|
||||||
|
|
||||||
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
||||||
if not browserBotUrl:
|
if not browserBotUrl:
|
||||||
raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
|
||||||
|
|
||||||
browserBotUrl = browserBotUrl.rstrip("/")
|
browserBotUrl = browserBotUrl.rstrip("/")
|
||||||
payload = {
|
payload = {
|
||||||
|
|
@ -1157,12 +1159,12 @@ async def listSessionScreenshots(
|
||||||
):
|
):
|
||||||
"""List debug screenshots for a session. Proxied from Browser Bot filesystem."""
|
"""List debug screenshots for a session. Proxied from Browser Bot filesystem."""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required"))
|
||||||
_validateInstanceAccess(instanceId, context)
|
_validateInstanceAccess(instanceId, context)
|
||||||
effectiveConfig = _getInstanceConfig(instanceId)
|
effectiveConfig = _getInstanceConfig(instanceId)
|
||||||
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
||||||
if not browserBotUrl:
|
if not browserBotUrl:
|
||||||
raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
browserBotUrl = browserBotUrl.rstrip("/")
|
browserBotUrl = browserBotUrl.rstrip("/")
|
||||||
|
|
@ -1194,16 +1196,16 @@ async def getScreenshotFile(
|
||||||
):
|
):
|
||||||
"""Serve a single debug screenshot image. Proxied from Browser Bot."""
|
"""Serve a single debug screenshot image. Proxied from Browser Bot."""
|
||||||
if not context.isSysAdmin:
|
if not context.isSysAdmin:
|
||||||
raise HTTPException(status_code=403, detail="SysAdmin privileges required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required"))
|
||||||
_validateInstanceAccess(instanceId, context)
|
_validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
if not filename.endswith(".png") or ".." in filename or "/" in filename or "\\" in filename:
|
if not filename.endswith(".png") or ".." in filename or "/" in filename or "\\" in filename:
|
||||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid filename"))
|
||||||
|
|
||||||
effectiveConfig = _getInstanceConfig(instanceId)
|
effectiveConfig = _getInstanceConfig(instanceId)
|
||||||
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
|
||||||
if not browserBotUrl:
|
if not browserBotUrl:
|
||||||
raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from fastapi.responses import Response as FastAPIResponse
|
from fastapi.responses import Response as FastAPIResponse
|
||||||
|
|
@ -1216,7 +1218,7 @@ async def getScreenshotFile(
|
||||||
imageBytes = await resp.read()
|
imageBytes = await resp.read()
|
||||||
return FastAPIResponse(content=imageBytes, media_type="image/png")
|
return FastAPIResponse(content=imageBytes, media_type="image/png")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=resp.status, detail="Screenshot not found")
|
raise HTTPException(status_code=resp.status, detail=routeApiMsg("Screenshot not found"))
|
||||||
except aiohttp.ClientError as e:
|
except aiohttp.ClientError as e:
|
||||||
logger.error(f"Screenshot file error: {e}")
|
logger.error(f"Screenshot file error: {e}")
|
||||||
raise HTTPException(status_code=503, detail=f"Browser Bot connection failed: {str(e)}")
|
raise HTTPException(status_code=503, detail=f"Browser Bot connection failed: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@ from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@i18nModel("Organisation")
|
||||||
class TrusteeOrganisation(PowerOnModel):
|
class TrusteeOrganisation(PowerOnModel):
|
||||||
"""Represents trustee organisations (companies) within the Trustee feature."""
|
"""Represents trustee organisations (companies) within the Trustee feature."""
|
||||||
id: str = Field( # Unique string label (PK), not UUID
|
id: str = Field( # Unique string label (PK), not UUID
|
||||||
description="Unique organisation identifier (label)",
|
description="Unique organisation identifier (label)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False, # Editable at creation, then readonly
|
"frontend_readonly": False, # Editable at creation, then readonly
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -24,6 +25,7 @@ class TrusteeOrganisation(PowerOnModel):
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Company name",
|
description="Company name",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Bezeichnung",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -33,6 +35,7 @@ class TrusteeOrganisation(PowerOnModel):
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether the organisation is enabled",
|
description="Whether the organisation is enabled",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Aktiviert",
|
||||||
"frontend_type": "checkbox",
|
"frontend_type": "checkbox",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -42,6 +45,7 @@ class TrusteeOrganisation(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID (system-level organisation)",
|
description="Mandate ID (system-level organisation)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -51,6 +55,7 @@ class TrusteeOrganisation(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation",
|
description="Feature Instance ID for instance-level isolation",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -59,25 +64,13 @@ class TrusteeOrganisation(PowerOnModel):
|
||||||
# System attributes are automatically set by DatabaseConnector:
|
# System attributes are automatically set by DatabaseConnector:
|
||||||
# sysCreatedAt, sysModifiedAt, sysCreatedBy, sysModifiedBy (PowerOnModel)
|
# sysCreatedAt, sysModifiedAt, sysCreatedBy, sysModifiedBy (PowerOnModel)
|
||||||
|
|
||||||
|
@i18nModel("Rolle")
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeOrganisation",
|
|
||||||
{"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrusteeRole(PowerOnModel):
|
class TrusteeRole(PowerOnModel):
|
||||||
"""Defines roles within the Trustee feature."""
|
"""Defines roles within the Trustee feature."""
|
||||||
id: str = Field( # Unique string label (PK), not UUID
|
id: str = Field( # Unique string label (PK), not UUID
|
||||||
description="Unique role identifier (label)",
|
description="Unique role identifier (label)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -86,6 +79,7 @@ class TrusteeRole(PowerOnModel):
|
||||||
desc: str = Field(
|
desc: str = Field(
|
||||||
description="Role description",
|
description="Role description",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Beschreibung",
|
||||||
"frontend_type": "textarea",
|
"frontend_type": "textarea",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -95,6 +89,7 @@ class TrusteeRole(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID",
|
description="Mandate ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -104,6 +99,7 @@ class TrusteeRole(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation",
|
description="Feature Instance ID for instance-level isolation",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -111,25 +107,14 @@ class TrusteeRole(PowerOnModel):
|
||||||
)
|
)
|
||||||
# System attributes are automatically set by DatabaseConnector
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
@i18nModel("Zugriff")
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeRole",
|
|
||||||
{"en": "Role", "fr": "Rôle", "de": "Rolle"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrusteeAccess(PowerOnModel):
|
class TrusteeAccess(PowerOnModel):
|
||||||
"""Defines user access to organisations with specific roles."""
|
"""Defines user access to organisations with specific roles."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique access ID",
|
description="Unique access ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -138,6 +123,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
organisationId: str = Field(
|
organisationId: str = Field(
|
||||||
description="Reference to TrusteeOrganisation.id",
|
description="Reference to TrusteeOrganisation.id",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Organisation",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -147,6 +133,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
roleId: str = Field(
|
roleId: str = Field(
|
||||||
description="Reference to TrusteeRole.id",
|
description="Reference to TrusteeRole.id",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Rolle",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -156,6 +143,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
userId: str = Field(
|
userId: str = Field(
|
||||||
description="User ID assigned to this role",
|
description="User ID assigned to this role",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Benutzer",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -166,6 +154,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional reference to TrusteeContract.id. If None, access is for full organisation. If set, access is limited to this specific contract.",
|
description="Optional reference to TrusteeContract.id. If None, access is for full organisation. If set, access is limited to this specific contract.",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Vertrag (optional)",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -177,6 +166,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID",
|
description="Mandate ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -186,6 +176,7 @@ class TrusteeAccess(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation",
|
description="Feature Instance ID for instance-level isolation",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -193,28 +184,14 @@ class TrusteeAccess(PowerOnModel):
|
||||||
)
|
)
|
||||||
# System attributes are automatically set by DatabaseConnector
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
@i18nModel("Vertrag")
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeAccess",
|
|
||||||
{"en": "Access", "fr": "Accès", "de": "Zugriff"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
|
||||||
"roleId": {"en": "Role", "fr": "Rôle", "de": "Rolle"},
|
|
||||||
"userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
|
|
||||||
"contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrusteeContract(PowerOnModel):
|
class TrusteeContract(PowerOnModel):
|
||||||
"""Defines customer contracts within organisations."""
|
"""Defines customer contracts within organisations."""
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique contract ID",
|
description="Unique contract ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -223,6 +200,7 @@ class TrusteeContract(PowerOnModel):
|
||||||
organisationId: str = Field(
|
organisationId: str = Field(
|
||||||
description="Reference to TrusteeOrganisation.id (immutable after creation)",
|
description="Reference to TrusteeOrganisation.id (immutable after creation)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Organisation",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False, # Editable at creation, then readonly
|
"frontend_readonly": False, # Editable at creation, then readonly
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -232,6 +210,7 @@ class TrusteeContract(PowerOnModel):
|
||||||
label: str = Field(
|
label: str = Field(
|
||||||
description="Label for the customer contract (e.g., 'Muster AG 2026')",
|
description="Label for the customer contract (e.g., 'Muster AG 2026')",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Bezeichnung",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -241,6 +220,7 @@ class TrusteeContract(PowerOnModel):
|
||||||
default=True,
|
default=True,
|
||||||
description="Whether the contract is enabled",
|
description="Whether the contract is enabled",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Aktiviert",
|
||||||
"frontend_type": "checkbox",
|
"frontend_type": "checkbox",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -250,6 +230,7 @@ class TrusteeContract(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID",
|
description="Mandate ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -259,6 +240,7 @@ class TrusteeContract(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation",
|
description="Feature Instance ID for instance-level isolation",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -266,21 +248,6 @@ class TrusteeContract(PowerOnModel):
|
||||||
)
|
)
|
||||||
# System attributes are automatically set by DatabaseConnector
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeContract",
|
|
||||||
{"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
|
|
||||||
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrusteeDocumentTypeEnum(str, Enum):
|
class TrusteeDocumentTypeEnum(str, Enum):
|
||||||
"""Document type for trustee documents (expense extraction, ingest, sync)."""
|
"""Document type for trustee documents (expense extraction, ingest, sync)."""
|
||||||
INVOICE = "invoice"
|
INVOICE = "invoice"
|
||||||
|
|
@ -290,7 +257,7 @@ class TrusteeDocumentTypeEnum(str, Enum):
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
AUTO = "auto"
|
AUTO = "auto"
|
||||||
|
|
||||||
|
@i18nModel("Dokument")
|
||||||
class TrusteeDocument(PowerOnModel):
|
class TrusteeDocument(PowerOnModel):
|
||||||
"""Contains document references for bookings.
|
"""Contains document references for bookings.
|
||||||
|
|
||||||
|
|
@ -305,6 +272,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique document ID",
|
description="Unique document ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -314,6 +282,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Reference to central Files table (Files.id)",
|
description="Reference to central Files table (Files.id)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Datei-Referenz",
|
||||||
"frontend_type": "file_reference",
|
"frontend_type": "file_reference",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -322,6 +291,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
documentName: str = Field(
|
documentName: str = Field(
|
||||||
description="File name (e.g., 'Beleg.pdf')",
|
description="File name (e.g., 'Beleg.pdf')",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Dokumentname",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -331,6 +301,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default="application/octet-stream",
|
default="application/octet-stream",
|
||||||
description="MIME type of the document",
|
description="MIME type of the document",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "MIME-Typ",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True,
|
"frontend_required": True,
|
||||||
|
|
@ -341,6 +312,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Source type (e.g., 'sharepoint', 'upload', 'email')",
|
description="Source type (e.g., 'sharepoint', 'upload', 'email')",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Quelltyp",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -350,6 +322,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Original source location (e.g., SharePoint path)",
|
description="Original source location (e.g., SharePoint path)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Quellort",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -359,6 +332,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID (auto-set from context)",
|
description="Mandate ID (auto-set from context)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -369,6 +343,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -379,6 +354,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Document type (e.g. invoice, expense_receipt, bank_document, contract); use TrusteeDocumentTypeEnum values",
|
description="Document type (e.g. invoice, expense_receipt, bank_document, contract); use TrusteeDocumentTypeEnum values",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Dokumenttyp",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -388,6 +364,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="External Beleg-ID in accounting system (e.g. RMA); set on first successful upload, reused on re-sync",
|
description="External Beleg-ID in accounting system (e.g. RMA); set on first successful upload, reused on re-sync",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Beleg-ID (Buchhaltung)",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -396,25 +373,7 @@ class TrusteeDocument(PowerOnModel):
|
||||||
)
|
)
|
||||||
# System attributes are automatically set by DatabaseConnector
|
# System attributes are automatically set by DatabaseConnector
|
||||||
|
|
||||||
|
@i18nModel("Position")
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDocument",
|
|
||||||
{"en": "Document", "fr": "Document", "de": "Dokument"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"fileId": {"en": "File Reference", "fr": "Référence du fichier", "de": "Datei-Referenz"},
|
|
||||||
"documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
|
|
||||||
"documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
|
|
||||||
"sourceType": {"en": "Source Type", "fr": "Type de source", "de": "Quelltyp"},
|
|
||||||
"sourceLocation": {"en": "Source Location", "fr": "Emplacement source", "de": "Quellort"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
"documentType": {"en": "Document Type", "fr": "Type de document", "de": "Dokumenttyp"},
|
|
||||||
"externalBelegId": {"en": "Beleg ID (Accounting)", "fr": "ID Beleg (Comptabilité)", "de": "Beleg-ID (Buchhaltung)"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TrusteePosition(PowerOnModel):
|
class TrusteePosition(PowerOnModel):
|
||||||
"""Contains booking positions (expense entries).
|
"""Contains booking positions (expense entries).
|
||||||
|
|
||||||
|
|
@ -425,6 +384,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default_factory=lambda: str(uuid.uuid4()),
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
description="Unique position ID",
|
description="Unique position ID",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -434,6 +394,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Reference to TrusteeDocument.id (Beleg / primary document)",
|
description="Reference to TrusteeDocument.id (Beleg / primary document)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Dokument",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -444,6 +405,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Reference to TrusteeDocument.id (Bank-Referenz / second document)",
|
description="Reference to TrusteeDocument.id (Bank-Referenz / second document)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Bank-Referenz",
|
||||||
"frontend_type": "select",
|
"frontend_type": "select",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -454,6 +416,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Value date (ISO format: YYYY-MM-DD)",
|
description="Value date (ISO format: YYYY-MM-DD)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Valutadatum",
|
||||||
"frontend_type": "date",
|
"frontend_type": "date",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -463,6 +426,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Transaction timestamp (UTC timestamp in seconds)",
|
description="Transaction timestamp (UTC timestamp in seconds)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Transaktionszeitpunkt",
|
||||||
"frontend_type": "timestamp",
|
"frontend_type": "timestamp",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -472,6 +436,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default="",
|
default="",
|
||||||
description="Company name",
|
description="Company name",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Firma",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -481,6 +446,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default="",
|
default="",
|
||||||
description="Description",
|
description="Description",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Beschreibung",
|
||||||
"frontend_type": "textarea",
|
"frontend_type": "textarea",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -490,6 +456,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default="",
|
default="",
|
||||||
description="Tags (comma-separated)",
|
description="Tags (comma-separated)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Tags",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -514,6 +481,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="Booking amount",
|
description="Booking amount",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Buchungsbetrag",
|
||||||
"frontend_type": "number",
|
"frontend_type": "number",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -538,6 +506,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="Original amount (manual input, no automatic currency conversion)",
|
description="Original amount (manual input, no automatic currency conversion)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Originalbetrag",
|
||||||
"frontend_type": "number",
|
"frontend_type": "number",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": True
|
"frontend_required": True
|
||||||
|
|
@ -547,6 +516,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="VAT percentage",
|
description="VAT percentage",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "MwSt-Prozentsatz",
|
||||||
"frontend_type": "number",
|
"frontend_type": "number",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -556,6 +526,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=0.0,
|
default=0.0,
|
||||||
description="VAT amount (calculated: bookingAmount * vatPercentage / 100, can be manually overridden)",
|
description="VAT amount (calculated: bookingAmount * vatPercentage / 100, can be manually overridden)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "MwSt-Betrag",
|
||||||
"frontend_type": "number",
|
"frontend_type": "number",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -565,6 +536,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Debit account number (e.g. '4200' for expenses)",
|
description="Debit account number (e.g. '4200' for expenses)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Soll-Konto",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -574,6 +546,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Credit account number (e.g. '1020' for bank)",
|
description="Credit account number (e.g. '1020' for bank)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Haben-Konto",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -583,6 +556,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Tax code for the accounting system",
|
description="Tax code for the accounting system",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Steuercode",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -592,6 +566,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Cost center identifier",
|
description="Cost center identifier",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Kostenstelle",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -601,6 +576,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Booking reference (e.g. voucher number)",
|
description="Booking reference (e.g. voucher number)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Buchungsreferenz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -626,6 +602,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="IBAN of the payment recipient (from invoice / QR code)",
|
description="IBAN of the payment recipient (from invoice / QR code)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Empfänger-IBAN",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -635,6 +612,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Bank or account holder name of the payment recipient",
|
description="Bank or account holder name of the payment recipient",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Empfänger-Name",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -644,6 +622,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="BIC / SWIFT code of the recipient bank",
|
description="BIC / SWIFT code of the recipient bank",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Empfänger-BIC",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -653,6 +632,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Structured payment reference (QR-Referenz, ESR, SCOR, Mitteilung)",
|
description="Structured payment reference (QR-Referenz, ESR, SCOR, Mitteilung)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Zahlungsreferenz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -662,6 +642,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Payment due date (ISO format: YYYY-MM-DD)",
|
description="Payment due date (ISO format: YYYY-MM-DD)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Fälligkeitsdatum",
|
||||||
"frontend_type": "date",
|
"frontend_type": "date",
|
||||||
"frontend_readonly": False,
|
"frontend_readonly": False,
|
||||||
"frontend_required": False
|
"frontend_required": False
|
||||||
|
|
@ -671,6 +652,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Mandate ID (auto-set from context)",
|
description="Mandate ID (auto-set from context)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Mandat",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -681,6 +663,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
description="Feature Instance ID for instance-level isolation (auto-set from context)",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Feature-Instanz",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -691,6 +674,7 @@ class TrusteePosition(PowerOnModel):
|
||||||
default=None,
|
default=None,
|
||||||
description="External ID (UUID) of the synced record in the accounting system; set by sync, used for duplicate check",
|
description="External ID (UUID) of the synced record in the accounting system; set by sync, used for duplicate check",
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
|
"label": "Buha-Sync-ID",
|
||||||
"frontend_type": "text",
|
"frontend_type": "text",
|
||||||
"frontend_readonly": True,
|
"frontend_readonly": True,
|
||||||
"frontend_required": False,
|
"frontend_required": False,
|
||||||
|
|
@ -698,283 +682,118 @@ class TrusteePosition(PowerOnModel):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteePosition",
|
|
||||||
{"en": "Position", "fr": "Position", "de": "Position"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
|
|
||||||
"bankDocumentId": {"en": "Bank Reference", "fr": "Référence bancaire", "de": "Bank-Referenz"},
|
|
||||||
"valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
|
|
||||||
"transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
|
|
||||||
"company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
|
|
||||||
"desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
|
|
||||||
"tags": {"en": "Tags", "fr": "Tags", "de": "Tags"},
|
|
||||||
"bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation", "de": "Buchungswährung"},
|
|
||||||
"bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation", "de": "Buchungsbetrag"},
|
|
||||||
"originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine", "de": "Originalwährung"},
|
|
||||||
"originalAmount": {"en": "Original Amount", "fr": "Montant d'origine", "de": "Originalbetrag"},
|
|
||||||
"vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
|
|
||||||
"vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
|
|
||||||
"debitAccountNumber": {"en": "Debit Account", "fr": "Compte débit", "de": "Soll-Konto"},
|
|
||||||
"creditAccountNumber": {"en": "Credit Account", "fr": "Compte crédit", "de": "Haben-Konto"},
|
|
||||||
"taxCode": {"en": "Tax Code", "fr": "Code TVA", "de": "Steuercode"},
|
|
||||||
"costCenter": {"en": "Cost Center", "fr": "Centre de coûts", "de": "Kostenstelle"},
|
|
||||||
"bookingReference": {"en": "Booking Reference", "fr": "Référence de réservation", "de": "Buchungsreferenz"},
|
|
||||||
"documentType": {"en": "Document Type", "fr": "Type de document", "de": "Dokumenttyp"},
|
|
||||||
"payeeIban": {"en": "Payee IBAN", "fr": "IBAN bénéficiaire", "de": "Empfänger-IBAN"},
|
|
||||||
"payeeName": {"en": "Payee Name", "fr": "Nom du bénéficiaire", "de": "Empfänger-Name"},
|
|
||||||
"payeeBic": {"en": "Payee BIC/SWIFT", "fr": "BIC/SWIFT bénéficiaire", "de": "Empfänger-BIC"},
|
|
||||||
"paymentReference": {"en": "Payment Reference", "fr": "Référence de paiement", "de": "Zahlungsreferenz"},
|
|
||||||
"dueDate": {"en": "Due Date", "fr": "Date d'échéance", "de": "Fälligkeitsdatum"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
|
|
||||||
"accountingSyncId": {"en": "Accounting Sync ID", "fr": "ID sync comptabilité", "de": "Buha-Sync-ID"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ── TrusteeData* tables (synced from external accounting apps for analysis) ──
|
# ── TrusteeData* tables (synced from external accounting apps for analysis) ──
|
||||||
|
|
||||||
|
@i18nModel("Konto (Sync)")
|
||||||
class TrusteeDataAccount(PowerOnModel):
|
class TrusteeDataAccount(PowerOnModel):
|
||||||
"""Chart of accounts synced from external accounting system."""
|
"""Chart of accounts synced from external accounting system."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
accountNumber: str = Field(description="Account number (e.g. '1020')")
|
accountNumber: str = Field(description="Account number (e.g. '1020')", json_schema_extra={"label": "Kontonummer"})
|
||||||
label: str = Field(default="", description="Account name")
|
label: str = Field(default="", description="Account name", json_schema_extra={"label": "Bezeichnung"})
|
||||||
accountType: Optional[str] = Field(default=None, description="asset / liability / equity / revenue / expense")
|
accountType: Optional[str] = Field(default=None, description="asset / liability / equity / revenue / expense", json_schema_extra={"label": "Typ"})
|
||||||
accountGroup: Optional[str] = Field(default=None, description="Account group/category")
|
accountGroup: Optional[str] = Field(default=None, description="Account group/category", json_schema_extra={"label": "Gruppe"})
|
||||||
currency: str = Field(default="CHF", description="Account currency")
|
currency: str = Field(default="CHF", description="Account currency", json_schema_extra={"label": "Währung"})
|
||||||
isActive: bool = Field(default=True)
|
isActive: bool = Field(default=True, json_schema_extra={"label": "Aktiv"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
featureInstanceId: Optional[str] = Field(default=None)
|
featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDataAccount",
|
|
||||||
{"en": "Account (Synced)", "de": "Konto (Sync)", "fr": "Compte (Sync)"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"accountNumber": {"en": "Account Number", "de": "Kontonummer", "fr": "Numéro de compte"},
|
|
||||||
"label": {"en": "Name", "de": "Bezeichnung", "fr": "Libellé"},
|
|
||||||
"accountType": {"en": "Type", "de": "Typ", "fr": "Type"},
|
|
||||||
"accountGroup": {"en": "Group", "de": "Gruppe", "fr": "Groupe"},
|
|
||||||
"currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
|
|
||||||
"isActive": {"en": "Active", "de": "Aktiv", "fr": "Actif"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Buchung (Sync)")
|
||||||
class TrusteeDataJournalEntry(PowerOnModel):
|
class TrusteeDataJournalEntry(PowerOnModel):
|
||||||
"""Journal entry header synced from external accounting system."""
|
"""Journal entry header synced from external accounting system."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
externalId: Optional[str] = Field(default=None, description="ID in the source system")
|
externalId: Optional[str] = Field(default=None, description="ID in the source system", json_schema_extra={"label": "Externe ID"})
|
||||||
bookingDate: Optional[str] = Field(default=None, description="Booking date (YYYY-MM-DD)")
|
bookingDate: Optional[str] = Field(default=None, description="Booking date (YYYY-MM-DD)", json_schema_extra={"label": "Datum"})
|
||||||
reference: Optional[str] = Field(default=None, description="Booking reference / voucher number")
|
reference: Optional[str] = Field(default=None, description="Booking reference / voucher number", json_schema_extra={"label": "Referenz"})
|
||||||
description: str = Field(default="", description="Booking text")
|
description: str = Field(default="", description="Booking text", json_schema_extra={"label": "Beschreibung"})
|
||||||
currency: str = Field(default="CHF")
|
currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
|
||||||
totalAmount: float = Field(default=0.0, description="Total amount of entry")
|
totalAmount: float = Field(default=0.0, description="Total amount of entry", json_schema_extra={"label": "Betrag"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
featureInstanceId: Optional[str] = Field(default=None)
|
featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDataJournalEntry",
|
|
||||||
{"en": "Journal Entry (Synced)", "de": "Buchung (Sync)", "fr": "Écriture (Sync)"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
|
|
||||||
"bookingDate": {"en": "Date", "de": "Datum", "fr": "Date"},
|
|
||||||
"reference": {"en": "Reference", "de": "Referenz", "fr": "Référence"},
|
|
||||||
"description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
|
|
||||||
"currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
|
|
||||||
"totalAmount": {"en": "Amount", "de": "Betrag", "fr": "Montant"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Buchungszeile (Sync)")
|
||||||
class TrusteeDataJournalLine(PowerOnModel):
|
class TrusteeDataJournalLine(PowerOnModel):
|
||||||
"""Journal entry line (debit/credit) synced from external accounting system."""
|
"""Journal entry line (debit/credit) synced from external accounting system."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
journalEntryId: str = Field(description="FK → TrusteeDataJournalEntry.id")
|
journalEntryId: str = Field(description="FK → TrusteeDataJournalEntry.id", json_schema_extra={"label": "Buchung"})
|
||||||
accountNumber: str = Field(description="Account number")
|
accountNumber: str = Field(description="Account number", json_schema_extra={"label": "Konto"})
|
||||||
debitAmount: float = Field(default=0.0)
|
debitAmount: float = Field(default=0.0, json_schema_extra={"label": "Soll"})
|
||||||
creditAmount: float = Field(default=0.0)
|
creditAmount: float = Field(default=0.0, json_schema_extra={"label": "Haben"})
|
||||||
currency: str = Field(default="CHF")
|
currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
|
||||||
taxCode: Optional[str] = Field(default=None)
|
taxCode: Optional[str] = Field(default=None, json_schema_extra={"label": "Steuercode"})
|
||||||
costCenter: Optional[str] = Field(default=None)
|
costCenter: Optional[str] = Field(default=None, json_schema_extra={"label": "Kostenstelle"})
|
||||||
description: str = Field(default="")
|
description: str = Field(default="", json_schema_extra={"label": "Beschreibung"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
featureInstanceId: Optional[str] = Field(default=None)
|
featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDataJournalLine",
|
|
||||||
{"en": "Journal Line (Synced)", "de": "Buchungszeile (Sync)", "fr": "Ligne écriture (Sync)"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"journalEntryId": {"en": "Journal Entry", "de": "Buchung", "fr": "Écriture"},
|
|
||||||
"accountNumber": {"en": "Account", "de": "Konto", "fr": "Compte"},
|
|
||||||
"debitAmount": {"en": "Debit", "de": "Soll", "fr": "Débit"},
|
|
||||||
"creditAmount": {"en": "Credit", "de": "Haben", "fr": "Crédit"},
|
|
||||||
"currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
|
|
||||||
"taxCode": {"en": "Tax Code", "de": "Steuercode", "fr": "Code TVA"},
|
|
||||||
"costCenter": {"en": "Cost Center", "de": "Kostenstelle", "fr": "Centre de coûts"},
|
|
||||||
"description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Kontakt (Sync)")
|
||||||
class TrusteeDataContact(PowerOnModel):
|
class TrusteeDataContact(PowerOnModel):
|
||||||
"""Customer or vendor synced from external accounting system."""
|
"""Customer or vendor synced from external accounting system."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
externalId: Optional[str] = Field(default=None, description="ID in the source system")
|
externalId: Optional[str] = Field(default=None, description="ID in the source system", json_schema_extra={"label": "Externe ID"})
|
||||||
contactType: str = Field(default="customer", description="customer / vendor / both")
|
contactType: str = Field(default="customer", description="customer / vendor / both", json_schema_extra={"label": "Typ"})
|
||||||
contactNumber: Optional[str] = Field(default=None, description="Customer/vendor number")
|
contactNumber: Optional[str] = Field(default=None, description="Customer/vendor number", json_schema_extra={"label": "Nummer"})
|
||||||
name: str = Field(default="", description="Name / company")
|
name: str = Field(default="", description="Name / company", json_schema_extra={"label": "Name"})
|
||||||
address: Optional[str] = Field(default=None)
|
address: Optional[str] = Field(default=None, json_schema_extra={"label": "Adresse"})
|
||||||
zip: Optional[str] = Field(default=None)
|
zip: Optional[str] = Field(default=None, json_schema_extra={"label": "PLZ"})
|
||||||
city: Optional[str] = Field(default=None)
|
city: Optional[str] = Field(default=None, json_schema_extra={"label": "Ort"})
|
||||||
country: Optional[str] = Field(default=None)
|
country: Optional[str] = Field(default=None, json_schema_extra={"label": "Land"})
|
||||||
email: Optional[str] = Field(default=None)
|
email: Optional[str] = Field(default=None, json_schema_extra={"label": "E-Mail"})
|
||||||
phone: Optional[str] = Field(default=None)
|
phone: Optional[str] = Field(default=None, json_schema_extra={"label": "Telefon"})
|
||||||
vatNumber: Optional[str] = Field(default=None)
|
vatNumber: Optional[str] = Field(default=None, json_schema_extra={"label": "MWST-Nr."})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
featureInstanceId: Optional[str] = Field(default=None)
|
featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDataContact",
|
|
||||||
{"en": "Contact (Synced)", "de": "Kontakt (Sync)", "fr": "Contact (Sync)"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
|
|
||||||
"contactType": {"en": "Type", "de": "Typ", "fr": "Type"},
|
|
||||||
"contactNumber": {"en": "Number", "de": "Nummer", "fr": "Numéro"},
|
|
||||||
"name": {"en": "Name", "de": "Name", "fr": "Nom"},
|
|
||||||
"address": {"en": "Address", "de": "Adresse", "fr": "Adresse"},
|
|
||||||
"zip": {"en": "ZIP", "de": "PLZ", "fr": "NPA"},
|
|
||||||
"city": {"en": "City", "de": "Ort", "fr": "Ville"},
|
|
||||||
"country": {"en": "Country", "de": "Land", "fr": "Pays"},
|
|
||||||
"email": {"en": "Email", "de": "E-Mail", "fr": "E-mail"},
|
|
||||||
"phone": {"en": "Phone", "de": "Telefon", "fr": "Téléphone"},
|
|
||||||
"vatNumber": {"en": "VAT Number", "de": "MWST-Nr.", "fr": "N° TVA"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Kontosaldo (Sync)")
|
||||||
class TrusteeDataAccountBalance(PowerOnModel):
|
class TrusteeDataAccountBalance(PowerOnModel):
|
||||||
"""Account balance per period, derived from journal lines or directly from accounting system."""
|
"""Account balance per period, derived from journal lines or directly from accounting system."""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
accountNumber: str = Field(description="Account number")
|
accountNumber: str = Field(description="Account number", json_schema_extra={"label": "Konto"})
|
||||||
periodYear: int = Field(description="Fiscal year")
|
periodYear: int = Field(description="Fiscal year", json_schema_extra={"label": "Jahr"})
|
||||||
periodMonth: int = Field(default=0, description="Month (1-12); 0 = annual total")
|
periodMonth: int = Field(default=0, description="Month (1-12); 0 = annual total", json_schema_extra={"label": "Monat"})
|
||||||
openingBalance: float = Field(default=0.0)
|
openingBalance: float = Field(default=0.0, json_schema_extra={"label": "Eröffnungssaldo"})
|
||||||
debitTotal: float = Field(default=0.0)
|
debitTotal: float = Field(default=0.0, json_schema_extra={"label": "Soll-Umsatz"})
|
||||||
creditTotal: float = Field(default=0.0)
|
creditTotal: float = Field(default=0.0, json_schema_extra={"label": "Haben-Umsatz"})
|
||||||
closingBalance: float = Field(default=0.0)
|
closingBalance: float = Field(default=0.0, json_schema_extra={"label": "Schlusssaldo"})
|
||||||
currency: str = Field(default="CHF")
|
currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
featureInstanceId: Optional[str] = Field(default=None)
|
featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeDataAccountBalance",
|
|
||||||
{"en": "Account Balance (Synced)", "de": "Kontosaldo (Sync)", "fr": "Solde compte (Sync)"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "de": "ID", "fr": "ID"},
|
|
||||||
"accountNumber": {"en": "Account", "de": "Konto", "fr": "Compte"},
|
|
||||||
"periodYear": {"en": "Year", "de": "Jahr", "fr": "Année"},
|
|
||||||
"periodMonth": {"en": "Month", "de": "Monat", "fr": "Mois"},
|
|
||||||
"openingBalance": {"en": "Opening Balance", "de": "Eröffnungssaldo", "fr": "Solde d'ouverture"},
|
|
||||||
"debitTotal": {"en": "Debit Total", "de": "Soll-Umsatz", "fr": "Total débit"},
|
|
||||||
"creditTotal": {"en": "Credit Total", "de": "Haben-Umsatz", "fr": "Total crédit"},
|
|
||||||
"closingBalance": {"en": "Closing Balance", "de": "Schlusssaldo", "fr": "Solde de clôture"},
|
|
||||||
"currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
|
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Buchhaltungs-Konfiguration")
|
||||||
class TrusteeAccountingConfig(PowerOnModel):
|
class TrusteeAccountingConfig(PowerOnModel):
|
||||||
"""Per-instance accounting system configuration with encrypted credentials.
|
"""Per-instance accounting system configuration with encrypted credentials.
|
||||||
|
|
||||||
Each feature instance can connect to exactly one accounting system.
|
Each feature instance can connect to exactly one accounting system.
|
||||||
Credentials are stored encrypted (decrypted at runtime by the AccountingBridge).
|
Credentials are stored encrypted (decrypted at runtime by the AccountingBridge).
|
||||||
"""
|
"""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
featureInstanceId: str = Field(description="FK -> FeatureInstance.id (1:1)")
|
featureInstanceId: str = Field(description="FK -> FeatureInstance.id (1:1)", json_schema_extra={"label": "Feature-Instanz"})
|
||||||
connectorType: str = Field(description="Connector type key, e.g. 'rma', 'bexio', 'abacus'")
|
connectorType: str = Field(description="Connector type key, e.g. 'rma', 'bexio', 'abacus'", json_schema_extra={"label": "System"})
|
||||||
displayLabel: str = Field(default="", description="User-visible label for this integration")
|
displayLabel: str = Field(default="", description="User-visible label for this integration", json_schema_extra={"label": "Bezeichnung"})
|
||||||
encryptedConfig: str = Field(default="", description="Encrypted JSON blob with connector credentials")
|
encryptedConfig: str = Field(default="", description="Encrypted JSON blob with connector credentials", json_schema_extra={"label": "Verschlüsselte Konfiguration"})
|
||||||
isActive: bool = Field(default=True)
|
isActive: bool = Field(default=True, json_schema_extra={"label": "Aktiv"})
|
||||||
lastSyncAt: Optional[float] = Field(default=None, description="Timestamp of last sync attempt")
|
lastSyncAt: Optional[float] = Field(default=None, description="Timestamp of last sync attempt", json_schema_extra={"label": "Letzte Synchronisation"})
|
||||||
lastSyncStatus: Optional[str] = Field(default=None, description="Last sync result: success, error, partial")
|
lastSyncStatus: Optional[str] = Field(default=None, description="Last sync result: success, error, partial", json_schema_extra={"label": "Status"})
|
||||||
lastSyncErrorMessage: Optional[str] = Field(default=None, description="Error message when lastSyncStatus is error")
|
lastSyncErrorMessage: Optional[str] = Field(default=None, description="Error message when lastSyncStatus is error", json_schema_extra={"label": "Fehlermeldung"})
|
||||||
cachedChartOfAccounts: Optional[str] = Field(default=None, description="JSON-serialised chart of accounts cache (list of {accountNumber, label, accountType})")
|
cachedChartOfAccounts: Optional[str] = Field(default=None, description="JSON-serialised chart of accounts cache (list of {accountNumber, label, accountType})", json_schema_extra={"label": "Cached Kontoplan"})
|
||||||
chartCachedAt: Optional[float] = Field(default=None, description="Timestamp when cachedChartOfAccounts was last refreshed")
|
chartCachedAt: Optional[float] = Field(default=None, description="Timestamp when cachedChartOfAccounts was last refreshed", json_schema_extra={"label": "Kontoplan-Cache-Zeitpunkt"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeAccountingConfig",
|
|
||||||
{"en": "Accounting Configuration", "de": "Buchhaltungs-Konfiguration", "fr": "Configuration comptable"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"featureInstanceId": {"en": "Feature Instance", "fr": "Instance", "de": "Feature-Instanz"},
|
|
||||||
"connectorType": {"en": "System", "fr": "Système", "de": "System"},
|
|
||||||
"displayLabel": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
|
|
||||||
"isActive": {"en": "Active", "fr": "Actif", "de": "Aktiv"},
|
|
||||||
"lastSyncAt": {"en": "Last Sync", "fr": "Dernière sync.", "de": "Letzte Synchronisation"},
|
|
||||||
"lastSyncStatus": {"en": "Status", "fr": "Statut", "de": "Status"},
|
|
||||||
"lastSyncErrorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehlermeldung"},
|
|
||||||
"cachedChartOfAccounts": {"en": "Cached Chart", "de": "Cached Kontoplan", "fr": "Plan comptable en cache"},
|
|
||||||
"chartCachedAt": {"en": "Chart Cached At", "de": "Kontoplan-Cache-Zeitpunkt", "fr": "Horodatage cache plan comptable"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Buchhaltungs-Synchronisation")
|
||||||
class TrusteeAccountingSync(PowerOnModel):
|
class TrusteeAccountingSync(PowerOnModel):
|
||||||
"""Tracks which position was synced to which external system and when.
|
"""Tracks which position was synced to which external system and when.
|
||||||
|
|
||||||
Used for duplicate prevention, audit trail, and retry logic.
|
Used for duplicate prevention, audit trail, and retry logic.
|
||||||
"""
|
"""
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
|
||||||
positionId: str = Field(description="FK -> TrusteePosition.id")
|
positionId: str = Field(description="FK -> TrusteePosition.id", json_schema_extra={"label": "Position"})
|
||||||
featureInstanceId: str = Field(description="FK -> FeatureInstance.id")
|
featureInstanceId: str = Field(description="FK -> FeatureInstance.id", json_schema_extra={"label": "Feature-Instanz"})
|
||||||
connectorType: str = Field(description="Connector type at time of sync")
|
connectorType: str = Field(description="Connector type at time of sync", json_schema_extra={"label": "System"})
|
||||||
externalId: Optional[str] = Field(default=None, description="ID assigned by the external system")
|
externalId: Optional[str] = Field(default=None, description="ID assigned by the external system", json_schema_extra={"label": "Externe ID"})
|
||||||
externalReference: Optional[str] = Field(default=None, description="Reference in the external system")
|
externalReference: Optional[str] = Field(default=None, description="Reference in the external system", json_schema_extra={"label": "Externe Referenz"})
|
||||||
syncStatus: str = Field(default="pending", description="pending | synced | error | cancelled")
|
syncStatus: str = Field(default="pending", description="pending | synced | error | cancelled", json_schema_extra={"label": "Status"})
|
||||||
syncDirection: str = Field(default="push", description="push (local->ext) or pull (ext->local)")
|
syncDirection: str = Field(default="push", description="push (local->ext) or pull (ext->local)", json_schema_extra={"label": "Richtung"})
|
||||||
syncedAt: Optional[float] = Field(default=None, description="Timestamp of successful sync")
|
syncedAt: Optional[float] = Field(default=None, description="Timestamp of successful sync", json_schema_extra={"label": "Synchronisiert am"})
|
||||||
errorMessage: Optional[str] = Field(default=None)
|
errorMessage: Optional[str] = Field(default=None, json_schema_extra={"label": "Fehler"})
|
||||||
bookingPayload: Optional[dict] = Field(default=None, description="Payload sent to the external system (audit)")
|
bookingPayload: Optional[dict] = Field(default=None, description="Payload sent to the external system (audit)", json_schema_extra={"label": "Buchungs-Payload"})
|
||||||
mandateId: Optional[str] = Field(default=None)
|
mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
|
||||||
|
|
||||||
|
|
||||||
registerModelLabels(
|
|
||||||
"TrusteeAccountingSync",
|
|
||||||
{"en": "Accounting Sync", "de": "Buchhaltungs-Synchronisation", "fr": "Synchronisation comptable"},
|
|
||||||
{
|
|
||||||
"id": {"en": "ID", "fr": "ID", "de": "ID"},
|
|
||||||
"positionId": {"en": "Position", "fr": "Position", "de": "Position"},
|
|
||||||
"connectorType": {"en": "System", "fr": "Système", "de": "System"},
|
|
||||||
"externalId": {"en": "External ID", "fr": "ID Externe", "de": "Externe ID"},
|
|
||||||
"syncStatus": {"en": "Status", "fr": "Statut", "de": "Status"},
|
|
||||||
"syncDirection": {"en": "Direction", "fr": "Direction", "de": "Richtung"},
|
|
||||||
"syncedAt": {"en": "Synced At", "fr": "Synchronisé à", "de": "Synchronisiert am"},
|
|
||||||
"errorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehler"},
|
|
||||||
"mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Feature metadata
|
# Feature metadata
|
||||||
FEATURE_CODE = "trustee"
|
FEATURE_CODE = "trustee"
|
||||||
FEATURE_LABEL = {"en": "Trustee", "de": "Treuhand", "fr": "Fiduciaire"}
|
FEATURE_LABEL = "Treuhand"
|
||||||
FEATURE_ICON = "mdi-briefcase"
|
FEATURE_ICON = "mdi-briefcase"
|
||||||
|
|
||||||
# UI Objects for RBAC catalog
|
# UI Objects for RBAC catalog
|
||||||
|
|
@ -20,37 +20,47 @@ FEATURE_ICON = "mdi-briefcase"
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.dashboard",
|
"objectKey": "ui.feature.trustee.dashboard",
|
||||||
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
"label": "Dashboard",
|
||||||
"meta": {"area": "dashboard"}
|
"meta": {"area": "dashboard"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.positions",
|
"objectKey": "ui.feature.trustee.positions",
|
||||||
"label": {"en": "Positions", "de": "Positionen", "fr": "Positions"},
|
"label": "Positionen",
|
||||||
"meta": {"area": "positions"}
|
"meta": {"area": "positions"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.documents",
|
"objectKey": "ui.feature.trustee.documents",
|
||||||
"label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
|
"label": "Dokumente",
|
||||||
"meta": {"area": "documents"}
|
"meta": {"area": "documents"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.expense-import",
|
"objectKey": "ui.feature.trustee.expense-import",
|
||||||
"label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
|
"label": "Spesen Import",
|
||||||
"meta": {"area": "expense-import"}
|
"meta": {"area": "expense-import"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.scan-upload",
|
"objectKey": "ui.feature.trustee.scan-upload",
|
||||||
"label": {"en": "Scan / Upload", "de": "Scannen / Hochladen", "fr": "Scanner / Téléverser"},
|
"label": "Scannen / Hochladen",
|
||||||
"meta": {"area": "scan-upload"}
|
"meta": {"area": "scan-upload"}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"objectKey": "ui.feature.trustee.analyse",
|
||||||
|
"label": "Analyse & Reporting",
|
||||||
|
"meta": {"area": "analyse"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectKey": "ui.feature.trustee.abschluss",
|
||||||
|
"label": "Abschluss & Prüfung",
|
||||||
|
"meta": {"area": "abschluss"}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.settings",
|
"objectKey": "ui.feature.trustee.settings",
|
||||||
"label": {"en": "Accounting Settings", "de": "Buchhaltungs-Einstellungen", "fr": "Paramètres comptables"},
|
"label": "Buchhaltungs-Einstellungen",
|
||||||
"meta": {"area": "settings", "admin_only": True}
|
"meta": {"area": "settings", "admin_only": True}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.trustee.instance-roles",
|
"objectKey": "ui.feature.trustee.instance-roles",
|
||||||
"label": {"en": "Instance Roles & Permissions", "de": "Instanz-Rollen & Berechtigungen", "fr": "Rôles et permissions d'instance"},
|
"label": "Instanz-Rollen & Berechtigungen",
|
||||||
"meta": {"area": "admin", "admin_only": True}
|
"meta": {"area": "admin", "admin_only": True}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -60,7 +70,7 @@ UI_OBJECTS = [
|
||||||
DATA_OBJECTS = [
|
DATA_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeOrganisation",
|
"objectKey": "data.feature.trustee.TrusteeOrganisation",
|
||||||
"label": {"en": "Organisation", "de": "Organisation", "fr": "Organisation"},
|
"label": "Organisation",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TrusteeOrganisation",
|
"table": "TrusteeOrganisation",
|
||||||
"fields": ["id", "label", "enabled"],
|
"fields": ["id", "label", "enabled"],
|
||||||
|
|
@ -70,7 +80,7 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteePosition",
|
"objectKey": "data.feature.trustee.TrusteePosition",
|
||||||
"label": {"en": "Position", "de": "Position", "fr": "Position"},
|
"label": "Position",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TrusteePosition",
|
"table": "TrusteePosition",
|
||||||
"fields": ["id", "label", "description", "organisationId"],
|
"fields": ["id", "label", "description", "organisationId"],
|
||||||
|
|
@ -80,12 +90,12 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDocument",
|
"objectKey": "data.feature.trustee.TrusteeDocument",
|
||||||
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
|
"label": "Dokument",
|
||||||
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
|
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeAccountingConfig",
|
"objectKey": "data.feature.trustee.TrusteeAccountingConfig",
|
||||||
"label": {"en": "Accounting Config", "de": "Buchhaltungs-Konfiguration", "fr": "Config. comptable"},
|
"label": "Buchhaltungs-Konfiguration",
|
||||||
"meta": {
|
"meta": {
|
||||||
"table": "TrusteeAccountingConfig",
|
"table": "TrusteeAccountingConfig",
|
||||||
"fields": ["id", "connectorType", "displayLabel", "encryptedConfig", "isActive"],
|
"fields": ["id", "connectorType", "displayLabel", "encryptedConfig", "isActive"],
|
||||||
|
|
@ -95,37 +105,37 @@ DATA_OBJECTS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeAccountingSync",
|
"objectKey": "data.feature.trustee.TrusteeAccountingSync",
|
||||||
"label": {"en": "Accounting Sync", "de": "Buchhaltungs-Synchronisation", "fr": "Sync. comptable"},
|
"label": "Buchhaltungs-Synchronisation",
|
||||||
"meta": {"table": "TrusteeAccountingSync", "fields": ["id", "positionId", "syncStatus", "externalId"]}
|
"meta": {"table": "TrusteeAccountingSync", "fields": ["id", "positionId", "syncStatus", "externalId"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDataAccount",
|
"objectKey": "data.feature.trustee.TrusteeDataAccount",
|
||||||
"label": {"en": "Accounts (Synced)", "de": "Kontenplan (Sync)", "fr": "Plan comptable (Sync)"},
|
"label": "Kontenplan (Sync)",
|
||||||
"meta": {"table": "TrusteeDataAccount", "fields": ["id", "accountNumber", "label", "accountType", "accountGroup", "currency", "isActive"]}
|
"meta": {"table": "TrusteeDataAccount", "fields": ["id", "accountNumber", "label", "accountType", "accountGroup", "currency", "isActive"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDataJournalEntry",
|
"objectKey": "data.feature.trustee.TrusteeDataJournalEntry",
|
||||||
"label": {"en": "Journal Entries (Synced)", "de": "Buchungen (Sync)", "fr": "Écritures (Sync)"},
|
"label": "Buchungen (Sync)",
|
||||||
"meta": {"table": "TrusteeDataJournalEntry", "fields": ["id", "externalId", "bookingDate", "reference", "description", "currency", "totalAmount"]}
|
"meta": {"table": "TrusteeDataJournalEntry", "fields": ["id", "externalId", "bookingDate", "reference", "description", "currency", "totalAmount"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDataJournalLine",
|
"objectKey": "data.feature.trustee.TrusteeDataJournalLine",
|
||||||
"label": {"en": "Journal Lines (Synced)", "de": "Buchungszeilen (Sync)", "fr": "Lignes écriture (Sync)"},
|
"label": "Buchungszeilen (Sync)",
|
||||||
"meta": {"table": "TrusteeDataJournalLine", "fields": ["id", "journalEntryId", "accountNumber", "debitAmount", "creditAmount", "currency", "taxCode", "costCenter", "description"]}
|
"meta": {"table": "TrusteeDataJournalLine", "fields": ["id", "journalEntryId", "accountNumber", "debitAmount", "creditAmount", "currency", "taxCode", "costCenter", "description"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDataContact",
|
"objectKey": "data.feature.trustee.TrusteeDataContact",
|
||||||
"label": {"en": "Contacts (Synced)", "de": "Kontakte (Sync)", "fr": "Contacts (Sync)"},
|
"label": "Kontakte (Sync)",
|
||||||
"meta": {"table": "TrusteeDataContact", "fields": ["id", "externalId", "contactType", "contactNumber", "name", "address", "zip", "city", "country", "email", "phone", "vatNumber"]}
|
"meta": {"table": "TrusteeDataContact", "fields": ["id", "externalId", "contactType", "contactNumber", "name", "address", "zip", "city", "country", "email", "phone", "vatNumber"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.TrusteeDataAccountBalance",
|
"objectKey": "data.feature.trustee.TrusteeDataAccountBalance",
|
||||||
"label": {"en": "Account Balances (Synced)", "de": "Kontosalden (Sync)", "fr": "Soldes comptes (Sync)"},
|
"label": "Kontosalden (Sync)",
|
||||||
"meta": {"table": "TrusteeDataAccountBalance", "fields": ["id", "accountNumber", "periodYear", "periodMonth", "openingBalance", "debitTotal", "creditTotal", "closingBalance", "currency"]}
|
"meta": {"table": "TrusteeDataAccountBalance", "fields": ["id", "accountNumber", "periodYear", "periodMonth", "openingBalance", "debitTotal", "creditTotal", "closingBalance", "currency"]}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "data.feature.trustee.*",
|
"objectKey": "data.feature.trustee.*",
|
||||||
"label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
|
"label": "Alle Treuhand-Daten",
|
||||||
"meta": {"wildcard": True, "description": "Wildcard for all trustee data tables"}
|
"meta": {"wildcard": True, "description": "Wildcard for all trustee data tables"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -135,127 +145,379 @@ DATA_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.documents.create",
|
"objectKey": "resource.feature.trustee.documents.create",
|
||||||
"label": {"en": "Upload Document", "de": "Dokument hochladen", "fr": "Télécharger document"},
|
"label": "Dokument hochladen",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/documents", "method": "POST"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/documents", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.documents.update",
|
"objectKey": "resource.feature.trustee.documents.update",
|
||||||
"label": {"en": "Update Document", "de": "Dokument aktualisieren", "fr": "Modifier document"},
|
"label": "Dokument aktualisieren",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "PUT"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "PUT"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.documents.delete",
|
"objectKey": "resource.feature.trustee.documents.delete",
|
||||||
"label": {"en": "Delete Document", "de": "Dokument löschen", "fr": "Supprimer document"},
|
"label": "Dokument löschen",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "DELETE"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "DELETE"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.positions.create",
|
"objectKey": "resource.feature.trustee.positions.create",
|
||||||
"label": {"en": "Create Position", "de": "Position erstellen", "fr": "Créer position"},
|
"label": "Position erstellen",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/positions", "method": "POST"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/positions", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.positions.update",
|
"objectKey": "resource.feature.trustee.positions.update",
|
||||||
"label": {"en": "Update Position", "de": "Position aktualisieren", "fr": "Modifier position"},
|
"label": "Position aktualisieren",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "PUT"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "PUT"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.positions.delete",
|
"objectKey": "resource.feature.trustee.positions.delete",
|
||||||
"label": {"en": "Delete Position", "de": "Position löschen", "fr": "Supprimer position"},
|
"label": "Position löschen",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "DELETE"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "DELETE"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.instance-roles.manage",
|
"objectKey": "resource.feature.trustee.instance-roles.manage",
|
||||||
"label": {"en": "Manage Instance Roles", "de": "Instanz-Rollen verwalten", "fr": "Gérer les rôles d'instance"},
|
"label": "Instanz-Rollen verwalten",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/instance-roles", "method": "ALL", "admin_only": True}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/instance-roles", "method": "ALL", "admin_only": True}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.accounting.manage",
|
"objectKey": "resource.feature.trustee.accounting.manage",
|
||||||
"label": {"en": "Manage Accounting Integration", "de": "Buchhaltungs-Integration verwalten", "fr": "Gérer l'intégration comptable"},
|
"label": "Buchhaltungs-Integration verwalten",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/config", "method": "ALL", "admin_only": True}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/config", "method": "ALL", "admin_only": True}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.accounting.sync",
|
"objectKey": "resource.feature.trustee.accounting.sync",
|
||||||
"label": {"en": "Sync to Accounting", "de": "Buchhaltung synchronisieren", "fr": "Synchroniser la comptabilité"},
|
"label": "Buchhaltung synchronisieren",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync", "method": "POST"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.trustee.accounting.view",
|
"objectKey": "resource.feature.trustee.accounting.view",
|
||||||
"label": {"en": "View Sync Status", "de": "Sync-Status einsehen", "fr": "Voir le statut de synchronisation"},
|
"label": "Sync-Status einsehen",
|
||||||
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync-status", "method": "GET"}
|
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync-status", "method": "GET"}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"objectKey": "resource.feature.trustee.workflows.view",
|
||||||
|
"label": "Workflows einsehen",
|
||||||
|
"meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "GET"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectKey": "resource.feature.trustee.workflows.execute",
|
||||||
|
"label": "Workflows ausführen",
|
||||||
|
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectKey": "resource.feature.trustee.workflows.manage",
|
||||||
|
"label": "Workflows verwalten",
|
||||||
|
"meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "ALL", "admin_only": True}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Template roles for this feature with AccessRules
|
# Template roles for this feature with AccessRules
|
||||||
# Each role defines default UI and DATA permissions
|
# Each role defines default UI and DATA permissions
|
||||||
# Note: UI item=None means ALL views, specific items restrict to named views
|
# Note: UI item=None means ALL views, specific items restrict to named views
|
||||||
# IMPORTANT: item uses vollqualifizierte ObjectKeys (gemäss Navigation-API-Konzept)
|
# IMPORTANT: item uses vollqualifizierte ObjectKeys (gemäss Navigation-API-Konzept)
|
||||||
|
QUICK_ACTION_CATEGORIES = [
|
||||||
|
{"id": "import", "label": "Import & Verarbeitung", "sortOrder": 1},
|
||||||
|
{"id": "analyse", "label": "Analyse & Reporting", "sortOrder": 2},
|
||||||
|
{"id": "abschluss", "label": "Abschluss & Prüfung", "sortOrder": 3},
|
||||||
|
]
|
||||||
|
|
||||||
|
QUICK_ACTIONS = [
|
||||||
|
{
|
||||||
|
"id": "trustee-process-receipts",
|
||||||
|
"label": "Belege verarbeiten",
|
||||||
|
"description": "Belege aus SharePoint importieren, klassifizieren und verbuchen",
|
||||||
|
"icon": "mdi-file-document-check-outline",
|
||||||
|
"color": "#4CAF50",
|
||||||
|
"category": "import",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "expense-import"},
|
||||||
|
"requiredRoles": ["trustee-user", "trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-sync-accounting",
|
||||||
|
"label": "Daten synchronisieren",
|
||||||
|
"description": "Buchhaltungsdaten aus dem externen System aktualisieren",
|
||||||
|
"icon": "mdi-sync",
|
||||||
|
"color": "#FF9800",
|
||||||
|
"category": "import",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "settings"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-upload-receipt",
|
||||||
|
"label": "Beleg hochladen",
|
||||||
|
"description": "Beleg scannen oder als Datei hochladen",
|
||||||
|
"icon": "mdi-camera-document-outline",
|
||||||
|
"color": "#607D8B",
|
||||||
|
"category": "import",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "scan-upload"},
|
||||||
|
"requiredRoles": ["trustee-user", "trustee-client", "trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-budget-comparison",
|
||||||
|
"label": "Budget-Vergleich",
|
||||||
|
"description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
|
||||||
|
"icon": "mdi-chart-bar",
|
||||||
|
"color": "#2196F3",
|
||||||
|
"category": "analyse",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "analyse", "tab": "budget"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-kpi-dashboard",
|
||||||
|
"label": "KPI-Dashboard",
|
||||||
|
"description": "Kennzahlen berechnen und visualisieren",
|
||||||
|
"icon": "mdi-view-dashboard-outline",
|
||||||
|
"color": "#9C27B0",
|
||||||
|
"category": "analyse",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "analyse", "tab": "kpi"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-cashflow",
|
||||||
|
"label": "Cashflow-Rechnung",
|
||||||
|
"description": "Cashflow berechnen und analysieren",
|
||||||
|
"icon": "mdi-cash-multiple",
|
||||||
|
"color": "#009688",
|
||||||
|
"category": "analyse",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "analyse", "tab": "cashflow"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-forecast",
|
||||||
|
"label": "Prognose erstellen",
|
||||||
|
"description": "Trend-Analyse und Prognose der nächsten Monate",
|
||||||
|
"icon": "mdi-chart-timeline-variant",
|
||||||
|
"color": "#E91E63",
|
||||||
|
"category": "analyse",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "analyse", "tab": "forecast"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-year-end-check",
|
||||||
|
"label": "Jahresabschluss prüfen",
|
||||||
|
"description": "Automatische Prüfungen für den Jahresabschluss",
|
||||||
|
"icon": "mdi-clipboard-check-outline",
|
||||||
|
"color": "#795548",
|
||||||
|
"category": "abschluss",
|
||||||
|
"actionType": "link",
|
||||||
|
"config": {"targetView": "abschluss", "tab": "year-end"},
|
||||||
|
"requiredRoles": ["trustee-accountant", "trustee-admin"],
|
||||||
|
"sortOrder": 8,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Template Workflows — bootstrapped into each new feature instance.
|
||||||
|
# Graphs use existing nodes: trigger.manual, trustee.refreshAccountingData, ai.prompt.
|
||||||
|
# The placeholder {{featureInstanceId}} is replaced by _copyTemplateWorkflows.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _buildAnalysisWorkflowGraph(prompt: str) -> Dict[str, Any]:
|
||||||
|
"""Build a standard analysis graph: trigger → refreshAccountingData → ai.prompt."""
|
||||||
|
return {
|
||||||
|
"nodes": [
|
||||||
|
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
|
||||||
|
{"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData",
|
||||||
|
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}},
|
||||||
|
{"id": "analyse", "type": "ai.prompt", "label": "Analyse", "_method": "ai", "_action": "process",
|
||||||
|
"parameters": {"prompt": prompt, "simpleMode": False}, "position": {"x": 500, "y": 0}},
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
|
||||||
|
{"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_WORKFLOWS = [
|
||||||
|
{
|
||||||
|
"id": "trustee-receipt-import",
|
||||||
|
"label": "Beleg-Import Pipeline",
|
||||||
|
"description": "Belege extrahieren, verarbeiten und in Buchhaltung synchronisieren",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-receipt-import"],
|
||||||
|
"graph": {
|
||||||
|
"nodes": [
|
||||||
|
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
|
||||||
|
{"id": "extract", "type": "trustee.extractFromFiles", "label": "Dokumente extrahieren", "_method": "trustee", "_action": "extractFromFiles",
|
||||||
|
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "prompt": ""}, "position": {"x": 250, "y": 0}},
|
||||||
|
{"id": "process", "type": "trustee.processDocuments", "label": "Verarbeiten", "_method": "trustee", "_action": "processDocuments",
|
||||||
|
"parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 500, "y": 0}},
|
||||||
|
{"id": "sync", "type": "trustee.syncToAccounting", "label": "Synchronisieren", "_method": "trustee", "_action": "syncToAccounting",
|
||||||
|
"parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 750, "y": 0}},
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"source": "trigger", "sourcePort": 0, "target": "extract", "targetPort": 0},
|
||||||
|
{"source": "extract", "sourcePort": 0, "target": "process", "targetPort": 0},
|
||||||
|
{"source": "process", "sourcePort": 0, "target": "sync", "targetPort": 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-sync-accounting",
|
||||||
|
"label": "Buchhaltung synchronisieren",
|
||||||
|
"description": "Buchhaltungsdaten aus dem externen System aktualisieren",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-sync-accounting"],
|
||||||
|
"graph": {
|
||||||
|
"nodes": [
|
||||||
|
{"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
|
||||||
|
{"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten aktualisieren", "_method": "trustee", "_action": "refreshAccountingData",
|
||||||
|
"parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": True}, "position": {"x": 250, "y": 0}},
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-budget-comparison",
|
||||||
|
"label": "Budget-Vergleich",
|
||||||
|
"description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-budget-comparison"],
|
||||||
|
"graph": _buildAnalysisWorkflowGraph(
|
||||||
|
"Ich möchte einen Budget-Soll/Ist-Vergleich durchführen. Bitte:\n"
|
||||||
|
"1. Frage mich nach der Budget-Datei (Excel) oder suche im Workspace nach einer Datei mit 'Budget' im Namen\n"
|
||||||
|
"2. Lade die aktuellen Buchhaltungsdaten (refreshTrusteeData falls nötig)\n"
|
||||||
|
"3. Vergleiche die Soll-Werte aus dem Budget mit den Ist-Werten aus der Buchhaltung pro Konto\n"
|
||||||
|
"4. Berechne die Abweichung (absolut und prozentual)\n"
|
||||||
|
"5. Erstelle ein Abweichungs-Chart (Balkendiagramm: Soll vs. Ist pro Konto)\n"
|
||||||
|
"6. Markiere kritische Abweichungen (>10%) und gib eine kurze Einschätzung"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-kpi-dashboard",
|
||||||
|
"label": "KPI-Dashboard",
|
||||||
|
"description": "Kennzahlen berechnen und visualisieren",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-kpi-dashboard"],
|
||||||
|
"graph": _buildAnalysisWorkflowGraph(
|
||||||
|
"Erstelle ein KPI-Dashboard basierend auf den aktuellen Buchhaltungsdaten. Berechne und visualisiere:\n"
|
||||||
|
"1. Bruttogewinn und Bruttogewinnmarge\n"
|
||||||
|
"2. EBIT (Betriebsergebnis)\n"
|
||||||
|
"3. Gewinnmarge (Reingewinn / Umsatz)\n"
|
||||||
|
"4. Eigenkapitalquote und Check auf hälftigen Kapitalverlust (OR Art. 725)\n"
|
||||||
|
"5. Liquiditätsgrad 1-3 (Cash Ratio, Quick Ratio, Current Ratio)\n"
|
||||||
|
"6. Überschuldungs-Check\n\n"
|
||||||
|
"Erstelle für jede Kennzahl einen kurzen Kommentar (gut/kritisch/Handlungsbedarf). "
|
||||||
|
"Erstelle mindestens 2 Charts: ein Übersichts-Chart der Margen und ein Liquiditäts-Chart."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-cashflow",
|
||||||
|
"label": "Cashflow-Rechnung",
|
||||||
|
"description": "Cashflow berechnen und analysieren",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-cashflow"],
|
||||||
|
"graph": _buildAnalysisWorkflowGraph(
|
||||||
|
"Erstelle eine Cashflow-Rechnung basierend auf den aktuellen Buchhaltungsdaten:\n"
|
||||||
|
"1. Operativer Cashflow: Starte vom Reingewinn, bereinige um nicht-cash-wirksame Positionen\n"
|
||||||
|
"2. Investitions-Cashflow: Investitionen in Sachanlagen, Finanzanlagen\n"
|
||||||
|
"3. Finanzierungs-Cashflow: Darlehensaufnahmen/-rückzahlungen, Dividenden, Kapitalerhöhungen\n"
|
||||||
|
"4. Netto-Cashflow und Veränderung der liquiden Mittel\n\n"
|
||||||
|
"Warne bei kritischen Werten. Erstelle ein Wasserfall-Chart oder gestapeltes Balkendiagramm."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-forecast",
|
||||||
|
"label": "Prognose erstellen",
|
||||||
|
"description": "Trend-Analyse und Prognose der nächsten Monate",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-forecast"],
|
||||||
|
"graph": _buildAnalysisWorkflowGraph(
|
||||||
|
"Erstelle eine Finanzprognose basierend auf den historischen Buchhaltungsdaten:\n"
|
||||||
|
"1. Analysiere die Umsatz- und Aufwandsentwicklung der letzten 6 Monate\n"
|
||||||
|
"2. Identifiziere Trends und Saisonalitäten\n"
|
||||||
|
"3. Prognostiziere Umsatz, Aufwand und Gewinn für die nächsten 3 Monate\n"
|
||||||
|
"4. Erstelle ein Chart mit Ist-Werten und Prognose-Korridor\n"
|
||||||
|
"5. Markiere Risiken\n\n"
|
||||||
|
"Nutze eine einfache lineare Extrapolation mit Saisonalitätskorrektur wo sinnvoll."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trustee-year-end-check",
|
||||||
|
"label": "Jahresabschluss prüfen",
|
||||||
|
"description": "Automatische Prüfungen für den Jahresabschluss",
|
||||||
|
"tags": ["feature:trustee", "template:trustee-year-end-check"],
|
||||||
|
"graph": _buildAnalysisWorkflowGraph(
|
||||||
|
"Führe eine automatische Jahresabschluss-Prüfung durch:\n"
|
||||||
|
"1. Saldovalidierung: Prüfe alle Bilanzkonten auf Plausibilität\n"
|
||||||
|
"2. Vorjahresvergleich: Vergleiche Bilanz- und ER-Positionen mit dem Vorjahr, markiere Abweichungen >20%\n"
|
||||||
|
"3. Abgrenzungen: Identifiziere potenzielle transitorische Aktiven/Passiven\n"
|
||||||
|
"4. Gesetzliche Prüfungen: Hälftiger Kapitalverlust (OR 725), Überschuldung, Mindestkapital\n"
|
||||||
|
"5. MWST-Plausibilisierung: Vorsteuer vs. geschätzter Aufwand, Umsatzsteuer vs. Umsatz\n\n"
|
||||||
|
"Erstelle eine Checkliste mit Status (OK / Warnung / Kritisch) pro Prüfpunkt."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-viewer",
|
"roleLabel": "trustee-viewer",
|
||||||
"description": {
|
"description": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)",
|
||||||
"en": "Trustee Viewer - View trustee data (read-only)",
|
|
||||||
"de": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur fiduciaire - Consulter les données fiduciaires (lecture seule)",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-user",
|
"roleLabel": "trustee-user",
|
||||||
"description": {
|
"description": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten",
|
||||||
"en": "Trustee User - Create and manage own trustee records",
|
|
||||||
"de": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten",
|
|
||||||
"fr": "Utilisateur fiduciaire - Créer et gérer ses propres données fiduciaires",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-admin",
|
"roleLabel": "trustee-admin",
|
||||||
"description": {
|
"description": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
|
||||||
"en": "Trustee Administrator - Full access to all trustee data and settings",
|
|
||||||
"de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
|
|
||||||
"fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
|
||||||
{"context": "RESOURCE", "item": "resource.feature.trustee.instance-roles.manage", "view": True},
|
{"context": "RESOURCE", "item": "resource.feature.trustee.instance-roles.manage", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.manage", "view": True},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-accountant",
|
"roleLabel": "trustee-accountant",
|
||||||
"description": {
|
"description": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
|
||||||
"en": "Trustee Accountant - Manage accounting and financial data",
|
|
||||||
"de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
|
|
||||||
"fr": "Comptable fiduciaire - Gérer les données comptables et financières",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
|
||||||
|
{"context": "UI", "item": "ui.feature.trustee.analyse", "view": True},
|
||||||
|
{"context": "UI", "item": "ui.feature.trustee.abschluss", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.settings", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.settings", "view": True},
|
||||||
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
|
||||||
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.sync", "view": True},
|
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.sync", "view": True},
|
||||||
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.view", "view": True},
|
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.view", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
|
||||||
|
{"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "trustee-client",
|
"roleLabel": "trustee-client",
|
||||||
"description": {
|
"description": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
|
||||||
"en": "Trustee Client - View own accounting data and documents",
|
|
||||||
"de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
|
|
||||||
"fr": "Client fiduciaire - Consulter ses propres données comptables et documents",
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
|
||||||
|
|
@ -293,6 +555,21 @@ def getTemplateRoles() -> List[Dict[str, Any]]:
|
||||||
return TEMPLATE_ROLES
|
return TEMPLATE_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
def getTemplateWorkflows() -> List[Dict[str, Any]]:
|
||||||
|
"""Return template workflow definitions for bootstrap on instance creation."""
|
||||||
|
return TEMPLATE_WORKFLOWS
|
||||||
|
|
||||||
|
|
||||||
|
def getQuickActions() -> List[Dict[str, Any]]:
|
||||||
|
"""Return quick action definitions for the Trustee dashboard."""
|
||||||
|
return QUICK_ACTIONS
|
||||||
|
|
||||||
|
|
||||||
|
def getQuickActionCategories() -> List[Dict[str, Any]]:
|
||||||
|
"""Return quick action category definitions."""
|
||||||
|
return QUICK_ACTION_CATEGORIES
|
||||||
|
|
||||||
|
|
||||||
def getDataObjects() -> List[Dict[str, Any]]:
|
def getDataObjects() -> List[Dict[str, Any]]:
|
||||||
"""Return DATA objects for RBAC catalog registration."""
|
"""Return DATA objects for RBAC catalog registration."""
|
||||||
return DATA_OBJECTS
|
return DATA_OBJECTS
|
||||||
|
|
@ -358,7 +635,8 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get existing template roles for this feature (Pydantic models)
|
# Get existing template roles for this feature (Pydantic models)
|
||||||
|
|
@ -378,7 +656,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
# Create new template role
|
# Create new template role
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None, # Global template
|
mandateId=None, # Global template
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ from modules.datamodels.datamodelPagination import (
|
||||||
PaginationMetadata,
|
PaginationMetadata,
|
||||||
normalize_pagination_dict,
|
normalize_pagination_dict,
|
||||||
)
|
)
|
||||||
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureTrustee")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -116,6 +120,78 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
|
||||||
return str(instance.mandateId)
|
return str(instance.mandateId)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# QUICK ACTIONS ENDPOINT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/quick-actions")
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
def getQuickActions(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str = Path(..., description="Feature Instance ID"),
|
||||||
|
language: str = Query(default="de", description="Language code for labels"),
|
||||||
|
context: RequestContext = Depends(getRequestContext),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Return RBAC-filtered quick actions for the Trustee dashboard."""
|
||||||
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
|
from .mainTrustee import QUICK_ACTIONS, QUICK_ACTION_CATEGORIES
|
||||||
|
|
||||||
|
userRoleLabels: set = set()
|
||||||
|
if context.hasSysAdminRole:
|
||||||
|
userRoleLabels.add("trustee-admin")
|
||||||
|
else:
|
||||||
|
rootInterface = getRootInterface()
|
||||||
|
featureAccesses = rootInterface.getFeatureAccessesForUser(str(context.user.id))
|
||||||
|
for fa in featureAccesses:
|
||||||
|
if str(fa.featureInstanceId) == instanceId and fa.enabled:
|
||||||
|
roleIds = fa.roleIds if hasattr(fa, "roleIds") and fa.roleIds else []
|
||||||
|
for rid in roleIds:
|
||||||
|
role = rootInterface.getRole(str(rid))
|
||||||
|
if role and role.roleLabel:
|
||||||
|
userRoleLabels.add(role.roleLabel)
|
||||||
|
|
||||||
|
def _resolveText(multilingual, lang: str) -> str:
|
||||||
|
if isinstance(multilingual, str):
|
||||||
|
return multilingual
|
||||||
|
if isinstance(multilingual, dict):
|
||||||
|
return multilingual.get(lang) or multilingual.get("en") or multilingual.get("de") or next(iter(multilingual.values()), "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
filteredActions = []
|
||||||
|
for action in QUICK_ACTIONS:
|
||||||
|
required = set(action.get("requiredRoles", []))
|
||||||
|
if not userRoleLabels and not context.hasSysAdminRole:
|
||||||
|
continue
|
||||||
|
if context.hasSysAdminRole or required.intersection(userRoleLabels):
|
||||||
|
resolved = {
|
||||||
|
"id": action["id"],
|
||||||
|
"label": _resolveText(action.get("label", {}), language),
|
||||||
|
"description": _resolveText(action.get("description", {}), language),
|
||||||
|
"icon": action.get("icon", ""),
|
||||||
|
"color": action.get("color", ""),
|
||||||
|
"category": action.get("category", ""),
|
||||||
|
"actionType": action.get("actionType", ""),
|
||||||
|
"config": action.get("config", {}),
|
||||||
|
"sortOrder": action.get("sortOrder", 99),
|
||||||
|
}
|
||||||
|
if resolved["actionType"] == "agentPrompt" and "config" in resolved:
|
||||||
|
cfg = dict(resolved["config"])
|
||||||
|
if "uploadHint" in cfg:
|
||||||
|
cfg["uploadHint"] = _resolveText(cfg["uploadHint"], language)
|
||||||
|
resolved["config"] = cfg
|
||||||
|
filteredActions.append(resolved)
|
||||||
|
|
||||||
|
filteredActions.sort(key=lambda a: a["sortOrder"])
|
||||||
|
|
||||||
|
resolvedCategories = [
|
||||||
|
{"id": c["id"], "label": _resolveText(c.get("label", {}), language), "sortOrder": c.get("sortOrder", 99)}
|
||||||
|
for c in QUICK_ACTION_CATEGORIES
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"actions": filteredActions, "categories": resolvedCategories}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ATTRIBUTES ENDPOINT (for FormGeneratorTable)
|
# ATTRIBUTES ENDPOINT (for FormGeneratorTable)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -385,7 +461,7 @@ def create_organisation(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
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=routeApiMsg("Failed to create organisation"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -408,7 +484,7 @@ def update_organisation(
|
||||||
|
|
||||||
result = interface.updateOrganisation(orgId, data.model_dump(exclude={"id"}))
|
result = interface.updateOrganisation(orgId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update organisation")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update organisation"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -430,7 +506,7 @@ def delete_organisation(
|
||||||
|
|
||||||
success = interface.deleteOrganisation(orgId)
|
success = interface.deleteOrganisation(orgId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete organisation")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete organisation"))
|
||||||
return {"message": f"Organisation {orgId} deleted"}
|
return {"message": f"Organisation {orgId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -498,7 +574,7 @@ def create_role(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
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=routeApiMsg("Failed to create role"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -521,7 +597,7 @@ def update_role(
|
||||||
|
|
||||||
result = interface.updateRole(roleId, data.model_dump(exclude={"id"}))
|
result = interface.updateRole(roleId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update role")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update role"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -543,7 +619,7 @@ def delete_role(
|
||||||
|
|
||||||
success = interface.deleteRole(roleId)
|
success = interface.deleteRole(roleId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete role (may be in use)")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete role (may be in use)"))
|
||||||
return {"message": f"Role {roleId} deleted"}
|
return {"message": f"Role {roleId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -641,7 +717,7 @@ def create_access(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
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=routeApiMsg("Failed to create access"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -664,7 +740,7 @@ def update_access(
|
||||||
|
|
||||||
result = interface.updateAccess(accessId, data.model_dump(exclude={"id"}))
|
result = interface.updateAccess(accessId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update access")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update access"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -686,7 +762,7 @@ def delete_access(
|
||||||
|
|
||||||
success = interface.deleteAccess(accessId)
|
success = interface.deleteAccess(accessId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete access")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete access"))
|
||||||
return {"message": f"Access {accessId} deleted"}
|
return {"message": f"Access {accessId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -769,7 +845,7 @@ def create_contract(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
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=routeApiMsg("Failed to create contract"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -792,7 +868,7 @@ def update_contract(
|
||||||
|
|
||||||
result = interface.updateContract(contractId, data.model_dump(exclude={"id"}))
|
result = interface.updateContract(contractId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update contract (organisationId cannot be changed)")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update contract (organisationId cannot be changed)"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -814,7 +890,7 @@ def delete_contract(
|
||||||
|
|
||||||
success = interface.deleteContract(contractId)
|
success = interface.deleteContract(contractId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete contract")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete contract"))
|
||||||
return {"message": f"Contract {contractId} deleted"}
|
return {"message": f"Contract {contractId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -938,7 +1014,7 @@ def get_document_data(
|
||||||
|
|
||||||
data = interface.getDocumentData(documentId)
|
data = interface.getDocumentData(documentId)
|
||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(status_code=404, detail="Document data not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Document data not found"))
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
io.BytesIO(data),
|
io.BytesIO(data),
|
||||||
|
|
@ -995,7 +1071,7 @@ async def create_document(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.createDocument(body)
|
result = interface.createDocument(body)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create document"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1025,7 +1101,7 @@ async def upload_document(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||||
result = interface.createDocument(docData)
|
result = interface.createDocument(docData)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to create document")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create document"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1048,7 +1124,7 @@ def update_document(
|
||||||
|
|
||||||
result = interface.updateDocument(documentId, data.model_dump(exclude={"id"}))
|
result = interface.updateDocument(documentId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update document")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update document"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1070,7 +1146,7 @@ def delete_document(
|
||||||
|
|
||||||
success = interface.deleteDocument(documentId)
|
success = interface.deleteDocument(documentId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete document")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete document"))
|
||||||
return {"message": f"Document {documentId} deleted"}
|
return {"message": f"Document {documentId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1220,7 +1296,7 @@ def create_position(
|
||||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
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=routeApiMsg("Failed to create position"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1243,7 +1319,7 @@ def update_position(
|
||||||
|
|
||||||
result = interface.updatePosition(positionId, data.model_dump(exclude={"id"}))
|
result = interface.updatePosition(positionId, data.model_dump(exclude={"id"}))
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=400, detail="Failed to update position")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update position"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1265,7 +1341,7 @@ def delete_position(
|
||||||
|
|
||||||
success = interface.deletePosition(positionId)
|
success = interface.deletePosition(positionId)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to delete position")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete position"))
|
||||||
return {"message": f"Position {positionId} deleted"}
|
return {"message": f"Position {positionId} deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1398,7 +1474,7 @@ async def save_accounting_config(
|
||||||
if not plainConfig:
|
if not plainConfig:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="config is required for new integration (e.g. clientName, apiKey)."
|
detail=routeApiMsg("config is required for new integration (e.g. clientName, apiKey).")
|
||||||
)
|
)
|
||||||
encryptedConfig = encryptValue(json.dumps(plainConfig), keyName="accountingConfig")
|
encryptedConfig = encryptValue(json.dumps(plainConfig), keyName="accountingConfig")
|
||||||
|
|
||||||
|
|
@ -1511,7 +1587,7 @@ async def sync_positions_to_accounting(
|
||||||
|
|
||||||
positionIds = data.get("positionIds", [])
|
positionIds = data.get("positionIds", [])
|
||||||
if not positionIds:
|
if not positionIds:
|
||||||
raise HTTPException(status_code=400, detail="positionIds required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("positionIds required"))
|
||||||
|
|
||||||
results = await bridge.pushBatchToAccounting(instanceId, positionIds)
|
results = await bridge.pushBatchToAccounting(instanceId, positionIds)
|
||||||
failed = [r for r in results if not r.success]
|
failed = [r for r in results if not r.success]
|
||||||
|
|
@ -1678,8 +1754,6 @@ def get_positions_by_document(
|
||||||
# ===== Instance Roles Management =====
|
# ===== Instance Roles Management =====
|
||||||
# These endpoints allow feature admins to manage instance-specific roles and their AccessRules
|
# These endpoints allow feature admins to manage instance-specific roles and their AccessRules
|
||||||
|
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
|
||||||
|
|
||||||
|
|
||||||
def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
|
def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1711,7 +1785,7 @@ def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
|
||||||
if not hasAdminPermission:
|
if not hasAdminPermission:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Keine Berechtigung zur Rollenverwaltung"
|
detail=routeApiMsg("Keine Berechtigung zur Rollenverwaltung")
|
||||||
)
|
)
|
||||||
|
|
||||||
return mandateId
|
return mandateId
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,32 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.datamodels.datamodelBase import PowerOnModel
|
from modules.datamodels.datamodelBase import PowerOnModel
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.i18nRegistry import i18nModel
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
@i18nModel("Workspace Benutzereinstellungen")
|
||||||
class WorkspaceUserSettings(PowerOnModel):
|
class WorkspaceUserSettings(PowerOnModel):
|
||||||
"""Per-user workspace settings. None values mean 'use instance default'."""
|
"""Benutzerspezifische Workspace-Einstellungen. None = Instanz-Standard."""
|
||||||
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(
|
||||||
userId: str = Field(description="User ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
mandateId: str = Field(description="Mandate ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
description="Primary key",
|
||||||
featureInstanceId: str = Field(description="Feature Instance ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
|
json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
|
||||||
maxAgentRounds: Optional[int] = Field(default=None, description="Max agent rounds override (None = instance default)", json_schema_extra={"frontend_type": "number", "frontend_readonly": False, "frontend_required": False})
|
)
|
||||||
|
userId: str = Field(
|
||||||
|
description="User ID",
|
||||||
registerModelLabels(
|
json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
"WorkspaceUserSettings",
|
)
|
||||||
{"en": "Workspace User Settings", "de": "Workspace Benutzereinstellungen"},
|
mandateId: str = Field(
|
||||||
{
|
description="Mandate ID",
|
||||||
"id": {"en": "ID", "de": "ID"},
|
json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
"userId": {"en": "User ID", "de": "Benutzer-ID"},
|
)
|
||||||
"mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
|
featureInstanceId: str = Field(
|
||||||
"featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID"},
|
description="Feature Instance ID",
|
||||||
"maxAgentRounds": {"en": "Max Agent Rounds", "de": "Max. Agenten-Runden"},
|
json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
|
||||||
},
|
)
|
||||||
)
|
maxAgentRounds: Optional[int] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Max agent rounds override (None = instance default)",
|
||||||
|
json_schema_extra={"label": "Max. Agenten-Runden", "frontend_type": "number", "frontend_readonly": False, "frontend_required": False},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,32 +12,28 @@ from typing import Dict, List, Any
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
FEATURE_CODE = "workspace"
|
FEATURE_CODE = "workspace"
|
||||||
FEATURE_LABEL = {"en": "AI Workspace", "de": "AI Workspace", "fr": "AI Workspace"}
|
FEATURE_LABEL = "AI Workspace"
|
||||||
FEATURE_ICON = "mdi-brain"
|
FEATURE_ICON = "mdi-brain"
|
||||||
|
|
||||||
UI_OBJECTS = [
|
UI_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.workspace.dashboard",
|
"objectKey": "ui.feature.workspace.dashboard",
|
||||||
"label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
|
"label": "Dashboard",
|
||||||
"meta": {"area": "dashboard"}
|
"meta": {"area": "dashboard"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.workspace.editor",
|
"objectKey": "ui.feature.workspace.editor",
|
||||||
"label": {"en": "Editor", "de": "Editor", "fr": "Editeur"},
|
"label": "Editor",
|
||||||
"meta": {"area": "editor"}
|
"meta": {"area": "editor"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.workspace.settings",
|
"objectKey": "ui.feature.workspace.settings",
|
||||||
"label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"},
|
"label": "Einstellungen",
|
||||||
"meta": {"area": "settings"}
|
"meta": {"area": "settings"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "ui.feature.workspace.rag-insights",
|
"objectKey": "ui.feature.workspace.rag-insights",
|
||||||
"label": {
|
"label": "Wissens-Insights",
|
||||||
"en": "Knowledge insights",
|
|
||||||
"de": "Wissens-Insights",
|
|
||||||
"fr": "Aperçu des connaissances",
|
|
||||||
},
|
|
||||||
"meta": {"area": "rag-insights"},
|
"meta": {"area": "rag-insights"},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -45,37 +41,37 @@ UI_OBJECTS = [
|
||||||
RESOURCE_OBJECTS = [
|
RESOURCE_OBJECTS = [
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.start",
|
"objectKey": "resource.feature.workspace.start",
|
||||||
"label": {"en": "Start Agent", "de": "Agent starten", "fr": "Demarrer agent"},
|
"label": "Agent starten",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/start/stream", "method": "POST"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/start/stream", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.stop",
|
"objectKey": "resource.feature.workspace.stop",
|
||||||
"label": {"en": "Stop Agent", "de": "Agent stoppen", "fr": "Arreter agent"},
|
"label": "Agent stoppen",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/{workflowId}/stop", "method": "POST"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/{workflowId}/stop", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.files",
|
"objectKey": "resource.feature.workspace.files",
|
||||||
"label": {"en": "Manage Files", "de": "Dateien verwalten", "fr": "Gerer fichiers"},
|
"label": "Dateien verwalten",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/files", "method": "GET"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/files", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.folders",
|
"objectKey": "resource.feature.workspace.folders",
|
||||||
"label": {"en": "Manage Folders", "de": "Ordner verwalten", "fr": "Gerer dossiers"},
|
"label": "Ordner verwalten",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/folders", "method": "GET"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/folders", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.datasources",
|
"objectKey": "resource.feature.workspace.datasources",
|
||||||
"label": {"en": "Data Sources", "de": "Datenquellen", "fr": "Sources de donnees"},
|
"label": "Datenquellen",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/datasources", "method": "GET"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/datasources", "method": "GET"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.voice",
|
"objectKey": "resource.feature.workspace.voice",
|
||||||
"label": {"en": "Voice Input/Output", "de": "Spracheingabe/-ausgabe", "fr": "Entree/sortie vocale"},
|
"label": "Spracheingabe/-ausgabe",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/voice/*", "method": "POST"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/voice/*", "method": "POST"}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"objectKey": "resource.feature.workspace.edits",
|
"objectKey": "resource.feature.workspace.edits",
|
||||||
"label": {"en": "Review File Edits", "de": "Datei-Aenderungen pruefen", "fr": "Verifier les modifications de fichiers"},
|
"label": "Datei-Aenderungen pruefen",
|
||||||
"meta": {"endpoint": "/api/workspace/{instanceId}/edit/*", "method": "POST"}
|
"meta": {"endpoint": "/api/workspace/{instanceId}/edit/*", "method": "POST"}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -83,11 +79,7 @@ RESOURCE_OBJECTS = [
|
||||||
TEMPLATE_ROLES = [
|
TEMPLATE_ROLES = [
|
||||||
{
|
{
|
||||||
"roleLabel": "workspace-viewer",
|
"roleLabel": "workspace-viewer",
|
||||||
"description": {
|
"description": "Workspace Betrachter - Workspace ansehen (nur lesen)",
|
||||||
"en": "Workspace Viewer - View workspace (read-only)",
|
|
||||||
"de": "Workspace Betrachter - Workspace ansehen (nur lesen)",
|
|
||||||
"fr": "Visualiseur Workspace - Consulter le workspace (lecture seule)"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
|
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
|
||||||
|
|
@ -98,11 +90,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "workspace-user",
|
"roleLabel": "workspace-user",
|
||||||
"description": {
|
"description": "Workspace Benutzer - AI Workspace und Tools nutzen",
|
||||||
"en": "Workspace User - Use AI workspace and tools",
|
|
||||||
"de": "Workspace Benutzer - AI Workspace und Tools nutzen",
|
|
||||||
"fr": "Utilisateur Workspace - Utiliser l'espace de travail AI et les outils"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
|
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
|
||||||
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
|
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
|
||||||
|
|
@ -120,11 +108,7 @@ TEMPLATE_ROLES = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"roleLabel": "workspace-admin",
|
"roleLabel": "workspace-admin",
|
||||||
"description": {
|
"description": "Workspace Admin - Alle UI- und API-Aktionen; Daten immer nur eigene Datensätze (gleiche Privatsphäre wie User)",
|
||||||
"en": "Workspace Admin - All UI and API actions; data is always scoped to own records (same privacy as users)",
|
|
||||||
"de": "Workspace Admin - Alle UI- und API-Aktionen; Daten immer nur eigene Datensätze (gleiche Privatsphäre wie User)",
|
|
||||||
"fr": "Administrateur Workspace - Toute l'UI et les API; donnees limitees a ses propres enregistrements"
|
|
||||||
},
|
|
||||||
"accessRules": [
|
"accessRules": [
|
||||||
{"context": "UI", "item": None, "view": True},
|
{"context": "UI", "item": None, "view": True},
|
||||||
{"context": "RESOURCE", "item": None, "view": True},
|
{"context": "RESOURCE", "item": None, "view": True},
|
||||||
|
|
@ -194,6 +178,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -211,7 +196,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleTemplate.get("description", {}),
|
description=coerce_text_multilingual(roleTemplate.get("description", {})),
|
||||||
featureCode=FEATURE_CODE,
|
featureCode=FEATURE_CODE,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ from modules.interfaces.interfaceAiObjects import AiObjects
|
||||||
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
from modules.serviceCenter.core.serviceStreaming import get_event_manager
|
||||||
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum, PendingFileEdit
|
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum, PendingFileEdit
|
||||||
from modules.shared.timeUtils import parseTimestamp
|
from modules.shared.timeUtils import parseTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeFeatureWorkspace")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -127,7 +129,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext):
|
||||||
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
|
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
|
||||||
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
|
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
|
||||||
if not featureAccess or not featureAccess.enabled:
|
if not featureAccess or not featureAccess.enabled:
|
||||||
raise HTTPException(status_code=403, detail="Access denied to this feature instance")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied to this feature instance"))
|
||||||
mandateId = str(instance.mandateId) if instance.mandateId else None
|
mandateId = str(instance.mandateId) if instance.mandateId else None
|
||||||
instanceConfig = instance.config if hasattr(instance, "config") and instance.config else {}
|
instanceConfig = instance.config if hasattr(instance, "config") and instance.config else {}
|
||||||
return mandateId, instanceConfig
|
return mandateId, instanceConfig
|
||||||
|
|
@ -1178,10 +1180,10 @@ async def getFileContent(
|
||||||
fileData = fileRecord if isinstance(fileRecord, dict) else fileRecord.model_dump()
|
fileData = fileRecord if isinstance(fileRecord, dict) else fileRecord.model_dump()
|
||||||
filePath = fileData.get("filePath")
|
filePath = fileData.get("filePath")
|
||||||
if not filePath:
|
if not filePath:
|
||||||
raise HTTPException(status_code=404, detail="File has no stored path")
|
raise HTTPException(status_code=404, detail=routeApiMsg("File has no stored path"))
|
||||||
import os
|
import os
|
||||||
if not os.path.isfile(filePath):
|
if not os.path.isfile(filePath):
|
||||||
raise HTTPException(status_code=404, detail="File not found on disk")
|
raise HTTPException(status_code=404, detail=routeApiMsg("File not found on disk"))
|
||||||
mimeType = fileData.get("mimeType", "application/octet-stream")
|
mimeType = fileData.get("mimeType", "application/octet-stream")
|
||||||
with open(filePath, "rb") as fh:
|
with open(filePath, "rb") as fh:
|
||||||
content = fh.read()
|
content = fh.read()
|
||||||
|
|
@ -1436,11 +1438,11 @@ async def listFeatureConnectionTables(
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
inst = rootIf.getFeatureInstance(fiId)
|
inst = rootIf.getFeatureInstance(fiId)
|
||||||
if not inst:
|
if not inst:
|
||||||
raise HTTPException(status_code=404, detail="Feature instance not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Feature instance not found"))
|
||||||
|
|
||||||
mandateId = str(inst.mandateId) if inst.mandateId else None
|
mandateId = str(inst.mandateId) if inst.mandateId else None
|
||||||
if wsMandateId and mandateId and mandateId != wsMandateId:
|
if wsMandateId and mandateId and mandateId != wsMandateId:
|
||||||
raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
|
||||||
catalog = getCatalogService()
|
catalog = getCatalogService()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1495,12 +1497,12 @@ async def listParentObjects(
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
inst = rootIf.getFeatureInstance(fiId)
|
inst = rootIf.getFeatureInstance(fiId)
|
||||||
if not inst:
|
if not inst:
|
||||||
raise HTTPException(status_code=404, detail="Feature instance not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Feature instance not found"))
|
||||||
|
|
||||||
featureCode = inst.featureCode
|
featureCode = inst.featureCode
|
||||||
mandateId = str(inst.mandateId) if inst.mandateId else ""
|
mandateId = str(inst.mandateId) if inst.mandateId else ""
|
||||||
if wsMandateId and mandateId and mandateId != wsMandateId:
|
if wsMandateId and mandateId and mandateId != wsMandateId:
|
||||||
raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
|
||||||
catalog = getCatalogService()
|
catalog = getCatalogService()
|
||||||
|
|
||||||
parentObj = None
|
parentObj = None
|
||||||
|
|
@ -1614,7 +1616,7 @@ async def createFeatureDataSource(
|
||||||
inst = rootIf.getFeatureInstance(body.featureInstanceId)
|
inst = rootIf.getFeatureInstance(body.featureInstanceId)
|
||||||
mandateId = str(inst.mandateId) if inst else (str(context.mandateId) if context.mandateId else "")
|
mandateId = str(inst.mandateId) if inst else (str(context.mandateId) if context.mandateId else "")
|
||||||
if wsMandateId and mandateId and mandateId != wsMandateId:
|
if wsMandateId and mandateId and mandateId != wsMandateId:
|
||||||
raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
|
||||||
|
|
||||||
fds = FeatureDataSource(
|
fds = FeatureDataSource(
|
||||||
featureInstanceId=body.featureInstanceId,
|
featureInstanceId=body.featureInstanceId,
|
||||||
|
|
@ -1814,7 +1816,7 @@ async def synthesizeVoice(
|
||||||
_validateInstanceAccess(instanceId, context)
|
_validateInstanceAccess(instanceId, context)
|
||||||
text = body.get("text", "")
|
text = body.get("text", "")
|
||||||
if not text:
|
if not text:
|
||||||
raise HTTPException(status_code=400, detail="text is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("text is required"))
|
||||||
return JSONResponse({"audio": None, "note": "TTS via browser Speech Synthesis API recommended"})
|
return JSONResponse({"audio": None, "note": "TTS via browser Speech Synthesis API recommended"})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1858,7 +1860,7 @@ async def acceptEdit(
|
||||||
try:
|
try:
|
||||||
success = dbMgmt.updateFileData(edit.fileId, edit.newContent.encode("utf-8"))
|
success = dbMgmt.updateFileData(edit.fileId, edit.newContent.encode("utf-8"))
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=500, detail="Failed to update file data")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to update file data"))
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from modules.datamodels.datamodelRbac import (
|
||||||
AccessRuleContext,
|
AccessRuleContext,
|
||||||
Role,
|
Role,
|
||||||
)
|
)
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
from modules.datamodels.datamodelUam import AccessLevel
|
from modules.datamodels.datamodelUam import AccessLevel
|
||||||
from modules.datamodels.datamodelMembership import (
|
from modules.datamodels.datamodelMembership import (
|
||||||
UserMandate,
|
UserMandate,
|
||||||
|
|
@ -547,7 +548,7 @@ def initRoles(db: DatabaseConnector) -> None:
|
||||||
standardRoles = [
|
standardRoles = [
|
||||||
Role(
|
Role(
|
||||||
roleLabel="admin",
|
roleLabel="admin",
|
||||||
description={"en": "Administrator - Manage users and resources within mandate scope", "de": "Administrator - Benutzer und Ressourcen im Mandanten verwalten", "fr": "Administrateur - Gérer les utilisateurs et ressources dans le périmètre du mandat"},
|
description=coerce_text_multilingual("Administrator - Benutzer und Ressourcen im Mandanten verwalten"),
|
||||||
mandateId=None, # Global template role
|
mandateId=None, # Global template role
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
featureCode=None,
|
featureCode=None,
|
||||||
|
|
@ -555,7 +556,7 @@ def initRoles(db: DatabaseConnector) -> None:
|
||||||
),
|
),
|
||||||
Role(
|
Role(
|
||||||
roleLabel="user",
|
roleLabel="user",
|
||||||
description={"en": "User - Standard user with access to own records", "de": "Benutzer - Standard-Benutzer mit Zugriff auf eigene Datensätze", "fr": "Utilisateur - Utilisateur standard avec accès à ses propres enregistrements"},
|
description="Benutzer - Standard-Benutzer mit Zugriff auf eigene Datensätze",
|
||||||
mandateId=None, # Global template role
|
mandateId=None, # Global template role
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
featureCode=None,
|
featureCode=None,
|
||||||
|
|
@ -563,7 +564,7 @@ def initRoles(db: DatabaseConnector) -> None:
|
||||||
),
|
),
|
||||||
Role(
|
Role(
|
||||||
roleLabel="viewer",
|
roleLabel="viewer",
|
||||||
description={"en": "Viewer - Read-only access to group records", "de": "Betrachter - Nur-Lese-Zugriff auf Gruppen-Datensätze", "fr": "Visualiseur - Accès en lecture seule aux enregistrements du groupe"},
|
description=coerce_text_multilingual("Betrachter - Nur-Lese-Zugriff auf Gruppen-Datensätze"),
|
||||||
mandateId=None, # Global template role
|
mandateId=None, # Global template role
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
featureCode=None,
|
featureCode=None,
|
||||||
|
|
@ -728,7 +729,7 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
id=newRoleId,
|
id=newRoleId,
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=templateRole.get("description", {}),
|
description=coerce_text_multilingual(templateRole.get("description", {})),
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
featureCode=None,
|
featureCode=None,
|
||||||
|
|
@ -797,11 +798,7 @@ def _initSysAdminRole(db: DatabaseConnector, mandateId: str) -> Optional[str]:
|
||||||
logger.info("Creating sysadmin role in root mandate")
|
logger.info("Creating sysadmin role in root mandate")
|
||||||
sysadminRole = Role(
|
sysadminRole = Role(
|
||||||
roleLabel="sysadmin",
|
roleLabel="sysadmin",
|
||||||
description={
|
description=coerce_text_multilingual("System-Administrator - Vollständiger administrativer Zugriff über alle Mandanten"),
|
||||||
"en": "System Administrator - Full administrative access across all mandates",
|
|
||||||
"de": "System-Administrator - Vollständiger administrativer Zugriff über alle Mandanten",
|
|
||||||
"fr": "Administrateur système - Accès administratif complet à tous les mandats"
|
|
||||||
},
|
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
featureCode=None,
|
featureCode=None,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
|
from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule
|
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -198,6 +199,9 @@ class FeatureInterface:
|
||||||
# Copy template roles if requested
|
# Copy template roles if requested
|
||||||
if copyTemplateRoles:
|
if copyTemplateRoles:
|
||||||
self._copyTemplateRoles(featureCode, mandateId, instanceId)
|
self._copyTemplateRoles(featureCode, mandateId, instanceId)
|
||||||
|
|
||||||
|
# Copy template workflows (if feature defines TEMPLATE_WORKFLOWS)
|
||||||
|
self._copyTemplateWorkflows(featureCode, mandateId, instanceId)
|
||||||
|
|
||||||
cleanedRecord = dict(createdInstance)
|
cleanedRecord = dict(createdInstance)
|
||||||
return FeatureInstance(**cleanedRecord)
|
return FeatureInstance(**cleanedRecord)
|
||||||
|
|
@ -206,6 +210,72 @@ class FeatureInterface:
|
||||||
logger.error(f"Error creating feature instance: {e}")
|
logger.error(f"Error creating feature instance: {e}")
|
||||||
raise ValueError(f"Failed to create feature instance: {e}")
|
raise ValueError(f"Failed to create feature instance: {e}")
|
||||||
|
|
||||||
|
def _copyTemplateWorkflows(self, featureCode: str, mandateId: str, instanceId: str) -> int:
|
||||||
|
"""
|
||||||
|
Copy feature-specific template workflows to a new instance.
|
||||||
|
|
||||||
|
Loads TEMPLATE_WORKFLOWS from the feature module and creates
|
||||||
|
AutoWorkflow records in the graphicalEditor DB, scoped to
|
||||||
|
(mandateId, instanceId). The placeholder {{featureInstanceId}}
|
||||||
|
in graph parameters is replaced with the actual instanceId.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
featureCode: Feature code (e.g. "trustee")
|
||||||
|
mandateId: Mandate ID
|
||||||
|
instanceId: New FeatureInstance ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of workflows copied
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
try:
|
||||||
|
featureModule = importlib.import_module(f"modules.features.{featureCode}.main{featureCode.capitalize()}")
|
||||||
|
getTemplateWorkflows = getattr(featureModule, "getTemplateWorkflows", None)
|
||||||
|
if not getTemplateWorkflows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
templateWorkflows = getTemplateWorkflows()
|
||||||
|
if not templateWorkflows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
from modules.features.graphicalEditor.interfaceFeatureGraphicalEditor import getGraphicalEditorInterface
|
||||||
|
from modules.auth.authModels import SystemUser
|
||||||
|
systemUser = SystemUser()
|
||||||
|
geInterface = getGraphicalEditorInterface(systemUser, mandateId, instanceId)
|
||||||
|
|
||||||
|
copied = 0
|
||||||
|
for template in templateWorkflows:
|
||||||
|
graphJson = json.dumps(template.get("graph", {}))
|
||||||
|
graphJson = graphJson.replace("{{featureInstanceId}}", instanceId)
|
||||||
|
graph = json.loads(graphJson)
|
||||||
|
|
||||||
|
labelDict = template.get("label", {})
|
||||||
|
label = labelDict.get("de") or labelDict.get("en") or str(labelDict) if isinstance(labelDict, dict) else str(labelDict)
|
||||||
|
|
||||||
|
geInterface.createWorkflow({
|
||||||
|
"label": label,
|
||||||
|
"graph": graph,
|
||||||
|
"tags": template.get("tags", [f"feature:{featureCode}"]),
|
||||||
|
"isTemplate": False,
|
||||||
|
"templateSourceId": template["id"],
|
||||||
|
"templateScope": "instance",
|
||||||
|
"active": True,
|
||||||
|
})
|
||||||
|
copied += 1
|
||||||
|
|
||||||
|
if copied > 0:
|
||||||
|
logger.info(f"Feature '{featureCode}': Copied {copied} template workflows to instance {instanceId}")
|
||||||
|
return copied
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
logger.debug(f"No feature module found for '{featureCode}' — skipping workflow bootstrap")
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error copying template workflows for '{featureCode}' instance {instanceId}: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
def _copyTemplateRoles(self, featureCode: str, mandateId: str, instanceId: str) -> int:
|
def _copyTemplateRoles(self, featureCode: str, mandateId: str, instanceId: str) -> int:
|
||||||
"""
|
"""
|
||||||
Copy feature-specific template roles to a new instance.
|
Copy feature-specific template roles to a new instance.
|
||||||
|
|
@ -268,7 +338,7 @@ class FeatureInterface:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
id=newRoleId,
|
id=newRoleId,
|
||||||
roleLabel=templateRole.get("roleLabel"),
|
roleLabel=templateRole.get("roleLabel"),
|
||||||
description=templateRole.get("description", {}),
|
description=coerce_text_multilingual(templateRole.get("description", {})),
|
||||||
featureCode=featureCode,
|
featureCode=featureCode,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
featureInstanceId=instanceId,
|
featureInstanceId=instanceId,
|
||||||
|
|
@ -354,7 +424,7 @@ class FeatureInterface:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
id=newRoleId,
|
id=newRoleId,
|
||||||
roleLabel=templateRole.get("roleLabel"),
|
roleLabel=templateRole.get("roleLabel"),
|
||||||
description=templateRole.get("description", {}),
|
description=coerce_text_multilingual(templateRole.get("description", {})),
|
||||||
featureCode=featureCode,
|
featureCode=featureCode,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
featureInstanceId=featureInstanceId,
|
featureInstanceId=featureInstanceId,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.auth import limiter, getCurrentUser
|
from modules.auth import limiter, getCurrentUser
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeAdmin")
|
||||||
|
|
||||||
# Static folder setup - using absolute path from app root
|
# Static folder setup - using absolute path from app root
|
||||||
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
|
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
|
||||||
|
|
@ -39,7 +41,7 @@ def root(request: Request) -> Dict[str, str]:
|
||||||
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
|
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
|
||||||
if not allowedOrigins:
|
if not allowedOrigins:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="APP_ALLOWED_ORIGINS configuration is required"
|
status_code=500, detail=routeApiMsg("APP_ALLOWED_ORIGINS configuration is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -59,17 +61,17 @@ def get_environment(
|
||||||
apiBaseUrl = APP_CONFIG.get("APP_API_URL")
|
apiBaseUrl = APP_CONFIG.get("APP_API_URL")
|
||||||
if not apiBaseUrl:
|
if not apiBaseUrl:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="APP_API_URL configuration is required"
|
status_code=500, detail=routeApiMsg("APP_API_URL configuration is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
environment = APP_CONFIG.get("APP_ENV")
|
environment = APP_CONFIG.get("APP_ENV")
|
||||||
if not environment:
|
if not environment:
|
||||||
raise HTTPException(status_code=500, detail="APP_ENV configuration is required")
|
raise HTTPException(status_code=500, detail=routeApiMsg("APP_ENV configuration is required"))
|
||||||
|
|
||||||
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
|
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
|
||||||
if not instanceLabel:
|
if not instanceLabel:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="APP_ENV_LABEL configuration is required"
|
status_code=500, detail=routeApiMsg("APP_ENV_LABEL configuration is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -91,5 +93,5 @@ def options_route(request: Request, fullPath: str) -> Response:
|
||||||
def favicon(request: Request) -> FileResponse:
|
def favicon(request: Request) -> FileResponse:
|
||||||
favicon_path = staticFolder / "favicon.ico"
|
favicon_path = staticFolder / "favicon.ico"
|
||||||
if not favicon_path.exists():
|
if not favicon_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Favicon not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Favicon not found"))
|
||||||
return FileResponse(str(favicon_path), media_type="image/x-icon")
|
return FileResponse(str(favicon_path), media_type="image/x-icon")
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||||
from modules.security.rbacCatalog import getCatalogService
|
from modules.security.rbacCatalog import getCatalogService
|
||||||
from modules.routes.routeNotifications import create_access_change_notification
|
from modules.routes.routeNotifications import create_access_change_notification
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeAdminFeatures")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -418,7 +420,7 @@ def list_feature_instances(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -483,7 +485,7 @@ def get_feature_instance_filter_values(
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Return distinct filter values for a column in feature instances."""
|
"""Return distinct filter values for a column in feature instances."""
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="X-Mandate-Id header is required")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("X-Mandate-Id header is required"))
|
||||||
try:
|
try:
|
||||||
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
@ -530,7 +532,7 @@ def get_feature_instance(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
return instance.model_dump()
|
return instance.model_dump()
|
||||||
|
|
@ -563,14 +565,14 @@ def create_feature_instance(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to create feature instances"
|
detail=routeApiMsg("Mandate-Admin role required to create feature instances")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -670,14 +672,14 @@ def delete_feature_instance(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to delete feature instances"
|
detail=routeApiMsg("Mandate-Admin role required to delete feature instances")
|
||||||
)
|
)
|
||||||
|
|
||||||
featureInterface.deleteFeatureInstance(instanceId)
|
featureInterface.deleteFeatureInstance(instanceId)
|
||||||
|
|
@ -737,14 +739,14 @@ def updateFeatureInstance(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to update feature instances"
|
detail=routeApiMsg("Mandate-Admin role required to update feature instances")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build update data (only non-None values)
|
# Build update data (only non-None values)
|
||||||
|
|
@ -763,7 +765,7 @@ def updateFeatureInstance(
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update feature instance"
|
detail=routeApiMsg("Failed to update feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clear chatbot config cache when config was updated for chatbot instances
|
# Clear chatbot config cache when config was updated for chatbot instances
|
||||||
|
|
@ -820,14 +822,14 @@ def sync_instance_roles(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check admin permission (Mandate-Admin or Feature-Admin)
|
# Check admin permission (Mandate-Admin or Feature-Admin)
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to sync roles"
|
detail=routeApiMsg("Admin role required to sync roles")
|
||||||
)
|
)
|
||||||
|
|
||||||
result = featureInterface.syncRolesFromTemplate(instanceId, addOnly)
|
result = featureInterface.syncRolesFromTemplate(instanceId, addOnly)
|
||||||
|
|
@ -1061,7 +1063,7 @@ def list_feature_instance_users(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all FeatureAccess records for this instance (Pydantic models)
|
# Get all FeatureAccess records for this instance (Pydantic models)
|
||||||
|
|
@ -1152,7 +1154,7 @@ def get_feature_instance_users_filter_values(
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature instance '{instanceId}' not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature instance '{instanceId}' not found")
|
||||||
if context.mandateId and str(instance.mandateId) != str(context.mandateId):
|
if context.mandateId and str(instance.mandateId) != str(context.mandateId):
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this feature instance")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Access denied to this feature instance"))
|
||||||
featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
|
featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
|
||||||
result = []
|
result = []
|
||||||
for fa in featureAccesses:
|
for fa in featureAccesses:
|
||||||
|
|
@ -1217,14 +1219,14 @@ def add_user_to_feature_instance(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check admin permission
|
# Check admin permission
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to add users to feature instances"
|
detail=routeApiMsg("Admin role required to add users to feature instances")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify user exists
|
# Verify user exists
|
||||||
|
|
@ -1238,7 +1240,7 @@ def add_user_to_feature_instance(
|
||||||
if not data.roleIds:
|
if not data.roleIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="At least one role is required to grant feature access"
|
detail=routeApiMsg("At least one role is required to grant feature access")
|
||||||
)
|
)
|
||||||
|
|
||||||
from modules.datamodels.datamodelRbac import Role
|
from modules.datamodels.datamodelRbac import Role
|
||||||
|
|
@ -1325,14 +1327,14 @@ def remove_user_from_feature_instance(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check admin permission
|
# Check admin permission
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to remove users from feature instances"
|
detail=routeApiMsg("Admin role required to remove users from feature instances")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find FeatureAccess record
|
# Find FeatureAccess record
|
||||||
|
|
@ -1341,7 +1343,7 @@ def remove_user_from_feature_instance(
|
||||||
if not existingAccess:
|
if not existingAccess:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="User does not have access to this feature instance"
|
detail=routeApiMsg("User does not have access to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
featureAccessId = str(existingAccess.id)
|
featureAccessId = str(existingAccess.id)
|
||||||
|
|
@ -1415,14 +1417,14 @@ def update_feature_instance_user_roles(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check admin permission
|
# Check admin permission
|
||||||
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to update user roles"
|
detail=routeApiMsg("Admin role required to update user roles")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find FeatureAccess record
|
# Find FeatureAccess record
|
||||||
|
|
@ -1431,7 +1433,7 @@ def update_feature_instance_user_roles(
|
||||||
if not existingAccess:
|
if not existingAccess:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="User does not have access to this feature instance"
|
detail=routeApiMsg("User does not have access to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
featureAccessId = str(existingAccess.id)
|
featureAccessId = str(existingAccess.id)
|
||||||
|
|
@ -1523,7 +1525,7 @@ def get_feature_instance_available_roles(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this feature instance"
|
detail=routeApiMsg("Access denied to this feature instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get roles for this instance using interface method
|
# Get roles for this instance using interface method
|
||||||
|
|
@ -1619,7 +1621,7 @@ def _renameFeatureInstance(
|
||||||
|
|
||||||
instance = featureInterface.getFeatureInstance(instanceId)
|
instance = featureInterface.getFeatureInstance(instanceId)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feature instance not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Feature instance not found"))
|
||||||
|
|
||||||
userId = str(context.user.id)
|
userId = str(context.user.id)
|
||||||
isInstanceAdmin = False
|
isInstanceAdmin = False
|
||||||
|
|
@ -1637,11 +1639,11 @@ def _renameFeatureInstance(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not isInstanceAdmin:
|
if not isInstanceAdmin:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Instance admin role required to rename")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Instance admin role required to rename"))
|
||||||
|
|
||||||
updated = featureInterface.updateFeatureInstance(instanceId, {"label": data.label.strip()})
|
updated = featureInterface.updateFeatureInstance(instanceId, {"label": data.label.strip()})
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update instance")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to update instance"))
|
||||||
|
|
||||||
return {"id": instanceId, "label": updated.label}
|
return {"id": instanceId, "label": updated.label}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,11 @@ from pydantic import BaseModel, Field
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext, requireSysAdminRole
|
from modules.auth import limiter, getRequestContext, RequestContext, requireSysAdminRole
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule
|
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||||
|
from modules.datamodels.datamodelUtils import coerce_text_multilingual
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeAdminRbacExport")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -165,7 +168,7 @@ async def import_global_rbac(
|
||||||
if "roles" not in data:
|
if "roles" not in data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Missing 'roles' field in import data"
|
detail=routeApiMsg("Missing 'roles' field in import data")
|
||||||
)
|
)
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
@ -227,7 +230,7 @@ async def import_global_rbac(
|
||||||
# Create new role
|
# Create new role
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleData.get("description", {}),
|
description=coerce_text_multilingual(roleData.get("description", {})),
|
||||||
featureCode=featureCode,
|
featureCode=featureCode,
|
||||||
mandateId=None,
|
mandateId=None,
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
@ -298,14 +301,14 @@ def export_mandate_rbac(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to export RBAC"
|
detail=routeApiMsg("Mandate-Admin role required to export RBAC")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -392,14 +395,14 @@ async def import_mandate_rbac(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to import RBAC"
|
detail=routeApiMsg("Mandate-Admin role required to import RBAC")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -417,7 +420,7 @@ async def import_mandate_rbac(
|
||||||
if "roles" not in data:
|
if "roles" not in data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Missing 'roles' field in import data"
|
detail=routeApiMsg("Missing 'roles' field in import data")
|
||||||
)
|
)
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
@ -482,7 +485,7 @@ async def import_mandate_rbac(
|
||||||
# Create new role at mandate level
|
# Create new role at mandate level
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
description=roleData.get("description", {}),
|
description=coerce_text_multilingual(roleData.get("description", {})),
|
||||||
featureCode=featureCode,
|
featureCode=featureCode,
|
||||||
mandateId=str(context.mandateId),
|
mandateId=str(context.mandateId),
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ from modules.datamodels.datamodelRbac import AccessRuleContext, AccessRule, Role
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
from modules.datamodels.datamodelMembership import UserMandate
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeAdminRbacRules")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -113,7 +115,7 @@ def get_permissions(
|
||||||
if not interface.rbac:
|
if not interface.rbac:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="RBAC interface not available"
|
detail=routeApiMsg("RBAC interface not available")
|
||||||
)
|
)
|
||||||
|
|
||||||
# MULTI-TENANT: Get permissions using context (mandateId/featureInstanceId)
|
# MULTI-TENANT: Get permissions using context (mandateId/featureInstanceId)
|
||||||
|
|
@ -189,7 +191,7 @@ def get_all_permissions(
|
||||||
if not interface.rbac:
|
if not interface.rbac:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="RBAC interface not available"
|
detail=routeApiMsg("RBAC interface not available")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine which contexts to fetch
|
# Determine which contexts to fetch
|
||||||
|
|
@ -363,7 +365,7 @@ def get_access_rules(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# Get interface - uses root interface for admin access
|
# Get interface - uses root interface for admin access
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -488,11 +490,11 @@ def get_access_rules_by_role(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# MandateAdmin: verify role belongs to their mandates
|
# MandateAdmin: verify role belongs to their mandates
|
||||||
if not isSysAdmin and not _isRoleInAdminMandates(roleId, adminMandateIds):
|
if not isSysAdmin and not _isRoleInAdminMandates(roleId, adminMandateIds):
|
||||||
raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -535,7 +537,7 @@ def get_access_rule(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# Get interface - uses root interface for admin access
|
# Get interface - uses root interface for admin access
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -550,7 +552,7 @@ def get_access_rule(
|
||||||
|
|
||||||
# MandateAdmin: verify rule's role belongs to their mandates
|
# MandateAdmin: verify rule's role belongs to their mandates
|
||||||
if not isSysAdmin and not _isRoleInAdminMandates(str(rule.roleId), adminMandateIds):
|
if not isSysAdmin and not _isRoleInAdminMandates(str(rule.roleId), adminMandateIds):
|
||||||
raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
|
||||||
|
|
||||||
# Convert to dict for JSON serialization
|
# Convert to dict for JSON serialization
|
||||||
return rule.model_dump()
|
return rule.model_dump()
|
||||||
|
|
@ -586,7 +588,7 @@ def create_access_rule(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# Get interface - uses root interface for admin access
|
# Get interface - uses root interface for admin access
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -621,7 +623,7 @@ def create_access_rule(
|
||||||
# MandateAdmin: verify the rule's role belongs to their mandates
|
# MandateAdmin: verify the rule's role belongs to their mandates
|
||||||
if not isSysAdmin and accessRule.roleId:
|
if not isSysAdmin and accessRule.roleId:
|
||||||
if not _isRoleInAdminMandates(str(accessRule.roleId), adminMandateIds):
|
if not _isRoleInAdminMandates(str(accessRule.roleId), adminMandateIds):
|
||||||
raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
|
||||||
|
|
||||||
# Create rule
|
# Create rule
|
||||||
createdRule = interface.createAccessRule(accessRule)
|
createdRule = interface.createAccessRule(accessRule)
|
||||||
|
|
@ -666,7 +668,7 @@ def update_access_rule(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# Get interface - uses root interface for admin access
|
# Get interface - uses root interface for admin access
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -681,7 +683,7 @@ def update_access_rule(
|
||||||
|
|
||||||
# MandateAdmin: verify existing rule's role belongs to their mandates
|
# MandateAdmin: verify existing rule's role belongs to their mandates
|
||||||
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
|
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
|
||||||
raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
|
||||||
|
|
||||||
# Validate and parse access rule data
|
# Validate and parse access rule data
|
||||||
try:
|
try:
|
||||||
|
|
@ -754,7 +756,7 @@ def delete_access_rule(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# Get interface - uses root interface for admin access
|
# Get interface - uses root interface for admin access
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -769,7 +771,7 @@ def delete_access_rule(
|
||||||
|
|
||||||
# MandateAdmin: verify rule's role belongs to their mandates
|
# MandateAdmin: verify rule's role belongs to their mandates
|
||||||
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
|
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
|
||||||
raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
|
||||||
|
|
||||||
# Delete rule
|
# Delete rule
|
||||||
success = interface.deleteAccessRule(ruleId)
|
success = interface.deleteAccessRule(ruleId)
|
||||||
|
|
@ -835,7 +837,7 @@ def list_roles(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -1008,7 +1010,7 @@ def get_roles_filter_values(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
dbRoles = interface.getAllRoles(pagination=None)
|
dbRoles = interface.getAllRoles(pagination=None)
|
||||||
|
|
@ -1083,12 +1085,12 @@ def create_role(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
# MandateAdmin: can only create roles in their own mandates
|
# MandateAdmin: can only create roles in their own mandates
|
||||||
if not isSysAdmin:
|
if not isSysAdmin:
|
||||||
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
|
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: can only create roles in your own mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: can only create roles in your own mandates"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -1142,7 +1144,7 @@ def get_role(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -1156,7 +1158,7 @@ def get_role(
|
||||||
# MandateAdmin: verify role belongs to their mandates
|
# MandateAdmin: verify role belongs to their mandates
|
||||||
if not isSysAdmin:
|
if not isSysAdmin:
|
||||||
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
|
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": role.id,
|
"id": role.id,
|
||||||
|
|
@ -1203,7 +1205,7 @@ def update_role(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -1213,9 +1215,9 @@ def update_role(
|
||||||
if not existingRole:
|
if not existingRole:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
if existingRole.isSystemRole and not existingRole.mandateId:
|
if existingRole.isSystemRole and not existingRole.mandateId:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: cannot modify template/system roles")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: cannot modify template/system roles"))
|
||||||
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
|
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
|
||||||
|
|
||||||
updatedRole = interface.updateRole(roleId, role)
|
updatedRole = interface.updateRole(roleId, role)
|
||||||
|
|
||||||
|
|
@ -1267,7 +1269,7 @@ def delete_role(
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
|
||||||
if not isSysAdmin and not adminMandateIds:
|
if not isSysAdmin and not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
|
|
@ -1277,9 +1279,9 @@ def delete_role(
|
||||||
if not existingRole:
|
if not existingRole:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
if existingRole.isSystemRole and not existingRole.mandateId:
|
if existingRole.isSystemRole and not existingRole.mandateId:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: cannot delete template/system roles")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: cannot delete template/system roles"))
|
||||||
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
|
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
|
||||||
|
|
||||||
success = interface.deleteRole(roleId)
|
success = interface.deleteRole(roleId)
|
||||||
if not success:
|
if not success:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ from modules.datamodels.datamodelMembership import (
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelFeatures import FeatureInstance, Feature
|
from modules.datamodels.datamodelFeatures import FeatureInstance, Feature
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeAdminUserAccessOverview")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -116,7 +118,7 @@ def listUsersForOverview(
|
||||||
- List of user dictionaries with basic info
|
- List of user dictionaries with basic info
|
||||||
"""
|
"""
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für die Benutzerzugriffsübersicht")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Keine Berechtigung für die Benutzerzugriffsübersicht"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -209,7 +211,7 @@ def getUserAccessOverview(
|
||||||
- Resource access (what resources the user can use)
|
- Resource access (what resources the user can use)
|
||||||
"""
|
"""
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(status_code=403, detail="Keine Berechtigung für die Benutzerzugriffsübersicht")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Keine Berechtigung für die Benutzerzugriffsübersicht"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -239,7 +241,7 @@ def getUserAccessOverview(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not userInAdminMandate:
|
if not userInAdminMandate:
|
||||||
raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Benutzer gehört nicht zu Ihrem Mandate"))
|
||||||
|
|
||||||
# Get user
|
# Get user
|
||||||
user = interface.getUser(userId)
|
user = interface.getUser(userId)
|
||||||
|
|
@ -528,7 +530,7 @@ def getEffectivePermissions(
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
# Check if user has admin role in any mandate
|
# Check if user has admin role in any mandate
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
@ -550,7 +552,7 @@ def getEffectivePermissions(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not adminMandateIds:
|
if not adminMandateIds:
|
||||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Insufficient permissions"))
|
||||||
|
|
||||||
userInAdminMandate = False
|
userInAdminMandate = False
|
||||||
for mid in adminMandateIds:
|
for mid in adminMandateIds:
|
||||||
|
|
@ -559,7 +561,7 @@ def getEffectivePermissions(
|
||||||
break
|
break
|
||||||
|
|
||||||
if not userInAdminMandate:
|
if not userInAdminMandate:
|
||||||
raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Benutzer gehört nicht zu Ihrem Mandate"))
|
||||||
|
|
||||||
# Get user
|
# Get user
|
||||||
user = interface.getUser(userId)
|
user = interface.getUser(userId)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ from modules.auth import limiter
|
||||||
|
|
||||||
# Import the attribute definition and helper functions
|
# Import the attribute definition and helper functions
|
||||||
from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
|
||||||
|
routeApiMsg = apiRouteContext("routeAttributes")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -42,8 +45,8 @@ def get_entity_attributes(
|
||||||
# Check if entity type is known
|
# Check if entity type is known
|
||||||
if entityType not in modelClasses:
|
if entityType not in modelClasses:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Entity type '{entityType}' not found."
|
detail=routeApiMsg("Entitätstyp nicht gefunden.") + f" ({entityType})",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get model class and derive attributes from it
|
# Get model class and derive attributes from it
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ from modules.datamodels.datamodelBilling import (
|
||||||
BillingStatisticsChartData,
|
BillingStatisticsChartData,
|
||||||
BillingCheckResult,
|
BillingCheckResult,
|
||||||
)
|
)
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
|
||||||
|
routeApiMsg = apiRouteContext("routeBilling")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -337,9 +340,9 @@ def _creditStripeSessionIfNeeded(
|
||||||
amount_chf_str = metadata.get("amountChf", "0")
|
amount_chf_str = metadata.get("amountChf", "0")
|
||||||
|
|
||||||
if not session_id:
|
if not session_id:
|
||||||
raise HTTPException(status_code=400, detail="Stripe session id missing")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Stripe session id missing"))
|
||||||
if not mandate_id:
|
if not mandate_id:
|
||||||
raise HTTPException(status_code=400, detail="Invalid session metadata: mandateId missing")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session metadata: mandateId missing"))
|
||||||
|
|
||||||
existing_payment_tx = billingInterface.getPaymentTransactionByReferenceId(session_id)
|
existing_payment_tx = billingInterface.getPaymentTransactionByReferenceId(session_id)
|
||||||
if existing_payment_tx:
|
if existing_payment_tx:
|
||||||
|
|
@ -363,11 +366,11 @@ def _creditStripeSessionIfNeeded(
|
||||||
if amount_total is not None:
|
if amount_total is not None:
|
||||||
amount_chf = amount_total / 100.0
|
amount_chf = amount_total / 100.0
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Invalid amount in Stripe session")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid amount in Stripe session"))
|
||||||
|
|
||||||
settings = billingInterface.getSettings(mandate_id)
|
settings = billingInterface.getSettings(mandate_id)
|
||||||
if not settings:
|
if not settings:
|
||||||
raise HTTPException(status_code=404, detail="Billing settings not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
|
||||||
|
|
||||||
account = billingInterface.getOrCreateMandateAccount(mandate_id, initialBalance=0.0)
|
account = billingInterface.getOrCreateMandateAccount(mandate_id, initialBalance=0.0)
|
||||||
|
|
||||||
|
|
@ -537,10 +540,10 @@ def getStatistics(
|
||||||
try:
|
try:
|
||||||
# Validate period
|
# Validate period
|
||||||
if period not in ["day", "month", "year"]:
|
if period not in ["day", "month", "year"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid period. Use 'day', 'month', or 'year'")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid period. Use 'day', 'month', or 'year'"))
|
||||||
|
|
||||||
if period == "day" and not month:
|
if period == "day" and not month:
|
||||||
raise HTTPException(status_code=400, detail="Month is required for 'day' period")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Month is required for 'day' period"))
|
||||||
|
|
||||||
billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
|
billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
|
||||||
settings = billingInterface.getSettings(ctx.mandateId)
|
settings = billingInterface.getSettings(ctx.mandateId)
|
||||||
|
|
@ -642,13 +645,13 @@ def getSettingsAdmin(
|
||||||
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
||||||
"""
|
"""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
||||||
settings = billingInterface.getSettings(targetMandateId)
|
settings = billingInterface.getSettings(targetMandateId)
|
||||||
|
|
||||||
if not settings:
|
if not settings:
|
||||||
raise HTTPException(status_code=404, detail="Billing settings not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
@ -672,7 +675,7 @@ def createOrUpdateSettings(
|
||||||
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
||||||
"""
|
"""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
||||||
existingSettings = billingInterface.getSettings(targetMandateId)
|
existingSettings = billingInterface.getSettings(targetMandateId)
|
||||||
|
|
@ -742,12 +745,12 @@ def addCredit(
|
||||||
settings = billingInterface.getSettings(targetMandateId)
|
settings = billingInterface.getSettings(targetMandateId)
|
||||||
|
|
||||||
if not settings:
|
if not settings:
|
||||||
raise HTTPException(status_code=404, detail="Billing settings not found for this mandate")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found for this mandate"))
|
||||||
|
|
||||||
account = billingInterface.getOrCreateMandateAccount(targetMandateId, initialBalance=0.0)
|
account = billingInterface.getOrCreateMandateAccount(targetMandateId, initialBalance=0.0)
|
||||||
|
|
||||||
if creditRequest.amount == 0:
|
if creditRequest.amount == 0:
|
||||||
raise HTTPException(status_code=400, detail="Amount must not be zero")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Amount must not be zero"))
|
||||||
|
|
||||||
from modules.datamodels.datamodelBilling import BillingTransaction
|
from modules.datamodels.datamodelBilling import BillingTransaction
|
||||||
|
|
||||||
|
|
@ -794,10 +797,10 @@ def createCheckoutSession(
|
||||||
settings = billingInterface.getSettings(targetMandateId)
|
settings = billingInterface.getSettings(targetMandateId)
|
||||||
|
|
||||||
if not settings:
|
if not settings:
|
||||||
raise HTTPException(status_code=404, detail="Billing settings not found for this mandate")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found for this mandate"))
|
||||||
|
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=403, detail="Mandate admin role required to load mandate credit")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Mandate admin role required to load mandate credit"))
|
||||||
|
|
||||||
from modules.serviceCenter.services.serviceBilling.stripeCheckout import create_checkout_session
|
from modules.serviceCenter.services.serviceBilling.stripeCheckout import create_checkout_session
|
||||||
redirect_url = create_checkout_session(
|
redirect_url = create_checkout_session(
|
||||||
|
|
@ -832,7 +835,7 @@ def confirmCheckoutSession(
|
||||||
stripe = _getStripeClient()
|
stripe = _getStripeClient()
|
||||||
session = stripe.checkout.Session.retrieve(confirmRequest.sessionId)
|
session = stripe.checkout.Session.retrieve(confirmRequest.sessionId)
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=404, detail="Stripe Checkout Session not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Stripe Checkout Session not found"))
|
||||||
|
|
||||||
from modules.shared.stripeClient import stripeToDict
|
from modules.shared.stripeClient import stripeToDict
|
||||||
session_dict = stripeToDict(session)
|
session_dict = stripeToDict(session)
|
||||||
|
|
@ -841,7 +844,7 @@ def confirmCheckoutSession(
|
||||||
user_id = metadata.get("userId") or None
|
user_id = metadata.get("userId") or None
|
||||||
|
|
||||||
if not mandate_id:
|
if not mandate_id:
|
||||||
raise HTTPException(status_code=400, detail="Invalid session metadata: mandateId missing")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session metadata: mandateId missing"))
|
||||||
|
|
||||||
payment_status = session_dict.get("payment_status")
|
payment_status = session_dict.get("payment_status")
|
||||||
if payment_status != "paid":
|
if payment_status != "paid":
|
||||||
|
|
@ -850,10 +853,10 @@ def confirmCheckoutSession(
|
||||||
billingInterface = getBillingInterface(ctx.user, mandate_id)
|
billingInterface = getBillingInterface(ctx.user, mandate_id)
|
||||||
settings = billingInterface.getSettings(mandate_id)
|
settings = billingInterface.getSettings(mandate_id)
|
||||||
if not settings:
|
if not settings:
|
||||||
raise HTTPException(status_code=404, detail="Billing settings not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
|
||||||
|
|
||||||
if not _isAdminOfMandate(ctx, mandate_id):
|
if not _isAdminOfMandate(ctx, mandate_id):
|
||||||
raise HTTPException(status_code=403, detail="Mandate admin role required")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Mandate admin role required"))
|
||||||
|
|
||||||
root_billing_interface = _getRootInterface()
|
root_billing_interface = _getRootInterface()
|
||||||
return _creditStripeSessionIfNeeded(root_billing_interface, session_dict, eventId=None)
|
return _creditStripeSessionIfNeeded(root_billing_interface, session_dict, eventId=None)
|
||||||
|
|
@ -880,10 +883,10 @@ async def stripeWebhook(
|
||||||
webhook_secret = APP_CONFIG.get("STRIPE_WEBHOOK_SECRET")
|
webhook_secret = APP_CONFIG.get("STRIPE_WEBHOOK_SECRET")
|
||||||
if not webhook_secret:
|
if not webhook_secret:
|
||||||
logger.error("STRIPE_WEBHOOK_SECRET not configured")
|
logger.error("STRIPE_WEBHOOK_SECRET not configured")
|
||||||
raise HTTPException(status_code=500, detail="Webhook not configured")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Webhook not configured"))
|
||||||
|
|
||||||
if not stripe_signature:
|
if not stripe_signature:
|
||||||
raise HTTPException(status_code=400, detail="Missing Stripe-Signature header")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Missing Stripe-Signature header"))
|
||||||
|
|
||||||
payload = await request.body()
|
payload = await request.body()
|
||||||
|
|
||||||
|
|
@ -894,10 +897,10 @@ async def stripeWebhook(
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.warning(f"Stripe webhook invalid payload: {e}")
|
logger.warning(f"Stripe webhook invalid payload: {e}")
|
||||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid payload"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Stripe webhook signature verification failed: {e}")
|
logger.warning(f"Stripe webhook signature verification failed: {e}")
|
||||||
raise HTTPException(status_code=400, detail="Invalid signature")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid signature"))
|
||||||
|
|
||||||
logger.info(f"Stripe webhook received: event={event.id}, type={event.type}")
|
logger.info(f"Stripe webhook received: event={event.id}, type={event.type}")
|
||||||
|
|
||||||
|
|
@ -1243,7 +1246,7 @@ def getAccounts(
|
||||||
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
|
||||||
"""
|
"""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
billingInterface = getBillingInterface(ctx.user, targetMandateId)
|
||||||
|
|
||||||
|
|
@ -1291,7 +1294,7 @@ def getUsersForMandate(
|
||||||
Used by billing admin to select users for credit assignment.
|
Used by billing admin to select users for credit assignment.
|
||||||
"""
|
"""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
||||||
|
|
||||||
|
|
@ -1414,7 +1417,7 @@ def getTransactionsAdmin(
|
||||||
):
|
):
|
||||||
"""Get all transactions for a mandate with pagination support."""
|
"""Get all transactions for a mandate with pagination support."""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
paginationParams: Optional[PaginationParams] = None
|
paginationParams: Optional[PaginationParams] = None
|
||||||
if pagination:
|
if pagination:
|
||||||
|
|
@ -1461,7 +1464,7 @@ def getTransactionFilterValues(
|
||||||
):
|
):
|
||||||
"""Return distinct filter values for a column in mandate transactions."""
|
"""Return distinct filter values for a column in mandate transactions."""
|
||||||
if not _isAdminOfMandate(ctx, targetMandateId):
|
if not _isAdminOfMandate(ctx, targetMandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
|
||||||
try:
|
try:
|
||||||
crossFilterParams: Optional[PaginationParams] = None
|
crossFilterParams: Optional[PaginationParams] = None
|
||||||
if pagination:
|
if pagination:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ from modules.auth import getCurrentUser, limiter
|
||||||
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
|
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
|
||||||
from modules.interfaces.interfaceDbApp import getInterface
|
from modules.interfaces.interfaceDbApp import getInterface
|
||||||
from modules.serviceHub import getInterface as getServices
|
from modules.serviceHub import getInterface as getServices
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeClickup")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -42,12 +44,12 @@ def _getUserConnection(interface, connection_id: str, user_id: str) -> Optional[
|
||||||
def _clickup_connection_or_404(interface, connection_id: str, user_id: str) -> UserConnection:
|
def _clickup_connection_or_404(interface, connection_id: str, user_id: str) -> UserConnection:
|
||||||
connection = _getUserConnection(interface, connection_id, user_id)
|
connection = _getUserConnection(interface, connection_id, user_id)
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Connection not found"))
|
||||||
authority = connection.authority.value if hasattr(connection.authority, "value") else str(connection.authority)
|
authority = connection.authority.value if hasattr(connection.authority, "value") else str(connection.authority)
|
||||||
if authority.lower() != AuthAuthority.CLICKUP.value:
|
if authority.lower() != AuthAuthority.CLICKUP.value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Connection is not a ClickUp connection",
|
detail=routeApiMsg("Connection is not a ClickUp connection"),
|
||||||
)
|
)
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|
@ -57,7 +59,7 @@ def _svc_for_connection(current_user: User, connection: UserConnection):
|
||||||
if not services.clickup.setAccessTokenFromConnection(connection):
|
if not services.clickup.setAccessTokenFromConnection(connection):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Failed to set ClickUp access token",
|
detail=routeApiMsg("Failed to set ClickUp access token"),
|
||||||
)
|
)
|
||||||
return services.clickup
|
return services.clickup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ from modules.datamodels.datamodelPagination import PaginationParams, PaginatedRe
|
||||||
from modules.interfaces.interfaceDbApp import getInterface
|
from modules.interfaces.interfaceDbApp import getInterface
|
||||||
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
|
||||||
from modules.interfaces.interfaceDbManagement import ComponentObjects
|
from modules.interfaces.interfaceDbManagement import ComponentObjects
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataConnections")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -414,7 +416,7 @@ def update_connection(
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Connection not found"
|
detail=routeApiMsg("Connection not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update connection fields
|
# Update connection fields
|
||||||
|
|
@ -486,7 +488,7 @@ def connect_service(
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Connection not found"
|
detail=routeApiMsg("Connection not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Data-app OAuth (JWT state issued server-side in /auth/connect)
|
# Data-app OAuth (JWT state issued server-side in /auth/connect)
|
||||||
|
|
@ -542,7 +544,7 @@ def disconnect_service(
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Connection not found"
|
detail=routeApiMsg("Connection not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update connection status
|
# Update connection status
|
||||||
|
|
@ -592,7 +594,7 @@ def delete_connection(
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Connection not found"
|
detail=routeApiMsg("Connection not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove the connection - only need connectionId since permissions are verified
|
# Remove the connection - only need connectionId since permissions are verified
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ from modules.datamodels.datamodelFileFolder import FileFolder
|
||||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
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.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataFiles")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -422,7 +424,7 @@ def create_folder(
|
||||||
name = body.get("name", "")
|
name = body.get("name", "")
|
||||||
parentId = body.get("parentId")
|
parentId = body.get("parentId")
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(status_code=400, detail="name is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("name is required"))
|
||||||
try:
|
try:
|
||||||
mgmt = interfaceDbManagement.getInterface(
|
mgmt = interfaceDbManagement.getInterface(
|
||||||
currentUser,
|
currentUser,
|
||||||
|
|
@ -449,7 +451,7 @@ def rename_folder(
|
||||||
"""Rename a folder."""
|
"""Rename a folder."""
|
||||||
newName = body.get("name", "")
|
newName = body.get("name", "")
|
||||||
if not newName:
|
if not newName:
|
||||||
raise HTTPException(status_code=400, detail="name is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("name is required"))
|
||||||
try:
|
try:
|
||||||
mgmt = interfaceDbManagement.getInterface(
|
mgmt = interfaceDbManagement.getInterface(
|
||||||
currentUser,
|
currentUser,
|
||||||
|
|
@ -554,7 +556,7 @@ def download_folder(
|
||||||
|
|
||||||
fileEntries = _collectFiles(folderId, "")
|
fileEntries = _collectFiles(folderId, "")
|
||||||
if not fileEntries:
|
if not fileEntries:
|
||||||
raise HTTPException(status_code=404, detail="Folder is empty")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Folder is empty"))
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
|
@ -595,7 +597,7 @@ def batch_delete_items(
|
||||||
recursiveFolders = bool(body.get("recursiveFolders", True))
|
recursiveFolders = bool(body.get("recursiveFolders", True))
|
||||||
|
|
||||||
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
|
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
|
||||||
raise HTTPException(status_code=400, detail="fileIds and folderIds must be arrays")
|
raise HTTPException(status_code=400, detail=routeApiMsg("fileIds and folderIds must be arrays"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mgmt = interfaceDbManagement.getInterface(
|
mgmt = interfaceDbManagement.getInterface(
|
||||||
|
|
@ -638,7 +640,7 @@ def batch_move_items(
|
||||||
targetParentId = body.get("targetParentId")
|
targetParentId = body.get("targetParentId")
|
||||||
|
|
||||||
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
|
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
|
||||||
raise HTTPException(status_code=400, detail="fileIds and folderIds must be arrays")
|
raise HTTPException(status_code=400, detail=routeApiMsg("fileIds and folderIds must be arrays"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mgmt = interfaceDbManagement.getInterface(
|
mgmt = interfaceDbManagement.getInterface(
|
||||||
|
|
@ -683,7 +685,7 @@ def updateFileScope(
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {validScopes}")
|
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {validScopes}")
|
||||||
|
|
||||||
if scope == "global" and not context.hasSysAdminRole:
|
if scope == "global" and not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=403, detail="Only sysadmins can set global scope")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Only sysadmins can set global scope"))
|
||||||
|
|
||||||
managementInterface = interfaceDbManagement.getInterface(
|
managementInterface = interfaceDbManagement.getInterface(
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -875,14 +877,14 @@ def update_file(
|
||||||
if file_info.get("scope") == "global" and not _hasSysAdminRole(str(currentUser.id)):
|
if file_info.get("scope") == "global" and not _hasSysAdminRole(str(currentUser.id)):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Only sysadmins can set global scope",
|
detail=routeApiMsg("Only sysadmins can set global scope"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if user has access to the file using RBAC
|
# Check if user has access to the file using RBAC
|
||||||
if not managementInterface.checkRbacPermission(FileItem, "update", fileId):
|
if not managementInterface.checkRbacPermission(FileItem, "update", fileId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to update this file"
|
detail=routeApiMsg("Not authorized to update this file")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update the file
|
# Update the file
|
||||||
|
|
@ -890,7 +892,7 @@ def update_file(
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update file"
|
detail=routeApiMsg("Failed to update file")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get updated file
|
# Get updated file
|
||||||
|
|
@ -928,7 +930,7 @@ def delete_file(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error deleting the file"
|
detail=routeApiMsg("Error deleting the file")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"File with ID {fileId} successfully deleted"}
|
return {"message": f"File with ID {fileId} successfully deleted"}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ from modules.datamodels.datamodelRbac import Role
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
from modules.routes.routeNotifications import create_access_change_notification
|
from modules.routes.routeNotifications import create_access_change_notification
|
||||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionCapacityException
|
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionCapacityException
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataMandates")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -103,7 +105,7 @@ def get_mandates(
|
||||||
if not adminMandateIds:
|
if not adminMandateIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required"
|
detail=routeApiMsg("Admin role required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse pagination parameter
|
# Parse pagination parameter
|
||||||
|
|
@ -180,7 +182,7 @@ def get_mandate_filter_values(
|
||||||
if not isSysAdmin:
|
if not isSysAdmin:
|
||||||
adminMandateIds = _getAdminMandateIds(context)
|
adminMandateIds = _getAdminMandateIds(context)
|
||||||
if not adminMandateIds:
|
if not adminMandateIds:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required"))
|
||||||
|
|
||||||
appInterface = interfaceDbApp.getRootInterface()
|
appInterface = interfaceDbApp.getRootInterface()
|
||||||
|
|
||||||
|
|
@ -248,7 +250,7 @@ def get_mandate(
|
||||||
if mandateId not in adminMandateIds:
|
if mandateId not in adminMandateIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required for this mandate"
|
detail=routeApiMsg("Admin role required for this mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
appInterface = interfaceDbApp.getRootInterface()
|
appInterface = interfaceDbApp.getRootInterface()
|
||||||
|
|
@ -289,7 +291,7 @@ def create_mandate(
|
||||||
if not name or (isinstance(name, str) and name.strip() == ''):
|
if not name or (isinstance(name, str) and name.strip() == ''):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Mandate name is required"
|
detail=routeApiMsg("Mandate name is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get optional fields with defaults
|
# Get optional fields with defaults
|
||||||
|
|
@ -308,7 +310,7 @@ def create_mandate(
|
||||||
if not newMandate:
|
if not newMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to create mandate"
|
detail=routeApiMsg("Failed to create mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -392,7 +394,7 @@ def update_mandate(
|
||||||
if not updatedMandate:
|
if not updatedMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update mandate"
|
detail=routeApiMsg("Failed to update mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Mandate {mandateId} updated by SysAdmin {currentUser.id}")
|
logger.info(f"Mandate {mandateId} updated by SysAdmin {currentUser.id}")
|
||||||
|
|
@ -438,7 +440,7 @@ def delete_mandate(
|
||||||
if confirmName != mandateName:
|
if confirmName != mandateName:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Hard-delete requires X-Confirm-Name header matching the mandate name"
|
detail=routeApiMsg("Hard-delete requires X-Confirm-Name header matching the mandate name")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -487,7 +489,7 @@ def list_mandate_users(
|
||||||
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required"
|
detail=routeApiMsg("Mandate-Admin role required")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -647,7 +649,7 @@ def get_mandate_users_filter_values(
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Return distinct filter values for a column in mandate users."""
|
"""Return distinct filter values for a column in mandate users."""
|
||||||
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
|
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate-Admin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate-Admin role required"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
||||||
|
|
@ -714,7 +716,7 @@ def add_user_to_mandate(
|
||||||
if not _hasMandateAdminRole(context, targetMandateId):
|
if not _hasMandateAdminRole(context, targetMandateId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to add users"
|
detail=routeApiMsg("Mandate-Admin role required to add users")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -831,7 +833,7 @@ def remove_user_from_mandate(
|
||||||
if not _hasMandateAdminRole(context, targetMandateId):
|
if not _hasMandateAdminRole(context, targetMandateId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required"
|
detail=routeApiMsg("Mandate-Admin role required")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -857,7 +859,7 @@ def remove_user_from_mandate(
|
||||||
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
|
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Cannot remove the last admin from a mandate. Assign another admin first."
|
detail=routeApiMsg("Cannot remove the last admin from a mandate. Assign another admin first.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
|
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
|
||||||
|
|
@ -920,7 +922,7 @@ def update_user_roles_in_mandate(
|
||||||
if not _hasMandateAdminRole(context, targetMandateId):
|
if not _hasMandateAdminRole(context, targetMandateId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required"
|
detail=routeApiMsg("Mandate-Admin role required")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -953,7 +955,7 @@ def update_user_roles_in_mandate(
|
||||||
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
|
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Cannot remove admin role from the last admin. Assign another admin first."
|
detail=routeApiMsg("Cannot remove admin role from the last admin. Assign another admin first.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove existing role assignments
|
# Remove existing role assignments
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import modules.interfaces.interfaceDbManagement as interfaceDbManagement
|
||||||
from modules.datamodels.datamodelUtils import Prompt
|
from modules.datamodels.datamodelUtils import Prompt
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
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.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataPrompts")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -173,7 +175,7 @@ def update_prompt(
|
||||||
if not updatedPrompt:
|
if not updatedPrompt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error updating the prompt"
|
detail=routeApiMsg("Error updating the prompt")
|
||||||
)
|
)
|
||||||
|
|
||||||
return Prompt(**updatedPrompt)
|
return Prompt(**updatedPrompt)
|
||||||
|
|
@ -207,7 +209,7 @@ def delete_prompt(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error deleting the prompt"
|
detail=routeApiMsg("Error deleting the prompt")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"Prompt with ID {promptId} successfully deleted"}
|
return {"message": f"Prompt with ID {promptId} successfully deleted"}
|
||||||
|
|
@ -10,6 +10,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
from modules.auth.authentication import _hasSysAdminRole
|
from modules.auth.authentication import _hasSysAdminRole
|
||||||
from modules.datamodels.datamodelDataSource import DataSource
|
from modules.datamodels.datamodelDataSource import DataSource
|
||||||
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataSources")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -52,7 +54,7 @@ def _updateDataSourceScope(
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {_VALID_SCOPES}")
|
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {_VALID_SCOPES}")
|
||||||
|
|
||||||
if scope == "global" and not _hasSysAdminRole(context.user):
|
if scope == "global" and not _hasSysAdminRole(context.user):
|
||||||
raise HTTPException(status_code=403, detail="Only sysadmins can set global scope")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Only sysadmins can set global scope"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
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.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeDataUsers")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -297,7 +299,7 @@ def get_user_options(
|
||||||
elif context.hasSysAdminRole:
|
elif context.hasSysAdminRole:
|
||||||
users = appInterface.getAllUsers()
|
users = appInterface.getAllUsers()
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{"value": user.id, "label": user.fullName or user.username or user.email or user.id}
|
{"value": user.id, "label": user.fullName or user.username or user.email or user.id}
|
||||||
|
|
@ -420,7 +422,7 @@ def get_users(
|
||||||
if not adminMandateIds:
|
if not adminMandateIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="No admin access to any mandate"
|
detail=routeApiMsg("No admin access to any mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
from modules.datamodels.datamodelMembership import UserMandate as UserMandateModel
|
from modules.datamodels.datamodelMembership import UserMandate as UserMandateModel
|
||||||
|
|
@ -581,7 +583,7 @@ def get_user(
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="User not in your mandate"
|
detail=routeApiMsg("User not in your mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
@ -636,7 +638,7 @@ def create_user(
|
||||||
if not userRole:
|
if not userRole:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="No 'user' role found in system — cannot assign user to mandate"
|
detail=routeApiMsg("No 'user' role found in system — cannot assign user to mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
appInterface.createUserMandate(
|
appInterface.createUserMandate(
|
||||||
|
|
@ -667,7 +669,7 @@ def update_user(
|
||||||
if not isSelfUpdate and not _isAdminForUser(context, userId):
|
if not isSelfUpdate and not _isAdminForUser(context, userId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to update other users"
|
detail=routeApiMsg("Admin role required to update other users")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use rootInterface for user lookup/update (avoids RBAC filtering on User table)
|
# Use rootInterface for user lookup/update (avoids RBAC filtering on User table)
|
||||||
|
|
@ -687,7 +689,7 @@ def update_user(
|
||||||
if not updatedUser:
|
if not updatedUser:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error updating the user"
|
detail=routeApiMsg("Error updating the user")
|
||||||
)
|
)
|
||||||
|
|
||||||
return updatedUser
|
return updatedUser
|
||||||
|
|
@ -709,7 +711,7 @@ def reset_user_password(
|
||||||
if not _isAdminForUser(context, userId):
|
if not _isAdminForUser(context, userId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to reset passwords"
|
detail=routeApiMsg("Admin role required to reset passwords")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get user interface
|
# Get user interface
|
||||||
|
|
@ -719,7 +721,7 @@ def reset_user_password(
|
||||||
if len(newPassword) < 8:
|
if len(newPassword) < 8:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Password must be at least 8 characters long"
|
detail=routeApiMsg("Password must be at least 8 characters long")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset password
|
# Reset password
|
||||||
|
|
@ -727,7 +729,7 @@ def reset_user_password(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to reset password"
|
detail=routeApiMsg("Failed to reset password")
|
||||||
)
|
)
|
||||||
|
|
||||||
# SECURITY: Automatically revoke all tokens for the user after password reset
|
# SECURITY: Automatically revoke all tokens for the user after password reset
|
||||||
|
|
@ -792,14 +794,14 @@ def change_password(
|
||||||
if not appInterface.verifyPassword(currentPassword, context.user.passwordHash):
|
if not appInterface.verifyPassword(currentPassword, context.user.passwordHash):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Current password is incorrect"
|
detail=routeApiMsg("Current password is incorrect")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate new password strength
|
# Validate new password strength
|
||||||
if len(newPassword) < 8:
|
if len(newPassword) < 8:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="New password must be at least 8 characters long"
|
detail=routeApiMsg("New password must be at least 8 characters long")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Change password
|
# Change password
|
||||||
|
|
@ -807,7 +809,7 @@ def change_password(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to change password"
|
detail=routeApiMsg("Failed to change password")
|
||||||
)
|
)
|
||||||
|
|
||||||
# SECURITY: Automatically revoke all tokens for the user after password change
|
# SECURITY: Automatically revoke all tokens for the user after password change
|
||||||
|
|
@ -877,7 +879,7 @@ def send_password_link(
|
||||||
if not _isAdminForUser(context, userId):
|
if not _isAdminForUser(context, userId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin role required to send password links"
|
detail=routeApiMsg("Admin role required to send password links")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get user interface
|
# Get user interface
|
||||||
|
|
@ -888,14 +890,14 @@ def send_password_link(
|
||||||
if not targetUser:
|
if not targetUser:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="User not found"
|
detail=routeApiMsg("User not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if user has an email
|
# Check if user has an email
|
||||||
if not targetUser.email:
|
if not targetUser.email:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="User has no email address configured"
|
detail=routeApiMsg("User has no email address configured")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use root interface for token operations
|
# Use root interface for token operations
|
||||||
|
|
@ -942,7 +944,7 @@ def send_password_link(
|
||||||
logger.warning(f"Failed to send password setup email to {targetUser.email}")
|
logger.warning(f"Failed to send password setup email to {targetUser.email}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to send email"
|
detail=routeApiMsg("Failed to send email")
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
@ -1010,7 +1012,7 @@ def delete_user(
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Cannot delete user outside your mandate"
|
detail=routeApiMsg("Cannot delete user outside your mandate")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete UserMandate entries for this user first
|
# Delete UserMandate entries for this user first
|
||||||
|
|
@ -1022,7 +1024,7 @@ def delete_user(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error deleting the user"
|
detail=routeApiMsg("Error deleting the user")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"User with ID {userId} successfully deleted"}
|
return {"message": f"User with ID {userId} successfully deleted"}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
from modules.shared.auditLogger import audit_logger
|
from modules.shared.auditLogger import audit_logger
|
||||||
from modules.shared.gdprDeletion import deleteUserDataAcrossAllDatabases, buildDeletionSummary
|
from modules.shared.gdprDeletion import deleteUserDataAcrossAllDatabases, buildDeletionSummary
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeGdpr")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -316,14 +318,14 @@ def delete_account(
|
||||||
if not confirmDeletion:
|
if not confirmDeletion:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Deletion not confirmed. Set confirmDeletion=true to proceed."
|
detail=routeApiMsg("Deletion not confirmed. Set confirmDeletion=true to proceed.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prevent SysAdmin self-deletion (safety measure)
|
# Prevent SysAdmin self-deletion (safety measure)
|
||||||
if getattr(currentUser, "isSysAdmin", False):
|
if getattr(currentUser, "isSysAdmin", False):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="SysAdmin accounts cannot be self-deleted. Contact another SysAdmin."
|
detail=routeApiMsg("SysAdmin accounts cannot be self-deleted. Contact another SysAdmin.")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,11 @@ from modules.datamodels.datamodelNotification import NotificationType
|
||||||
from modules.interfaces.interfaceDbManagement import getInterface as getMgmtInterface
|
from modules.interfaces.interfaceDbManagement import getInterface as getMgmtInterface
|
||||||
from modules.routes.routeNotifications import _createNotification
|
from modules.routes.routeNotifications import _createNotification
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.shared.i18nRegistry import _loadCache as _reloadI18nCache, apiRouteContext
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
|
||||||
|
routeApiMsg = apiRouteContext("routeI18n")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
|
|
@ -270,16 +273,28 @@ async def _translateBatch(
|
||||||
finally:
|
finally:
|
||||||
aiObjects.billingCallback = None
|
aiObjects.billingCallback = None
|
||||||
|
|
||||||
|
_matchCapitalization(keysToTranslate, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _matchCapitalization(originals: Dict[str, str], translations: Dict[str, str]) -> None:
|
||||||
|
"""Ensure translations preserve the capitalisation pattern of the original key."""
|
||||||
|
for key, translated in translations.items():
|
||||||
|
if not key or not translated:
|
||||||
|
continue
|
||||||
|
if key[0].isupper() and translated[0].islower():
|
||||||
|
translations[key] = translated[0].upper() + translated[1:]
|
||||||
|
elif key[0].islower() and translated[0].isupper():
|
||||||
|
translations[key] = translated[0].lower() + translated[1:]
|
||||||
|
|
||||||
|
|
||||||
def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
|
def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
|
||||||
userId = str(currentUser.id)
|
userId = str(currentUser.id)
|
||||||
memberIds = _userMemberMandateIds(currentUser)
|
memberIds = _userMemberMandateIds(currentUser)
|
||||||
if not memberIds:
|
if not memberIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Mindestens eine Mandats-Mitgliedschaft ist für die AI-Nutzung erforderlich.",
|
detail=routeApiMsg("Mindestens eine Mandats-Mitgliedschaft ist für die AI-Nutzung erforderlich."),
|
||||||
)
|
)
|
||||||
|
|
||||||
headerRaw = (
|
headerRaw = (
|
||||||
|
|
@ -289,7 +304,7 @@ def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
|
||||||
if headerRaw not in memberIds:
|
if headerRaw not in memberIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="X-Mandate-Id ist kein Mandat Ihrer Mitgliedschaft.",
|
detail=routeApiMsg("X-Mandate-Id ist kein Mandat Ihrer Mitgliedschaft."),
|
||||||
)
|
)
|
||||||
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
|
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
|
||||||
return headerRaw
|
return headerRaw
|
||||||
|
|
@ -298,7 +313,7 @@ def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
|
||||||
return mid
|
return mid
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
detail="Nicht genügend AI-Guthaben (Mandats-Pool) für diese Aktion.",
|
detail=routeApiMsg("Nicht genügend AI-Guthaben (Mandats-Pool) für diese Aktion."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -348,7 +363,7 @@ async def _readOptionalEntriesFromBody(request: Request) -> Optional[List[dict]]
|
||||||
if not isinstance(entries, list):
|
if not isinstance(entries, list):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Feld «entries» muss ein JSON-Array sein.",
|
detail=routeApiMsg("Feld «entries» muss ein JSON-Array sein."),
|
||||||
)
|
)
|
||||||
result = []
|
result = []
|
||||||
for e in entries:
|
for e in entries:
|
||||||
|
|
@ -363,11 +378,10 @@ async def _readOptionalEntriesFromBody(request: Request) -> Optional[List[dict]]
|
||||||
|
|
||||||
|
|
||||||
def _syncXxMaster(db, userId: Optional[str], incomingEntries: List[dict]) -> Dict[str, Any]:
|
def _syncXxMaster(db, userId: Optional[str], incomingEntries: List[dict]) -> Dict[str, Any]:
|
||||||
"""Synchronise the xx base set with incoming entries (from build bundle or codebase scan).
|
"""Synchronise the xx base set with incoming UI entries.
|
||||||
|
|
||||||
- Keys in incoming but not in DB -> add
|
Only touches entries whose context is "ui". Gateway entries (api.*, table.*)
|
||||||
- Keys in DB but not in incoming -> remove
|
written by _syncRegistryToDb at boot are preserved untouched.
|
||||||
- Keys in both -> update context (value)
|
|
||||||
"""
|
"""
|
||||||
if not incomingEntries:
|
if not incomingEntries:
|
||||||
logger.warning("i18n xx-sync: no entries — aborting")
|
logger.warning("i18n xx-sync: no entries — aborting")
|
||||||
|
|
@ -394,39 +408,45 @@ def _syncXxMaster(db, userId: Optional[str], incomingEntries: List[dict]) -> Dic
|
||||||
|
|
||||||
row = dict(rows[0])
|
row = dict(rows[0])
|
||||||
curEntries = _rowEntries(row)
|
curEntries = _rowEntries(row)
|
||||||
curByKey = {e["key"]: e for e in curEntries}
|
|
||||||
|
gatewayEntries = [e for e in curEntries if e.get("context", "ui") != "ui"]
|
||||||
|
curUiByKey = {e["key"]: e for e in curEntries if e.get("context", "ui") == "ui"}
|
||||||
incomingByKey = {e["key"]: e for e in incomingEntries}
|
incomingByKey = {e["key"]: e for e in incomingEntries}
|
||||||
|
|
||||||
incomingKeys = set(incomingByKey.keys())
|
incomingKeys = set(incomingByKey.keys())
|
||||||
dbKeys = set(curByKey.keys())
|
dbUiKeys = set(curUiByKey.keys())
|
||||||
|
|
||||||
added = sorted(incomingKeys - dbKeys)
|
added = sorted(incomingKeys - dbUiKeys)
|
||||||
removed = sorted(dbKeys - incomingKeys)
|
removed = sorted(dbUiKeys - incomingKeys)
|
||||||
|
|
||||||
newEntries = []
|
newUiEntries = [
|
||||||
for e in incomingEntries:
|
{"context": e["context"], "key": e["key"], "value": e["value"]}
|
||||||
newEntries.append({"context": e["context"], "key": e["key"], "value": e["value"]})
|
for e in incomingEntries
|
||||||
for e in curEntries:
|
]
|
||||||
if e["key"] not in incomingKeys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not added and not removed and all(
|
if not added and not removed and all(
|
||||||
curByKey.get(e["key"], {}).get("value") == e["value"]
|
curUiByKey.get(e["key"], {}).get("value") == e["value"]
|
||||||
and curByKey.get(e["key"], {}).get("context") == e["context"]
|
and curUiByKey.get(e["key"], {}).get("context") == e["context"]
|
||||||
for e in incomingEntries
|
for e in incomingEntries
|
||||||
):
|
):
|
||||||
return {"added": [], "removed": [], "entriesCount": len(newEntries)}
|
total = len(newUiEntries) + len(gatewayEntries)
|
||||||
|
return {"added": [], "removed": [], "entriesCount": total}
|
||||||
|
|
||||||
|
mergedEntries = gatewayEntries + newUiEntries
|
||||||
|
|
||||||
now = getUtcTimestamp()
|
now = getUtcTimestamp()
|
||||||
row["entries"] = newEntries
|
row["entries"] = mergedEntries
|
||||||
if "keys" in row:
|
if "keys" in row:
|
||||||
del row["keys"]
|
del row["keys"]
|
||||||
row["sysModifiedAt"] = now
|
row["sysModifiedAt"] = now
|
||||||
row["sysModifiedBy"] = userId
|
row["sysModifiedBy"] = userId
|
||||||
db.recordModify(UiLanguageSet, "xx", row)
|
db.recordModify(UiLanguageSet, "xx", row)
|
||||||
|
|
||||||
logger.info("i18n xx-master sync: +%d added, -%d removed, total=%d", len(added), len(removed), len(newEntries))
|
logger.info(
|
||||||
return {"added": added, "removed": removed, "entriesCount": len(newEntries)}
|
"i18n xx-master sync: +%d added, -%d removed (ui=%d, gateway=%d, total=%d)",
|
||||||
|
len(added), len(removed), len(newUiEntries), len(gatewayEntries), len(mergedEntries),
|
||||||
|
)
|
||||||
|
return {"added": added, "removed": removed, "entriesCount": len(mergedEntries)}
|
||||||
|
|
||||||
|
|
||||||
# --- Public -----------------------------------------------------------------
|
# --- Public -----------------------------------------------------------------
|
||||||
|
|
@ -439,6 +459,8 @@ async def list_language_codes():
|
||||||
out = []
|
out = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
entries = _rowEntries(r)
|
entries = _rowEntries(r)
|
||||||
|
uiCount = sum(1 for e in entries if e.get("context", "ui") == "ui")
|
||||||
|
gatewayCount = len(entries) - uiCount
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
"code": r["id"],
|
"code": r["id"],
|
||||||
|
|
@ -446,6 +468,8 @@ async def list_language_codes():
|
||||||
"status": r.get("status"),
|
"status": r.get("status"),
|
||||||
"isDefault": bool(r.get("isDefault")),
|
"isDefault": bool(r.get("isDefault")),
|
||||||
"entriesCount": len(entries),
|
"entriesCount": len(entries),
|
||||||
|
"uiCount": uiCount,
|
||||||
|
"gatewayCount": gatewayCount,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return sorted(out, key=lambda x: (not x.get("isDefault"), x["code"]))
|
return sorted(out, key=lambda x: (not x.get("isDefault"), x["code"]))
|
||||||
|
|
@ -456,7 +480,7 @@ async def get_language_set(code: str):
|
||||||
db = _publicMgmtDb()
|
db = _publicMgmtDb()
|
||||||
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
||||||
if not rows:
|
if not rows:
|
||||||
raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
|
||||||
return _row_to_public(rows[0])
|
return _row_to_public(rows[0])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -472,7 +496,7 @@ def _validate_iso2_code(code: str) -> str:
|
||||||
c = code.strip().lower()
|
c = code.strip().lower()
|
||||||
if not re.fullmatch(r"[a-z]{2}", c):
|
if not re.fullmatch(r"[a-z]{2}", c):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400, detail="Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt."
|
status_code=400, detail=routeApiMsg("Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt.")
|
||||||
)
|
)
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
@ -530,6 +554,7 @@ async def _run_create_language_job_async(userId: str, code: str, label: str, cur
|
||||||
title="Sprachset erstellt",
|
title="Sprachset erstellt",
|
||||||
message=f"Die Sprache «{label}» ({code}) wurde per KI übersetzt{statusHint}.",
|
message=f"Die Sprache «{label}» ({code}) wurde per KI übersetzt{statusHint}.",
|
||||||
)
|
)
|
||||||
|
await _reloadI18nCache()
|
||||||
logger.info("i18n create job done: code=%s, translated=%d/%d", code, len(translated), len(xxEntries))
|
logger.info("i18n create job done: code=%s, translated=%d/%d", code, len(translated), len(xxEntries))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("create language job failed: %s", e)
|
logger.exception("create language job failed: %s", e)
|
||||||
|
|
@ -551,16 +576,16 @@ async def create_language_set(
|
||||||
mandateId = _resolveMandateIdForAiI18n(request, currentUser)
|
mandateId = _resolveMandateIdForAiI18n(request, currentUser)
|
||||||
code = _validate_iso2_code(body.code)
|
code = _validate_iso2_code(body.code)
|
||||||
if code == "xx":
|
if code == "xx":
|
||||||
raise HTTPException(status_code=400, detail="Das Basisset «xx» kann nicht manuell angelegt werden.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Das Basisset «xx» kann nicht manuell angelegt werden."))
|
||||||
|
|
||||||
db = _publicMgmtDb()
|
db = _publicMgmtDb()
|
||||||
existing = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
existing = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=409, detail="Dieses Sprachset existiert bereits.")
|
raise HTTPException(status_code=409, detail=routeApiMsg("Dieses Sprachset existiert bereits."))
|
||||||
|
|
||||||
xxEntries = _loadMasterXxEntries(db)
|
xxEntries = _loadMasterXxEntries(db)
|
||||||
if not xxEntries:
|
if not xxEntries:
|
||||||
raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden. Bitte zuerst UI-Keys einlesen.")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden. Bitte zuerst UI-Keys einlesen."))
|
||||||
|
|
||||||
resolvedLabel = (body.label or "").strip() if body.label else ""
|
resolvedLabel = (body.label or "").strip() if body.label else ""
|
||||||
if not resolvedLabel:
|
if not resolvedLabel:
|
||||||
|
|
@ -594,54 +619,59 @@ async def create_language_set(
|
||||||
def _compute_language_sync_diff(db, code: str) -> dict:
|
def _compute_language_sync_diff(db, code: str) -> dict:
|
||||||
"""Return key sync metrics before AI translate (no DB writes)."""
|
"""Return key sync metrics before AI translate (no DB writes)."""
|
||||||
if code == "xx":
|
if code == "xx":
|
||||||
raise HTTPException(status_code=400, detail="Das xx-Set wird separat synchronisiert.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird separat synchronisiert."))
|
||||||
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
||||||
if not rows:
|
if not rows:
|
||||||
raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
|
||||||
xx_entries = _loadMasterXxEntries(db)
|
xxEntries = _loadMasterXxEntries(db)
|
||||||
if not xx_entries:
|
if not xxEntries:
|
||||||
raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden.")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden."))
|
||||||
row = dict(rows[0])
|
row = dict(rows[0])
|
||||||
cur_entries = _rowEntries(row)
|
curEntries = _rowEntries(row)
|
||||||
cur_by_key = {e["key"]: e for e in cur_entries}
|
masterIds = {_entryId(e) for e in xxEntries}
|
||||||
xx_by_key = {e["key"]: e for e in xx_entries}
|
currentIds = {_entryId(e) for e in curEntries}
|
||||||
master_keys = set(xx_by_key.keys())
|
|
||||||
current_keys = set(cur_by_key.keys())
|
|
||||||
added_count = len(master_keys - current_keys)
|
|
||||||
removed_count = len(current_keys - master_keys)
|
|
||||||
return {
|
return {
|
||||||
"code": code,
|
"code": code,
|
||||||
"addedCount": added_count,
|
"addedCount": len(masterIds - currentIds),
|
||||||
"removedCount": removed_count,
|
"removedCount": len(currentIds - masterIds),
|
||||||
"masterEntryCount": len(master_keys),
|
"masterEntryCount": len(masterIds),
|
||||||
"currentEntryCount": len(current_keys),
|
"currentEntryCount": len(currentIds),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _entryId(e: dict) -> tuple:
|
||||||
|
"""Composite identifier for an i18n entry: (key, context)."""
|
||||||
|
return (e["key"], e.get("context", "ui"))
|
||||||
|
|
||||||
|
|
||||||
async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: Optional[User] = None) -> dict:
|
async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: Optional[User] = None) -> dict:
|
||||||
"""Synchronise a language set (incl. de) against the xx base set via AI."""
|
"""Synchronise a language set (incl. de) against the xx base set via AI.
|
||||||
|
|
||||||
|
Entries are identified by (key, context) — the same text can appear
|
||||||
|
with different contexts (e.g. "ui" and "api.routeXyz").
|
||||||
|
"""
|
||||||
if code == "xx":
|
if code == "xx":
|
||||||
raise HTTPException(status_code=400, detail="Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert."))
|
||||||
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
|
||||||
if not rows:
|
if not rows:
|
||||||
raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
|
||||||
xxEntries = _loadMasterXxEntries(db)
|
xxEntries = _loadMasterXxEntries(db)
|
||||||
if not xxEntries:
|
if not xxEntries:
|
||||||
raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden.")
|
raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden."))
|
||||||
|
|
||||||
row = dict(rows[0])
|
row = dict(rows[0])
|
||||||
curEntries = _rowEntries(row)
|
curEntries = _rowEntries(row)
|
||||||
curByKey = {e["key"]: e for e in curEntries}
|
curById = {_entryId(e): e for e in curEntries}
|
||||||
xxByKey = {e["key"]: e for e in xxEntries}
|
xxById = {_entryId(e): e for e in xxEntries}
|
||||||
|
|
||||||
masterKeys = set(xxByKey.keys())
|
masterIds = set(xxById.keys())
|
||||||
currentKeys = set(curByKey.keys())
|
currentIds = set(curById.keys())
|
||||||
removedKeys = sorted(currentKeys - masterKeys)
|
removedIds = currentIds - masterIds
|
||||||
addedKeys = sorted(masterKeys - currentKeys)
|
addedIds = masterIds - currentIds
|
||||||
|
|
||||||
translatedCount = 0
|
translatedCount = 0
|
||||||
if addedKeys:
|
if addedIds:
|
||||||
toTranslate = {k: xxByKey[k].get("value", "") for k in addedKeys}
|
toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds}
|
||||||
langLabel = row.get("label") or code
|
langLabel = row.get("label") or code
|
||||||
billingCb = None
|
billingCb = None
|
||||||
if adminUser:
|
if adminUser:
|
||||||
|
|
@ -650,28 +680,29 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O
|
||||||
billingCb = _makeBillingCallback(adminUser, memberIds[0])
|
billingCb = _makeBillingCallback(adminUser, memberIds[0])
|
||||||
try:
|
try:
|
||||||
translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb)
|
translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb)
|
||||||
translatedCount = sum(1 for k in addedKeys if k in translated)
|
translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("AI translation during sync failed for %s: %s", code, e)
|
logger.error("AI translation during sync failed for %s: %s", code, e)
|
||||||
translated = {}
|
translated = {}
|
||||||
|
|
||||||
for k in addedKeys:
|
for eid in addedIds:
|
||||||
curByKey[k] = {
|
xxEntry = xxById[eid]
|
||||||
"context": xxByKey[k]["context"],
|
curById[eid] = {
|
||||||
"key": k,
|
"context": xxEntry["context"],
|
||||||
"value": translated.get(k, f"[{k}]"),
|
"key": xxEntry["key"],
|
||||||
|
"value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for k in removedKeys:
|
for eid in removedIds:
|
||||||
del curByKey[k]
|
del curById[eid]
|
||||||
|
|
||||||
for k in masterKeys & currentKeys:
|
for eid in masterIds & currentIds:
|
||||||
curByKey[k]["context"] = xxByKey[k]["context"]
|
curById[eid]["context"] = xxById[eid]["context"]
|
||||||
|
|
||||||
newEntries = [curByKey[k] for k in sorted(curByKey.keys(), key=lambda x: x.lower())]
|
newEntries = sorted(curById.values(), key=lambda e: (e["key"].lower(), e.get("context", "")))
|
||||||
|
|
||||||
now = getUtcTimestamp()
|
now = getUtcTimestamp()
|
||||||
untranslated = len(addedKeys) - translatedCount
|
untranslated = len(addedIds) - translatedCount
|
||||||
row["entries"] = newEntries
|
row["entries"] = newEntries
|
||||||
if "keys" in row:
|
if "keys" in row:
|
||||||
del row["keys"]
|
del row["keys"]
|
||||||
|
|
@ -681,8 +712,8 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O
|
||||||
db.recordModify(UiLanguageSet, code, row)
|
db.recordModify(UiLanguageSet, code, row)
|
||||||
return {
|
return {
|
||||||
"code": code,
|
"code": code,
|
||||||
"added": addedKeys,
|
"added": sorted({xxById[eid]["key"] for eid in addedIds}),
|
||||||
"removed": removedKeys,
|
"removed": sorted({eid[0] for eid in removedIds}),
|
||||||
"translated": translatedCount,
|
"translated": translatedCount,
|
||||||
"entriesCount": len(newEntries),
|
"entriesCount": len(newEntries),
|
||||||
}
|
}
|
||||||
|
|
@ -701,7 +732,9 @@ async def sync_xx_master(
|
||||||
db = getMgmtInterface(adminUser, mandateId=None).db
|
db = getMgmtInterface(adminUser, mandateId=None).db
|
||||||
fromBody = await _readOptionalEntriesFromBody(request)
|
fromBody = await _readOptionalEntriesFromBody(request)
|
||||||
entries = fromBody if fromBody is not None else _scanCodebaseKeys()
|
entries = fromBody if fromBody is not None else _scanCodebaseKeys()
|
||||||
return _syncXxMaster(db, str(adminUser.id), entries)
|
result = _syncXxMaster(db, str(adminUser.id), entries)
|
||||||
|
await _reloadI18nCache()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/sets/update-all")
|
@router.put("/sets/update-all")
|
||||||
|
|
@ -727,6 +760,7 @@ async def update_all_language_sets(
|
||||||
continue
|
continue
|
||||||
res = await _syncLanguageWithXx(db, cid, str(adminUser.id), adminUser=adminUser)
|
res = await _syncLanguageWithXx(db, cid, str(adminUser.id), adminUser=adminUser)
|
||||||
results.append(res)
|
results.append(res)
|
||||||
|
await _reloadI18nCache()
|
||||||
return {"xxSync": xxSync, "updated": results}
|
return {"xxSync": xxSync, "updated": results}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -738,7 +772,7 @@ async def get_language_sync_diff(
|
||||||
"""How many keys would be added/removed vs xx before running a full sync (SysAdmin)."""
|
"""How many keys would be added/removed vs xx before running a full sync (SysAdmin)."""
|
||||||
c = code.strip().lower()
|
c = code.strip().lower()
|
||||||
if c in ("update-all", "sync-xx", "sync-de"):
|
if c in ("update-all", "sync-xx", "sync-de"):
|
||||||
raise HTTPException(status_code=400, detail="Ungültiger Sprachcode.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Ungültiger Sprachcode."))
|
||||||
db = getMgmtInterface(adminUser, mandateId=None).db
|
db = getMgmtInterface(adminUser, mandateId=None).db
|
||||||
return _compute_language_sync_diff(db, c)
|
return _compute_language_sync_diff(db, c)
|
||||||
|
|
||||||
|
|
@ -750,11 +784,13 @@ async def update_language_set(
|
||||||
):
|
):
|
||||||
c = code.strip().lower()
|
c = code.strip().lower()
|
||||||
if c in ("update-all", "sync-xx", "sync-de"):
|
if c in ("update-all", "sync-xx", "sync-de"):
|
||||||
raise HTTPException(status_code=400, detail="Ungültiger Sprachcode.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Ungültiger Sprachcode."))
|
||||||
if c == "xx":
|
if c == "xx":
|
||||||
raise HTTPException(status_code=400, detail="Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert."))
|
||||||
db = getMgmtInterface(adminUser, mandateId=None).db
|
db = getMgmtInterface(adminUser, mandateId=None).db
|
||||||
return await _syncLanguageWithXx(db, c, str(adminUser.id), adminUser=adminUser)
|
result = await _syncLanguageWithXx(db, c, str(adminUser.id), adminUser=adminUser)
|
||||||
|
await _reloadI18nCache()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/sets/{code}")
|
@router.delete("/sets/{code}")
|
||||||
|
|
@ -768,7 +804,8 @@ async def delete_language_set(
|
||||||
db = getMgmtInterface(adminUser, mandateId=None).db
|
db = getMgmtInterface(adminUser, mandateId=None).db
|
||||||
ok = db.recordDelete(UiLanguageSet, c)
|
ok = db.recordDelete(UiLanguageSet, c)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
|
||||||
|
await _reloadI18nCache()
|
||||||
return {"deleted": c}
|
return {"deleted": c}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -780,7 +817,7 @@ async def download_language_set(
|
||||||
db = _publicMgmtDb()
|
db = _publicMgmtDb()
|
||||||
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code.strip().lower()})
|
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code.strip().lower()})
|
||||||
if not rows:
|
if not rows:
|
||||||
raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
|
||||||
payload = _row_to_public(rows[0])
|
payload = _row_to_public(rows[0])
|
||||||
raw = json.dumps(payload, ensure_ascii=False, indent=2)
|
raw = json.dumps(payload, ensure_ascii=False, indent=2)
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -828,7 +865,7 @@ async def import_language_sets(
|
||||||
adminUser: User = Depends(requireSysAdminRole),
|
adminUser: User = Depends(requireSysAdminRole),
|
||||||
):
|
):
|
||||||
if not file.filename or not file.filename.endswith(".json"):
|
if not file.filename or not file.filename.endswith(".json"):
|
||||||
raise HTTPException(status_code=400, detail="Nur .json-Dateien erlaubt.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Nur .json-Dateien erlaubt."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = await file.read()
|
raw = await file.read()
|
||||||
|
|
@ -837,7 +874,7 @@ async def import_language_sets(
|
||||||
raise HTTPException(status_code=400, detail=f"Ungültiges JSON: {e}")
|
raise HTTPException(status_code=400, detail=f"Ungültiges JSON: {e}")
|
||||||
|
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
raise HTTPException(status_code=400, detail="JSON muss ein Array von Sprachsets sein.")
|
raise HTTPException(status_code=400, detail=routeApiMsg("JSON muss ein Array von Sprachsets sein."))
|
||||||
|
|
||||||
db = getMgmtInterface(adminUser, mandateId=None).db
|
db = getMgmtInterface(adminUser, mandateId=None).db
|
||||||
now = getUtcTimestamp()
|
now = getUtcTimestamp()
|
||||||
|
|
@ -893,4 +930,44 @@ async def import_language_sets(
|
||||||
created.append(code)
|
created.append(code)
|
||||||
|
|
||||||
logger.info("i18n import: created=%s, updated=%s", created, updated)
|
logger.info("i18n import: created=%s, updated=%s", created, updated)
|
||||||
|
await _reloadI18nCache()
|
||||||
return {"created": created, "updated": updated, "totalProcessed": len(created) + len(updated)}
|
return {"created": created, "updated": updated, "totalProcessed": len(created) + len(updated)}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Phase 7b: translate-field — on-demand translation for TextMultilingual fields
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_TRANSLATE_FIELD_MAX_LEN = 2000
|
||||||
|
|
||||||
|
|
||||||
|
class TranslateFieldRequest(BaseModel):
|
||||||
|
sourceText: str = Field(..., min_length=1, max_length=_TRANSLATE_FIELD_MAX_LEN)
|
||||||
|
sourceLang: str = Field(default="de", min_length=2, max_length=5)
|
||||||
|
targetLangs: List[str] = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/translate-field")
|
||||||
|
async def translateField(
|
||||||
|
body: TranslateFieldRequest,
|
||||||
|
request: Request,
|
||||||
|
currentUser: User = Depends(getCurrentUser),
|
||||||
|
):
|
||||||
|
"""Translate a single text into one or more target languages (for TextMultilingual fields)."""
|
||||||
|
targets = [c for c in body.targetLangs if c != body.sourceLang]
|
||||||
|
if not targets:
|
||||||
|
return {"translations": {}}
|
||||||
|
|
||||||
|
mandateId = _resolveMandateIdForAiI18n(request, currentUser)
|
||||||
|
billingCb = _makeBillingCallback(currentUser, mandateId)
|
||||||
|
|
||||||
|
results: Dict[str, str] = {}
|
||||||
|
for targetCode in targets:
|
||||||
|
targetLabel = _ISO_LABELS.get(targetCode, targetCode)
|
||||||
|
keysToTranslate = {body.sourceText: "TextMultilingual field"}
|
||||||
|
translated = await _translateBatch(keysToTranslate, targetLabel, targetCode, billingCb)
|
||||||
|
val = translated.get(body.sourceText, "")
|
||||||
|
if val:
|
||||||
|
results[targetCode] = val
|
||||||
|
|
||||||
|
return {"translations": results}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ from modules.routes.routeDataUsers import _applyFiltersAndSort
|
||||||
from modules.datamodels.datamodelInvitation import Invitation
|
from modules.datamodels.datamodelInvitation import Invitation
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeInvitations")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -161,7 +163,7 @@ def create_invitation(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required for mandate-level invitations"
|
detail=routeApiMsg("X-Mandate-Id header is required for mandate-level invitations")
|
||||||
)
|
)
|
||||||
mandateId = str(context.mandateId)
|
mandateId = str(context.mandateId)
|
||||||
# Validate roles are mandate-level (no featureInstanceId)
|
# Validate roles are mandate-level (no featureInstanceId)
|
||||||
|
|
@ -188,12 +190,12 @@ def create_invitation(
|
||||||
if str(context.mandateId) != mandateId:
|
if str(context.mandateId) != mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this mandate"
|
detail=routeApiMsg("Access denied to this mandate")
|
||||||
)
|
)
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to create invitations"
|
detail=routeApiMsg("Mandate-Admin role required to create invitations")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate expiration time
|
# Calculate expiration time
|
||||||
|
|
@ -427,14 +429,14 @@ def list_invitations(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to list invitations"
|
detail=routeApiMsg("Mandate-Admin role required to list invitations")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -522,9 +524,9 @@ def get_invitation_filter_values(
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Return distinct filter values for a column in invitations."""
|
"""Return distinct filter values for a column in invitations."""
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="X-Mandate-Id header is required")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("X-Mandate-Id header is required"))
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate-Admin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate-Admin role required"))
|
||||||
try:
|
try:
|
||||||
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
from modules.routes.routeDataUsers import _handleFilterValuesRequest
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
@ -575,14 +577,14 @@ def revoke_invitation(
|
||||||
if not context.mandateId:
|
if not context.mandateId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="X-Mandate-Id header is required"
|
detail=routeApiMsg("X-Mandate-Id header is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check mandate admin permission
|
# Check mandate admin permission
|
||||||
if not _hasMandateAdminRole(context):
|
if not _hasMandateAdminRole(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Mandate-Admin role required to revoke invitations"
|
detail=routeApiMsg("Mandate-Admin role required to revoke invitations")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -601,14 +603,14 @@ def revoke_invitation(
|
||||||
if str(invitation.mandateId) != str(context.mandateId):
|
if str(invitation.mandateId) != str(context.mandateId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this invitation"
|
detail=routeApiMsg("Access denied to this invitation")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Already revoked?
|
# Already revoked?
|
||||||
if invitation.revokedAt:
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation is already revoked"
|
detail=routeApiMsg("Invitation is already revoked")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Revoke invitation
|
# Revoke invitation
|
||||||
|
|
@ -781,14 +783,14 @@ def accept_invitation(
|
||||||
if not invitation:
|
if not invitation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Invitation not found"
|
detail=routeApiMsg("Invitation not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate invitation
|
# Validate invitation
|
||||||
if invitation.revokedAt:
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has been revoked"
|
detail=routeApiMsg("Invitation has been revoked")
|
||||||
)
|
)
|
||||||
|
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
|
|
@ -796,7 +798,7 @@ def accept_invitation(
|
||||||
if expiresAt < currentTime:
|
if expiresAt < currentTime:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has expired"
|
detail=routeApiMsg("Invitation has expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
currentUses = invitation.currentUses or 0
|
currentUses = invitation.currentUses or 0
|
||||||
|
|
@ -804,7 +806,7 @@ def accept_invitation(
|
||||||
if currentUses >= maxUses:
|
if currentUses >= maxUses:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has reached maximum uses"
|
detail=routeApiMsg("Invitation has reached maximum uses")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate user matches - invitation is bound by username or email
|
# Validate user matches - invitation is bound by username or email
|
||||||
|
|
@ -833,7 +835,7 @@ def accept_invitation(
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has no target user or email"
|
detail=routeApiMsg("Invitation has no target user or email")
|
||||||
)
|
)
|
||||||
|
|
||||||
mandateId = str(invitation.mandateId) if invitation.mandateId else None
|
mandateId = str(invitation.mandateId) if invitation.mandateId else None
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ from modules.datamodels.datamodelMessaging import (
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
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.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeMessaging")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -139,7 +141,7 @@ def update_subscription(
|
||||||
if not updatedSubscription:
|
if not updatedSubscription:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error updating the subscription"
|
detail=routeApiMsg("Error updating the subscription")
|
||||||
)
|
)
|
||||||
|
|
||||||
return MessagingSubscription(**updatedSubscription)
|
return MessagingSubscription(**updatedSubscription)
|
||||||
|
|
@ -166,7 +168,7 @@ def delete_subscription(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error deleting the subscription"
|
detail=routeApiMsg("Error deleting the subscription")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"Subscription with ID {subscriptionId} successfully deleted"}
|
return {"message": f"Subscription with ID {subscriptionId} successfully deleted"}
|
||||||
|
|
@ -263,7 +265,7 @@ def unsubscribe_user(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Registration not found"
|
detail=routeApiMsg("Registration not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"Successfully unsubscribed from {subscriptionId} for channel {channel.value}"}
|
return {"message": f"Successfully unsubscribed from {subscriptionId} for channel {channel.value}"}
|
||||||
|
|
@ -339,7 +341,7 @@ def update_registration(
|
||||||
if not updatedRegistration:
|
if not updatedRegistration:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error updating the registration"
|
detail=routeApiMsg("Error updating the registration")
|
||||||
)
|
)
|
||||||
|
|
||||||
return MessagingSubscriptionRegistration(**updatedRegistration)
|
return MessagingSubscriptionRegistration(**updatedRegistration)
|
||||||
|
|
@ -366,7 +368,7 @@ def delete_registration(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Error deleting the registration"
|
detail=routeApiMsg("Error deleting the registration")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": f"Registration with ID {registrationId} successfully deleted"}
|
return {"message": f"Registration with ID {registrationId} successfully deleted"}
|
||||||
|
|
@ -397,7 +399,7 @@ def trigger_subscription(
|
||||||
if not _hasTriggerPermission(context):
|
if not _hasTriggerPermission(context):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Admin or Mandate-Admin role required to trigger subscriptions"
|
detail=routeApiMsg("Admin or Mandate-Admin role required to trigger subscriptions")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get messaging service from request app state
|
# Get messaging service from request app state
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ from modules.datamodels.datamodelNotification import (
|
||||||
from modules.datamodels.datamodelRbac import Role
|
from modules.datamodels.datamodelRbac import Role
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeNotifications")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -238,14 +240,14 @@ def markAsRead(
|
||||||
if not notification:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail=routeApiMsg("Notification not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if str(notification.userId) != str(currentUser.id):
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to access this notification"
|
detail=routeApiMsg("Not authorized to access this notification")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update status
|
# Update status
|
||||||
|
|
@ -332,21 +334,21 @@ def executeAction(
|
||||||
if not notification:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail=routeApiMsg("Notification not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if str(notification.userId) != str(currentUser.id):
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to access this notification"
|
detail=routeApiMsg("Not authorized to access this notification")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if already actioned
|
# Check if already actioned
|
||||||
if notification.status == NotificationStatus.ACTIONED.value:
|
if notification.status == NotificationStatus.ACTIONED.value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Notification has already been actioned"
|
detail=routeApiMsg("Notification has already been actioned")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate action exists
|
# Validate action exists
|
||||||
|
|
@ -416,7 +418,7 @@ def _handleInvitationAction(
|
||||||
if not invitationId:
|
if not invitationId:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="No invitation reference found"
|
detail=routeApiMsg("No invitation reference found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the invitation (Pydantic model)
|
# Get the invitation (Pydantic model)
|
||||||
|
|
@ -425,7 +427,7 @@ def _handleInvitationAction(
|
||||||
if not invitation:
|
if not invitation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Invitation not found"
|
detail=routeApiMsg("Invitation not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify user matches (username or email)
|
# Verify user matches (username or email)
|
||||||
|
|
@ -436,18 +438,18 @@ def _handleInvitationAction(
|
||||||
if currentUser.username != targetUsername:
|
if currentUser.username != targetUsername:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="This invitation is for a different user"
|
detail=routeApiMsg("This invitation is for a different user")
|
||||||
)
|
)
|
||||||
elif invitationEmail:
|
elif invitationEmail:
|
||||||
if not currentUserEmail or currentUserEmail != invitationEmail:
|
if not currentUserEmail or currentUserEmail != invitationEmail:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="This invitation is for a different user"
|
detail=routeApiMsg("This invitation is for a different user")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has no target user or email"
|
detail=routeApiMsg("Invitation has no target user or email")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if invitation is still valid
|
# Check if invitation is still valid
|
||||||
|
|
@ -456,13 +458,13 @@ def _handleInvitationAction(
|
||||||
if expiresAt < currentTime:
|
if expiresAt < currentTime:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has expired"
|
detail=routeApiMsg("Invitation has expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
if invitation.revokedAt:
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has been revoked"
|
detail=routeApiMsg("Invitation has been revoked")
|
||||||
)
|
)
|
||||||
|
|
||||||
currentUses = invitation.currentUses or 0
|
currentUses = invitation.currentUses or 0
|
||||||
|
|
@ -470,7 +472,7 @@ def _handleInvitationAction(
|
||||||
if currentUses >= maxUses:
|
if currentUses >= maxUses:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has reached maximum uses"
|
detail=routeApiMsg("Invitation has reached maximum uses")
|
||||||
)
|
)
|
||||||
|
|
||||||
if actionId == "accept":
|
if actionId == "accept":
|
||||||
|
|
@ -565,14 +567,14 @@ def deleteNotification(
|
||||||
if not notification:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail=routeApiMsg("Notification not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if str(notification.userId) != str(currentUser.id):
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to delete this notification"
|
detail=routeApiMsg("Not authorized to delete this notification")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark as dismissed (soft delete)
|
# Mark as dismissed (soft delete)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ from modules.routes.routeRealEstateScraping import (
|
||||||
|
|
||||||
# Import attribute utilities for model schema
|
# Import attribute utilities for model schema
|
||||||
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeRealEstate")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -308,7 +310,7 @@ async def update_project(
|
||||||
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
updated = interface.updateProjekt(projectId, data)
|
updated = interface.updateProjekt(projectId, data)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail="Update failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -329,7 +331,7 @@ async def delete_project(
|
||||||
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
|
||||||
if not interface.deleteProjekt(projectId):
|
if not interface.deleteProjekt(projectId):
|
||||||
raise HTTPException(status_code=500, detail="Delete failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
|
||||||
|
|
||||||
|
|
||||||
# ----- Parcels CRUD -----
|
# ----- Parcels CRUD -----
|
||||||
|
|
@ -429,7 +431,7 @@ async def update_parcel(
|
||||||
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
updated = interface.updateParzelle(parcelId, data)
|
updated = interface.updateParzelle(parcelId, data)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail="Update failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -450,7 +452,7 @@ async def delete_parcel(
|
||||||
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
|
||||||
if not interface.deleteParzelle(parcelId):
|
if not interface.deleteParzelle(parcelId):
|
||||||
raise HTTPException(status_code=500, detail="Delete failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -495,7 +497,7 @@ async def process_command(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -503,7 +505,7 @@ async def process_command(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -513,7 +515,7 @@ async def process_command(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Processing command request from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
logger.info(f"Processing command request from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
||||||
|
|
@ -566,7 +568,7 @@ async def get_available_tables(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -574,7 +576,7 @@ async def get_available_tables(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -584,7 +586,7 @@ async def get_available_tables(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Getting available tables for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
logger.info(f"Getting available tables for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
||||||
|
|
@ -675,7 +677,7 @@ async def get_table_data(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -683,7 +685,7 @@ async def get_table_data(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -693,7 +695,7 @@ async def get_table_data(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Getting table data for '{table}' from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
logger.info(f"Getting table data for '{table}' from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
||||||
|
|
@ -844,7 +846,7 @@ async def create_table_record(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -852,7 +854,7 @@ async def create_table_record(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -862,7 +864,7 @@ async def create_table_record(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Special handling for Projekt with parcel data
|
# Special handling for Projekt with parcel data
|
||||||
|
|
@ -874,7 +876,7 @@ async def create_table_record(
|
||||||
if not label:
|
if not label:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="label is required"
|
detail=routeApiMsg("label is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
status_prozess = data.get("statusProzess", "Eingang")
|
status_prozess = data.get("statusProzess", "Eingang")
|
||||||
|
|
@ -887,7 +889,7 @@ async def create_table_record(
|
||||||
if not isinstance(parzellen_data, list):
|
if not isinstance(parzellen_data, list):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="parzellen must be an array"
|
detail=routeApiMsg("parzellen must be an array")
|
||||||
)
|
)
|
||||||
elif "parzelle" in data:
|
elif "parzelle" in data:
|
||||||
# Single parcel (backward compatibility)
|
# Single parcel (backward compatibility)
|
||||||
|
|
@ -898,7 +900,7 @@ async def create_table_record(
|
||||||
if not parzellen_data:
|
if not parzellen_data:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="parzelle or parzellen data is required"
|
detail=routeApiMsg("parzelle or parzellen data is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use helper function to create project with parcel data
|
# Use helper function to create project with parcel data
|
||||||
|
|
@ -1073,7 +1075,7 @@ async def search_parcel(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Searching parcel for user {currentUser.id} (mandate: {currentUser.mandateId}) with location: {location}")
|
logger.info(f"Searching parcel for user {currentUser.id} (mandate: {currentUser.mandateId}) with location: {location}")
|
||||||
|
|
@ -2059,21 +2061,21 @@ async def add_parcel_to_project(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate CSRF token format
|
# Validate CSRF token format
|
||||||
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
int(csrf_token, 16)
|
int(csrf_token, 16)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Adding parcel to project {projekt_id} for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
logger.info(f"Adding parcel to project {projekt_id} for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
||||||
|
|
@ -2294,7 +2296,7 @@ async def get_bzo_information(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/bzo-information from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/bzo-information from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -2302,7 +2304,7 @@ async def get_bzo_information(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/bzo-information from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/bzo-information from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -2312,7 +2314,7 @@ async def get_bzo_information(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/bzo-information from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/bzo-information from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Extracting BZO information for Gemeinde '{gemeinde}', Bauzone '{bauzone}' (user: {currentUser.id}, mandate: {currentUser.mandateId})")
|
logger.info(f"Extracting BZO information for Gemeinde '{gemeinde}', Bauzone '{bauzone}' (user: {currentUser.id}, mandate: {currentUser.mandateId})")
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ from modules.connectors.connectorOerebWfs import OerebWfsConnector
|
||||||
|
|
||||||
# Import Tavily connector for BZO document search
|
# Import Tavily connector for BZO document search
|
||||||
from modules.aicore.aicorePluginTavily import AiTavily
|
from modules.aicore.aicorePluginTavily import AiTavily
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeRealEstateScraping")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -107,7 +109,7 @@ async def scrape_switzerland_route(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -115,7 +117,7 @@ async def scrape_switzerland_route(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -125,7 +127,7 @@ async def scrape_switzerland_route(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract parameters from body with defaults
|
# Extract parameters from body with defaults
|
||||||
|
|
@ -137,19 +139,19 @@ async def scrape_switzerland_route(
|
||||||
if grid_size <= 0 or grid_size > 10000:
|
if grid_size <= 0 or grid_size > 10000:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="grid_size must be between 0 and 10000 meters"
|
detail=routeApiMsg("grid_size must be between 0 and 10000 meters")
|
||||||
)
|
)
|
||||||
|
|
||||||
if max_concurrent <= 0 or max_concurrent > 200:
|
if max_concurrent <= 0 or max_concurrent > 200:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="max_concurrent must be between 1 and 200"
|
detail=routeApiMsg("max_concurrent must be between 1 and 200")
|
||||||
)
|
)
|
||||||
|
|
||||||
if batch_size <= 0 or batch_size > 1000:
|
if batch_size <= 0 or batch_size > 1000:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="batch_size must be between 1 and 1000"
|
detail=routeApiMsg("batch_size must be between 1 and 1000")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -246,7 +248,7 @@ async def get_all_gemeinden(
|
||||||
logger.warning(f"CSRF token missing for GET /api/realestate/gemeinden from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for GET /api/realestate/gemeinden from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -254,7 +256,7 @@ async def get_all_gemeinden(
|
||||||
logger.warning(f"Invalid CSRF token format for GET /api/realestate/gemeinden from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/gemeinden from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -264,7 +266,7 @@ async def get_all_gemeinden(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/gemeinden from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/gemeinden from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Fetching all Gemeinden for user {currentUser.id} (mandate: {currentUser.mandateId}), only_current={only_current}")
|
logger.info(f"Fetching all Gemeinden for user {currentUser.id} (mandate: {currentUser.mandateId}), only_current={only_current}")
|
||||||
|
|
@ -548,7 +550,7 @@ async def fetch_bzo_documents(
|
||||||
logger.warning(f"CSRF token missing for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
logger.warning(f"CSRF token missing for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing. Please include X-CSRF-Token header."
|
detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Basic CSRF token format validation
|
# Basic CSRF token format validation
|
||||||
|
|
@ -556,7 +558,7 @@ async def fetch_bzo_documents(
|
||||||
logger.warning(f"Invalid CSRF token format for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate token is hex string
|
# Validate token is hex string
|
||||||
|
|
@ -566,7 +568,7 @@ async def fetch_bzo_documents(
|
||||||
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Invalid CSRF token format"
|
detail=routeApiMsg("Invalid CSRF token format")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Starting BZO document fetch for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
logger.info(f"Starting BZO document fetch for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
||||||
from modules.datamodels.datamodelSecurity import Token
|
from modules.datamodels.datamodelSecurity import Token
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSecurityAdmin")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -132,7 +134,7 @@ def list_tokens(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing tokens: {str(e)}")
|
logger.error(f"Error listing tokens: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to list tokens")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to list tokens"))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tokens/revoke/user")
|
@router.post("/tokens/revoke/user")
|
||||||
|
|
@ -151,7 +153,7 @@ def revoke_tokens_by_user(
|
||||||
authority = payload.get("authority")
|
authority = payload.get("authority")
|
||||||
reason = payload.get("reason", "sysadmin revoke")
|
reason = payload.get("reason", "sysadmin revoke")
|
||||||
if not userId:
|
if not userId:
|
||||||
raise HTTPException(status_code=400, detail="userId is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("userId is required"))
|
||||||
|
|
||||||
appInterface = getRootInterface()
|
appInterface = getRootInterface()
|
||||||
# MULTI-TENANT: SysAdmin can revoke any user's tokens (no mandate restriction)
|
# MULTI-TENANT: SysAdmin can revoke any user's tokens (no mandate restriction)
|
||||||
|
|
@ -167,7 +169,7 @@ def revoke_tokens_by_user(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error revoking tokens by user: {str(e)}")
|
logger.error(f"Error revoking tokens by user: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to revoke tokens")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke tokens"))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tokens/revoke/session")
|
@router.post("/tokens/revoke/session")
|
||||||
|
|
@ -187,7 +189,7 @@ def revoke_tokens_by_session(
|
||||||
authority = payload.get("authority", "local")
|
authority = payload.get("authority", "local")
|
||||||
reason = payload.get("reason", "sysadmin session revoke")
|
reason = payload.get("reason", "sysadmin session revoke")
|
||||||
if not userId or not sessionId:
|
if not userId or not sessionId:
|
||||||
raise HTTPException(status_code=400, detail="userId and sessionId are required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("userId and sessionId are required"))
|
||||||
|
|
||||||
appInterface = getRootInterface()
|
appInterface = getRootInterface()
|
||||||
# MULTI-TENANT: SysAdmin can revoke any session (no mandate check)
|
# MULTI-TENANT: SysAdmin can revoke any session (no mandate check)
|
||||||
|
|
@ -203,7 +205,7 @@ def revoke_tokens_by_session(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error revoking tokens by session: {str(e)}")
|
logger.error(f"Error revoking tokens by session: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to revoke session tokens")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke session tokens"))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tokens/revoke/id")
|
@router.post("/tokens/revoke/id")
|
||||||
|
|
@ -221,7 +223,7 @@ def revoke_token_by_id(
|
||||||
tokenId = payload.get("tokenId")
|
tokenId = payload.get("tokenId")
|
||||||
reason = payload.get("reason", "sysadmin revoke")
|
reason = payload.get("reason", "sysadmin revoke")
|
||||||
if not tokenId:
|
if not tokenId:
|
||||||
raise HTTPException(status_code=400, detail="tokenId is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("tokenId is required"))
|
||||||
appInterface = getRootInterface()
|
appInterface = getRootInterface()
|
||||||
# MULTI-TENANT: SysAdmin can revoke any token (no mandate check)
|
# MULTI-TENANT: SysAdmin can revoke any token (no mandate check)
|
||||||
ok = appInterface.revokeTokenById(tokenId, revokedBy=currentUser.id, reason=reason)
|
ok = appInterface.revokeTokenById(tokenId, revokedBy=currentUser.id, reason=reason)
|
||||||
|
|
@ -230,7 +232,7 @@ def revoke_token_by_id(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error revoking token by id: {str(e)}")
|
logger.error(f"Error revoking token by id: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to revoke token")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke token"))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/tokens/revoke/mandate")
|
@router.post("/tokens/revoke/mandate")
|
||||||
|
|
@ -249,7 +251,7 @@ def revoke_tokens_by_mandate(
|
||||||
authority = payload.get("authority", "local")
|
authority = payload.get("authority", "local")
|
||||||
reason = payload.get("reason", "sysadmin mandate revoke")
|
reason = payload.get("reason", "sysadmin mandate revoke")
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=400, detail="mandateId is required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("mandateId is required"))
|
||||||
|
|
||||||
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
|
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
|
||||||
appInterface = getRootInterface()
|
appInterface = getRootInterface()
|
||||||
|
|
@ -271,7 +273,7 @@ def revoke_tokens_by_mandate(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error revoking tokens by mandate: {str(e)}")
|
logger.error(f"Error revoking tokens by mandate: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to revoke mandate tokens")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke mandate tokens"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -295,7 +297,7 @@ def list_databases(
|
||||||
return {"databases": databases}
|
return {"databases": databases}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load databases from host: {e}")
|
logger.error(f"Failed to load databases from host: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to load databases from host")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to load databases from host"))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/databases/{database_name}/tables")
|
@router.get("/databases/{database_name}/tables")
|
||||||
|
|
@ -310,7 +312,7 @@ def get_database_tables(
|
||||||
MULTI-TENANT: SysAdmin-only (infrastructure management).
|
MULTI-TENANT: SysAdmin-only (infrastructure management).
|
||||||
"""
|
"""
|
||||||
if not database_name.startswith("poweron_"):
|
if not database_name.startswith("poweron_"):
|
||||||
raise HTTPException(status_code=400, detail="Invalid database name format")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name format"))
|
||||||
|
|
||||||
connector = None
|
connector = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -341,7 +343,7 @@ def drop_table(
|
||||||
MULTI-TENANT: SysAdmin-only (infrastructure management).
|
MULTI-TENANT: SysAdmin-only (infrastructure management).
|
||||||
"""
|
"""
|
||||||
if not database_name.startswith("poweron_"):
|
if not database_name.startswith("poweron_"):
|
||||||
raise HTTPException(status_code=400, detail="Invalid database name format")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name format"))
|
||||||
|
|
||||||
connector = None
|
connector = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -354,7 +356,7 @@ def drop_table(
|
||||||
WHERE table_schema = 'public' AND table_name = %s
|
WHERE table_schema = 'public' AND table_name = %s
|
||||||
""", (table_name,))
|
""", (table_name,))
|
||||||
if not cursor.fetchone():
|
if not cursor.fetchone():
|
||||||
raise HTTPException(status_code=404, detail="Table not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Table not found"))
|
||||||
|
|
||||||
# Drop the table
|
# Drop the table
|
||||||
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
|
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
|
||||||
|
|
@ -369,7 +371,7 @@ def drop_table(
|
||||||
logger.error(f"Error dropping table: {str(e)}")
|
logger.error(f"Error dropping table: {str(e)}")
|
||||||
if connector and connector.connection:
|
if connector and connector.connection:
|
||||||
connector.connection.rollback()
|
connector.connection.rollback()
|
||||||
raise HTTPException(status_code=500, detail="Failed to drop table")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to drop table"))
|
||||||
finally:
|
finally:
|
||||||
if connector:
|
if connector:
|
||||||
connector.close()
|
connector.close()
|
||||||
|
|
@ -389,7 +391,7 @@ def drop_database(
|
||||||
dbName = payload.get("database")
|
dbName = payload.get("database")
|
||||||
|
|
||||||
if not dbName or not dbName.startswith("poweron_"):
|
if not dbName or not dbName.startswith("poweron_"):
|
||||||
raise HTTPException(status_code=400, detail="Invalid database name")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name"))
|
||||||
|
|
||||||
# Validate database exists
|
# Validate database exists
|
||||||
try:
|
try:
|
||||||
|
|
@ -425,7 +427,7 @@ def drop_database(
|
||||||
logger.error(f"Error dropping database tables: {str(e)}")
|
logger.error(f"Error dropping database tables: {str(e)}")
|
||||||
if connector and connector.connection:
|
if connector and connector.connection:
|
||||||
connector.connection.rollback()
|
connector.connection.rollback()
|
||||||
raise HTTPException(status_code=500, detail="Failed to drop database tables")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to drop database tables"))
|
||||||
finally:
|
finally:
|
||||||
if connector:
|
if connector:
|
||||||
connector.close()
|
connector.close()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatu
|
||||||
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
|
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
|
||||||
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
|
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
|
||||||
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp
|
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSecurityClickup")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -53,7 +55,7 @@ def _require_clickup_config():
|
||||||
if not CLIENT_ID or not CLIENT_SECRET or not REDIRECT_URI:
|
if not CLIENT_ID or not CLIENT_SECRET or not REDIRECT_URI:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="ClickUp OAuth is not configured (Service_CLICKUP_CLIENT_ID, Service_CLICKUP_CLIENT_SECRET, Service_CLICKUP_OAUTH_REDIRECT_URI)",
|
detail=routeApiMsg("ClickUp OAuth is not configured (Service_CLICKUP_CLIENT_ID, Service_CLICKUP_CLIENT_SECRET, Service_CLICKUP_OAUTH_REDIRECT_URI)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -87,7 +89,7 @@ def auth_connect(
|
||||||
connection = conn
|
connection = conn
|
||||||
break
|
break
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ClickUp connection not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("ClickUp connection not found"))
|
||||||
|
|
||||||
state_jwt = _issue_oauth_state(
|
state_jwt = _issue_oauth_state(
|
||||||
{
|
{
|
||||||
|
|
@ -123,11 +125,11 @@ async def auth_connect_callback(
|
||||||
"""OAuth callback for ClickUp data connection."""
|
"""OAuth callback for ClickUp data connection."""
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != _FLOW_CONNECT:
|
if state_data.get("flow") != _FLOW_CONNECT:
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
connection_id = state_data.get("connectionId")
|
connection_id = state_data.get("connectionId")
|
||||||
user_id = state_data.get("userId")
|
user_id = state_data.get("userId")
|
||||||
if not connection_id or not user_id:
|
if not connection_id or not user_id:
|
||||||
raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
|
||||||
|
|
||||||
_require_clickup_config()
|
_require_clickup_config()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ from modules.auth import (
|
||||||
from modules.auth.tokenManager import TokenManager
|
from modules.auth.tokenManager import TokenManager
|
||||||
from modules.auth.oauthProviderConfig import googleAuthScopes, googleDataScopes
|
from modules.auth.oauthProviderConfig import googleAuthScopes, googleDataScopes
|
||||||
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSecurityGoogle")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -131,7 +133,7 @@ def _require_google_auth_config():
|
||||||
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
|
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Google Auth OAuth is not configured (Service_GOOGLE_AUTH_*)",
|
detail=routeApiMsg("Google Auth OAuth is not configured (Service_GOOGLE_AUTH_*)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,7 +141,7 @@ def _require_google_data_config():
|
||||||
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
|
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Google Data OAuth is not configured (Service_GOOGLE_DATA_*)",
|
detail=routeApiMsg("Google Data OAuth is not configured (Service_GOOGLE_DATA_*)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,7 +181,7 @@ async def auth_login_callback(
|
||||||
"""OAuth callback for Google Auth app (login only)."""
|
"""OAuth callback for Google Auth app (login only)."""
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != _FLOW_LOGIN:
|
if state_data.get("flow") != _FLOW_LOGIN:
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
|
|
||||||
_require_google_auth_config()
|
_require_google_auth_config()
|
||||||
oauth = OAuth2Session(client_id=AUTH_CLIENT_ID, redirect_uri=AUTH_REDIRECT_URI)
|
oauth = OAuth2Session(client_id=AUTH_CLIENT_ID, redirect_uri=AUTH_REDIRECT_URI)
|
||||||
|
|
@ -214,7 +216,7 @@ async def auth_login_callback(
|
||||||
if user_info_response.status_code != 200:
|
if user_info_response.status_code != 200:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to get user info from Google",
|
detail=routeApiMsg("Failed to get user info from Google"),
|
||||||
)
|
)
|
||||||
user_info = user_info_response.json()
|
user_info = user_info_response.json()
|
||||||
|
|
||||||
|
|
@ -310,7 +312,7 @@ def auth_connect(
|
||||||
connection = conn
|
connection = conn
|
||||||
break
|
break
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Google connection not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Google connection not found"))
|
||||||
|
|
||||||
state_jwt = _issue_oauth_state(
|
state_jwt = _issue_oauth_state(
|
||||||
{
|
{
|
||||||
|
|
@ -359,11 +361,11 @@ async def auth_connect_callback(
|
||||||
"""OAuth callback for Google Data app (UserConnection)."""
|
"""OAuth callback for Google Data app (UserConnection)."""
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != _FLOW_CONNECT:
|
if state_data.get("flow") != _FLOW_CONNECT:
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
connection_id = state_data.get("connectionId")
|
connection_id = state_data.get("connectionId")
|
||||||
user_id = state_data.get("userId")
|
user_id = state_data.get("userId")
|
||||||
if not connection_id or not user_id:
|
if not connection_id or not user_id:
|
||||||
raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
|
||||||
|
|
||||||
_require_google_data_config()
|
_require_google_data_config()
|
||||||
oauth = OAuth2Session(client_id=DATA_CLIENT_ID, redirect_uri=DATA_REDIRECT_URI)
|
oauth = OAuth2Session(client_id=DATA_CLIENT_ID, redirect_uri=DATA_REDIRECT_URI)
|
||||||
|
|
@ -419,7 +421,7 @@ async def auth_connect_callback(
|
||||||
if user_info_response.status_code != 200:
|
if user_info_response.status_code != 200:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to get user info from Google",
|
detail=routeApiMsg("Failed to get user info from Google"),
|
||||||
)
|
)
|
||||||
user_info = user_info_response.json()
|
user_info = user_info_response.json()
|
||||||
|
|
||||||
|
|
@ -557,7 +559,7 @@ def logout(
|
||||||
if not token:
|
if not token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="No token found",
|
detail=routeApiMsg("No token found"),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -568,7 +570,7 @@ def logout(
|
||||||
logger.error(f"Failed to decode JWT on Google logout: {str(e)}")
|
logger.error(f"Failed to decode JWT on Google logout: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invalid token",
|
detail=routeApiMsg("Invalid token"),
|
||||||
)
|
)
|
||||||
|
|
||||||
revoked = 0
|
revoked = 0
|
||||||
|
|
@ -635,13 +637,13 @@ async def verify_token(
|
||||||
if not google_connection:
|
if not google_connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Google connection found for current user",
|
detail=routeApiMsg("No Google connection found for current user"),
|
||||||
)
|
)
|
||||||
current_token = TokenManager().getFreshToken(google_connection.id)
|
current_token = TokenManager().getFreshToken(google_connection.id)
|
||||||
if not current_token:
|
if not current_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Google token found for this connection",
|
detail=routeApiMsg("No Google token found for this connection"),
|
||||||
)
|
)
|
||||||
token_verification = await verify_google_token(current_token.tokenAccess)
|
token_verification = await verify_google_token(current_token.tokenAccess)
|
||||||
return {
|
return {
|
||||||
|
|
@ -690,7 +692,7 @@ async def refresh_token(
|
||||||
if not google_connection:
|
if not google_connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Requested Google connection not found for current user",
|
detail=routeApiMsg("Requested Google connection not found for current user"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
|
|
@ -700,13 +702,13 @@ async def refresh_token(
|
||||||
if not google_connection:
|
if not google_connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Google connection found for current user",
|
detail=routeApiMsg("No Google connection found for current user"),
|
||||||
)
|
)
|
||||||
current_token = TokenManager().getFreshToken(google_connection.id)
|
current_token = TokenManager().getFreshToken(google_connection.id)
|
||||||
if not current_token:
|
if not current_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Google token found for this connection",
|
detail=routeApiMsg("No Google token found for this connection"),
|
||||||
)
|
)
|
||||||
expiresAtValue = parseTimestamp(current_token.expiresAt)
|
expiresAtValue = parseTimestamp(current_token.expiresAt)
|
||||||
google_connection.expiresAt = (
|
google_connection.expiresAt = (
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority, Manda
|
||||||
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
|
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.shared.timeUtils import getUtcTimestamp
|
from modules.shared.timeUtils import getUtcTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSecurityLocal")
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -231,7 +233,7 @@ def login(
|
||||||
if not csrf_token:
|
if not csrf_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="CSRF token missing"
|
detail=routeApiMsg("CSRF token missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get gateway interface with root privileges for authentication
|
# Get gateway interface with root privileges for authentication
|
||||||
|
|
@ -248,7 +250,7 @@ def login(
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid username or password",
|
detail=routeApiMsg("Invalid username or password"),
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -280,7 +282,7 @@ def login(
|
||||||
expires_at = datetime.fromtimestamp(payload.get("exp"))
|
expires_at = datetime.fromtimestamp(payload.get("exp"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to decode access token: {str(e)}")
|
logger.error(f"Failed to decode access token: {str(e)}")
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to finalize token")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to finalize token"))
|
||||||
|
|
||||||
# Get user-specific interface for token operations
|
# Get user-specific interface for token operations
|
||||||
userInterface = getInterface(user)
|
userInterface = getInterface(user)
|
||||||
|
|
@ -425,7 +427,7 @@ def register_user(
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Failed to register user"
|
detail=routeApiMsg("Failed to register user")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for pending invitations BEFORE provisioning.
|
# Check for pending invitations BEFORE provisioning.
|
||||||
|
|
@ -581,32 +583,32 @@ def refresh_token(
|
||||||
# Get refresh token from cookie
|
# Get refresh token from cookie
|
||||||
refresh_token = request.cookies.get('refresh_token')
|
refresh_token = request.cookies.get('refresh_token')
|
||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
raise HTTPException(status_code=401, detail="No refresh token found")
|
raise HTTPException(status_code=401, detail=routeApiMsg("No refresh token found"))
|
||||||
|
|
||||||
# Validate refresh token
|
# Validate refresh token
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
if payload.get("type") != "refresh":
|
if payload.get("type") != "refresh":
|
||||||
raise HTTPException(status_code=401, detail="Invalid refresh token type")
|
raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token type"))
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
raise HTTPException(status_code=401, detail="Refresh token expired")
|
raise HTTPException(status_code=401, detail=routeApiMsg("Refresh token expired"))
|
||||||
except jwt.JWTError:
|
except jwt.JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token"))
|
||||||
|
|
||||||
# Get user information from refresh token payload
|
# Get user information from refresh token payload
|
||||||
user_id = payload.get("userId")
|
user_id = payload.get("userId")
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=401, detail="Invalid refresh token - missing user ID")
|
raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token - missing user ID"))
|
||||||
|
|
||||||
# Get user from database using the user ID from refresh token
|
# Get user from database using the user ID from refresh token
|
||||||
try:
|
try:
|
||||||
app_interface = getRootInterface()
|
app_interface = getRootInterface()
|
||||||
current_user = app_interface.getUser(user_id)
|
current_user = app_interface.getUser(user_id)
|
||||||
if not current_user:
|
if not current_user:
|
||||||
raise HTTPException(status_code=401, detail="User not found")
|
raise HTTPException(status_code=401, detail=routeApiMsg("User not found"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get user from database: {str(e)}")
|
logger.error(f"Failed to get user from database: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to validate user")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to validate user"))
|
||||||
|
|
||||||
# Create new token data
|
# Create new token data
|
||||||
# MULTI-TENANT: Token does NOT contain mandateId anymore
|
# MULTI-TENANT: Token does NOT contain mandateId anymore
|
||||||
|
|
@ -627,7 +629,7 @@ def refresh_token(
|
||||||
expires_at = datetime.fromtimestamp(payload.get("exp"))
|
expires_at = datetime.fromtimestamp(payload.get("exp"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to decode new access token: {str(e)}")
|
logger.error(f"Failed to decode new access token: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to create new token")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Failed to create new token"))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "token_refresh_success",
|
"type": "token_refresh_success",
|
||||||
|
|
@ -643,7 +645,7 @@ def refresh_token(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Token refresh error: {str(e)}")
|
logger.error(f"Token refresh error: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Token refresh failed")
|
raise HTTPException(status_code=500, detail=routeApiMsg("Token refresh failed"))
|
||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
|
|
@ -661,7 +663,7 @@ def logout(request: Request, response: Response, currentUser: User = Depends(get
|
||||||
token = auth_header.split(" ", 1)[1].strip()
|
token = auth_header.split(" ", 1)[1].strip()
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No token found")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("No token found"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
|
@ -669,7 +671,7 @@ def logout(request: Request, response: Response, currentUser: User = Depends(get
|
||||||
jti = payload.get("jti")
|
jti = payload.get("jti")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to decode JWT on logout: {str(e)}")
|
logger.error(f"Failed to decode JWT on logout: {str(e)}")
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("Invalid token"))
|
||||||
|
|
||||||
revoked = 0
|
revoked = 0
|
||||||
if session_id:
|
if session_id:
|
||||||
|
|
@ -927,14 +929,14 @@ def password_reset(
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Ungültiger oder abgelaufener Reset-Link"
|
detail=routeApiMsg("Ungültiger oder abgelaufener Reset-Link")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate password strength
|
# Validate password strength
|
||||||
if len(password) < 8:
|
if len(password) < 8:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Passwort muss mindestens 8 Zeichen lang sein"
|
detail=routeApiMsg("Passwort muss mindestens 8 Zeichen lang sein")
|
||||||
)
|
)
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
@ -945,7 +947,7 @@ def password_reset(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Ungültiger oder abgelaufener Reset-Link"
|
detail=routeApiMsg("Ungültiger oder abgelaufener Reset-Link")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log success
|
# Log success
|
||||||
|
|
@ -968,7 +970,7 @@ def password_reset(
|
||||||
logger.error(f"Error in password reset: {str(e)}")
|
logger.error(f"Error in password reset: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Passwort-Zurücksetzung fehlgeschlagen"
|
detail=routeApiMsg("Passwort-Zurücksetzung fehlgeschlagen")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1005,10 +1007,10 @@ def _deleteNeutralizationMapping(
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
records = rootIf.db.getRecordset(DataNeutralizerAttributes, recordFilter={"id": mappingId})
|
records = rootIf.db.getRecordset(DataNeutralizerAttributes, recordFilter={"id": mappingId})
|
||||||
if not records:
|
if not records:
|
||||||
raise HTTPException(status_code=404, detail="Mapping not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Mapping not found"))
|
||||||
rec = records[0]
|
rec = records[0]
|
||||||
recUserId = rec.get("userId") if isinstance(rec, dict) else getattr(rec, "userId", None)
|
recUserId = rec.get("userId") if isinstance(rec, dict) else getattr(rec, "userId", None)
|
||||||
if recUserId != userId:
|
if recUserId != userId:
|
||||||
raise HTTPException(status_code=403, detail="Not your mapping")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Not your mapping"))
|
||||||
rootIf.db.recordDelete(DataNeutralizerAttributes, mappingId)
|
rootIf.db.recordDelete(DataNeutralizerAttributes, mappingId)
|
||||||
return {"deleted": True, "id": mappingId}
|
return {"deleted": True, "id": mappingId}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ from modules.auth import (
|
||||||
from modules.auth.tokenManager import TokenManager
|
from modules.auth.tokenManager import TokenManager
|
||||||
from modules.auth.oauthProviderConfig import msftAuthScopes, msftDataScopes, msftDataScopesForRefresh
|
from modules.auth.oauthProviderConfig import msftAuthScopes, msftDataScopes, msftDataScopesForRefresh
|
||||||
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSecurityMsft")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -80,7 +82,7 @@ def _require_msft_auth_config():
|
||||||
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
|
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Microsoft Auth OAuth is not configured (Service_MSFT_AUTH_*)",
|
detail=routeApiMsg("Microsoft Auth OAuth is not configured (Service_MSFT_AUTH_*)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -88,7 +90,7 @@ def _require_msft_data_config():
|
||||||
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
|
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Microsoft Data OAuth is not configured (Service_MSFT_DATA_*)",
|
detail=routeApiMsg("Microsoft Data OAuth is not configured (Service_MSFT_DATA_*)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -140,7 +142,7 @@ async def auth_login_callback(
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != _FLOW_LOGIN:
|
if state_data.get("flow") != _FLOW_LOGIN:
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
|
|
||||||
_require_msft_auth_config()
|
_require_msft_auth_config()
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
|
@ -171,7 +173,7 @@ async def auth_login_callback(
|
||||||
if user_info_response.status_code != 200:
|
if user_info_response.status_code != 200:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to get user info from Microsoft",
|
detail=routeApiMsg("Failed to get user info from Microsoft"),
|
||||||
)
|
)
|
||||||
user_info = user_info_response.json()
|
user_info = user_info_response.json()
|
||||||
|
|
||||||
|
|
@ -256,7 +258,7 @@ def auth_connect(
|
||||||
break
|
break
|
||||||
if not connection:
|
if not connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Microsoft connection not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Microsoft connection not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
|
@ -301,11 +303,11 @@ async def auth_connect_callback(
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != _FLOW_CONNECT:
|
if state_data.get("flow") != _FLOW_CONNECT:
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
connection_id = state_data.get("connectionId")
|
connection_id = state_data.get("connectionId")
|
||||||
user_id = state_data.get("userId")
|
user_id = state_data.get("userId")
|
||||||
if not connection_id or not user_id:
|
if not connection_id or not user_id:
|
||||||
raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
|
||||||
|
|
||||||
_require_msft_data_config()
|
_require_msft_data_config()
|
||||||
msal_app = msal.ConfidentialClientApplication(
|
msal_app = msal.ConfidentialClientApplication(
|
||||||
|
|
@ -343,7 +345,7 @@ async def auth_connect_callback(
|
||||||
if user_info_response.status_code != 200:
|
if user_info_response.status_code != 200:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to get user info from Microsoft",
|
detail=routeApiMsg("Failed to get user info from Microsoft"),
|
||||||
)
|
)
|
||||||
user_info = user_info_response.json()
|
user_info = user_info_response.json()
|
||||||
|
|
||||||
|
|
@ -465,7 +467,7 @@ def adminconsent(request: Request) -> RedirectResponse:
|
||||||
if not redirect_uri:
|
if not redirect_uri:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Could not derive admin consent redirect URI from Service_MSFT_DATA_REDIRECT_URI",
|
detail=routeApiMsg("Could not derive admin consent redirect URI from Service_MSFT_DATA_REDIRECT_URI"),
|
||||||
)
|
)
|
||||||
state_jwt = _issue_oauth_state({"flow": "admin_consent"})
|
state_jwt = _issue_oauth_state({"flow": "admin_consent"})
|
||||||
scope_param = _msft_data_admin_consent_scope_param()
|
scope_param = _msft_data_admin_consent_scope_param()
|
||||||
|
|
@ -528,7 +530,7 @@ def adminconsent_callback(
|
||||||
|
|
||||||
state_data = _parse_oauth_state(state)
|
state_data = _parse_oauth_state(state)
|
||||||
if state_data.get("flow") != "admin_consent":
|
if state_data.get("flow") != "admin_consent":
|
||||||
raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
|
||||||
|
|
||||||
granted = str(admin_consent or "").strip().lower() in ("true", "1", "yes")
|
granted = str(admin_consent or "").strip().lower() in ("true", "1", "yes")
|
||||||
if not granted:
|
if not granted:
|
||||||
|
|
@ -615,7 +617,7 @@ def logout(
|
||||||
if not token:
|
if not token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="No token found",
|
detail=routeApiMsg("No token found"),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -626,7 +628,7 @@ def logout(
|
||||||
logger.error(f"Failed to decode JWT on Microsoft logout: {str(e)}")
|
logger.error(f"Failed to decode JWT on Microsoft logout: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invalid token",
|
detail=routeApiMsg("Invalid token"),
|
||||||
)
|
)
|
||||||
|
|
||||||
revoked = 0
|
revoked = 0
|
||||||
|
|
@ -720,7 +722,7 @@ async def refresh_token(
|
||||||
if not msft_connection:
|
if not msft_connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Requested Microsoft connection not found for current user",
|
detail=routeApiMsg("Requested Microsoft connection not found for current user"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
|
|
@ -730,13 +732,13 @@ async def refresh_token(
|
||||||
if not msft_connection:
|
if not msft_connection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Microsoft connection found for current user",
|
detail=routeApiMsg("No Microsoft connection found for current user"),
|
||||||
)
|
)
|
||||||
current_token = TokenManager().getFreshToken(msft_connection.id)
|
current_token = TokenManager().getFreshToken(msft_connection.id)
|
||||||
if not current_token:
|
if not current_token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No Microsoft token found for this connection",
|
detail=routeApiMsg("No Microsoft token found for this connection"),
|
||||||
)
|
)
|
||||||
token_manager = TokenManager()
|
token_manager = TokenManager()
|
||||||
refreshed_token = token_manager.refreshToken(current_token)
|
refreshed_token = token_manager.refreshToken(current_token)
|
||||||
|
|
@ -760,7 +762,7 @@ async def refresh_token(
|
||||||
}
|
}
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to refresh token",
|
detail=routeApiMsg("Failed to refresh token"),
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ from modules.auth import limiter, getCurrentUser
|
||||||
from modules.datamodels.datamodelUam import User, UserConnection
|
from modules.datamodels.datamodelUam import User, UserConnection
|
||||||
from modules.interfaces.interfaceDbApp import getInterface
|
from modules.interfaces.interfaceDbApp import getInterface
|
||||||
from modules.serviceHub import getInterface as getServices
|
from modules.serviceHub import getInterface as getServices
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSharepoint")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -111,7 +113,7 @@ async def get_sharepoint_sites(
|
||||||
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Failed to set SharePoint access token. Connection may be expired or invalid."
|
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Discover SharePoint sites
|
# Discover SharePoint sites
|
||||||
|
|
@ -164,7 +166,7 @@ async def list_sharepoint_folders(
|
||||||
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Failed to set SharePoint access token. Connection may be expired or invalid."
|
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize folder path (empty string for root)
|
# Normalize folder path (empty string for root)
|
||||||
|
|
@ -229,7 +231,7 @@ async def getSharepointFolderOptions(
|
||||||
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Failed to set SharePoint access token. Connection may be expired or invalid."
|
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mode 1: Return sites list if no siteId specified
|
# Mode 1: Return sites list if no siteId specified
|
||||||
|
|
@ -343,7 +345,7 @@ async def getSharepointFolderOptionsByReference(
|
||||||
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
if not services.sharepoint.setAccessTokenFromConnection(connection):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Failed to set SharePoint access token. Connection may be expired or invalid."
|
detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mode 1: Return sites list if no siteId specified
|
# Mode 1: Return sites list if no siteId specified
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ from modules.interfaces.interfaceFeatures import getFeatureInterface
|
||||||
from modules.security.rbacCatalog import getCatalogService
|
from modules.security.rbacCatalog import getCatalogService
|
||||||
from modules.security.rbac import RbacClass
|
from modules.security.rbac import RbacClass
|
||||||
from modules.security.rootAccess import getRootDbAppConnector
|
from modules.security.rootAccess import getRootDbAppConnector
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeStore")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -327,7 +329,7 @@ def activateStoreFeature(
|
||||||
mandateId = data.mandateId
|
mandateId = data.mandateId
|
||||||
|
|
||||||
if not _isUserAdminInMandate(db, userId, mandateId):
|
if not _isUserAdminInMandate(db, userId, mandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not admin in target mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Not admin in target mandate"))
|
||||||
|
|
||||||
# ── 1. Resolve subscription & plan ──────────────────────────────
|
# ── 1. Resolve subscription & plan ──────────────────────────────
|
||||||
from modules.datamodels.datamodelSubscription import MandateSubscription, BUILTIN_PLANS, SubscriptionStatusEnum
|
from modules.datamodels.datamodelSubscription import MandateSubscription, BUILTIN_PLANS, SubscriptionStatusEnum
|
||||||
|
|
@ -353,7 +355,7 @@ def activateStoreFeature(
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
detail="Kein aktives Abonnement. Bitte zuerst ein Abo abschliessen.",
|
detail=routeApiMsg("Kein aktives Abonnement. Bitte zuerst ein Abo abschliessen."),
|
||||||
)
|
)
|
||||||
|
|
||||||
planKey = operative.get("planKey", "")
|
planKey = operative.get("planKey", "")
|
||||||
|
|
@ -382,7 +384,7 @@ def activateStoreFeature(
|
||||||
)
|
)
|
||||||
|
|
||||||
if not instance:
|
if not instance:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create feature instance")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to create feature instance"))
|
||||||
|
|
||||||
instanceId = instance.get("id") if isinstance(instance, dict) else instance.id
|
instanceId = instance.get("id") if isinstance(instance, dict) else instance.id
|
||||||
|
|
||||||
|
|
@ -460,12 +462,12 @@ def deactivateStoreFeature(
|
||||||
# Verify instance exists in mandate
|
# Verify instance exists in mandate
|
||||||
instances = db.getRecordset(FeatureInstance, recordFilter={"id": instanceId, "mandateId": mandateId})
|
instances = db.getRecordset(FeatureInstance, recordFilter={"id": instanceId, "mandateId": mandateId})
|
||||||
if not instances:
|
if not instances:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feature instance not found in mandate")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Feature instance not found in mandate"))
|
||||||
|
|
||||||
# Find user's FeatureAccess
|
# Find user's FeatureAccess
|
||||||
accesses = db.getRecordset(FeatureAccess, recordFilter={"userId": userId, "featureInstanceId": instanceId})
|
accesses = db.getRecordset(FeatureAccess, recordFilter={"userId": userId, "featureInstanceId": instanceId})
|
||||||
if not accesses:
|
if not accesses:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No active access found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("No active access found"))
|
||||||
|
|
||||||
featureAccessId = accesses[0].get("id")
|
featureAccessId = accesses[0].get("id")
|
||||||
db.recordDelete(FeatureAccess, featureAccessId)
|
db.recordDelete(FeatureAccess, featureAccessId)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ from pydantic import BaseModel, Field
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
from modules.routes.routeDataUsers import _applyFiltersAndSort, _extractDistinctValues
|
from modules.routes.routeDataUsers import _applyFiltersAndSort, _extractDistinctValues
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeSubscription")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -53,7 +55,7 @@ def _assertMandateAdmin(context: RequestContext, mandateId: str) -> None:
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate admin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate admin role required"))
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -169,7 +171,7 @@ def activatePlan(
|
||||||
)
|
)
|
||||||
mandateId = _resolveMandateId(context)
|
mandateId = _resolveMandateId(context)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
|
||||||
_assertMandateAdmin(context, mandateId)
|
_assertMandateAdmin(context, mandateId)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -195,7 +197,7 @@ def cancelSubscription(
|
||||||
)
|
)
|
||||||
mandateId = _resolveMandateId(context)
|
mandateId = _resolveMandateId(context)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
|
||||||
_assertMandateAdmin(context, mandateId)
|
_assertMandateAdmin(context, mandateId)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -221,7 +223,7 @@ def reactivateSubscription(
|
||||||
)
|
)
|
||||||
mandateId = _resolveMandateId(context)
|
mandateId = _resolveMandateId(context)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
|
||||||
_assertMandateAdmin(context, mandateId)
|
_assertMandateAdmin(context, mandateId)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -243,7 +245,7 @@ def forceCancel(
|
||||||
):
|
):
|
||||||
"""Sysadmin: immediately expire any non-terminal subscription."""
|
"""Sysadmin: immediately expire any non-terminal subscription."""
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
|
||||||
|
|
||||||
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import (
|
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import (
|
||||||
getService as getSubscriptionService,
|
getService as getSubscriptionService,
|
||||||
|
|
@ -251,7 +253,7 @@ def forceCancel(
|
||||||
from modules.interfaces.interfaceDbSubscription import _getRootInterface as getSubRootInterface
|
from modules.interfaces.interfaceDbSubscription import _getRootInterface as getSubRootInterface
|
||||||
sub = getSubRootInterface().getById(data.subscriptionId)
|
sub = getSubRootInterface().getById(data.subscriptionId)
|
||||||
if not sub:
|
if not sub:
|
||||||
raise HTTPException(status_code=404, detail="Subscription not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Subscription not found"))
|
||||||
mandateId = sub["mandateId"]
|
mandateId = sub["mandateId"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -278,7 +280,7 @@ def verifyCheckout(
|
||||||
"""
|
"""
|
||||||
mandateId = _resolveMandateId(context)
|
mandateId = _resolveMandateId(context)
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
|
raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
|
||||||
_assertMandateAdmin(context, mandateId)
|
_assertMandateAdmin(context, mandateId)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -288,7 +290,7 @@ def verifyCheckout(
|
||||||
session = stripeToDict(rawSession)
|
session = stripeToDict(rawSession)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to retrieve checkout session %s: %s", data.sessionId, e)
|
logger.error("Failed to retrieve checkout session %s: %s", data.sessionId, e)
|
||||||
raise HTTPException(status_code=400, detail="Invalid session ID")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session ID"))
|
||||||
|
|
||||||
payStatus = session.get("payment_status")
|
payStatus = session.get("payment_status")
|
||||||
if session.get("status") != "complete":
|
if session.get("status") != "complete":
|
||||||
|
|
@ -297,7 +299,7 @@ def verifyCheckout(
|
||||||
return {"status": "pending", "message": "Checkout not yet completed"}
|
return {"status": "pending", "message": "Checkout not yet completed"}
|
||||||
|
|
||||||
if session.get("mode") != "subscription":
|
if session.get("mode") != "subscription":
|
||||||
raise HTTPException(status_code=400, detail="Not a subscription checkout session")
|
raise HTTPException(status_code=400, detail=routeApiMsg("Not a subscription checkout session"))
|
||||||
|
|
||||||
from modules.routes.routeBilling import _handleSubscriptionCheckoutCompleted
|
from modules.routes.routeBilling import _handleSubscriptionCheckoutCompleted
|
||||||
|
|
||||||
|
|
@ -421,7 +423,7 @@ def getAllSubscriptions(
|
||||||
):
|
):
|
||||||
"""SysAdmin: list ALL subscriptions across all mandates with enriched metadata."""
|
"""SysAdmin: list ALL subscriptions across all mandates with enriched metadata."""
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
|
||||||
|
|
||||||
paginationParams: Optional[PaginationParams] = None
|
paginationParams: Optional[PaginationParams] = None
|
||||||
if pagination:
|
if pagination:
|
||||||
|
|
@ -467,7 +469,7 @@ def getFilterValues(
|
||||||
):
|
):
|
||||||
"""Return distinct values for a column, respecting all active filters except the requested one."""
|
"""Return distinct values for a column, respecting all active filters except the requested one."""
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
|
||||||
|
|
||||||
crossFilterParams: Optional[PaginationParams] = None
|
crossFilterParams: Optional[PaginationParams] = None
|
||||||
if pagination:
|
if pagination:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Navigation API Konzept:
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional
|
||||||
from fastapi import APIRouter, Depends, Request, Query
|
from fastapi import APIRouter, Depends, Request
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
|
|
||||||
|
|
@ -130,11 +130,11 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
|
||||||
|
|
||||||
def _buildDynamicBlock(
|
def _buildDynamicBlock(
|
||||||
userId: str,
|
userId: str,
|
||||||
language: str,
|
|
||||||
isSysAdmin: bool
|
isSysAdmin: bool
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Build the dynamic features block with mandates, features, and instances.
|
Build the dynamic features block with mandates, features, and instances.
|
||||||
|
Labels are German base texts (i18n keys). Frontend translates via t().
|
||||||
|
|
||||||
Returns None if user has no feature instances.
|
Returns None if user has no feature instances.
|
||||||
"""
|
"""
|
||||||
|
|
@ -181,21 +181,29 @@ def _buildDynamicBlock(
|
||||||
if featureKey not in featuresMap:
|
if featureKey not in featuresMap:
|
||||||
feature = featureInterface.getFeature(instance.featureCode)
|
feature = featureInterface.getFeature(instance.featureCode)
|
||||||
|
|
||||||
# Handle featureLabel - could be a dict or a Pydantic model (TextMultilingual)
|
# Handle featureLabel — TextMultilingual dict, plain str (German key), or legacy object
|
||||||
if feature and hasattr(feature, 'label'):
|
if feature and hasattr(feature, 'label'):
|
||||||
featureLabel = feature.label
|
featureLabel = feature.label
|
||||||
# Convert Pydantic model to dict if needed
|
|
||||||
if hasattr(featureLabel, 'model_dump'):
|
if hasattr(featureLabel, 'model_dump'):
|
||||||
featureLabel = featureLabel.model_dump()
|
featureLabel = featureLabel.model_dump()
|
||||||
|
elif isinstance(featureLabel, str):
|
||||||
|
pass
|
||||||
elif not isinstance(featureLabel, dict):
|
elif not isinstance(featureLabel, dict):
|
||||||
# Fallback: try to access as attributes
|
featureLabel = {
|
||||||
featureLabel = {"de": getattr(featureLabel, 'de', instance.featureCode), "en": getattr(featureLabel, 'en', instance.featureCode)}
|
"de": getattr(featureLabel, 'de', instance.featureCode),
|
||||||
|
"en": getattr(featureLabel, 'en', instance.featureCode),
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
featureLabel = {"de": instance.featureCode, "en": instance.featureCode}
|
featureLabel = {"de": instance.featureCode, "en": instance.featureCode}
|
||||||
|
|
||||||
|
if isinstance(featureLabel, str):
|
||||||
|
resolvedFeatureLabel = featureLabel
|
||||||
|
else:
|
||||||
|
resolvedFeatureLabel = featureLabel.get("de", featureLabel.get("en", instance.featureCode))
|
||||||
|
|
||||||
featuresMap[featureKey] = {
|
featuresMap[featureKey] = {
|
||||||
"uiComponent": f"feature.{instance.featureCode}",
|
"uiComponent": f"feature.{instance.featureCode}",
|
||||||
"uiLabel": featureLabel.get(language, featureLabel.get("en", instance.featureCode)),
|
"uiLabel": resolvedFeatureLabel,
|
||||||
"order": 10,
|
"order": 10,
|
||||||
"instances": [],
|
"instances": [],
|
||||||
"_mandateId": mandateId,
|
"_mandateId": mandateId,
|
||||||
|
|
@ -228,9 +236,8 @@ def _buildDynamicBlock(
|
||||||
# Build path for this view
|
# Build path for this view
|
||||||
viewPath = f"/mandates/{mandateId}/{instance.featureCode}/{instance.id}/{viewName}"
|
viewPath = f"/mandates/{mandateId}/{instance.featureCode}/{instance.id}/{viewName}"
|
||||||
|
|
||||||
# Get label in requested language
|
|
||||||
label = uiObj.get("label", {})
|
label = uiObj.get("label", {})
|
||||||
uiLabel = label.get(language, label.get("en", viewName))
|
uiLabel = label.get("de", label.get("en", viewName)) if isinstance(label, dict) else label
|
||||||
|
|
||||||
views.append({
|
views.append({
|
||||||
"uiComponent": f"page.feature.{instance.featureCode}.{viewName}",
|
"uiComponent": f"page.feature.{instance.featureCode}.{viewName}",
|
||||||
|
|
@ -347,7 +354,6 @@ def _getInstanceViewPermissions(
|
||||||
|
|
||||||
def _filterItems(
|
def _filterItems(
|
||||||
items: List[Dict[str, Any]],
|
items: List[Dict[str, Any]],
|
||||||
language: str,
|
|
||||||
isSysAdmin: bool,
|
isSysAdmin: bool,
|
||||||
roleIds: List[str],
|
roleIds: List[str],
|
||||||
hasGlobalPermission: bool
|
hasGlobalPermission: bool
|
||||||
|
|
@ -361,19 +367,18 @@ def _filterItems(
|
||||||
if item.get("sysAdminOnly") and not isSysAdmin:
|
if item.get("sysAdminOnly") and not isSysAdmin:
|
||||||
continue
|
continue
|
||||||
if item.get("public"):
|
if item.get("public"):
|
||||||
filteredItems.append(_formatBlockItem(item, language))
|
filteredItems.append(_formatBlockItem(item))
|
||||||
continue
|
continue
|
||||||
if isSysAdmin:
|
if isSysAdmin:
|
||||||
filteredItems.append(_formatBlockItem(item, language))
|
filteredItems.append(_formatBlockItem(item))
|
||||||
continue
|
continue
|
||||||
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
|
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
|
||||||
filteredItems.append(_formatBlockItem(item, language))
|
filteredItems.append(_formatBlockItem(item))
|
||||||
filteredItems.sort(key=lambda i: i["order"])
|
filteredItems.sort(key=lambda i: i["order"])
|
||||||
return filteredItems
|
return filteredItems
|
||||||
|
|
||||||
|
|
||||||
def _buildStaticBlocks(
|
def _buildStaticBlocks(
|
||||||
language: str,
|
|
||||||
isSysAdmin: bool,
|
isSysAdmin: bool,
|
||||||
roleIds: List[str],
|
roleIds: List[str],
|
||||||
hasGlobalPermission: bool
|
hasGlobalPermission: bool
|
||||||
|
|
@ -381,8 +386,8 @@ def _buildStaticBlocks(
|
||||||
"""
|
"""
|
||||||
Build static navigation blocks from NAVIGATION_SECTIONS.
|
Build static navigation blocks from NAVIGATION_SECTIONS.
|
||||||
|
|
||||||
Returns list of blocks with items filtered by permissions.
|
Labels/titles are plain German strings (i18n base keys).
|
||||||
Supports subgroups within sections.
|
The frontend translates them via t().
|
||||||
"""
|
"""
|
||||||
blocks = []
|
blocks = []
|
||||||
|
|
||||||
|
|
@ -397,12 +402,12 @@ def _buildStaticBlocks(
|
||||||
filteredSubgroups = []
|
filteredSubgroups = []
|
||||||
for subgroup in section["subgroups"]:
|
for subgroup in section["subgroups"]:
|
||||||
subItems = _filterItems(
|
subItems = _filterItems(
|
||||||
subgroup.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
|
subgroup.get("items", []), isSysAdmin, roleIds, hasGlobalPermission
|
||||||
)
|
)
|
||||||
if subItems:
|
if subItems:
|
||||||
filteredSubgroups.append({
|
filteredSubgroups.append({
|
||||||
"id": subgroup["id"],
|
"id": subgroup["id"],
|
||||||
"title": subgroup["title"].get(language, subgroup["title"].get("en", subgroup["id"])),
|
"title": subgroup["title"],
|
||||||
"order": subgroup.get("order", 50),
|
"order": subgroup.get("order", 50),
|
||||||
"items": subItems,
|
"items": subItems,
|
||||||
})
|
})
|
||||||
|
|
@ -412,28 +417,28 @@ def _buildStaticBlocks(
|
||||||
topLevelItems = []
|
topLevelItems = []
|
||||||
if hasItems:
|
if hasItems:
|
||||||
topLevelItems = _filterItems(
|
topLevelItems = _filterItems(
|
||||||
section["items"], language, isSysAdmin, roleIds, hasGlobalPermission
|
section["items"], isSysAdmin, roleIds, hasGlobalPermission
|
||||||
)
|
)
|
||||||
|
|
||||||
if filteredSubgroups or topLevelItems:
|
if filteredSubgroups or topLevelItems:
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"id": section["id"],
|
"id": section["id"],
|
||||||
"title": section["title"].get(language, section["title"].get("en", section["id"])),
|
"title": section["title"],
|
||||||
"order": section.get("order", 50),
|
"order": section.get("order", 50),
|
||||||
"items": topLevelItems,
|
"items": topLevelItems,
|
||||||
"subgroups": filteredSubgroups,
|
"subgroups": filteredSubgroups,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
filteredItems = _filterItems(
|
filteredItems = _filterItems(
|
||||||
section.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
|
section.get("items", []), isSysAdmin, roleIds, hasGlobalPermission
|
||||||
)
|
)
|
||||||
|
|
||||||
if filteredItems:
|
if filteredItems:
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"id": section["id"],
|
"id": section["id"],
|
||||||
"title": section["title"].get(language, section["title"].get("en", section["id"])),
|
"title": section["title"],
|
||||||
"order": section.get("order", 50),
|
"order": section.get("order", 50),
|
||||||
"items": filteredItems,
|
"items": filteredItems,
|
||||||
})
|
})
|
||||||
|
|
@ -441,19 +446,19 @@ def _buildStaticBlocks(
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
|
def _formatBlockItem(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Format a navigation item for the new API response.
|
Format a navigation item for the API response.
|
||||||
|
|
||||||
Uses new field names: uiComponent, uiLabel, uiPath
|
Labels are plain German strings (i18n base keys).
|
||||||
Does NOT include icon (UI maps via uiComponent)
|
The frontend translates them via t().
|
||||||
"""
|
"""
|
||||||
objectKey = item["objectKey"]
|
objectKey = item["objectKey"]
|
||||||
uiComponent = _objectKeyToUiComponent(objectKey)
|
uiComponent = _objectKeyToUiComponent(objectKey)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uiComponent": uiComponent,
|
"uiComponent": uiComponent,
|
||||||
"uiLabel": item["label"].get(language, item["label"].get("en", item["id"])),
|
"uiLabel": item["label"],
|
||||||
"uiPath": item["path"],
|
"uiPath": item["path"],
|
||||||
"order": item.get("order", 50),
|
"order": item.get("order", 50),
|
||||||
"objectKey": objectKey,
|
"objectKey": objectKey,
|
||||||
|
|
@ -464,52 +469,15 @@ def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
|
||||||
@limiter.limit("60/minute")
|
@limiter.limit("60/minute")
|
||||||
def get_navigation(
|
def get_navigation(
|
||||||
request: Request,
|
request: Request,
|
||||||
language: str = Query("de", description="Language for labels (en, de, fr)"),
|
|
||||||
reqContext: RequestContext = Depends(getRequestContext)
|
reqContext: RequestContext = Depends(getRequestContext)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get unified navigation structure with blocks.
|
Get unified navigation structure with blocks.
|
||||||
|
|
||||||
Single Source of Truth für Navigation - UI rendert nur was es erhält.
|
All labels are German base texts (i18n keys).
|
||||||
|
The frontend translates them via t().
|
||||||
|
|
||||||
Endpoint: GET /api/navigation
|
Endpoint: GET /api/navigation
|
||||||
|
|
||||||
Block order:
|
|
||||||
- System (10)
|
|
||||||
- Dynamic/Features (15) - only if user has feature instances
|
|
||||||
- Workflows (20)
|
|
||||||
- Basisdaten (30)
|
|
||||||
- Migrate (40)
|
|
||||||
- Administration (200)
|
|
||||||
|
|
||||||
Response format:
|
|
||||||
{
|
|
||||||
"language": "de",
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"type": "static",
|
|
||||||
"id": "system",
|
|
||||||
"title": "SYSTEM",
|
|
||||||
"order": 10,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"uiComponent": "page.system.home",
|
|
||||||
"uiLabel": "Übersicht",
|
|
||||||
"uiPath": "/",
|
|
||||||
"order": 10,
|
|
||||||
"objectKey": "ui.system.home"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "dynamic",
|
|
||||||
"id": "features",
|
|
||||||
"title": "MEINE FEATURES",
|
|
||||||
"order": 15,
|
|
||||||
"mandates": [...]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
isSysAdmin = reqContext.hasSysAdminRole
|
isSysAdmin = reqContext.hasSysAdminRole
|
||||||
|
|
@ -526,11 +494,11 @@ def get_navigation(
|
||||||
hasGlobalPermission = _checkUiPermission(roleIds, "_global_check")
|
hasGlobalPermission = _checkUiPermission(roleIds, "_global_check")
|
||||||
|
|
||||||
# Build static blocks from NAVIGATION_SECTIONS
|
# Build static blocks from NAVIGATION_SECTIONS
|
||||||
blocks = _buildStaticBlocks(language, isSysAdmin, roleIds, hasGlobalPermission)
|
blocks = _buildStaticBlocks(isSysAdmin, roleIds, hasGlobalPermission)
|
||||||
|
|
||||||
# Build dynamic block (features) if user has feature instances
|
# Build dynamic block (features) if user has feature instances
|
||||||
if userId:
|
if userId:
|
||||||
dynamicBlock = _buildDynamicBlock(userId, language, isSysAdmin)
|
dynamicBlock = _buildDynamicBlock(userId, isSysAdmin)
|
||||||
if dynamicBlock:
|
if dynamicBlock:
|
||||||
blocks.append(dynamicBlock)
|
blocks.append(dynamicBlock)
|
||||||
|
|
||||||
|
|
@ -538,14 +506,12 @@ def get_navigation(
|
||||||
blocks.sort(key=lambda b: b["order"])
|
blocks.sort(key=lambda b: b["order"])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"language": language,
|
|
||||||
"blocks": blocks,
|
"blocks": blocks,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting navigation: {e}")
|
logger.error(f"Error getting navigation: {e}")
|
||||||
return {
|
return {
|
||||||
"language": language,
|
|
||||||
"blocks": [],
|
"blocks": [],
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ from typing import Optional, Dict, Any, List
|
||||||
from modules.auth import getCurrentUser, getRequestContext, RequestContext, limiter
|
from modules.auth import getCurrentUser, getRequestContext, RequestContext, limiter
|
||||||
from modules.datamodels.datamodelUam import User
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface, VoiceObjects
|
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface, VoiceObjects
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeVoiceGoogle")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter(prefix="/voice-google", tags=["Voice Google"])
|
router = APIRouter(prefix="/voice-google", tags=["Voice Google"])
|
||||||
|
|
@ -132,7 +134,7 @@ async def detect_language(
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Empty text provided for language detection"
|
detail=routeApiMsg("Empty text provided for language detection")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get voice interface
|
# Get voice interface
|
||||||
|
|
@ -176,7 +178,7 @@ async def translate_text(
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Empty text provided for translation"
|
detail=routeApiMsg("Empty text provided for translation")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get voice interface
|
# Get voice interface
|
||||||
|
|
@ -306,7 +308,7 @@ async def text_to_speech(
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Empty text provided for text-to-speech"
|
detail=routeApiMsg("Empty text provided for text-to-speech")
|
||||||
)
|
)
|
||||||
|
|
||||||
mandateId = str(getattr(context, "mandateId", "") or "")
|
mandateId = str(getattr(context, "mandateId", "") or "")
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ from modules.auth import getCurrentUser, limiter
|
||||||
from modules.datamodels.datamodelUam import User, UserVoicePreferences, _normalizeTtsVoiceMap
|
from modules.datamodels.datamodelUam import User, UserVoicePreferences, _normalizeTtsVoiceMap
|
||||||
from modules.interfaces.interfaceDbApp import getRootInterface
|
from modules.interfaces.interfaceDbApp import getRootInterface
|
||||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
routeApiMsg = apiRouteContext("routeVoiceUser")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -176,7 +178,7 @@ def _resolveMandateIdForVoiceTestAi(request: Request, currentUser: User) -> str:
|
||||||
if headerRaw not in memberIds:
|
if headerRaw not in memberIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="X-Mandate-Id is not a mandate you belong to.",
|
detail=routeApiMsg("X-Mandate-Id is not a mandate you belong to."),
|
||||||
)
|
)
|
||||||
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
|
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -294,7 +296,7 @@ async def _generateTtsSampleTextForLocale(
|
||||||
logger.warning("Voice test AI sample empty or errorCount=%s", getattr(response, "errorCount", None))
|
logger.warning("Voice test AI sample empty or errorCount=%s", getattr(response, "errorCount", None))
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
detail="Could not generate voice test sample text.",
|
detail=routeApiMsg("Could not generate voice test sample text."),
|
||||||
)
|
)
|
||||||
if len(content) > 500:
|
if len(content) > 500:
|
||||||
content = content[:500].rstrip()
|
content = content[:500].rstrip()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ from modules.datamodels.datamodelPagination import PaginationParams
|
||||||
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import (
|
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import (
|
||||||
AutoRun, AutoStepLog, AutoWorkflow, AutoTask,
|
AutoRun, AutoStepLog, AutoWorkflow, AutoTask,
|
||||||
)
|
)
|
||||||
|
from modules.shared.i18nRegistry import apiRouteContext
|
||||||
|
|
||||||
|
routeApiMsg = apiRouteContext("routeWorkflowDashboard")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
@ -239,11 +242,11 @@ def get_run_steps(
|
||||||
"""Get step logs for a specific run (with access check)."""
|
"""Get step logs for a specific run (with access check)."""
|
||||||
db = _getDb()
|
db = _getDb()
|
||||||
if not db._ensureTableExists(AutoRun):
|
if not db._ensureTableExists(AutoRun):
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||||
|
|
||||||
runs = db.getRecordset(AutoRun, recordFilter={"id": runId})
|
runs = db.getRecordset(AutoRun, recordFilter={"id": runId})
|
||||||
if not runs:
|
if not runs:
|
||||||
raise HTTPException(status_code=404, detail="Run not found")
|
raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
|
||||||
run = dict(runs[0])
|
run = dict(runs[0])
|
||||||
|
|
||||||
if not context.hasSysAdminRole:
|
if not context.hasSysAdminRole:
|
||||||
|
|
@ -256,7 +259,7 @@ def get_run_steps(
|
||||||
elif runMandate and userId and _isUserMandateAdmin(userId, runMandate):
|
elif runMandate and userId and _isUserMandateAdmin(userId, runMandate):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
|
|
||||||
if not db._ensureTableExists(AutoStepLog):
|
if not db._ensureTableExists(AutoStepLog):
|
||||||
return {"steps": []}
|
return {"steps": []}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Feature-Container register their RBAC objects via mainXxx.py at startup.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional, Union
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -43,7 +43,7 @@ class RbacCatalogService:
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
logger.info("RBAC Catalog Service initialized")
|
logger.info("RBAC Catalog Service initialized")
|
||||||
|
|
||||||
def registerUiObject(self, featureCode: str, objectKey: str, label: Dict[str, str], meta: Optional[Dict[str, Any]] = None) -> bool:
|
def registerUiObject(self, featureCode: str, objectKey: str, label: Union[str, Dict[str, str]], meta: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
"""Register a UI object for a feature."""
|
"""Register a UI object for a feature."""
|
||||||
try:
|
try:
|
||||||
self._uiObjects[objectKey] = {"objectKey": objectKey, "featureCode": featureCode, "label": label, "meta": meta or {}, "type": "UI"}
|
self._uiObjects[objectKey] = {"objectKey": objectKey, "featureCode": featureCode, "label": label, "meta": meta or {}, "type": "UI"}
|
||||||
|
|
@ -84,7 +84,7 @@ class RbacCatalogService:
|
||||||
logger.error(f"Failed to register DATA object {objectKey}: {e}")
|
logger.error(f"Failed to register DATA object {objectKey}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def registerFeatureDefinition(self, featureCode: str, label: Dict[str, str], icon: str) -> bool:
|
def registerFeatureDefinition(self, featureCode: str, label: Union[str, Dict[str, str]], icon: str) -> bool:
|
||||||
"""Register a feature definition."""
|
"""Register a feature definition."""
|
||||||
try:
|
try:
|
||||||
self._featureDefinitions[featureCode] = {"code": featureCode, "label": label, "icon": icon}
|
self._featureDefinitions[featureCode] = {"code": featureCode, "label": label, "icon": icon}
|
||||||
|
|
|
||||||
|
|
@ -33,98 +33,98 @@ IMPORTABLE_SERVICES: Dict[str, Dict[str, Any]] = {
|
||||||
"class": "TicketService",
|
"class": "TicketService",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"objectKey": "service.ticket",
|
"objectKey": "service.ticket",
|
||||||
"label": {"en": "Ticket System", "de": "Ticket-System", "fr": "Système de tickets"},
|
"label": "Ticket-System",
|
||||||
},
|
},
|
||||||
"messaging": {
|
"messaging": {
|
||||||
"module": "modules.serviceCenter.services.serviceMessaging.mainServiceMessaging",
|
"module": "modules.serviceCenter.services.serviceMessaging.mainServiceMessaging",
|
||||||
"class": "MessagingService",
|
"class": "MessagingService",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"objectKey": "service.messaging",
|
"objectKey": "service.messaging",
|
||||||
"label": {"en": "Messaging", "de": "Nachrichten", "fr": "Messagerie"},
|
"label": "Nachrichten",
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"module": "modules.serviceCenter.services.serviceBilling.mainServiceBilling",
|
"module": "modules.serviceCenter.services.serviceBilling.mainServiceBilling",
|
||||||
"class": "BillingService",
|
"class": "BillingService",
|
||||||
"dependencies": ["subscription"],
|
"dependencies": ["subscription"],
|
||||||
"objectKey": "service.billing",
|
"objectKey": "service.billing",
|
||||||
"label": {"en": "Billing", "de": "Abrechnung", "fr": "Facturation"},
|
"label": "Abrechnung",
|
||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"module": "modules.serviceCenter.services.serviceSubscription.mainServiceSubscription",
|
"module": "modules.serviceCenter.services.serviceSubscription.mainServiceSubscription",
|
||||||
"class": "SubscriptionService",
|
"class": "SubscriptionService",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"objectKey": "service.subscription",
|
"objectKey": "service.subscription",
|
||||||
"label": {"en": "Subscription", "de": "Abonnement", "fr": "Abonnement"},
|
"label": "Abonnement",
|
||||||
},
|
},
|
||||||
"sharepoint": {
|
"sharepoint": {
|
||||||
"module": "modules.serviceCenter.services.serviceSharepoint.mainServiceSharepoint",
|
"module": "modules.serviceCenter.services.serviceSharepoint.mainServiceSharepoint",
|
||||||
"class": "SharepointService",
|
"class": "SharepointService",
|
||||||
"dependencies": ["security"],
|
"dependencies": ["security"],
|
||||||
"objectKey": "service.sharepoint",
|
"objectKey": "service.sharepoint",
|
||||||
"label": {"en": "SharePoint", "de": "SharePoint", "fr": "SharePoint"},
|
"label": "SharePoint",
|
||||||
},
|
},
|
||||||
"clickup": {
|
"clickup": {
|
||||||
"module": "modules.serviceCenter.services.serviceClickup.mainServiceClickup",
|
"module": "modules.serviceCenter.services.serviceClickup.mainServiceClickup",
|
||||||
"class": "ClickupService",
|
"class": "ClickupService",
|
||||||
"dependencies": ["security"],
|
"dependencies": ["security"],
|
||||||
"objectKey": "service.clickup",
|
"objectKey": "service.clickup",
|
||||||
"label": {"en": "ClickUp", "de": "ClickUp", "fr": "ClickUp"},
|
"label": "ClickUp",
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"module": "modules.serviceCenter.services.serviceChat.mainServiceChat",
|
"module": "modules.serviceCenter.services.serviceChat.mainServiceChat",
|
||||||
"class": "ChatService",
|
"class": "ChatService",
|
||||||
"dependencies": ["utils"],
|
"dependencies": ["utils"],
|
||||||
"objectKey": "service.chat",
|
"objectKey": "service.chat",
|
||||||
"label": {"en": "Chat", "de": "Chat", "fr": "Chat"},
|
"label": "Chat",
|
||||||
},
|
},
|
||||||
"extraction": {
|
"extraction": {
|
||||||
"module": "modules.serviceCenter.services.serviceExtraction.mainServiceExtraction",
|
"module": "modules.serviceCenter.services.serviceExtraction.mainServiceExtraction",
|
||||||
"class": "ExtractionService",
|
"class": "ExtractionService",
|
||||||
"dependencies": ["chat", "utils"],
|
"dependencies": ["chat", "utils"],
|
||||||
"objectKey": "service.extraction",
|
"objectKey": "service.extraction",
|
||||||
"label": {"en": "Extraction", "de": "Extraktion", "fr": "Extraction"},
|
"label": "Extraktion",
|
||||||
},
|
},
|
||||||
"generation": {
|
"generation": {
|
||||||
"module": "modules.serviceCenter.services.serviceGeneration.mainServiceGeneration",
|
"module": "modules.serviceCenter.services.serviceGeneration.mainServiceGeneration",
|
||||||
"class": "GenerationService",
|
"class": "GenerationService",
|
||||||
"dependencies": ["utils", "chat"],
|
"dependencies": ["utils", "chat"],
|
||||||
"objectKey": "service.generation",
|
"objectKey": "service.generation",
|
||||||
"label": {"en": "Generation", "de": "Generierung", "fr": "Génération"},
|
"label": "Generierung",
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"module": "modules.serviceCenter.services.serviceAi.mainServiceAi",
|
"module": "modules.serviceCenter.services.serviceAi.mainServiceAi",
|
||||||
"class": "AiService",
|
"class": "AiService",
|
||||||
"dependencies": ["chat", "utils", "extraction", "billing"],
|
"dependencies": ["chat", "utils", "extraction", "billing"],
|
||||||
"objectKey": "service.ai",
|
"objectKey": "service.ai",
|
||||||
"label": {"en": "AI", "de": "KI", "fr": "IA"},
|
"label": "KI",
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"module": "modules.serviceCenter.services.serviceWeb.mainServiceWeb",
|
"module": "modules.serviceCenter.services.serviceWeb.mainServiceWeb",
|
||||||
"class": "WebService",
|
"class": "WebService",
|
||||||
"dependencies": ["ai", "chat", "utils"],
|
"dependencies": ["ai", "chat", "utils"],
|
||||||
"objectKey": "service.web",
|
"objectKey": "service.web",
|
||||||
"label": {"en": "Web Research", "de": "Web-Recherche", "fr": "Recherche Web"},
|
"label": "Web-Recherche",
|
||||||
},
|
},
|
||||||
"neutralization": {
|
"neutralization": {
|
||||||
"module": "modules.features.neutralization.serviceNeutralization.mainServiceNeutralization",
|
"module": "modules.features.neutralization.serviceNeutralization.mainServiceNeutralization",
|
||||||
"class": "NeutralizationService",
|
"class": "NeutralizationService",
|
||||||
"dependencies": ["extraction", "generation"],
|
"dependencies": ["extraction", "generation"],
|
||||||
"objectKey": "service.neutralization",
|
"objectKey": "service.neutralization",
|
||||||
"label": {"en": "Neutralization", "de": "Neutralisierung", "fr": "Neutralisation"},
|
"label": "Neutralisierung",
|
||||||
},
|
},
|
||||||
"agent": {
|
"agent": {
|
||||||
"module": "modules.serviceCenter.services.serviceAgent.mainServiceAgent",
|
"module": "modules.serviceCenter.services.serviceAgent.mainServiceAgent",
|
||||||
"class": "AgentService",
|
"class": "AgentService",
|
||||||
"dependencies": ["ai", "chat", "utils", "extraction", "billing", "streaming", "knowledge"],
|
"dependencies": ["ai", "chat", "utils", "extraction", "billing", "streaming", "knowledge"],
|
||||||
"objectKey": "service.agent",
|
"objectKey": "service.agent",
|
||||||
"label": {"en": "Agent", "de": "Agent", "fr": "Agent"},
|
"label": "Agent",
|
||||||
},
|
},
|
||||||
"knowledge": {
|
"knowledge": {
|
||||||
"module": "modules.serviceCenter.services.serviceKnowledge.mainServiceKnowledge",
|
"module": "modules.serviceCenter.services.serviceKnowledge.mainServiceKnowledge",
|
||||||
"class": "KnowledgeService",
|
"class": "KnowledgeService",
|
||||||
"dependencies": ["ai"],
|
"dependencies": ["ai"],
|
||||||
"objectKey": "service.knowledge",
|
"objectKey": "service.knowledge",
|
||||||
"label": {"en": "Knowledge Store", "de": "Wissensspeicher", "fr": "Base de connaissances"},
|
"label": "Wissensspeicher",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ from modules.datamodels.datamodelExtraction import ContentPart
|
||||||
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
|
||||||
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
|
||||||
|
|
||||||
|
|
||||||
|
class _AiResponseFallback:
|
||||||
|
"""Lightweight wrapper used when AI JSON parsing fails but raw content must be preserved."""
|
||||||
|
def __init__(self, content):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -719,12 +725,8 @@ class StructureFiller:
|
||||||
|
|
||||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
|
||||||
|
|
||||||
class _AiResponse:
|
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
responseElements = await self._processAiResponseForSection(
|
responseElements = await self._processAiResponseForSection(
|
||||||
aiResponse=_AiResponse(aiResponseJson),
|
aiResponse=_AiResponseFallback(aiResponseJson),
|
||||||
contentType=contentType,
|
contentType=contentType,
|
||||||
operationType=operationType,
|
operationType=operationType,
|
||||||
sectionId=sectionId,
|
sectionId=sectionId,
|
||||||
|
|
@ -1032,17 +1034,10 @@ class StructureFiller:
|
||||||
else:
|
else:
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
except Exception as parseError:
|
except Exception as parseError:
|
||||||
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
@ -1200,17 +1195,10 @@ class StructureFiller:
|
||||||
else:
|
else:
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
except Exception as parseError:
|
except Exception as parseError:
|
||||||
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
@ -1467,17 +1455,10 @@ class StructureFiller:
|
||||||
else:
|
else:
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
except Exception as parseError:
|
except Exception as parseError:
|
||||||
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
|
||||||
class AiResponse:
|
aiResponse = _AiResponseFallback(aiResponseJson)
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
aiResponse = AiResponse(aiResponseJson)
|
|
||||||
generatedElements = []
|
generatedElements = []
|
||||||
|
|
||||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
|
||||||
|
|
@ -36,42 +36,44 @@ class AttributeDefinition(BaseModel):
|
||||||
placeholder: Optional[str] = None
|
placeholder: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Global registry for model labels
|
def _getModelLabelEntry(modelName: str) -> Dict[str, Any]:
|
||||||
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
|
"""Resolve label data produced by @i18nModel (see modules.shared.i18nRegistry.MODEL_LABELS)."""
|
||||||
|
try:
|
||||||
|
from modules.shared.i18nRegistry import MODEL_LABELS as i18nModelLabels
|
||||||
def registerModelLabels(modelName: str, modelLabel: Dict[str, str], labels: Dict[str, Dict[str, str]]):
|
except ImportError:
|
||||||
"""
|
return {}
|
||||||
Register labels for a model's attributes and the model itself.
|
return i18nModelLabels.get(modelName) or {}
|
||||||
|
|
||||||
Args:
|
|
||||||
modelName: Name of the model class
|
|
||||||
modelLabel: Dictionary mapping language codes to model labels
|
|
||||||
e.g. {"en": "Prompt", "fr": "Invite"}
|
|
||||||
labels: Dictionary mapping attribute names to their translations
|
|
||||||
e.g. {"name": {"en": "Name", "fr": "Nom"}}
|
|
||||||
"""
|
|
||||||
MODEL_LABELS[modelName] = {"model": modelLabel, "attributes": labels}
|
|
||||||
|
|
||||||
|
|
||||||
def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
|
def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
|
||||||
"""
|
"""Get labels for a model's attributes in the specified language.
|
||||||
Get labels for a model's attributes in the specified language.
|
|
||||||
|
|
||||||
Args:
|
Reads @i18nModel registration (German base strings); non-German languages use the i18n cache.
|
||||||
modelName: Name of the model class
|
Attribute values are strings; dict-shaped entries are still accepted for unusual callers.
|
||||||
language: Language code (default: "en")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary mapping attribute names to their labels in the specified language
|
|
||||||
"""
|
"""
|
||||||
modelData = MODEL_LABELS.get(modelName, {})
|
modelData = _getModelLabelEntry(modelName)
|
||||||
attributeLabels = modelData.get("attributes", {})
|
attributeLabels = modelData.get("attributes", {})
|
||||||
|
|
||||||
return {
|
result: Dict[str, str] = {}
|
||||||
attr: translations.get(language, translations.get("en", attr))
|
for attr, translations in attributeLabels.items():
|
||||||
for attr, translations in attributeLabels.items()
|
if isinstance(translations, dict):
|
||||||
}
|
result[attr] = translations.get(language, translations.get("en", attr))
|
||||||
|
elif isinstance(translations, str):
|
||||||
|
result[attr] = _resolveLabel(translations, language)
|
||||||
|
else:
|
||||||
|
result[attr] = attr
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _resolveLabel(germanText: str, language: str) -> str:
|
||||||
|
"""Resolve a German base label to the requested language via i18n cache."""
|
||||||
|
if language == "de":
|
||||||
|
return germanText
|
||||||
|
try:
|
||||||
|
from modules.shared.i18nRegistry import _CACHE
|
||||||
|
return _CACHE.get(language, {}).get(germanText, germanText)
|
||||||
|
except ImportError:
|
||||||
|
return germanText
|
||||||
|
|
||||||
|
|
||||||
def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Dict[str, str]:
|
def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Dict[str, str]:
|
||||||
|
|
@ -87,19 +89,14 @@ def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Di
|
||||||
|
|
||||||
|
|
||||||
def getModelLabel(modelName: str, language: str = "en") -> str:
|
def getModelLabel(modelName: str, language: str = "en") -> str:
|
||||||
"""
|
"""Get the label for a model in the specified language (see getModelLabels)."""
|
||||||
Get the label for a model in the specified language.
|
modelData = _getModelLabelEntry(modelName)
|
||||||
|
|
||||||
Args:
|
|
||||||
modelName: Name of the model class
|
|
||||||
language: Language code (default: "en")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Model label in the specified language, or model name if no label exists
|
|
||||||
"""
|
|
||||||
modelData = MODEL_LABELS.get(modelName, {})
|
|
||||||
modelLabel = modelData.get("model", {})
|
modelLabel = modelData.get("model", {})
|
||||||
return modelLabel.get(language, modelLabel.get("en", modelName))
|
if isinstance(modelLabel, dict):
|
||||||
|
return modelLabel.get(language, modelLabel.get("en", modelName))
|
||||||
|
elif isinstance(modelLabel, str):
|
||||||
|
return _resolveLabel(modelLabel, language)
|
||||||
|
return modelName
|
||||||
|
|
||||||
|
|
||||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue