From 8c9492715af638ac9dd32eedc5b9b603e985c2f0 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 30 May 2025 01:12:59 +0200
Subject: [PATCH] mvp running
---
app.py | 7 +
modules/agents/agentCoder.py | 1 +
modules/connectors/connectorDbJson.py | 193 +++++------
modules/interfaces/serviceAppClass.py | 97 +++++-
modules/interfaces/serviceAppModel.py | 275 ++++++++-------
modules/interfaces/serviceAppTokens.py | 27 --
modules/interfaces/serviceChatClass.py | 15 +-
modules/interfaces/serviceChatModel.py | 246 +++++++++++---
modules/interfaces/serviceManagementAccess.py | 29 +-
modules/interfaces/serviceManagementClass.py | 171 +++++++---
modules/interfaces/serviceManagementModel.py | 91 ++---
modules/routes/routeAttributes.py | 31 +-
modules/routes/routeDataConnections.py | 164 +++++++++
modules/routes/routeDataFiles.py | 70 ++--
modules/routes/routeDataMandates.py | 25 +-
modules/routes/routeDataPrompts.py | 36 +-
modules/routes/routeDataUsers.py | 45 +--
modules/routes/routeSecurityGoogle.py | 179 +++++++---
modules/routes/routeSecurityLocal.py | 24 +-
modules/routes/routeSecurityMsft.py | 175 +++++++---
modules/routes/routeWorkflows.py | 2 +-
modules/security/auth.py | 6 +-
modules/shared/attributeUtils.py | 313 ++++++++++--------
modules/workflow/documentManager.py | 89 ++---
modules/workflow/workflowManager.py | 17 +-
requirements.txt | 1 +
26 files changed, 1467 insertions(+), 862 deletions(-)
delete mode 100644 modules/interfaces/serviceAppTokens.py
create mode 100644 modules/routes/routeDataConnections.py
diff --git a/app.py b/app.py
index 2b6e1c4c..5a6994da 100644
--- a/app.py
+++ b/app.py
@@ -95,6 +95,10 @@ async def lifespan(app: FastAPI):
# Startup logic
logger.info("Application is starting up")
+ # Initialize root interface to ensure database is properly set up
+ from modules.interfaces.serviceAppClass import getRootInterface
+ getRootInterface()
+
yield
# Shutdown logic
@@ -146,6 +150,9 @@ app.include_router(fileRouter)
from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(promptRouter)
+from modules.routes.routeDataConnections import router as connectionsRouter
+app.include_router(connectionsRouter)
+
from modules.routes.routeWorkflows import router as workflowRouter
app.include_router(workflowRouter)
diff --git a/modules/agents/agentCoder.py b/modules/agents/agentCoder.py
index dffac935..8cb4d869 100644
--- a/modules/agents/agentCoder.py
+++ b/modules/agents/agentCoder.py
@@ -19,6 +19,7 @@ import uuid
from modules.workflow.agentBase import AgentBase
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
+from modules.shared.attributeUtils import ModelMixin
logger = logging.getLogger(__name__)
diff --git a/modules/connectors/connectorDbJson.py b/modules/connectors/connectorDbJson.py
index 878392d0..fe082956 100644
--- a/modules/connectors/connectorDbJson.py
+++ b/modules/connectors/connectorDbJson.py
@@ -4,6 +4,9 @@ from typing import List, Dict, Any, Optional, Union
import logging
from datetime import datetime
import uuid
+from pydantic import BaseModel
+
+from modules.shared.attributeUtils import to_dict
logger = logging.getLogger(__name__)
@@ -138,14 +141,61 @@ class DatabaseConnector:
if os.path.exists(recordPath):
with open(recordPath, 'r', encoding='utf-8') as f:
record = json.load(f)
- # Ensure ID is a string
- if "id" in record:
- record["id"] = str(record["id"])
return record
except Exception as e:
logger.error(f"Error loading record {recordId} from table {table}: {e}")
return None
+ def _saveRecord(self, table: str, recordId: str, record: Dict[str, Any]) -> bool:
+ """Saves a single record to the table."""
+ try:
+ # Ensure table directory exists
+ if not self._ensureTableDirectory(table):
+ raise ValueError(f"Error creating table directory for {table}")
+
+ # Ensure recordId is a string
+ recordId = str(recordId)
+
+ # Add metadata
+ currentTime = datetime.now().isoformat()
+ if "_createdAt" not in record:
+ record["_createdAt"] = currentTime
+ record["_createdBy"] = self.userId
+ record["_modifiedAt"] = currentTime
+ record["_modifiedBy"] = self.userId
+
+ # Save the record file
+ recordPath = self._getRecordPath(table, recordId)
+ os.makedirs(os.path.dirname(recordPath), exist_ok=True)
+
+ with open(recordPath, 'w', encoding='utf-8') as f:
+ json.dump(record, f, indent=2, ensure_ascii=False)
+
+ # Update metadata
+ metadata = self._loadTableMetadata(table)
+ if recordId not in metadata["recordIds"]:
+ metadata["recordIds"].append(recordId)
+ metadata["recordIds"].sort()
+ self._saveTableMetadata(table, metadata)
+
+ # Update cache if it exists
+ if table in self._tablesCache:
+ # Find and update existing record or append new one
+ found = False
+ for i, existing_record in enumerate(self._tablesCache[table]):
+ if str(existing_record.get("id")) == recordId:
+ self._tablesCache[table][i] = record
+ found = True
+ break
+ if not found:
+ self._tablesCache[table].append(record)
+
+ return True
+
+ except Exception as e:
+ logger.error(f"Error saving record {recordId} to table {table}: {e}")
+ return False
+
def _loadTable(self, table: str) -> List[Dict[str, Any]]:
"""Loads all records from a table folder."""
# If the table is the system table, load it directly
@@ -162,6 +212,9 @@ class DatabaseConnector:
# Load each record
for recordId in metadata["recordIds"]:
+ # Skip metadata file
+ if recordId == "_metadata":
+ continue
record = self._loadRecord(table, recordId)
if record:
records.append(record)
@@ -376,67 +429,37 @@ class DatabaseConnector:
return records
- def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
- """Creates a new record in the specified table."""
- try:
- # Ensure table directory exists
- if not self._ensureTableDirectory(table):
- raise ValueError(f"Error creating table directory for {table}")
+ def recordCreate(self, table: str, record: Dict[str, Any]) -> Dict[str, Any]:
+ """Creates a new record in a table."""
+ # Ensure record has an ID
+ if "id" not in record:
+ record["id"] = str(uuid.uuid4())
- # Load table metadata
- metadata = self._loadTableMetadata(table)
+ # If record is a Pydantic model, convert to dict
+ if isinstance(record, BaseModel):
+ record = to_dict(record)
- # Generate new ID if not provided
- if "id" not in recordData:
- recordData["id"] = str(uuid.uuid4())
- else:
- # Ensure ID is a string
- recordData["id"] = str(recordData["id"])
+ # Save record
+ self._saveRecord(table, record["id"], record)
+ return record
+
+ def recordModify(self, table: str, recordId: str, record: Dict[str, Any]) -> Dict[str, Any]:
+ """Modifies an existing record in a table."""
+ # Load existing record
+ existingRecord = self._loadRecord(table, recordId)
+ if not existingRecord:
+ raise ValueError(f"Record {recordId} not found in table {table}")
- # Add context fields
- recordData["userId"] = self.userId
+ # If record is a Pydantic model, convert to dict
+ if isinstance(record, BaseModel):
+ record = to_dict(record)
- # Add creation and modification tracking
- currentTime = self._getCurrentTimestamp()
- recordData["_createdAt"] = currentTime
- recordData["_modifiedAt"] = currentTime
- recordData["_createdBy"] = self.userId
- recordData["_modifiedBy"] = self.userId
+ # Update existing record with new data
+ existingRecord.update(record)
- # Save the record
- recordPath = self._getRecordPath(table, recordData["id"])
- os.makedirs(os.path.dirname(recordPath), exist_ok=True)
- with open(recordPath, 'w', encoding='utf-8') as f:
- json.dump(recordData, f, indent=2, ensure_ascii=False)
-
- # Update metadata with new record ID
- if recordData["id"] not in metadata["recordIds"]:
- metadata["recordIds"].append(recordData["id"])
- metadata["recordIds"].sort()
-
- # Save updated metadata
- if not self._saveTableMetadata(table, metadata):
- raise ValueError(f"Error saving metadata for table {table}")
-
- # Update both caches
- self._tableMetadataCache[table] = metadata
- if table in self._tablesCache:
- if isinstance(self._tablesCache[table], list):
- self._tablesCache[table].append(recordData)
- else:
- self._tablesCache[table] = [recordData]
- else:
- self._tablesCache[table] = [recordData]
-
- # Verify the record was created
- if not os.path.exists(recordPath):
- raise ValueError(f"Record file was not created at {recordPath}")
-
- return recordData
-
- except Exception as e:
- logger.error(f"Error creating record in table {table}: {str(e)}")
- raise ValueError(f"Error creating the record in table {table}")
+ # Save updated record
+ self._saveRecord(table, recordId, existingRecord)
+ return existingRecord
def recordDelete(self, table: str, recordId: str) -> bool:
"""Deletes a record from the table."""
@@ -473,56 +496,6 @@ class DatabaseConnector:
return False
- def recordModify(self, table: str, recordId: str, recordData: Dict[str, Any]) -> Dict[str, Any]:
- """Modifies a record in the table."""
- # Ensure table directory exists
- if not self._ensureTableDirectory(table):
- raise ValueError(f"Error creating table directory for {table}")
-
- # Load metadata to check if record exists
- metadata = self._loadTableMetadata(table)
-
- # Ensure recordId is a string
- recordId = str(recordId)
-
- if recordId not in metadata["recordIds"]:
- raise ValueError(f"Record with ID {recordId} not found in table {table}")
-
- # Prevent changing the ID
- if "id" in recordData and str(recordData["id"]) != recordId:
- raise ValueError(f"The ID of a record in table {table} cannot be changed")
-
- # Load existing record
- existingRecord = self._loadRecord(table, recordId)
- if not existingRecord:
- raise ValueError(f"Record with ID {recordId} not found in table {table}")
-
- # Update the record
- for key, value in recordData.items():
- existingRecord[key] = value
-
- # Update modified timestamp and user
- existingRecord["_modifiedAt"] = self._getCurrentTimestamp()
- existingRecord["_modifiedBy"] = self.userId
-
- # Save the updated record
- recordPath = self._getRecordPath(table, recordId)
- try:
- with open(recordPath, 'w', encoding='utf-8') as f:
- json.dump(existingRecord, f, indent=2, ensure_ascii=False)
-
- # Update table cache if it exists
- if table in self._tablesCache:
- for i, record in enumerate(self._tablesCache[table]):
- if str(record.get("id")) == recordId:
- self._tablesCache[table][i] = existingRecord
- break
-
- return existingRecord
- except Exception as e:
- logger.error(f"Error updating record file {recordPath}: {e}")
- raise ValueError(f"Error updating record in table {table}")
-
def hasInitialId(self, table: str) -> bool:
"""Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable()
@@ -538,4 +511,4 @@ class DatabaseConnector:
def getAllInitialIds(self) -> Dict[str, str]:
"""Returns all registered initial IDs."""
systemData = self._loadSystemTable()
- return systemData.copy() # Return a copy to protect the original
\ No newline at end of file
+ return systemData.copy() # Return a copy to protect the original
diff --git a/modules/interfaces/serviceAppClass.py b/modules/interfaces/serviceAppClass.py
index a614d282..a357763e 100644
--- a/modules/interfaces/serviceAppClass.py
+++ b/modules/interfaces/serviceAppClass.py
@@ -10,6 +10,7 @@ from typing import Dict, Any, List, Optional, Union
import importlib
import json
from passlib.context import CryptContext
+import uuid
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.shared.configuration import APP_CONFIG
@@ -17,8 +18,9 @@ from modules.interfaces.serviceAppAccess import AppAccess
from modules.interfaces.serviceAppModel import (
User, Mandate, UserInDB, UserConnection,
Session, AuthEvent, AuthAuthority, UserPrivilege,
- ConnectionStatus
+ ConnectionStatus, Token, LocalToken, GoogleToken, MsftToken
)
+from modules.shared.attributeUtils import ModelMixin
logger = logging.getLogger(__name__)
@@ -74,6 +76,9 @@ class GatewayInterface:
# Initialize access control with user context
self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed
+ # Update database context
+ self.db.updateContext(self.userId)
+
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self):
@@ -164,7 +169,17 @@ class GatewayInterface:
Returns:
Filtered recordset with access control attributes
"""
- return self.access.uam(table, recordset)
+ # First apply access control
+ filteredRecords = self.access.uam(table, recordset)
+
+ # Then filter out database-specific fields
+ cleanedRecords = []
+ for record in filteredRecords:
+ # Create a new dict with only non-database fields
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
+ cleanedRecords.append(cleanedRecord)
+
+ return cleanedRecords
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
@@ -445,6 +460,12 @@ class GatewayInterface:
self.db.recordDelete("auth_events", event["id"])
logger.debug(f"Deleted auth event {event['id']} for user {userId}")
+ # Delete user tokens
+ tokens = self.db.getRecordset("tokens", recordFilter={"userId": userId})
+ for token in tokens:
+ self.db.recordDelete("tokens", token["id"])
+ logger.debug(f"Deleted token {token['id']} for user {userId}")
+
# Delete user connections
user = self.getUser(userId)
if user and user.connections:
@@ -593,6 +614,78 @@ class GatewayInterface:
"message": f"Error checking username availability: {str(e)}"
}
+ def saveToken(self, token: Token) -> None:
+ """Save a token for the current user"""
+ try:
+ # Validate user context
+ if not self.currentUser or not self.currentUser.id:
+ raise ValueError("No valid user context available for token storage")
+
+ # Set the user ID and mandate ID
+ token.userId = self.currentUser.id
+
+ # Ensure token has required fields
+ if not token.id:
+ token.id = str(uuid.uuid4())
+ if not token.createdAt:
+ token.createdAt = datetime.now()
+
+ # Convert to dict and ensure all fields are properly set
+ token_dict = token.dict()
+ token_dict["userId"] = self.currentUser.id
+
+ # Convert datetime objects to ISO format strings
+ if isinstance(token_dict.get("createdAt"), datetime):
+ token_dict["createdAt"] = token_dict["createdAt"].isoformat()
+ if isinstance(token_dict.get("expiresAt"), datetime):
+ token_dict["expiresAt"] = token_dict["expiresAt"].isoformat()
+
+ # Save to database
+ self.db.recordCreate("tokens", token_dict)
+
+ logger.debug(f"Token saved for user {self.currentUser.id} with authority {token.authority}")
+
+ except Exception as e:
+ logger.error(f"Error saving token: {str(e)}")
+ raise
+
+ def getToken(self, authority: AuthAuthority) -> Optional[Token]:
+ """Get the latest token for the current user and authority"""
+ try:
+ # Get tokens for this user and authority
+ tokens = self.db.getRecordset("tokens", recordFilter={
+ "userId": self.currentUser.id,
+ "authority": authority
+ })
+
+ if not tokens:
+ return None
+
+ # Sort by creation date and get the latest
+ tokens.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
+ return Token(**tokens[0])
+
+ except Exception as e:
+ logger.error(f"Error getting token: {str(e)}")
+ return None
+
+ def deleteToken(self, authority: AuthAuthority) -> None:
+ """Delete all tokens for the current user and authority"""
+ try:
+ # Get tokens to delete
+ tokens = self.db.getRecordset("tokens", recordFilter={
+ "userId": self.currentUser.id,
+ "authority": authority
+ })
+
+ # Delete each token
+ for token in tokens:
+ self.db.recordDelete("tokens", token["id"])
+
+ except Exception as e:
+ logger.error(f"Error deleting token: {str(e)}")
+ raise
+
# Public Methods
def getInterface(currentUser: User) -> GatewayInterface:
diff --git a/modules/interfaces/serviceAppModel.py b/modules/interfaces/serviceAppModel.py
index 1eb98f1d..979efdda 100644
--- a/modules/interfaces/serviceAppModel.py
+++ b/modules/interfaces/serviceAppModel.py
@@ -7,26 +7,13 @@ from pydantic import BaseModel, Field, EmailStr
from typing import List, Dict, Any, Optional
from datetime import datetime
from enum import Enum
-
-from modules.shared.attributeUtils import Label, BaseModelWithUI
-
-class AttributeDefinition(BaseModel):
- """Definition of an attribute for UI forms"""
- name: str = Field(..., description="Name of the attribute")
- label: str = Field(..., description="Display label for the attribute")
- type: str = Field(..., description="Type of the attribute (string, number, boolean, etc.)")
- required: bool = Field(default=False, description="Whether the attribute is required")
- placeholder: Optional[str] = Field(None, description="Placeholder text for the input")
- editable: bool = Field(default=True, description="Whether the attribute can be edited")
- visible: bool = Field(default=True, description="Whether the attribute should be visible in forms")
- order: int = Field(default=0, description="Order in which to display the attribute")
+from modules.shared.attributeUtils import register_model_labels, AttributeDefinition, ModelMixin
class AuthAuthority(str, Enum):
- """Authentication authorities"""
- LOCAL = "local"
- MICROSOFT = "microsoft"
- GOOGLE = "google"
- EXTERNAL = "external"
+ """Authentication authority enum"""
+ LOCAL = "Local"
+ GOOGLE = "Google"
+ MSFT = "Msft"
class UserPrivilege(str, Enum):
"""User privilege levels"""
@@ -41,39 +28,24 @@ class ConnectionStatus(str, Enum):
REVOKED = "revoked"
PENDING = "pending"
-class Mandate(BaseModelWithUI):
+class Mandate(BaseModel, ModelMixin):
"""Data model for a mandate"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate")
language: str = Field(default="en", description="Default language of the mandate")
-
- label: Label = Field(
- default=Label(default="Mandate", translations={"en": "Mandate", "fr": "Mandat"}),
- description="Label for the class"
- )
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "name": Label(default="Name of the mandate", translations={"en": "Mandate name", "fr": "Nom du mandat"}),
- "language": Label(default="Language", translations={"en": "Language", "fr": "Langue"})
+# Register labels for Mandate
+register_model_labels(
+ "Mandate",
+ {"en": "Mandate", "fr": "Mandat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "name": {"en": "Name", "fr": "Nom"},
+ "language": {"en": "Language", "fr": "Langue"}
}
-
- @classmethod
- def get_validations(cls) -> Dict[str, Any]:
- """Get validation rules for frontend"""
- return {
- "name": {
- "required": True,
- "minLength": 2,
- "maxLength": 100
- },
- "language": {
- "required": True,
- "pattern": "^[a-z]{2}$"
- }
- }
+)
-class UserConnection(BaseModelWithUI):
+class UserConnection(BaseModel, ModelMixin):
"""Data model for a user's connection to an external service"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
authority: AuthAuthority = Field(description="Authentication authority")
@@ -84,25 +56,25 @@ class UserConnection(BaseModelWithUI):
connectedAt: datetime = Field(default_factory=datetime.now, description="When the connection was established")
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
-
- label: Label = Field(
- default=Label(default="User Connection", translations={"en": "User Connection", "fr": "Connexion utilisateur"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "authority": Label(default="Authority", translations={"en": "Authority", "fr": "Autorité"}),
- "externalId": Label(default="External ID", translations={"en": "External ID", "fr": "ID externe"}),
- "externalUsername": Label(default="External Username", translations={"en": "External Username", "fr": "Nom d'utilisateur externe"}),
- "externalEmail": Label(default="External Email", translations={"en": "External Email", "fr": "Email externe"}),
- "status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
- "connectedAt": Label(default="Connected At", translations={"en": "Connected At", "fr": "Connecté le"}),
- "lastChecked": Label(default="Last Checked", translations={"en": "Last Checked", "fr": "Dernière vérification"}),
- "expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"})
- }
-class Session(BaseModelWithUI):
+# Register labels for UserConnection
+register_model_labels(
+ "UserConnection",
+ {"en": "User Connection", "fr": "Connexion utilisateur"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "authority": {"en": "Authority", "fr": "Autorité"},
+ "externalId": {"en": "External ID", "fr": "ID externe"},
+ "externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
+ "externalEmail": {"en": "External Email", "fr": "Email externe"},
+ "status": {"en": "Status", "fr": "Statut"},
+ "connectedAt": {"en": "Connected At", "fr": "Connecté le"},
+ "lastChecked": {"en": "Last Checked", "fr": "Dernière vérification"},
+ "expiresAt": {"en": "Expires At", "fr": "Expire le"}
+ }
+)
+
+class Session(BaseModel, ModelMixin):
"""Data model for user sessions"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique session ID")
userId: str = Field(description="ID of the user")
@@ -111,23 +83,23 @@ class Session(BaseModelWithUI):
expiresAt: datetime = Field(description="When the session expires")
ipAddress: Optional[str] = Field(None, description="IP address of the session")
userAgent: Optional[str] = Field(None, description="User agent of the session")
-
- label: Label = Field(
- default=Label(default="Session", translations={"en": "Session", "fr": "Session"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
- "tokenId": Label(default="Token ID", translations={"en": "Token ID", "fr": "ID du token"}),
- "lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
- "expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"}),
- "ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
- "userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
- }
-class AuthEvent(BaseModelWithUI):
+# Register labels for Session
+register_model_labels(
+ "Session",
+ {"en": "Session", "fr": "Session"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "userId": {"en": "User ID", "fr": "ID utilisateur"},
+ "tokenId": {"en": "Token ID", "fr": "ID du token"},
+ "lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
+ "expiresAt": {"en": "Expires At", "fr": "Expire le"},
+ "ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
+ "userAgent": {"en": "User Agent", "fr": "User Agent"}
+ }
+)
+
+class AuthEvent(BaseModel, ModelMixin):
"""Data model for authentication events"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique event ID")
userId: str = Field(description="ID of the user")
@@ -136,23 +108,23 @@ class AuthEvent(BaseModelWithUI):
timestamp: datetime = Field(default_factory=datetime.now, description="When the event occurred")
ipAddress: Optional[str] = Field(None, description="IP address of the event")
userAgent: Optional[str] = Field(None, description="User agent of the event")
-
- label: Label = Field(
- default=Label(default="Auth Event", translations={"en": "Auth Event", "fr": "Événement d'authentification"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
- "eventType": Label(default="Event Type", translations={"en": "Event Type", "fr": "Type d'événement"}),
- "details": Label(default="Details", translations={"en": "Details", "fr": "Détails"}),
- "timestamp": Label(default="Timestamp", translations={"en": "Timestamp", "fr": "Horodatage"}),
- "ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
- "userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
- }
-class User(BaseModelWithUI):
+# Register labels for AuthEvent
+register_model_labels(
+ "AuthEvent",
+ {"en": "Auth Event", "fr": "Événement d'authentification"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "userId": {"en": "User ID", "fr": "ID utilisateur"},
+ "eventType": {"en": "Event Type", "fr": "Type d'événement"},
+ "details": {"en": "Details", "fr": "Détails"},
+ "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
+ "ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
+ "userAgent": {"en": "User Agent", "fr": "User Agent"}
+ }
+)
+
+class User(BaseModel, ModelMixin):
"""Data model for a user"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login")
@@ -164,59 +136,78 @@ class User(BaseModelWithUI):
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
-
- label: Label = Field(
- default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "username": Label(default="Username", translations={"en": "Username", "fr": "Nom d'utilisateur"}),
- "email": Label(default="Email", translations={"en": "Email", "fr": "Email"}),
- "fullName": Label(default="Full Name", translations={"en": "Full Name", "fr": "Nom complet"}),
- "language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
- "disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
- "privilege": Label(default="Privilege", translations={"en": "Privilege", "fr": "Privilège"}),
- "authenticationAuthority": Label(default="Auth Authority", translations={"en": "Auth Authority", "fr": "Autorité d'authentification"}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
- "connections": Label(default="Connections", translations={"en": "Connections", "fr": "Connexions"})
+
+# Register labels for User
+register_model_labels(
+ "User",
+ {"en": "User", "fr": "Utilisateur"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "username": {"en": "Username", "fr": "Nom d'utilisateur"},
+ "email": {"en": "Email", "fr": "Email"},
+ "fullName": {"en": "Full Name", "fr": "Nom complet"},
+ "language": {"en": "Language", "fr": "Langue"},
+ "disabled": {"en": "Disabled", "fr": "Désactivé"},
+ "privilege": {"en": "Privilege", "fr": "Privilège"},
+ "authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
+ "connections": {"en": "Connections", "fr": "Connexions"}
}
-
- @classmethod
- def get_validations(cls) -> Dict[str, Any]:
- """Get validation rules for frontend"""
- return {
- "username": {
- "required": True,
- "minLength": 3,
- "maxLength": 50,
- "pattern": "^[a-zA-Z0-9_-]+$"
- },
- "email": {
- "required": False,
- "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
- },
- "fullName": {
- "required": False,
- "maxLength": 100
- },
- "language": {
- "required": True,
- "pattern": "^[a-z]{2}$"
- }
- }
+)
class UserInDB(User):
"""Extended user class with password hash"""
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
- label: Label = Field(
- default=Label(default="User Access", translations={"en": "User Access", "fr": "Accès de l'utilisateur"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "hashedPassword": Label(default="Password hash", translations={"en": "Password hash", "fr": "Hachage de mot de passe"})
+# Register labels for UserInDB
+register_model_labels(
+ "UserInDB",
+ {"en": "User Access", "fr": "Accès de l'utilisateur"},
+ {
+ "hashedPassword": {"en": "Password hash", "fr": "Hachage de mot de passe"}
}
+)
+
+# Token Models
+class Token(BaseModel, ModelMixin):
+ """Token model for all authentication types"""
+ id: Optional[str] = None
+ userId: str
+ authority: AuthAuthority
+ tokenAccess: str
+ tokenType: str = "bearer"
+ expiresAt: float
+ tokenRefresh: Optional[str] = None
+ createdAt: Optional[datetime] = None
+
+ class Config:
+ useEnumValues = True
+
+# Register labels for Token
+register_model_labels(
+ "Token",
+ {"en": "Token", "fr": "Jeton"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "userId": {"en": "User ID", "fr": "ID utilisateur"},
+ "authority": {"en": "Authority", "fr": "Autorité"},
+ "tokenAccess": {"en": "Access Token", "fr": "Jeton d'accès"},
+ "tokenType": {"en": "Token Type", "fr": "Type de jeton"},
+ "expiresAt": {"en": "Expires At", "fr": "Expire le"},
+ "tokenRefresh": {"en": "Refresh Token", "fr": "Jeton de rafraîchissement"},
+ "createdAt": {"en": "Created At", "fr": "Créé le"}
+ }
+)
+
+class LocalToken(Token):
+ """Local authentication token model"""
+ pass
+
+class GoogleToken(Token):
+ """Google OAuth token model"""
+ pass
+
+class MsftToken(Token):
+ """Microsoft OAuth token model"""
+ pass
\ No newline at end of file
diff --git a/modules/interfaces/serviceAppTokens.py b/modules/interfaces/serviceAppTokens.py
deleted file mode 100644
index 68170861..00000000
--- a/modules/interfaces/serviceAppTokens.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""
-Token models and management for external authentication services.
-"""
-
-from pydantic import BaseModel
-from typing import Optional
-from datetime import datetime
-
-class GoogleToken(BaseModel):
- """Google OAuth token model"""
- access_token: str
- token_type: str = "bearer"
- expires_at: float
- refresh_token: Optional[str] = None
-
-class MsftToken(BaseModel):
- """Microsoft OAuth token model"""
- access_token: str
- token_type: str = "bearer"
- expires_at: float
- refresh_token: Optional[str] = None
-
-class LocalToken(BaseModel):
- """Local authentication token model"""
- access_token: str
- token_type: str = "bearer"
- expires_at: float
\ No newline at end of file
diff --git a/modules/interfaces/serviceChatClass.py b/modules/interfaces/serviceChatClass.py
index e7e4d3c6..562d61d8 100644
--- a/modules/interfaces/serviceChatClass.py
+++ b/modules/interfaces/serviceChatClass.py
@@ -92,6 +92,9 @@ class ChatInterface:
# Initialize access control with user context
self.access = ChatAccess(self.currentUser, self.db) # Convert to dict only when needed
+ # Update database context
+ self.db.updateContext(self.userId)
+
logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self):
@@ -176,7 +179,17 @@ class ChatInterface:
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
- return self.access.uam(table, recordset)
+ # First apply access control
+ filteredRecords = self.access.uam(table, recordset)
+
+ # Then filter out database-specific fields
+ cleanedRecords = []
+ for record in filteredRecords:
+ # Create a new dict with only non-database fields
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
+ cleanedRecords.append(cleanedRecord)
+
+ return cleanedRecords
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
diff --git a/modules/interfaces/serviceChatModel.py b/modules/interfaces/serviceChatModel.py
index 6bedd288..a37d0909 100644
--- a/modules/interfaces/serviceChatModel.py
+++ b/modules/interfaces/serviceChatModel.py
@@ -7,12 +7,11 @@ from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
-from modules.shared.attributeUtils import Label, BaseModelWithUI
-
+from modules.shared.attributeUtils import register_model_labels, ModelMixin
# WORKFLOW MODELS
-class ChatContent(BaseModelWithUI):
+class ChatContent(BaseModel, ModelMixin):
"""Data model for chat content"""
sequenceNr: int = Field(description="Sequence number of the content")
name: str = Field(description="Name of the content")
@@ -20,18 +19,45 @@ class ChatContent(BaseModelWithUI):
mimeType: str = Field(description="MIME type of the content")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
-class ChatDocument(BaseModelWithUI):
+# Register labels for ChatContent
+register_model_labels(
+ "ChatContent",
+ {"en": "Chat Content", "fr": "Contenu de chat"},
+ {
+ "sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
+ "name": {"en": "Name", "fr": "Nom"},
+ "data": {"en": "Data", "fr": "Données"},
+ "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
+ "metadata": {"en": "Metadata", "fr": "Métadonnées"}
+ }
+)
+
+class ChatDocument(BaseModel, ModelMixin):
"""Data model for a chat document"""
- id: str = Field(description="Primary key")
- fileId: int = Field(description="Foreign key to file")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
+ fileId: str = Field(description="Foreign key to file")
filename: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file")
mimeType: str = Field(description="MIME type of the file")
contents: List[ChatContent] = Field(default_factory=list, description="List of chat contents")
-class ChatStat(BaseModelWithUI):
+# Register labels for ChatDocument
+register_model_labels(
+ "ChatDocument",
+ {"en": "Chat Document", "fr": "Document de chat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "fileId": {"en": "File ID", "fr": "ID du fichier"},
+ "filename": {"en": "Filename", "fr": "Nom de fichier"},
+ "fileSize": {"en": "File Size", "fr": "Taille du fichier"},
+ "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
+ "contents": {"en": "Contents", "fr": "Contenus"}
+ }
+)
+
+class ChatStat(BaseModel, ModelMixin):
"""Data model for chat statistics"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Number of tokens processed")
bytesSent: Optional[int] = Field(None, description="Number of bytes sent")
@@ -39,9 +65,24 @@ class ChatStat(BaseModelWithUI):
successRate: Optional[float] = Field(None, description="Success rate of operations")
errorCount: Optional[int] = Field(None, description="Number of errors encountered")
-class ChatLog(BaseModelWithUI):
+# Register labels for ChatStat
+register_model_labels(
+ "ChatStat",
+ {"en": "Chat Statistics", "fr": "Statistiques de chat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
+ "tokenCount": {"en": "Token Count", "fr": "Nombre de tokens"},
+ "bytesSent": {"en": "Bytes Sent", "fr": "Octets envoyés"},
+ "bytesReceived": {"en": "Bytes Received", "fr": "Octets reçus"},
+ "successRate": {"en": "Success Rate", "fr": "Taux de succès"},
+ "errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"}
+ }
+)
+
+class ChatLog(BaseModel, ModelMixin):
"""Data model for a chat log"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
message: str = Field(description="Log message")
type: str = Field(description="Type of log entry")
@@ -51,9 +92,26 @@ class ChatLog(BaseModelWithUI):
progress: Optional[int] = Field(None, description="Progress percentage")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
-class ChatMessage(BaseModelWithUI):
+# Register labels for ChatLog
+register_model_labels(
+ "ChatLog",
+ {"en": "Chat Log", "fr": "Journal de chat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
+ "message": {"en": "Message", "fr": "Message"},
+ "type": {"en": "Type", "fr": "Type"},
+ "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
+ "agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
+ "status": {"en": "Status", "fr": "Statut"},
+ "progress": {"en": "Progress", "fr": "Progression"},
+ "performance": {"en": "Performance", "fr": "Performance"}
+ }
+)
+
+class ChatMessage(BaseModel, ModelMixin):
"""Data model for a chat message"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
agentName: Optional[str] = Field(None, description="Name of the agent")
@@ -67,9 +125,30 @@ class ChatMessage(BaseModelWithUI):
stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful")
-class Task(BaseModelWithUI):
+# Register labels for ChatMessage
+register_model_labels(
+ "ChatMessage",
+ {"en": "Chat Message", "fr": "Message de chat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
+ "parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
+ "agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
+ "documents": {"en": "Documents", "fr": "Documents"},
+ "message": {"en": "Message", "fr": "Message"},
+ "role": {"en": "Role", "fr": "Rôle"},
+ "status": {"en": "Status", "fr": "Statut"},
+ "sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
+ "startedAt": {"en": "Started At", "fr": "Démarré le"},
+ "finishedAt": {"en": "Finished At", "fr": "Terminé le"},
+ "stats": {"en": "Statistics", "fr": "Statistiques"},
+ "success": {"en": "Success", "fr": "Succès"}
+ }
+)
+
+class Task(BaseModel, ModelMixin):
"""Data model for a task"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
agentName: str = Field(description="Name of the agent assigned to this task")
status: str = Field(description="Current status of the task")
@@ -84,9 +163,31 @@ class Task(BaseModelWithUI):
finishedAt: Optional[str] = Field(None, description="When the task finished")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
-class ChatWorkflow(BaseModelWithUI):
+# Register labels for Task
+register_model_labels(
+ "Task",
+ {"en": "Task", "fr": "Tâche"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
+ "agentName": {"en": "Agent Name", "fr": "Nom de l'agent"},
+ "status": {"en": "Status", "fr": "Statut"},
+ "progress": {"en": "Progress", "fr": "Progression"},
+ "prompt": {"en": "Prompt", "fr": "Invite"},
+ "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
+ "filesInput": {"en": "Input Files", "fr": "Fichiers d'entrée"},
+ "filesOutput": {"en": "Output Files", "fr": "Fichiers de sortie"},
+ "result": {"en": "Result", "fr": "Résultat"},
+ "error": {"en": "Error", "fr": "Erreur"},
+ "startedAt": {"en": "Started At", "fr": "Démarré le"},
+ "finishedAt": {"en": "Finished At", "fr": "Terminé le"},
+ "performance": {"en": "Performance", "fr": "Performance"}
+ }
+)
+
+class ChatWorkflow(BaseModel, ModelMixin):
"""Data model for a chat workflow"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
status: str = Field(description="Current status of the workflow")
name: Optional[str] = Field(None, description="Name of the workflow")
@@ -97,57 +198,105 @@ class ChatWorkflow(BaseModelWithUI):
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
tasks: List[Task] = Field(default_factory=list, description="List of tasks in the workflow")
-
- label: Label = Field(
- default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
- "status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
- "name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
- "currentRound": Label(default="Current Round", translations={"en": "Current Round", "fr": "Tour actuel"}),
- "lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
- "startedAt": Label(default="Started At", translations={"en": "Started At", "fr": "Démarré le"}),
- "logs": Label(default="Logs", translations={"en": "Logs", "fr": "Journaux"}),
- "messages": Label(default="Messages", translations={"en": "Messages", "fr": "Messages"}),
- "stats": Label(default="Statistics", translations={"en": "Statistics", "fr": "Statistiques"}),
- "tasks": Label(default="Tasks", translations={"en": "Tasks", "fr": "Tâches"})
+
+# Register labels for ChatWorkflow
+register_model_labels(
+ "ChatWorkflow",
+ {"en": "Chat Workflow", "fr": "Flux de travail de chat"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
+ "status": {"en": "Status", "fr": "Statut"},
+ "name": {"en": "Name", "fr": "Nom"},
+ "currentRound": {"en": "Current Round", "fr": "Tour actuel"},
+ "lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
+ "startedAt": {"en": "Started At", "fr": "Démarré le"},
+ "logs": {"en": "Logs", "fr": "Journaux"},
+ "messages": {"en": "Messages", "fr": "Messages"},
+ "stats": {"en": "Statistics", "fr": "Statistiques"},
+ "tasks": {"en": "Tasks", "fr": "Tâches"}
}
+)
# AGENT AND TASK MODELS
-class Agent(BaseModelWithUI):
+class Agent(BaseModel, ModelMixin):
"""Data model for an agent"""
- id: str = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent")
capabilities: List[str] = Field(default_factory=list, description="List of agent capabilities")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics")
-class AgentResponse(BaseModelWithUI):
+# Register labels for Agent
+register_model_labels(
+ "Agent",
+ {"en": "Agent", "fr": "Agent"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "name": {"en": "Name", "fr": "Nom"},
+ "description": {"en": "Description", "fr": "Description"},
+ "capabilities": {"en": "Capabilities", "fr": "Capacités"},
+ "performance": {"en": "Performance", "fr": "Performance"}
+ }
+)
+
+class AgentResponse(BaseModel, ModelMixin):
"""Data model for an agent response"""
success: bool = Field(description="Whether the agent execution was successful")
message: ChatMessage = Field(description="Response message from the agent")
performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics")
progress: float = Field(description="Task progress (0-100)")
-class TaskPlan(BaseModelWithUI):
+# Register labels for AgentResponse
+register_model_labels(
+ "AgentResponse",
+ {"en": "Agent Response", "fr": "Réponse de l'agent"},
+ {
+ "success": {"en": "Success", "fr": "Succès"},
+ "message": {"en": "Message", "fr": "Message"},
+ "performance": {"en": "Performance", "fr": "Performance"},
+ "progress": {"en": "Progress", "fr": "Progression"}
+ }
+)
+
+class TaskPlan(BaseModel, ModelMixin):
"""Data model for a task plan"""
fileList: List[str] = Field(default_factory=list, description="List of files")
tasks: List[Task] = Field(default_factory=list, description="List of tasks in the plan")
userLanguage: str = Field(description="User's preferred language")
userResponse: str = Field(description="User's response or feedback")
-class UserInputRequest(BaseModelWithUI):
+# Register labels for TaskPlan
+register_model_labels(
+ "TaskPlan",
+ {"en": "Task Plan", "fr": "Plan de tâches"},
+ {
+ "fileList": {"en": "File List", "fr": "Liste de fichiers"},
+ "tasks": {"en": "Tasks", "fr": "Tâches"},
+ "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
+ "userResponse": {"en": "User Response", "fr": "Réponse de l'utilisateur"}
+ }
+)
+
+class UserInputRequest(BaseModel, ModelMixin):
"""Data model for a user input request"""
prompt: str = Field(description="Prompt for the user")
listFileId: List[int] = Field(default_factory=list, description="List of file IDs")
userLanguage: str = Field(default="en", description="User's preferred language")
-class AgentProfile(BaseModel):
+# Register labels for UserInputRequest
+register_model_labels(
+ "UserInputRequest",
+ {"en": "User Input Request", "fr": "Demande de saisie utilisateur"},
+ {
+ "prompt": {"en": "Prompt", "fr": "Invite"},
+ "listFileId": {"en": "File IDs", "fr": "IDs des fichiers"},
+ "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"}
+ }
+)
+
+class AgentProfile(BaseModel, ModelMixin):
"""Model for agent profile information."""
id: str
name: str
@@ -155,4 +304,19 @@ class AgentProfile(BaseModel):
capabilities: List[str] = Field(default_factory=list)
isAvailable: bool = True
lastActive: Optional[datetime] = None
- stats: Optional[Dict[str, Any]] = None
\ No newline at end of file
+ stats: Optional[Dict[str, Any]] = None
+
+# Register labels for AgentProfile
+register_model_labels(
+ "AgentProfile",
+ {"en": "Agent Profile", "fr": "Profil de l'agent"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "name": {"en": "Name", "fr": "Nom"},
+ "description": {"en": "Description", "fr": "Description"},
+ "capabilities": {"en": "Capabilities", "fr": "Capacités"},
+ "isAvailable": {"en": "Available", "fr": "Disponible"},
+ "lastActive": {"en": "Last Active", "fr": "Dernière activité"},
+ "stats": {"en": "Statistics", "fr": "Statistiques"}
+ }
+)
\ No newline at end of file
diff --git a/modules/interfaces/serviceManagementAccess.py b/modules/interfaces/serviceManagementAccess.py
index 7ae713dc..e781ded1 100644
--- a/modules/interfaces/serviceManagementAccess.py
+++ b/modules/interfaces/serviceManagementAccess.py
@@ -24,6 +24,25 @@ class ManagementAccess:
self.privilege = currentUser.privilege
self.db = db
+ def canModifyAttribute(self, table: str, attribute: str) -> bool:
+ """
+ Checks if the current user can modify a specific attribute in a table.
+
+ Args:
+ table: Name of the table
+ attribute: Name of the attribute
+
+ Returns:
+ Boolean indicating permission
+ """
+ userPrivilege = self.privilege
+
+ # Special case for mandateId in prompts table
+ if table == "prompts" and attribute == "mandateId":
+ return userPrivilege == "sysadmin"
+
+ return True
+
def uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Unified user access management function that filters data based on user privileges
@@ -37,7 +56,7 @@ class ManagementAccess:
Filtered recordset with access control attributes
"""
userPrivilege = self.privilege
- logger.debug(f"User privilege: {userPrivilege}, username: {self.currentUser.username}, email: {self.currentUser.email}")
+
filtered_records = []
# Apply filtering based on privilege
@@ -45,7 +64,7 @@ class ManagementAccess:
filtered_records = recordset # System admins see all records
elif userPrivilege == "admin":
# Admins see records in their mandate
- filtered_records = [r for r in recordset if r.get("mandateId","-") == self.mandateId]
+ filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
else: # Regular users
# For prompts, users can see all prompts from their mandate
if table == "prompts":
@@ -53,7 +72,7 @@ class ManagementAccess:
else:
# Users see only their records for other tables
filtered_records = [r for r in recordset
- if r.get("mandateId","-") == self.mandateId and r.get("_createdBy") == self.userId]
+ if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId]
# Add access control attributes to each record
for record in filtered_records:
@@ -64,6 +83,10 @@ class ManagementAccess:
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("prompts", record_id)
record["_hideDelete"] = not self.canModify("prompts", record_id)
+
+ # Add attribute-level permissions for mandateId
+ if "mandateId" in record:
+ record["_hideEdit_mandateId"] = not self.canModifyAttribute("prompts", "mandateId")
elif table == "files":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("files", record_id)
diff --git a/modules/interfaces/serviceManagementClass.py b/modules/interfaces/serviceManagementClass.py
index d1801626..27663604 100644
--- a/modules/interfaces/serviceManagementClass.py
+++ b/modules/interfaces/serviceManagementClass.py
@@ -58,18 +58,18 @@ class ServiceManagement:
def __init__(self):
"""Initializes the Management Interface."""
+ # Initialize variables first
+ self.currentUser: Optional[User] = None
+ self.userId: Optional[str] = None
+ self.access: Optional[ManagementAccess] = None # Will be set when user context is provided
+ self.aiService: Optional[ChatService] = None # Will be set when user context is provided
+
# Initialize database
self._initializeDatabase()
# Initialize standard records if needed
self._initRecords()
- # Initialize variables
- self.currentUser: Optional[User] = None
- self.userId: Optional[str] = None
- self.access: Optional[ManagementAccess] = None # Will be set when user context is provided
- self.aiService: Optional[ChatService] = None # Will be set when user context is provided
-
def setUserContext(self, currentUser: User):
"""Sets the user context for the interface."""
if not currentUser:
@@ -91,6 +91,9 @@ class ServiceManagement:
# Initialize AI service
self.aiService = ChatService()
+ # Update database context
+ self.db.updateContext(self.userId)
+
logger.debug(f"User context set: userId={self.userId}")
def _initializeDatabase(self):
@@ -128,54 +131,112 @@ class ServiceManagement:
logger.info("Standard records initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize standard records: {str(e)}")
- raise
+ # Don't raise the error, just log it
+ # This allows the interface to be created even if initialization fails
def _initializeStandardPrompts(self):
- """Creates standard prompts if they don't exist."""
- prompts = self.db.getRecordset("prompts")
- logger.debug(f"Found {len(prompts)} existing prompts")
-
- if not prompts:
- logger.debug("Creating standard prompts")
+ """Initializes standard prompts if they don't exist yet."""
+ try:
+ # Check if any prompts exist
+ existingPrompts = self.db.getRecordset("prompts")
+ if existingPrompts:
+ logger.info("Prompts already exist, skipping initialization")
+ return
+
+ # Get the root interface to access the initial mandate ID
+ from modules.interfaces.serviceAppClass import getRootInterface
+ rootInterface = getRootInterface()
+ # Get initial mandate ID through the root interface
+ mandateId = rootInterface.getInitialId("mandates")
+ if not mandateId:
+ logger.error("No initial mandate ID found")
+ return
+
+ # Get root user for initialization
+ rootUser = rootInterface.getUserByUsername("admin")
+ if not rootUser:
+ logger.error("Root user not found for initialization")
+ return
+
+ # Store current user context if it exists
+ currentUser = self.currentUser
+
+ # Set user context to root user for initialization
+ self.setUserContext(rootUser)
+
# Define standard prompts
standardPrompts = [
- {
- "content": "Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.",
- "name": "Web Research: Market Research"
- },
- {
- "content": "Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.",
- "name": "Analysis: Data Analysis"
- },
- {
- "content": "Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.",
- "name": "Protocol: Meeting Minutes"
- },
- {
- "content": "Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.",
- "name": "Design: UI/UX Design"
- },
- {
- "content": "Gib mir die ersten 1000 Primzahlen",
- "name": "Code: Primzahlen"
- },
- {
- "content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
- "name": "Mail: Vorbereitung"
- },
+ Prompt(
+ name="Market Research",
+ content="Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.",
+ mandateId=mandateId
+ ),
+ Prompt(
+ name="Data Analysis",
+ content="Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.",
+ mandateId=mandateId
+ ),
+ Prompt(
+ name="Meeting Protocol",
+ content="Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.",
+ mandateId=mandateId
+ ),
+ Prompt(
+ name="UI/UX Design",
+ content="Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.",
+ mandateId=mandateId
+ ),
+ Prompt(
+ name="Primzahlen",
+ content="Gib mir die ersten 1000 Primzahlen.",
+ mandateId=mandateId
+ ),
+ Prompt(
+ name="E-Mail",
+ content="Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
+ mandateId=mandateId
+ )
]
# Create prompts
- for promptData in standardPrompts:
- createdPrompt = self.db.recordCreate("prompts", promptData)
- logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('mandateId')}, user={createdPrompt.get('_createdBy')}")
- else:
- logger.debug("Prompts already exist, skipping creation")
+ for prompt in standardPrompts:
+ self.db.recordCreate("prompts", prompt.to_dict())
+ logger.info(f"Created standard prompt: {prompt.name}")
+
+ # Restore original user context if it existed
+ if currentUser:
+ self.setUserContext(currentUser)
+ else:
+ self.currentUser = None
+ self.userId = None
+ self.access = None
+ self.db.updateContext("") # Reset database context
+
+ except Exception as e:
+ logger.error(f"Error initializing standard prompts: {str(e)}")
+ # Ensure we restore user context even if there's an error
+ if 'currentUser' in locals() and currentUser:
+ self.setUserContext(currentUser)
+ else:
+ self.currentUser = None
+ self.userId = None
+ self.access = None
+ self.db.updateContext("") # Reset database context
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
- return self.access.uam(table, recordset)
+ # First apply access control
+ filteredRecords = self.access.uam(table, recordset)
+
+ # Then filter out database-specific fields
+ cleanedRecords = []
+ for record in filteredRecords:
+ # Create a new dict with only non-database fields
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
+ cleanedRecords.append(cleanedRecord)
+
+ return cleanedRecords
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
@@ -236,9 +297,16 @@ class ServiceManagement:
def getAllPrompts(self) -> List[Prompt]:
"""Returns prompts based on user access level."""
- allPrompts = self.db.getRecordset("prompts")
- filteredPrompts = self._uam("prompts", allPrompts)
- return [Prompt.from_dict(prompt) for prompt in filteredPrompts]
+ try:
+ allPrompts = self.db.getRecordset("prompts")
+ filteredPrompts = self._uam("prompts", allPrompts)
+
+ # Convert to Prompt objects
+ return [Prompt.from_dict(prompt) for prompt in filteredPrompts]
+
+ except Exception as e:
+ logger.error(f"Error getting prompts: {str(e)}")
+ return []
def getPrompt(self, promptId: str) -> Optional[Prompt]:
"""Returns a prompt by ID if user has access."""
@@ -269,20 +337,15 @@ class ServiceManagement:
if not prompt:
raise ValueError(f"Prompt {promptId} not found")
- # Update prompt data using model
- updatedData = prompt.to_dict()
- updatedData.update(updateData)
- updatedPrompt = Prompt.from_dict(updatedData)
-
- # Update prompt record
- self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
+ # Update prompt record directly with the update data
+ self.db.recordModify("prompts", promptId, updateData)
# Get updated prompt
updatedPrompt = self.getPrompt(promptId)
if not updatedPrompt:
raise ValueError("Failed to retrieve updated prompt")
- return updatedPrompt
+ return updatedPrompt.to_dict()
except Exception as e:
logger.error(f"Error updating prompt: {str(e)}")
diff --git a/modules/interfaces/serviceManagementModel.py b/modules/interfaces/serviceManagementModel.py
index 2599e700..1a7195a8 100644
--- a/modules/interfaces/serviceManagementModel.py
+++ b/modules/interfaces/serviceManagementModel.py
@@ -8,68 +8,69 @@ from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
-from modules.shared.attributeUtils import Label, BaseModelWithUI
+# Import for label registration
+from modules.shared.attributeUtils import register_model_labels, ModelMixin
# CORE MODELS
-class FileItem(BaseModelWithUI):
+class FileItem(BaseModel, ModelMixin):
"""Data model for a file item"""
- id: int = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this file belongs to")
filename: str = Field(description="Name of the file")
mimeType: str = Field(description="MIME type of the file")
workflowId: Optional[str] = Field(None, description="Foreign key to workflow")
fileHash: str = Field(description="Hash of the file")
fileSize: int = Field(description="Size of the file in bytes")
-
- label: Label = Field(
- default=Label(default="File Item", translations={"en": "File Item", "fr": "Élément de fichier"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
- "filename": Label(default="Filename", translations={"en": "Filename", "fr": "Nom de fichier"}),
- "mimeType": Label(default="MIME Type", translations={"en": "MIME Type", "fr": "Type MIME"}),
- "workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du flux de travail"}),
- "fileHash": Label(default="File Hash", translations={"en": "File Hash", "fr": "Hash du fichier"}),
- "fileSize": Label(default="File Size", translations={"en": "File Size", "fr": "Taille du fichier"})
- }
-class FileData(BaseModelWithUI):
+# Register labels for FileItem
+register_model_labels(
+ "FileItem",
+ {"en": "File Item", "fr": "Élément de fichier"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
+ "filename": {"en": "Filename", "fr": "Nom de fichier"},
+ "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
+ "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
+ "fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
+ "fileSize": {"en": "File Size", "fr": "Taille du fichier"}
+ }
+)
+
+class FileData(BaseModel, ModelMixin):
"""Data model for file data"""
- id: int = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
data: str = Field(description="File data content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
-
- label: Label = Field(
- default=Label(default="File Data", translations={"en": "File Data", "fr": "Données de fichier"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "data": Label(default="Data", translations={"en": "Data", "fr": "Données"}),
- "base64Encoded": Label(default="Base64 Encoded", translations={"en": "Base64 Encoded", "fr": "Encodé en Base64"})
- }
-class Prompt(BaseModelWithUI):
+# Register labels for FileData
+register_model_labels(
+ "FileData",
+ {"en": "File Data", "fr": "Données de fichier"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "data": {"en": "Data", "fr": "Données"},
+ "base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"}
+ }
+)
+
+class Prompt(BaseModel, ModelMixin):
"""Data model for a prompt"""
- id: int = Field(description="Primary key")
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Name of the prompt")
-
- label: Label = Field(
- default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
- description="Label for the class"
- )
-
- fieldLabels: Dict[str, Label] = {
- "id": Label(default="ID", translations={}),
- "mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
- "content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
- "name": Label(default="Name", translations={"en": "Name", "fr": "Nom"})
- }
+
+# Register labels for Prompt
+register_model_labels(
+ "Prompt",
+ {"en": "Prompt", "fr": "Invite"},
+ {
+ "id": {"en": "ID", "fr": "ID"},
+ "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
+ "content": {"en": "Content", "fr": "Contenu"},
+ "name": {"en": "Name", "fr": "Nom"}
+ }
+)
diff --git a/modules/routes/routeAttributes.py b/modules/routes/routeAttributes.py
index 53082129..0f9fec0f 100644
--- a/modules/routes/routeAttributes.py
+++ b/modules/routes/routeAttributes.py
@@ -11,35 +11,12 @@ import logging
from modules.security.auth import limiter, getCurrentUser
# Import the attribute definition and helper functions
-from modules.interfaces.serviceAppModel import AttributeDefinition, User
-from modules.shared.attributeUtils import getModelClasses
+from modules.interfaces.serviceAppModel import User
+from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Configure logger
logger = logging.getLogger(__name__)
-# Create a response model for better documentation
-class AttributeResponse(BaseModel):
- """Response model for entity attributes"""
- attributes: List[AttributeDefinition]
-
- class Config:
- schema_extra = {
- "example": {
- "attributes": [
- {
- "name": "username",
- "label": "Username",
- "type": "string",
- "required": True,
- "placeholder": "Please enter username",
- "editable": True,
- "visible": True,
- "order": 0
- }
- ]
- }
- }
-
# Create a router for the attribute endpoints
router = APIRouter(
prefix="/api/attributes",
@@ -75,11 +52,11 @@ async def get_entity_attributes(
# Get model class and derive attributes from it
modelClass = modelClasses[entityType]
- attributes = modelClass.getModelAttributeDefinitions()
+ attribute_defs = getModelAttributeDefinitions(modelClass)
# Convert dictionary attributes to AttributeDefinition objects
attribute_definitions = []
- for attr in attributes:
+ for attr in attribute_defs["attributes"]:
if isinstance(attr, dict) and attr.get('visible', True):
attribute_definitions.append(AttributeDefinition(**attr))
elif hasattr(attr, 'visible') and attr.visible:
diff --git a/modules/routes/routeDataConnections.py b/modules/routes/routeDataConnections.py
new file mode 100644
index 00000000..22c4da9a
--- /dev/null
+++ b/modules/routes/routeDataConnections.py
@@ -0,0 +1,164 @@
+"""
+Connection routes for the backend API.
+Implements the endpoints for connection management.
+"""
+
+from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
+from typing import List, Dict, Any, Optional
+from fastapi import status
+from datetime import datetime
+import logging
+
+from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus
+from modules.security.auth import getCurrentUser, limiter
+from modules.interfaces.serviceAppClass import getInterface, getRootInterface
+
+# Configure logger
+logger = logging.getLogger(__name__)
+
+router = APIRouter(
+ prefix="/api/connections",
+ tags=["Manage Connections"],
+ responses={404: {"description": "Not found"}}
+)
+
+@router.get("/", response_model=List[UserConnection])
+@limiter.limit("30/minute")
+async def get_connections(
+ request: Request,
+ currentUser: User = Depends(getCurrentUser)
+) -> List[UserConnection]:
+ """Get all connections for the current user or all connections if admin"""
+ try:
+ interface = getInterface(currentUser)
+ if currentUser.privilege in ['admin', 'sysadmin']:
+ # Admins can see all connections
+ users = interface.getAllUsers()
+ connections = []
+ for user in users:
+ connections.extend(user.connections)
+ return connections
+ else:
+ # Regular users can only see their own connections
+ return currentUser.connections
+ except Exception as e:
+ logger.error(f"Error getting connections: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Failed to get connections: {str(e)}"
+ )
+
+@router.post("/{connectionId}/connect")
+@limiter.limit("10/minute")
+async def connect_service(
+ request: Request,
+ connectionId: str = Path(..., description="The ID of the connection to connect"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """Connect to an external service"""
+ try:
+ interface = getInterface(currentUser)
+
+ # Find the connection
+ connection = None
+ if currentUser.privilege in ['admin', 'sysadmin']:
+ # Admins can connect any connection
+ users = interface.getAllUsers()
+ for user in users:
+ for conn in user.connections:
+ if conn.id == connectionId:
+ connection = conn
+ break
+ if connection:
+ break
+ else:
+ # Regular users can only connect their own connections
+ for conn in currentUser.connections:
+ if conn.id == connectionId:
+ connection = conn
+ break
+
+ if not connection:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Connection not found"
+ )
+
+ # Initiate OAuth flow with state=connect
+ auth_url = None
+ if connection.authority == AuthAuthority.MSFT:
+ auth_url = f"/api/msft/login?state=connect&connectionId={connectionId}"
+ elif connection.authority == AuthAuthority.GOOGLE:
+ auth_url = f"/api/google/login?state=connect&connectionId={connectionId}"
+ else:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"Unsupported authority: {connection.authority}"
+ )
+
+ return {"authUrl": auth_url}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error connecting service: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Failed to connect service: {str(e)}"
+ )
+
+@router.post("/{connectionId}/disconnect")
+@limiter.limit("10/minute")
+async def disconnect_service(
+ request: Request,
+ connectionId: str = Path(..., description="The ID of the connection to disconnect"),
+ currentUser: User = Depends(getCurrentUser)
+) -> Dict[str, Any]:
+ """Disconnect from an external service"""
+ try:
+ interface = getInterface(currentUser)
+
+ # Find the connection
+ connection = None
+ if currentUser.privilege in ['admin', 'sysadmin']:
+ # Admins can disconnect any connection
+ users = interface.getAllUsers()
+ for user in users:
+ for conn in user.connections:
+ if conn.id == connectionId:
+ connection = conn
+ break
+ if connection:
+ break
+ else:
+ # Regular users can only disconnect their own connections
+ for conn in currentUser.connections:
+ if conn.id == connectionId:
+ connection = conn
+ break
+
+ if not connection:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Connection not found"
+ )
+
+ # Update connection status
+ connection.status = ConnectionStatus.INACTIVE
+ connection.lastChecked = datetime.now()
+
+ # Update user record
+ interface.db.recordModify("users", connection.userId, {
+ "connections": [c.to_dict() for c in currentUser.connections]
+ })
+
+ return {"message": "Service disconnected successfully"}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error disconnecting service: {str(e)}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Failed to disconnect service: {str(e)}"
+ )
\ No newline at end of file
diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py
index 651c92c7..43bc7108 100644
--- a/modules/routes/routeDataFiles.py
+++ b/modules/routes/routeDataFiles.py
@@ -5,6 +5,10 @@ import logging
from datetime import datetime, timezone
from dataclasses import dataclass
import io
+import inspect
+import importlib
+import os
+from pydantic import BaseModel
# Import auth module
from modules.security.auth import limiter, getCurrentUser
@@ -12,8 +16,8 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import FileItem
-from modules.shared.attributeUtils import getModelAttributeDefinitions
-from modules.interfaces.serviceAppModel import AttributeDefinition, User
+from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
+from modules.interfaces.serviceAppModel import User
# Configure logger
logger = logging.getLogger(__name__)
@@ -199,49 +203,32 @@ async def update_file(
detail=str(e)
)
-@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
+@router.delete("/{fileId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_file(
request: Request,
- fileId: str,
+ fileId: str = Path(..., description="ID of the file to delete"),
currentUser: User = Depends(getCurrentUser)
-) -> JSONResponse:
+) -> Dict[str, Any]:
"""Delete a file"""
- try:
- managementInterface = serviceManagementClass.getInterface(currentUser)
-
- # Delete file via LucyDOM interface
- managementInterface.deleteFile(fileId)
-
- # Return successful deletion without content (204 No Content)
- return JSONResponse({
- "message": "File deleted successfully"
- })
-
- except serviceManagementClass.FileNotFoundError as e:
- logger.warning(f"File not found: {str(e)}")
+ managementInterface = serviceManagementClass.getInterface(currentUser)
+
+ # Check if the file exists
+ existingFile = managementInterface.getFile(fileId)
+ if not existingFile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail=str(e)
+ detail=f"File with ID {fileId} not found"
)
- except serviceManagementClass.FilePermissionError as e:
- logger.warning(f"No permission to delete file: {str(e)}")
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail=str(e)
- )
- except serviceManagementClass.FileDeletionError as e:
- logger.error(f"Error deleting file: {str(e)}")
+
+ success = managementInterface.deleteFile(fileId)
+ if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
- except Exception as e:
- logger.error(f"Unexpected error deleting file: {str(e)}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Error deleting file: {str(e)}"
+ detail="Error deleting the file"
)
+
+ return {"message": f"File with ID {fileId} successfully deleted"}
@router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute")
@@ -281,18 +268,3 @@ async def get_file_stats(
detail=f"Error retrieving file statistics: {str(e)}"
)
-@router.get("/attributes", response_model=List[AttributeDefinition])
-@limiter.limit("30/minute")
-async def get_file_attributes(
- request: Request,
- currentUser: User = Depends(getCurrentUser)
-) -> List[AttributeDefinition]:
- """
- Retrieves the attribute definitions for files.
- This can be used for dynamic form generation.
-
- Returns:
- - A list of attribute definitions that can be used to generate forms
- """
- # Get attributes from the FileItem model class
- return FileItem.getModelAttributeDefinitions()
\ No newline at end of file
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index 1cd20547..f5e708d7 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -6,17 +6,22 @@ Implements the endpoints for mandate management.
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
from typing import List, Dict, Any, Optional
from fastapi import status
+from datetime import datetime
import logging
+import inspect
+import importlib
+import os
+from pydantic import BaseModel
# Import auth module
from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
-from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Import the model classes
-from modules.interfaces.serviceAppModel import AttributeDefinition, Mandate, User
+from modules.interfaces.serviceAppModel import Mandate, User
# Configure logger
logger = logging.getLogger(__name__)
@@ -189,19 +194,3 @@ async def delete_mandate(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete mandate: {str(e)}"
)
-
-@router.get("/attributes", response_model=List[AttributeDefinition])
-@limiter.limit("30/minute")
-async def get_mandate_attributes(
- request: Request,
- currentUser: User = Depends(getCurrentUser)
-) -> List[AttributeDefinition]:
- """
- Retrieves the attribute definitions for mandates.
- This can be used for dynamic form generation.
-
- Returns:
- - A list of attribute definitions that can be used to generate forms
- """
- # Get attributes from the Mandate model class
- return Mandate.getModelAttributeDefinitions()
\ No newline at end of file
diff --git a/modules/routes/routeDataPrompts.py b/modules/routes/routeDataPrompts.py
index 72ecfea8..d816b245 100644
--- a/modules/routes/routeDataPrompts.py
+++ b/modules/routes/routeDataPrompts.py
@@ -1,8 +1,12 @@
-from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path, Request
+from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path, Request, Response
from typing import List, Dict, Any, Optional
from fastapi import status
from datetime import datetime
import logging
+import inspect
+import importlib
+import os
+from pydantic import BaseModel
# Import auth module
from modules.security.auth import limiter, getCurrentUser
@@ -10,7 +14,8 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import Prompt
-from modules.interfaces.serviceAppModel import AttributeDefinition, User
+from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
+from modules.interfaces.serviceAppModel import User
# Configure logger
logger = logging.getLogger(__name__)
@@ -31,7 +36,7 @@ async def get_prompts(
"""Get all prompts"""
managementInterface = serviceManagementClass.getInterface(currentUser)
prompts = managementInterface.getAllPrompts()
- return [Prompt.from_dict(prompt) for prompt in prompts]
+ return prompts
@router.post("", response_model=Prompt)
@limiter.limit("10/minute")
@@ -44,13 +49,14 @@ async def create_prompt(
managementInterface = serviceManagementClass.getInterface(currentUser)
# Convert Prompt to dict for interface
- prompt_data = prompt.to_dict()
+ prompt_data = prompt.dict()
# Create prompt
newPrompt = managementInterface.createPrompt(prompt_data)
# Set current time for createdAt if it exists in the model
- if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"):
+ promptAttributes = getModelAttributeDefinitions(Prompt)
+ if "createdAt" in promptAttributes["attributes"] and hasattr(newPrompt, "createdAt"):
newPrompt["createdAt"] = datetime.now().isoformat()
return Prompt.from_dict(newPrompt)
@@ -95,7 +101,7 @@ async def update_prompt(
)
# Convert Prompt to dict for interface
- update_data = promptData.to_dict()
+ update_data = promptData.dict()
# Update prompt
updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
@@ -133,20 +139,4 @@ async def delete_prompt(
detail="Error deleting the prompt"
)
- return {"message": f"Prompt with ID {promptId} successfully deleted"}
-
-@router.get("/attributes", response_model=List[AttributeDefinition])
-@limiter.limit("30/minute")
-async def get_prompt_attributes(
- request: Request,
- currentUser: User = Depends(getCurrentUser)
-) -> List[AttributeDefinition]:
- """
- Retrieves the attribute definitions for prompts.
- This can be used for dynamic form generation.
-
- Returns:
- - A list of attribute definitions that can be used to generate forms
- """
- # Get attributes from the Prompt model class
- return Prompt.getModelAttributeDefinitions()
\ No newline at end of file
+ return {"message": f"Prompt with ID {promptId} successfully deleted"}
\ No newline at end of file
diff --git a/modules/routes/routeDataUsers.py b/modules/routes/routeDataUsers.py
index 89aa0b05..39f83f80 100644
--- a/modules/routes/routeDataUsers.py
+++ b/modules/routes/routeDataUsers.py
@@ -18,8 +18,8 @@ import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions
-from modules.interfaces.serviceAppModel import User, AttributeDefinition as ServiceAppAttributeDefinition
-from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.interfaces.serviceAppModel import User, AttributeDefinition
+from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
# Configure logger
logger = logging.getLogger(__name__)
@@ -93,7 +93,8 @@ async def create_user(
newUser = managementInterface.createUser(user_data)
# Set current time for createdAt if it exists in the model
- if "createdAt" in User.getModelAttributeDefinitions() and hasattr(newUser, "createdAt"):
+ userAttributes = getModelAttributeDefinitions(User)
+ if "createdAt" in userAttributes["attributes"] and hasattr(newUser, "createdAt"):
newUser["createdAt"] = datetime.now().isoformat()
return User.from_dict(newUser)
@@ -139,34 +140,22 @@ async def delete_user(
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a user"""
- try:
- appInterface = serviceManagementClass.getInterface(currentUser)
- appInterface.deleteUser(userId)
- return {"message": f"User {userId} deleted successfully"}
- except ValueError as e:
+ appInterface = serviceManagementClass.getInterface(currentUser)
+
+ # Check if the user exists
+ existingUser = appInterface.getUser(userId)
+ if not existingUser:
raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail=str(e)
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"User with ID {userId} not found"
)
- except Exception as e:
- logger.error(f"Error deleting user {userId}: {str(e)}")
+
+ success = appInterface.deleteUser(userId)
+ if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to delete user: {str(e)}"
+ detail="Error deleting the user"
)
-
-@router.get("/attributes", response_model=List[ServiceAppAttributeDefinition])
-@limiter.limit("30/minute")
-async def get_user_attributes(
- request: Request,
- currentUser: User = Depends(getCurrentUser)
-) -> List[ServiceAppAttributeDefinition]:
- """
- Retrieves the attribute definitions for users.
- This can be used for dynamic form generation.
- Returns:
- - A list of attribute definitions that can be used to generate forms
- """
- # Get attributes from the User model class
- return User.getModelAttributeDefinitions()
+ return {"message": f"User with ID {userId} successfully deleted"}
+
diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py
index 1a73c7b5..4a5964ae 100644
--- a/modules/routes/routeSecurityGoogle.py
+++ b/modules/routes/routeSecurityGoogle.py
@@ -2,7 +2,7 @@
Routes for Google authentication.
"""
-from fastapi import APIRouter, HTTPException, Request, Response, status, Depends
+from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body, Query
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
@@ -11,12 +11,13 @@ from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request as GoogleRequest
+from googleapiclient.discovery import build
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
-from modules.interfaces.serviceAppModel import AuthAuthority, User
-from modules.interfaces.serviceAppTokens import GoogleToken
+from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
from modules.security.auth import getCurrentUser, limiter
+from modules.shared.attributeUtils import ModelMixin
# Configure logger
logger = logging.getLogger(__name__)
@@ -46,7 +47,11 @@ SCOPES = [
@router.get("/login")
@limiter.limit("5/minute")
-async def login(request: Request) -> RedirectResponse:
+async def login(
+ request: Request,
+ state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
+ connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
+) -> RedirectResponse:
"""Initiate Google login"""
try:
# Create OAuth flow
@@ -63,10 +68,14 @@ async def login(request: Request) -> RedirectResponse:
scopes=SCOPES
)
- # Generate auth URL
+ # Generate auth URL with state
auth_url, _ = flow.authorization_url(
access_type="offline",
- include_granted_scopes="true"
+ include_granted_scopes="true",
+ state=json.dumps({
+ "type": state,
+ "connectionId": connectionId
+ })
)
return RedirectResponse(auth_url)
@@ -79,9 +88,14 @@ async def login(request: Request) -> RedirectResponse:
)
@router.get("/auth/callback")
-async def auth_callback(code: str, request: Request) -> HTMLResponse:
+async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse:
"""Handle Google OAuth callback"""
try:
+ # Parse state
+ state_data = json.loads(state)
+ state_type = state_data.get("type", "login")
+ connection_id = state_data.get("connectionId")
+
# Create OAuth flow
flow = Flow.from_client_config(
{
@@ -93,46 +107,127 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
"redirect_uris": [REDIRECT_URI]
}
},
- scopes=SCOPES,
- redirect_uri=REDIRECT_URI
+ scopes=SCOPES
)
- # Exchange code for token
+ # Exchange code for credentials
flow.fetch_token(code=code)
credentials = flow.credentials
- # Create token data
- token_data = {
- "access_token": credentials.token,
- "refresh_token": credentials.refresh_token,
- "token_type": credentials.token_type,
- "expires_at": credentials.expiry.timestamp()
- }
+ # Get user info
+ user_info_response = flow.oauth2session.get("https://www.googleapis.com/oauth2/v2/userinfo")
+ user_info = user_info_response.json()
- # Save token data
- appInterface = getInterface()
- appInterface.saveToken("Google", token_data)
-
- # Return success page with token data
- return HTMLResponse(
- content=f"""
-
- Authentication Successful
-
-
-
-
- """
- )
+ if state_type == "login":
+ # Handle login flow
+ rootInterface = getRootInterface()
+ user = rootInterface.getUserByUsername(user_info.get("email"))
+
+ if not user:
+ # Create new user if doesn't exist
+ user = rootInterface.createUser(
+ username=user_info.get("email"),
+ email=user_info.get("email"),
+ fullName=user_info.get("name"),
+ authenticationAuthority=AuthAuthority.GOOGLE,
+ externalId=user_info.get("id"),
+ externalUsername=user_info.get("email"),
+ externalEmail=user_info.get("email")
+ )
+
+ # Create token
+ token = Token(
+ userId=user.id,
+ authority=AuthAuthority.GOOGLE,
+ tokenAccess=credentials.token,
+ tokenRefresh=credentials.refresh_token,
+ tokenType=credentials.token_type,
+ expiresAt=credentials.expiry.timestamp() if credentials.expiry else None
+ )
+
+ # Save token
+ appInterface = getInterface(user)
+ appInterface.saveToken(token)
+
+ # Return success page with token data
+ return HTMLResponse(
+ content=f"""
+
+ Authentication Successful
+
+
+
+
+ """
+ )
+ else:
+ # Handle connection flow
+ if not connection_id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Connection ID is required for connection flow"
+ )
+
+ # Get current user from session
+ current_user = await getCurrentUser(request)
+ if not current_user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User not authenticated"
+ )
+
+ # Find and update connection
+ interface = getInterface(current_user)
+ connection = None
+ for conn in current_user.connections:
+ if conn.id == connection_id:
+ connection = conn
+ break
+
+ if not connection:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Connection not found"
+ )
+
+ # Update connection
+ connection.status = ConnectionStatus.ACTIVE
+ connection.lastChecked = datetime.now()
+ connection.expiresAt = credentials.expiry if credentials.expiry else None
+
+ # Update user record
+ interface.db.recordModify("users", current_user.id, {
+ "connections": [c.to_dict() for c in current_user.connections]
+ })
+
+ # Return success page
+ return HTMLResponse(
+ content=f"""
+
+ Connection Successful
+
+
+
+
+ """
+ )
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
@@ -154,7 +249,7 @@ async def get_current_user(
logger.error(f"Error getting current user: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to get current user: {str(e)}"
+ detail=str(e)
)
@router.post("/logout")
diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py
index 4c0dce4a..1829befa 100644
--- a/modules/routes/routeSecurityLocal.py
+++ b/modules/routes/routeSecurityLocal.py
@@ -13,8 +13,8 @@ from pydantic import BaseModel
# Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
-from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege
-from modules.interfaces.serviceAppTokens import LocalToken
+from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token
+from modules.shared.attributeUtils import ModelMixin
# Configure logger
logger = logging.getLogger(__name__)
@@ -94,19 +94,23 @@ async def login(
# Get user-specific interface for token operations
userInterface = getInterface(user)
- # Save token data
- token_data = {
- "access_token": access_token,
- "token_type": "bearer",
- "expires_at": expires_at.timestamp()
- }
- userInterface.saveToken("Local", token_data)
+ # Create token
+ token = Token(
+ userId=user.id,
+ authority=AuthAuthority.LOCAL,
+ tokenAccess=access_token,
+ tokenType="bearer",
+ expiresAt=expires_at.timestamp()
+ )
+
+ # Save token
+ userInterface.saveToken(token)
# Create response data
response_data = {
"type": "local_auth_success",
"access_token": access_token,
- "token_data": token_data
+ "token_data": token.dict()
}
return response_data
diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py
index 3776ee55..bb767ef4 100644
--- a/modules/routes/routeSecurityMsft.py
+++ b/modules/routes/routeSecurityMsft.py
@@ -2,7 +2,7 @@
Routes for Microsoft authentication.
"""
-from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body
+from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body, Query
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
@@ -12,9 +12,9 @@ import msal
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
-from modules.interfaces.serviceAppModel import AuthAuthority, User
-from modules.interfaces.serviceAppTokens import MsftToken
+from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
from modules.security.auth import getCurrentUser, limiter
+from modules.shared.attributeUtils import ModelMixin
# Configure logger
logger = logging.getLogger(__name__)
@@ -42,7 +42,11 @@ SCOPES = ["Mail.ReadWrite", "User.Read"]
@router.get("/login")
@limiter.limit("5/minute")
-async def login(request: Request) -> RedirectResponse:
+async def login(
+ request: Request,
+ state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
+ connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
+) -> RedirectResponse:
"""Initiate Microsoft login"""
try:
# Create MSAL app
@@ -52,10 +56,14 @@ async def login(request: Request) -> RedirectResponse:
client_credential=CLIENT_SECRET
)
- # Generate auth URL
+ # Generate auth URL with state
auth_url = msal_app.get_authorization_request_url(
scopes=SCOPES,
- redirect_uri=REDIRECT_URI
+ redirect_uri=REDIRECT_URI,
+ state=json.dumps({
+ "type": state,
+ "connectionId": connectionId
+ })
)
return RedirectResponse(auth_url)
@@ -68,9 +76,14 @@ async def login(request: Request) -> RedirectResponse:
)
@router.get("/auth/callback")
-async def auth_callback(code: str, request: Request) -> HTMLResponse:
+async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse:
"""Handle Microsoft OAuth callback"""
try:
+ # Parse state
+ state_data = json.loads(state)
+ state_type = state_data.get("type", "login")
+ connection_id = state_data.get("connectionId")
+
# Create MSAL app
msal_app = msal.ConfidentialClientApplication(
CLIENT_ID,
@@ -91,38 +104,124 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
status_code=400
)
- # Create token data
- token_data = {
- "access_token": token_response["access_token"],
- "refresh_token": token_response.get("refresh_token", ""),
- "token_type": token_response.get("token_type", "bearer"),
- "expires_at": datetime.now().timestamp() + token_response.get("expires_in", 0)
- }
+ # Get user info from Microsoft
+ user_info = msal_app.acquire_token_for_client(scopes=["User.Read"])
+ if "error" in user_info:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Failed to get user info from Microsoft"
+ )
- # Save token data
- appInterface = getInterface()
- appInterface.saveToken("Msft", token_data)
-
- # Return success page with token data
- return HTMLResponse(
- content=f"""
-
- Authentication Successful
-
-
-
-
- """
- )
+ if state_type == "login":
+ # Handle login flow
+ rootInterface = getRootInterface()
+ user = rootInterface.getUserByUsername(user_info.get("preferred_username"))
+
+ if not user:
+ # Create new user if doesn't exist
+ user = rootInterface.createUser(
+ username=user_info.get("preferred_username"),
+ email=user_info.get("email"),
+ fullName=user_info.get("name"),
+ authenticationAuthority=AuthAuthority.MSFT,
+ externalId=user_info.get("id"),
+ externalUsername=user_info.get("preferred_username"),
+ externalEmail=user_info.get("email")
+ )
+
+ # Create token
+ token = Token(
+ userId=user.id,
+ authority=AuthAuthority.MSFT,
+ tokenAccess=token_response["access_token"],
+ tokenRefresh=token_response.get("refresh_token", ""),
+ tokenType=token_response.get("token_type", "bearer"),
+ expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0)
+ )
+
+ # Save token
+ appInterface = getInterface(user)
+ appInterface.saveToken(token)
+
+ # Return success page with token data
+ return HTMLResponse(
+ content=f"""
+
+ Authentication Successful
+
+
+
+
+ """
+ )
+ else:
+ # Handle connection flow
+ if not connection_id:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Connection ID is required for connection flow"
+ )
+
+ # Get current user from session
+ current_user = await getCurrentUser(request)
+ if not current_user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User not authenticated"
+ )
+
+ # Find and update connection
+ interface = getInterface(current_user)
+ connection = None
+ for conn in current_user.connections:
+ if conn.id == connection_id:
+ connection = conn
+ break
+
+ if not connection:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Connection not found"
+ )
+
+ # Update connection
+ connection.status = ConnectionStatus.ACTIVE
+ connection.lastChecked = datetime.now()
+ connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
+
+ # Update user record
+ interface.db.recordModify("users", current_user.id, {
+ "connections": [c.to_dict() for c in current_user.connections]
+ })
+
+ # Return success page
+ return HTMLResponse(
+ content=f"""
+
+ Connection Successful
+
+
+
+
+ """
+ )
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
diff --git a/modules/routes/routeWorkflows.py b/modules/routes/routeWorkflows.py
index 82d6e54f..47797b68 100644
--- a/modules/routes/routeWorkflows.py
+++ b/modules/routes/routeWorkflows.py
@@ -30,7 +30,7 @@ from modules.interfaces.serviceChatModel import (
ChatDocument,
UserInputRequest
)
-from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
from modules.interfaces.serviceAppModel import User
# Configure logger
diff --git a/modules/security/auth.py b/modules/security/auth.py
index a1f31591..fb75c375 100644
--- a/modules/security/auth.py
+++ b/modules/security/auth.py
@@ -129,7 +129,11 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
# Ensure the user has the correct context
if str(user.mandateId) != str(mandateId) or str(user.id) != str(userId):
logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.mandateId}, id={user.id})")
- raise credentialsException
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User context has changed. Please log in again.",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
return user
diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py
index f63e8531..0c4bcd04 100644
--- a/modules/shared/attributeUtils.py
+++ b/modules/shared/attributeUtils.py
@@ -3,129 +3,187 @@ Shared utilities for model attributes and labels.
"""
from pydantic import BaseModel, Field
-from typing import Dict, Any, List, Type, ClassVar
+from typing import Dict, Any, List, Type, Optional, Union
import inspect
import importlib
import os
+from datetime import datetime
-class BaseModelWithUI(BaseModel):
- """Base model class with UI support and common functionality"""
-
- @classmethod
- def get_ui_schema(cls) -> Dict[str, Any]:
- """Get UI schema for frontend"""
- return {
- "fields": cls.fieldLabels if hasattr(cls, 'fieldLabels') else {},
- "validations": cls.get_validations() if hasattr(cls, 'get_validations') else {}
- }
+class ModelMixin:
+ """Mixin class that provides serialization methods for Pydantic models."""
def to_dict(self) -> Dict[str, Any]:
- """Convert to dictionary with proper validation"""
- # Handle both Pydantic v1 and v2
+ """
+ Convert a Pydantic model to a dictionary.
+ Handles both Pydantic v1 and v2.
+
+ Returns:
+ Dict[str, Any]: Dictionary representation of the model
+ """
if hasattr(self, 'model_dump'):
return self.model_dump() # Pydantic v2
return self.dict() # Pydantic v1
@classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI':
- """Create instance from dictionary with validation"""
- return cls(**data)
-
- @classmethod
- def getModelAttributeDefinitions(cls) -> List[Dict[str, Any]]:
+ def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
"""
- Get attribute definitions for this model class.
- Override this method in model classes to provide custom attribute definitions.
-
- Returns:
- List[Dict[str, Any]]: List of attribute definitions
- """
- attributes = []
-
- # Handle both Pydantic v1 and v2
- if hasattr(cls, 'model_fields'): # Pydantic v2
- fields = cls.model_fields
- for name, field in fields.items():
- attributes.append({
- "name": name,
- "type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
- "required": field.is_required() if hasattr(field, "is_required") else True,
- "description": field.description if hasattr(field, "description") else "",
- "label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
- "placeholder": f"Please enter {name}",
- "editable": True,
- "visible": True,
- "order": len(attributes)
- })
- else: # Pydantic v1
- fields = cls.__fields__
- for name, field in fields.items():
- attributes.append({
- "name": name,
- "type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
- "required": field.required,
- "description": field.field_info.description if hasattr(field.field_info, "description") else "",
- "label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
- "placeholder": f"Please enter {name}",
- "editable": True,
- "visible": True,
- "order": len(attributes)
- })
-
- return attributes
-
-def getModelAttributes(modelClass):
- """
- Get all attributes of a model class.
-
- Args:
- modelClass: The model class to get attributes from
-
- Returns:
- List[str]: List of attribute names
- """
- return [attr for attr in dir(modelClass)
- if not callable(getattr(modelClass, attr))
- and not attr.startswith('_')
- and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')]
-
-class Label(BaseModel):
- """
- Label for an attribute or a class with support for multiple languages.
-
- Attributes:
- default: Default label text
- translations: Dictionary of translations for different languages
- """
- default: str = Field(..., description="Default label text")
- translations: Dict[str, str] = Field(default_factory=dict, description="Translations for different languages")
-
- class Config:
- title = "Label"
- description = "A label with support for multiple languages"
- schema_extra = {
- "example": {
- "default": "Document",
- "translations": {
- "en": "Document",
- "fr": "Document"
- }
- }
- }
-
- def getLabel(self, language: str = None) -> str:
- """
- Returns the label in the specified language, or the default value if not available.
+ Create a Pydantic model instance from a dictionary.
Args:
- language: Language code to get the label for
+ data: Dictionary containing the model data
Returns:
- str: Label text in the specified language or default
+ ModelMixin: New instance of the model class
"""
- if language and language in self.translations:
- return self.translations[language]
- return self.default
+ return cls(**data)
+
+# Define the AttributeDefinition class here instead of importing it
+class AttributeDefinition(BaseModel, ModelMixin):
+ """Definition of a model attribute with its metadata."""
+ name: str
+ type: str
+ label: str
+ description: Optional[str] = None
+ required: bool = False
+ default: Any = None
+ options: Optional[List[Any]] = None
+ validation: Optional[Dict[str, Any]] = None
+ ui: Optional[Dict[str, Any]] = None
+
+# Global registry for model labels
+MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
+
+def to_dict(model: BaseModel) -> Dict[str, Any]:
+ """
+ Convert a Pydantic model to a dictionary.
+ Handles both Pydantic v1 and v2.
+
+ Args:
+ model: The Pydantic model instance to convert
+
+ Returns:
+ Dict[str, Any]: Dictionary representation of the model
+ """
+ if hasattr(model, 'model_dump'):
+ return model.model_dump() # Pydantic v2
+ return model.dict() # Pydantic v1
+
+def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
+ """
+ Create a Pydantic model instance from a dictionary.
+
+ Args:
+ model_class: The Pydantic model class to instantiate
+ data: Dictionary containing the model data
+
+ Returns:
+ BaseModel: New instance of the model class
+ """
+ return model_class(**data)
+
+def register_model_labels(model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]):
+ """
+ Register labels for a model's attributes and the model itself.
+
+ Args:
+ model_name: Name of the model class
+ model_label: 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[model_name] = {
+ "model": model_label,
+ "attributes": labels
+ }
+
+def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
+ """
+ Get labels for a model's attributes in the specified language.
+
+ Args:
+ model_name: Name of the model class
+ language: Language code (default: "en")
+
+ Returns:
+ Dictionary mapping attribute names to their labels in the specified language
+ """
+ model_data = MODEL_LABELS.get(model_name, {})
+ attribute_labels = model_data.get("attributes", {})
+
+ return {
+ attr: translations.get(language, translations.get("en", attr))
+ for attr, translations in attribute_labels.items()
+ }
+
+def get_model_label(model_name: str, language: str = "en") -> str:
+ """
+ Get the label for a model in the specified language.
+
+ Args:
+ model_name: Name of the model class
+ language: Language code (default: "en")
+
+ Returns:
+ Model label in the specified language, or model name if no label exists
+ """
+ model_data = MODEL_LABELS.get(model_name, {})
+ model_label = model_data.get("model", {})
+ return model_label.get(language, model_label.get("en", model_name))
+
+def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
+ """
+ Get attribute definitions for a model class.
+
+ Args:
+ modelClass: The model class to get attributes for
+ userLanguage: Language code for translations (default: "en")
+
+ Returns:
+ Dictionary containing model label and attribute definitions
+ """
+ if not modelClass:
+ return {}
+
+ attributes = []
+ model_name = modelClass.__name__
+ labels = get_model_labels(model_name, userLanguage)
+ model_label = get_model_label(model_name, userLanguage)
+
+ # Handle both Pydantic v1 and v2
+ if hasattr(modelClass, 'model_fields'): # Pydantic v2
+ fields = modelClass.model_fields
+ for name, field in fields.items():
+ attributes.append({
+ "name": name,
+ "type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
+ "required": field.is_required() if hasattr(field, "is_required") else True,
+ "description": field.description if hasattr(field, "description") else "",
+ "label": labels.get(name, name),
+ "placeholder": f"Please enter {labels.get(name, name)}",
+ "editable": True,
+ "visible": True,
+ "order": len(attributes)
+ })
+ else: # Pydantic v1
+ fields = modelClass.__fields__
+ for name, field in fields.items():
+ attributes.append({
+ "name": name,
+ "type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
+ "required": field.required,
+ "description": field.field_info.description if hasattr(field.field_info, "description") else "",
+ "label": labels.get(name, name),
+ "placeholder": f"Please enter {labels.get(name, name)}",
+ "editable": True,
+ "visible": True,
+ "order": len(attributes)
+ })
+
+ return {
+ "model": model_label,
+ "attributes": attributes
+ }
def getModelClasses() -> Dict[str, Type[BaseModel]]:
"""
@@ -155,27 +213,24 @@ def getModelClasses() -> Dict[str, Type[BaseModel]]:
return modelClasses
-def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
- """
- Get attribute definitions for model classes.
- If modelClass is provided, returns attributes for that specific class.
- If no modelClass is provided, returns attributes for all model classes.
+class AttributeResponse(BaseModel):
+ """Response model for entity attributes"""
+ attributes: List[AttributeDefinition]
- Args:
- modelClass: Optional specific model class to get attributes for
- userLanguage: Language code for translations (default: "en")
-
- Returns:
- Dict[str, Any]: Dictionary of model class names to their attribute definitions
- """
- if modelClass:
- return getModelAttributes(modelClass)
-
- # Get all model classes
- modelClasses = getModelClasses()
-
- # Create dictionary of model class names to their attribute definitions
- return {
- name: getModelAttributes(cls)
- for name, cls in modelClasses.items()
- }
\ No newline at end of file
+ class Config:
+ schema_extra = {
+ "example": {
+ "attributes": [
+ {
+ "name": "username",
+ "label": "Username",
+ "type": "string",
+ "required": True,
+ "placeholder": "Please enter username",
+ "editable": True,
+ "visible": True,
+ "order": 0
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/modules/workflow/documentManager.py b/modules/workflow/documentManager.py
index 484daf5e..2a6dc8c1 100644
--- a/modules/workflow/documentManager.py
+++ b/modules/workflow/documentManager.py
@@ -7,6 +7,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime
from modules.interfaces.serviceChatModel import ChatDocument, ChatContent
from modules.workflow.documentProcessor import getDocumentContents
+import uuid
logger = logging.getLogger(__name__)
@@ -46,43 +47,33 @@ class DocumentManager:
self.service = service
return True
- async def extractContent(self, fileId: int) -> Optional[ChatDocument]:
- """
- Extract content from a file.
-
- Args:
- fileId: ID of the file to process
-
- Returns:
- ChatDocument object with extracted content or None if processing failed
- """
+ async def extractContent(self, fileId: str) -> Optional[ChatDocument]:
+ """Extracts content from a file and creates a chat document."""
try:
- # Get file metadata and content from service
- fileMetadata = await self.service.base.getFileMetadata(fileId)
- fileContent = await self.service.base.getFileContent(fileId)
-
- if not fileMetadata or not fileContent:
- logger.error(f"Could not retrieve file data for fileId {fileId}")
+ # Get file content
+ fileContent = await self.getFileContent(fileId)
+ if not fileContent:
return None
-
- # Extract content using documentProcessor
- contents = getDocumentContents(fileMetadata, fileContent)
-
- # Create ChatDocument
+
+ # Get file metadata
+ fileMetadata = await self.getFileMetadata(fileId)
+ if not fileMetadata:
+ return None
+
+ # Create chat document
return ChatDocument(
- id=str(fileId), # Using fileId as document id
+ id=str(uuid.uuid4()),
fileId=fileId,
- filename=fileMetadata.get("name", "unknown"),
+ filename=fileMetadata.get("name", "Unknown"),
fileSize=fileMetadata.get("size", 0),
- mimeType=fileMetadata.get("mimeType", "application/octet-stream"),
- contents=contents
+ content=fileContent.decode('utf-8', errors='ignore'),
+ mimeType=fileMetadata.get("mimeType", "text/plain")
)
-
except Exception as e:
- logger.error(f"Error extracting content from file {fileId}: {str(e)}", exc_info=True)
+ logger.error(f"Error extracting content from file {fileId}: {str(e)}")
return None
- async def processFileIds(self, fileIds: List[int]) -> List[ChatDocument]:
+ async def processFileIds(self, fileIds: List[str]) -> List[ChatDocument]:
"""
Process multiple files and extract their contents.
@@ -103,34 +94,18 @@ class DocumentManager:
continue
return documents
- async def getFileContent(self, fileId: int) -> Optional[bytes]:
- """
- Get raw file content.
-
- Args:
- fileId: ID of the file
-
- Returns:
- File content as bytes or None if not found
- """
+ async def getFileContent(self, fileId: str) -> Optional[bytes]:
+ """Gets the content of a file."""
try:
- return await self.service.base.getFileContent(fileId)
+ return self.service.functions.getFileData(fileId)
except Exception as e:
logger.error(f"Error getting file content for {fileId}: {str(e)}")
return None
- async def getFileMetadata(self, fileId: int) -> Optional[Dict[str, Any]]:
- """
- Get file metadata.
-
- Args:
- fileId: ID of the file
-
- Returns:
- File metadata dictionary or None if not found
- """
+ async def getFileMetadata(self, fileId: str) -> Optional[Dict[str, Any]]:
+ """Gets the metadata of a file."""
try:
- return await self.service.base.getFileMetadata(fileId)
+ return self.service.functions.getFile(fileId)
except Exception as e:
logger.error(f"Error getting file metadata for {fileId}: {str(e)}")
return None
@@ -153,18 +128,10 @@ class DocumentManager:
logger.error(f"Error saving file {filename}: {str(e)}")
return None
- async def deleteFile(self, fileId: int) -> bool:
- """
- Delete a file.
-
- Args:
- fileId: ID of the file to delete
-
- Returns:
- True if successful, False otherwise
- """
+ async def deleteFile(self, fileId: str) -> bool:
+ """Deletes a file."""
try:
- return await self.service.base.deleteFile(fileId)
+ return self.service.functions.deleteFile(fileId)
except Exception as e:
logger.error(f"Error deleting file {fileId}: {str(e)}")
return False
diff --git a/modules/workflow/workflowManager.py b/modules/workflow/workflowManager.py
index 79d6d56f..98df49fb 100644
--- a/modules/workflow/workflowManager.py
+++ b/modules/workflow/workflowManager.py
@@ -1259,17 +1259,14 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"stats": workflow.stats.to_dict()
}
- def _checkFileAccess(self, fileId: int) -> bool:
- """Checks if the current user has access to a file."""
- file = self.service.functions.getFile(fileId)
- if not file:
+ def _checkFileAccess(self, fileId: str) -> bool:
+ """Checks if the current user has access to the file."""
+ try:
+ file = self.service.functions.getFile(fileId)
+ return file is not None
+ except Exception as e:
+ logger.error(f"Error checking file access: {str(e)}")
return False
-
- if file.get("mandateId") != self.functions.mandateId:
- logger.warning(f"File {fileId} does not belong to mandate {self.functions.mandateId}")
- return False
-
- return True
# Singleton factory for the WorkflowManager
diff --git a/requirements.txt b/requirements.txt
index 07869b5b..28085d1d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,6 +12,7 @@ passlib==1.7.4
argon2-cffi>=21.3.0 # Für Passwort-Hashing in gateway_interface.py
google-auth-oauthlib==1.2.0 # Für Google OAuth
google-auth==2.27.0 # Für Google Authentication
+google-api-python-client==2.170.0 # For Google API integration
bcrypt==4.0.1 # For password hashing
python-jose[cryptography]==3.3.0 # For JWT tokens