mvp running

This commit is contained in:
ValueOn AG 2025-05-30 01:12:59 +02:00
parent ecf23255d2
commit 8c9492715a
26 changed files with 1467 additions and 862 deletions

7
app.py
View file

@ -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)

View file

@ -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__)

View file

@ -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
# Add context fields
recordData["userId"] = self.userId
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 creation and modification tracking
currentTime = self._getCurrentTimestamp()
recordData["_createdAt"] = currentTime
recordData["_modifiedAt"] = currentTime
recordData["_createdBy"] = self.userId
recordData["_modifiedBy"] = self.userId
# If record is a Pydantic model, convert to dict
if isinstance(record, BaseModel):
record = to_dict(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 existing record with new data
existingRecord.update(record)
# 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()

View file

@ -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:

View file

@ -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")
@ -85,24 +57,24 @@ class UserConnection(BaseModelWithUI):
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"})
# 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(BaseModelWithUI):
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")
@ -112,22 +84,22 @@ class Session(BaseModelWithUI):
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"})
# 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(BaseModelWithUI):
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")
@ -137,22 +109,22 @@ class AuthEvent(BaseModelWithUI):
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"})
# 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(BaseModelWithUI):
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")
@ -165,58 +137,77 @@ class User(BaseModelWithUI):
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"})
}
@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}$"
}
# 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"}
}
)
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

View file

@ -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

View file

@ -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."""

View file

@ -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")
@ -98,56 +199,104 @@ class ChatWorkflow(BaseModelWithUI):
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
@ -156,3 +305,18 @@ class AgentProfile(BaseModel):
isAvailable: bool = True
lastActive: Optional[datetime] = None
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"}
}
)

View file

@ -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)

View file

@ -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")
"""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
if not prompts:
logger.debug("Creating standard prompts")
# 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')}")
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:
logger.debug("Prompts already exist, skipping creation")
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,10 +297,17 @@ class ServiceManagement:
def getAllPrompts(self) -> List[Prompt]:
"""Returns prompts based on user access level."""
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."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
@ -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)}")

View file

@ -8,13 +8,14 @@ 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")
@ -22,54 +23,54 @@ class FileItem(BaseModelWithUI):
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"})
# 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(BaseModelWithUI):
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"})
# 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(BaseModelWithUI):
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"}
}
)

View file

@ -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:

View file

@ -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)}"
)

View file

@ -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,50 +203,33 @@ 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)}")
# 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")
async def get_file_stats(
@ -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()

View file

@ -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()

View file

@ -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)
@ -134,19 +140,3 @@ async def delete_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()

View file

@ -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:
# 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.
return {"message": f"User with ID {userId} successfully deleted"}
Returns:
- A list of attribute definitions that can be used to generate forms
"""
# Get attributes from the User model class
return User.getModelAttributeDefinitions()

View file

@ -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,25 +107,47 @@ 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)
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(
@ -124,7 +160,66 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
window.opener.postMessage({{
type: 'google_auth_success',
access_token: {json.dumps(credentials.token)},
token_data: {json.dumps(token_data)}
token_data: {json.dumps(token.to_dict())}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
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"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_success',
connectionId: {json.dumps(connection_id)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
@ -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")

View file

@ -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

View file

@ -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,17 +104,44 @@ 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)
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(
@ -114,7 +154,66 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
window.opener.postMessage({{
type: 'msft_auth_success',
access_token: {json.dumps(token_response["access_token"])},
token_data: {json.dumps(token_data)}
token_data: {json.dumps(token.to_dict())}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
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"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_success',
connectionId: {json.dumps(connection_id)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);

View file

@ -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

View file

@ -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

View file

@ -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.
Create a Pydantic model instance from a dictionary.
Args:
data: Dictionary containing the model data
Returns:
List[Dict[str, Any]]: List of attribute definitions
ModelMixin: New instance of the model class
"""
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(cls, 'model_fields'): # Pydantic v2
fields = cls.model_fields
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": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
"placeholder": f"Please enter {name}",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}",
"editable": True,
"visible": True,
"order": len(attributes)
})
else: # Pydantic v1
fields = cls.__fields__
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": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
"placeholder": f"Please enter {name}",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, 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"
return {
"model": model_label,
"attributes": attributes
}
}
}
def getLabel(self, language: str = None) -> str:
"""
Returns the label in the specified language, or the default value if not available.
Args:
language: Language code to get the label for
Returns:
str: Label text in the specified language or default
"""
if language and language in self.translations:
return self.translations[language]
return self.default
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()
class Config:
schema_extra = {
"example": {
"attributes": [
{
"name": "username",
"label": "Username",
"type": "string",
"required": True,
"placeholder": "Please enter username",
"editable": True,
"visible": True,
"order": 0
}
]
}
}

View file

@ -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)
# Get file metadata
fileMetadata = await self.getFileMetadata(fileId)
if not fileMetadata:
return None
# Create ChatDocument
# 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

View file

@ -1259,18 +1259,15 @@ 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."""
def _checkFileAccess(self, fileId: str) -> bool:
"""Checks if the current user has access to the file."""
try:
file = self.service.functions.getFile(fileId)
if not file:
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
_workflowManagers = {}

View file

@ -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