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 # Startup logic
logger.info("Application is starting up") logger.info("Application is starting up")
# Initialize root interface to ensure database is properly set up
from modules.interfaces.serviceAppClass import getRootInterface
getRootInterface()
yield yield
# Shutdown logic # Shutdown logic
@ -146,6 +150,9 @@ app.include_router(fileRouter)
from modules.routes.routeDataPrompts import router as promptRouter from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(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 from modules.routes.routeWorkflows import router as workflowRouter
app.include_router(workflowRouter) app.include_router(workflowRouter)

View file

@ -19,6 +19,7 @@ import uuid
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent from modules.interfaces.serviceChatModel import Task, ChatDocument, ChatContent
from modules.shared.attributeUtils import ModelMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -4,6 +4,9 @@ from typing import List, Dict, Any, Optional, Union
import logging import logging
from datetime import datetime from datetime import datetime
import uuid import uuid
from pydantic import BaseModel
from modules.shared.attributeUtils import to_dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -138,14 +141,61 @@ class DatabaseConnector:
if os.path.exists(recordPath): if os.path.exists(recordPath):
with open(recordPath, 'r', encoding='utf-8') as f: with open(recordPath, 'r', encoding='utf-8') as f:
record = json.load(f) record = json.load(f)
# Ensure ID is a string
if "id" in record:
record["id"] = str(record["id"])
return record return record
except Exception as e: except Exception as e:
logger.error(f"Error loading record {recordId} from table {table}: {e}") logger.error(f"Error loading record {recordId} from table {table}: {e}")
return None 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]]: def _loadTable(self, table: str) -> List[Dict[str, Any]]:
"""Loads all records from a table folder.""" """Loads all records from a table folder."""
# If the table is the system table, load it directly # If the table is the system table, load it directly
@ -162,6 +212,9 @@ class DatabaseConnector:
# Load each record # Load each record
for recordId in metadata["recordIds"]: for recordId in metadata["recordIds"]:
# Skip metadata file
if recordId == "_metadata":
continue
record = self._loadRecord(table, recordId) record = self._loadRecord(table, recordId)
if record: if record:
records.append(record) records.append(record)
@ -376,67 +429,37 @@ class DatabaseConnector:
return records return records
def recordCreate(self, table: str, recordData: Dict[str, Any]) -> Dict[str, Any]: def recordCreate(self, table: str, record: Dict[str, Any]) -> Dict[str, Any]:
"""Creates a new record in the specified table.""" """Creates a new record in a table."""
try: # Ensure record has an ID
# Ensure table directory exists if "id" not in record:
if not self._ensureTableDirectory(table): record["id"] = str(uuid.uuid4())
raise ValueError(f"Error creating table directory for {table}")
# Load table metadata # If record is a Pydantic model, convert to dict
metadata = self._loadTableMetadata(table) if isinstance(record, BaseModel):
record = to_dict(record)
# Generate new ID if not provided # Save record
if "id" not in recordData: self._saveRecord(table, record["id"], record)
recordData["id"] = str(uuid.uuid4()) return record
else:
# Ensure ID is a string
recordData["id"] = str(recordData["id"])
# Add context fields def recordModify(self, table: str, recordId: str, record: Dict[str, Any]) -> Dict[str, Any]:
recordData["userId"] = self.userId """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 # If record is a Pydantic model, convert to dict
currentTime = self._getCurrentTimestamp() if isinstance(record, BaseModel):
recordData["_createdAt"] = currentTime record = to_dict(record)
recordData["_modifiedAt"] = currentTime
recordData["_createdBy"] = self.userId
recordData["_modifiedBy"] = self.userId
# Save the record # Update existing record with new data
recordPath = self._getRecordPath(table, recordData["id"]) existingRecord.update(record)
os.makedirs(os.path.dirname(recordPath), exist_ok=True)
with open(recordPath, 'w', encoding='utf-8') as f:
json.dump(recordData, f, indent=2, ensure_ascii=False)
# Update metadata with new record ID # Save updated record
if recordData["id"] not in metadata["recordIds"]: self._saveRecord(table, recordId, existingRecord)
metadata["recordIds"].append(recordData["id"]) return existingRecord
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}")
def recordDelete(self, table: str, recordId: str) -> bool: def recordDelete(self, table: str, recordId: str) -> bool:
"""Deletes a record from the table.""" """Deletes a record from the table."""
@ -473,56 +496,6 @@ class DatabaseConnector:
return False 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: def hasInitialId(self, table: str) -> bool:
"""Checks if an initial ID is registered for a table.""" """Checks if an initial ID is registered for a table."""
systemData = self._loadSystemTable() systemData = self._loadSystemTable()

View file

@ -10,6 +10,7 @@ from typing import Dict, Any, List, Optional, Union
import importlib import importlib
import json import json
from passlib.context import CryptContext from passlib.context import CryptContext
import uuid
from modules.connectors.connectorDbJson import DatabaseConnector from modules.connectors.connectorDbJson import DatabaseConnector
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
@ -17,8 +18,9 @@ from modules.interfaces.serviceAppAccess import AppAccess
from modules.interfaces.serviceAppModel import ( from modules.interfaces.serviceAppModel import (
User, Mandate, UserInDB, UserConnection, User, Mandate, UserInDB, UserConnection,
Session, AuthEvent, AuthAuthority, UserPrivilege, Session, AuthEvent, AuthAuthority, UserPrivilege,
ConnectionStatus ConnectionStatus, Token, LocalToken, GoogleToken, MsftToken
) )
from modules.shared.attributeUtils import ModelMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,6 +76,9 @@ class GatewayInterface:
# Initialize access control with user context # Initialize access control with user context
self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed 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}") logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self): def _initializeDatabase(self):
@ -164,7 +169,17 @@ class GatewayInterface:
Returns: Returns:
Filtered recordset with access control attributes 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: def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
""" """
@ -445,6 +460,12 @@ class GatewayInterface:
self.db.recordDelete("auth_events", event["id"]) self.db.recordDelete("auth_events", event["id"])
logger.debug(f"Deleted auth event {event['id']} for user {userId}") 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 # Delete user connections
user = self.getUser(userId) user = self.getUser(userId)
if user and user.connections: if user and user.connections:
@ -593,6 +614,78 @@ class GatewayInterface:
"message": f"Error checking username availability: {str(e)}" "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 # Public Methods
def getInterface(currentUser: User) -> GatewayInterface: 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 typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from modules.shared.attributeUtils import register_model_labels, AttributeDefinition, ModelMixin
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")
class AuthAuthority(str, Enum): class AuthAuthority(str, Enum):
"""Authentication authorities""" """Authentication authority enum"""
LOCAL = "local" LOCAL = "Local"
MICROSOFT = "microsoft" GOOGLE = "Google"
GOOGLE = "google" MSFT = "Msft"
EXTERNAL = "external"
class UserPrivilege(str, Enum): class UserPrivilege(str, Enum):
"""User privilege levels""" """User privilege levels"""
@ -41,39 +28,24 @@ class ConnectionStatus(str, Enum):
REVOKED = "revoked" REVOKED = "revoked"
PENDING = "pending" PENDING = "pending"
class Mandate(BaseModelWithUI): class Mandate(BaseModel, ModelMixin):
"""Data model for a mandate""" """Data model for a mandate"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate") name: str = Field(description="Name of the mandate")
language: str = Field(default="en", description="Default language of the mandate") language: str = Field(default="en", description="Default language of the mandate")
label: Label = Field( # Register labels for Mandate
default=Label(default="Mandate", translations={"en": "Mandate", "fr": "Mandat"}), register_model_labels(
description="Label for the class" "Mandate",
) {"en": "Mandate", "fr": "Mandat"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "name": {"en": "Name", "fr": "Nom"},
"name": Label(default="Name of the mandate", translations={"en": "Mandate name", "fr": "Nom du mandat"}), "language": {"en": "Language", "fr": "Langue"}
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"})
} }
)
@classmethod class UserConnection(BaseModel, ModelMixin):
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):
"""Data model for a user's connection to an external service""" """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") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
authority: AuthAuthority = Field(description="Authentication authority") 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") lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
expiresAt: Optional[datetime] = Field(None, description="When the connection expires") expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
label: Label = Field( # Register labels for UserConnection
default=Label(default="User Connection", translations={"en": "User Connection", "fr": "Connexion utilisateur"}), register_model_labels(
description="Label for the class" "UserConnection",
) {"en": "User Connection", "fr": "Connexion utilisateur"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "authority": {"en": "Authority", "fr": "Autorité"},
"authority": Label(default="Authority", translations={"en": "Authority", "fr": "Autorité"}), "externalId": {"en": "External ID", "fr": "ID externe"},
"externalId": Label(default="External ID", translations={"en": "External ID", "fr": "ID externe"}), "externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
"externalUsername": Label(default="External Username", translations={"en": "External Username", "fr": "Nom d'utilisateur externe"}), "externalEmail": {"en": "External Email", "fr": "Email externe"},
"externalEmail": Label(default="External Email", translations={"en": "External Email", "fr": "Email externe"}), "status": {"en": "Status", "fr": "Statut"},
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}), "connectedAt": {"en": "Connected At", "fr": "Connecté le"},
"connectedAt": Label(default="Connected At", translations={"en": "Connected At", "fr": "Connecté le"}), "lastChecked": {"en": "Last Checked", "fr": "Dernière vérification"},
"lastChecked": Label(default="Last Checked", translations={"en": "Last Checked", "fr": "Dernière vérification"}), "expiresAt": {"en": "Expires At", "fr": "Expire le"}
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"})
} }
)
class Session(BaseModelWithUI): class Session(BaseModel, ModelMixin):
"""Data model for user sessions""" """Data model for user sessions"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique session ID") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique session ID")
userId: str = Field(description="ID of the user") 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") ipAddress: Optional[str] = Field(None, description="IP address of the session")
userAgent: Optional[str] = Field(None, description="User agent of the session") userAgent: Optional[str] = Field(None, description="User agent of the session")
label: Label = Field( # Register labels for Session
default=Label(default="Session", translations={"en": "Session", "fr": "Session"}), register_model_labels(
description="Label for the class" "Session",
) {"en": "Session", "fr": "Session"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "userId": {"en": "User ID", "fr": "ID utilisateur"},
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}), "tokenId": {"en": "Token ID", "fr": "ID du token"},
"tokenId": Label(default="Token ID", translations={"en": "Token ID", "fr": "ID du token"}), "lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}), "expiresAt": {"en": "Expires At", "fr": "Expire le"},
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"}), "ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}), "userAgent": {"en": "User Agent", "fr": "User Agent"}
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
} }
)
class AuthEvent(BaseModelWithUI): class AuthEvent(BaseModel, ModelMixin):
"""Data model for authentication events""" """Data model for authentication events"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique event ID") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique event ID")
userId: str = Field(description="ID of the user") 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") ipAddress: Optional[str] = Field(None, description="IP address of the event")
userAgent: Optional[str] = Field(None, description="User agent of the event") userAgent: Optional[str] = Field(None, description="User agent of the event")
label: Label = Field( # Register labels for AuthEvent
default=Label(default="Auth Event", translations={"en": "Auth Event", "fr": "Événement d'authentification"}), register_model_labels(
description="Label for the class" "AuthEvent",
) {"en": "Auth Event", "fr": "Événement d'authentification"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "userId": {"en": "User ID", "fr": "ID utilisateur"},
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}), "eventType": {"en": "Event Type", "fr": "Type d'événement"},
"eventType": Label(default="Event Type", translations={"en": "Event Type", "fr": "Type d'événement"}), "details": {"en": "Details", "fr": "Détails"},
"details": Label(default="Details", translations={"en": "Details", "fr": "Détails"}), "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
"timestamp": Label(default="Timestamp", translations={"en": "Timestamp", "fr": "Horodatage"}), "ipAddress": {"en": "IP Address", "fr": "Adresse IP"},
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}), "userAgent": {"en": "User Agent", "fr": "User Agent"}
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
} }
)
class User(BaseModelWithUI): class User(BaseModel, ModelMixin):
"""Data model for a user""" """Data model for a user"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login") 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") 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") connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
label: Label = Field( # Register labels for User
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}), register_model_labels(
description="Label for the class" "User",
) {"en": "User", "fr": "Utilisateur"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "username": {"en": "Username", "fr": "Nom d'utilisateur"},
"username": Label(default="Username", translations={"en": "Username", "fr": "Nom d'utilisateur"}), "email": {"en": "Email", "fr": "Email"},
"email": Label(default="Email", translations={"en": "Email", "fr": "Email"}), "fullName": {"en": "Full Name", "fr": "Nom complet"},
"fullName": Label(default="Full Name", translations={"en": "Full Name", "fr": "Nom complet"}), "language": {"en": "Language", "fr": "Langue"},
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}), "disabled": {"en": "Disabled", "fr": "Désactivé"},
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}), "privilege": {"en": "Privilege", "fr": "Privilège"},
"privilege": Label(default="Privilege", translations={"en": "Privilege", "fr": "Privilège"}), "authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
"authenticationAuthority": Label(default="Auth Authority", translations={"en": "Auth Authority", "fr": "Autorité d'authentification"}), "mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}), "connections": {"en": "Connections", "fr": "Connexions"}
"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}$"
}
}
class UserInDB(User): class UserInDB(User):
"""Extended user class with password hash""" """Extended user class with password hash"""
hashedPassword: Optional[str] = Field(None, description="Hash of the user password") hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
label: Label = Field( # Register labels for UserInDB
default=Label(default="User Access", translations={"en": "User Access", "fr": "Accès de l'utilisateur"}), register_model_labels(
description="Label for the class" "UserInDB",
) {"en": "User Access", "fr": "Accès de l'utilisateur"},
{
fieldLabels: Dict[str, Label] = { "hashedPassword": {"en": "Password hash", "fr": "Hachage de mot de passe"}
"hashedPassword": Label(default="Password hash", translations={"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 # Initialize access control with user context
self.access = ChatAccess(self.currentUser, self.db) # Convert to dict only when needed 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}") logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self): def _initializeDatabase(self):
@ -176,7 +179,17 @@ class ChatInterface:
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module.""" """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: def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module.""" """Delegate to access control module."""

View file

@ -7,12 +7,11 @@ from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
import uuid import uuid
from modules.shared.attributeUtils import Label, BaseModelWithUI from modules.shared.attributeUtils import register_model_labels, ModelMixin
# WORKFLOW MODELS # WORKFLOW MODELS
class ChatContent(BaseModelWithUI): class ChatContent(BaseModel, ModelMixin):
"""Data model for chat content""" """Data model for chat content"""
sequenceNr: int = Field(description="Sequence number of the content") sequenceNr: int = Field(description="Sequence number of the content")
name: str = Field(description="Name 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") mimeType: str = Field(description="MIME type of the content")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata") 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""" """Data model for a chat document"""
id: str = Field(description="Primary key") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
fileId: int = Field(description="Foreign key to file") fileId: str = Field(description="Foreign key to file")
filename: str = Field(description="Name of the file") filename: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file") fileSize: int = Field(description="Size of the file")
mimeType: str = Field(description="MIME type of the file") mimeType: str = Field(description="MIME type of the file")
contents: List[ChatContent] = Field(default_factory=list, description="List of chat contents") 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""" """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") processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Number of tokens processed") tokenCount: Optional[int] = Field(None, description="Number of tokens processed")
bytesSent: Optional[int] = Field(None, description="Number of bytes sent") 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") successRate: Optional[float] = Field(None, description="Success rate of operations")
errorCount: Optional[int] = Field(None, description="Number of errors encountered") 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""" """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") workflowId: str = Field(description="Foreign key to workflow")
message: str = Field(description="Log message") message: str = Field(description="Log message")
type: str = Field(description="Type of log entry") type: str = Field(description="Type of log entry")
@ -51,9 +92,26 @@ class ChatLog(BaseModelWithUI):
progress: Optional[int] = Field(None, description="Progress percentage") progress: Optional[int] = Field(None, description="Progress percentage")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics") 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""" """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") workflowId: str = Field(description="Foreign key to workflow")
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading") parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
agentName: Optional[str] = Field(None, description="Name of the agent") 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") stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
success: Optional[bool] = Field(None, description="Whether the message processing was successful") 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""" """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") workflowId: str = Field(description="Foreign key to workflow")
agentName: str = Field(description="Name of the agent assigned to this task") agentName: str = Field(description="Name of the agent assigned to this task")
status: str = Field(description="Current status of the 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") finishedAt: Optional[str] = Field(None, description="When the task finished")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics") 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""" """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") mandateId: str = Field(description="ID of the mandate this workflow belongs to")
status: str = Field(description="Current status of the workflow") status: str = Field(description="Current status of the workflow")
name: Optional[str] = Field(None, description="Name 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") stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
tasks: List[Task] = Field(default_factory=list, description="List of tasks in the workflow") tasks: List[Task] = Field(default_factory=list, description="List of tasks in the workflow")
label: Label = Field( # Register labels for ChatWorkflow
default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}), register_model_labels(
description="Label for the class" "ChatWorkflow",
) {"en": "Chat Workflow", "fr": "Flux de travail de chat"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}), "status": {"en": "Status", "fr": "Statut"},
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}), "name": {"en": "Name", "fr": "Nom"},
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}), "currentRound": {"en": "Current Round", "fr": "Tour actuel"},
"currentRound": Label(default="Current Round", translations={"en": "Current Round", "fr": "Tour actuel"}), "lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}), "startedAt": {"en": "Started At", "fr": "Démarré le"},
"startedAt": Label(default="Started At", translations={"en": "Started At", "fr": "Démarré le"}), "logs": {"en": "Logs", "fr": "Journaux"},
"logs": Label(default="Logs", translations={"en": "Logs", "fr": "Journaux"}), "messages": {"en": "Messages", "fr": "Messages"},
"messages": Label(default="Messages", translations={"en": "Messages", "fr": "Messages"}), "stats": {"en": "Statistics", "fr": "Statistiques"},
"stats": Label(default="Statistics", translations={"en": "Statistics", "fr": "Statistiques"}), "tasks": {"en": "Tasks", "fr": "Tâches"}
"tasks": Label(default="Tasks", translations={"en": "Tasks", "fr": "Tâches"})
} }
)
# AGENT AND TASK MODELS # AGENT AND TASK MODELS
class Agent(BaseModelWithUI): class Agent(BaseModel, ModelMixin):
"""Data model for an agent""" """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") name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent") description: str = Field(description="Description of the agent")
capabilities: List[str] = Field(default_factory=list, description="List of agent capabilities") capabilities: List[str] = Field(default_factory=list, description="List of agent capabilities")
performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics") 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""" """Data model for an agent response"""
success: bool = Field(description="Whether the agent execution was successful") success: bool = Field(description="Whether the agent execution was successful")
message: ChatMessage = Field(description="Response message from the agent") message: ChatMessage = Field(description="Response message from the agent")
performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics") performance: Dict[str, Any] = Field(default_factory=dict, description="Performance metrics")
progress: float = Field(description="Task progress (0-100)") 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""" """Data model for a task plan"""
fileList: List[str] = Field(default_factory=list, description="List of files") 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") tasks: List[Task] = Field(default_factory=list, description="List of tasks in the plan")
userLanguage: str = Field(description="User's preferred language") userLanguage: str = Field(description="User's preferred language")
userResponse: str = Field(description="User's response or feedback") 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""" """Data model for a user input request"""
prompt: str = Field(description="Prompt for the user") prompt: str = Field(description="Prompt for the user")
listFileId: List[int] = Field(default_factory=list, description="List of file IDs") listFileId: List[int] = Field(default_factory=list, description="List of file IDs")
userLanguage: str = Field(default="en", description="User's preferred language") 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.""" """Model for agent profile information."""
id: str id: str
name: str name: str
@ -156,3 +305,18 @@ class AgentProfile(BaseModel):
isAvailable: bool = True isAvailable: bool = True
lastActive: Optional[datetime] = None lastActive: Optional[datetime] = None
stats: Optional[Dict[str, Any]] = 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.privilege = currentUser.privilege
self.db = db 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]]: 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 Unified user access management function that filters data based on user privileges
@ -37,7 +56,7 @@ class ManagementAccess:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
userPrivilege = self.privilege userPrivilege = self.privilege
logger.debug(f"User privilege: {userPrivilege}, username: {self.currentUser.username}, email: {self.currentUser.email}")
filtered_records = [] filtered_records = []
# Apply filtering based on privilege # Apply filtering based on privilege
@ -45,7 +64,7 @@ class ManagementAccess:
filtered_records = recordset # System admins see all records filtered_records = recordset # System admins see all records
elif userPrivilege == "admin": elif userPrivilege == "admin":
# Admins see records in their mandate # 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 else: # Regular users
# For prompts, users can see all prompts from their mandate # For prompts, users can see all prompts from their mandate
if table == "prompts": if table == "prompts":
@ -53,7 +72,7 @@ class ManagementAccess:
else: else:
# Users see only their records for other tables # Users see only their records for other tables
filtered_records = [r for r in recordset 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 # Add access control attributes to each record
for record in filtered_records: for record in filtered_records:
@ -64,6 +83,10 @@ class ManagementAccess:
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("prompts", record_id) record["_hideEdit"] = not self.canModify("prompts", record_id)
record["_hideDelete"] = 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": elif table == "files":
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("files", record_id) record["_hideEdit"] = not self.canModify("files", record_id)

View file

@ -58,18 +58,18 @@ class ServiceManagement:
def __init__(self): def __init__(self):
"""Initializes the Management Interface.""" """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 # Initialize database
self._initializeDatabase() self._initializeDatabase()
# Initialize standard records if needed # Initialize standard records if needed
self._initRecords() 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): def setUserContext(self, currentUser: User):
"""Sets the user context for the interface.""" """Sets the user context for the interface."""
if not currentUser: if not currentUser:
@ -91,6 +91,9 @@ class ServiceManagement:
# Initialize AI service # Initialize AI service
self.aiService = ChatService() self.aiService = ChatService()
# Update database context
self.db.updateContext(self.userId)
logger.debug(f"User context set: userId={self.userId}") logger.debug(f"User context set: userId={self.userId}")
def _initializeDatabase(self): def _initializeDatabase(self):
@ -128,54 +131,112 @@ class ServiceManagement:
logger.info("Standard records initialized successfully") logger.info("Standard records initialized successfully")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize standard records: {str(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): def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist.""" """Initializes standard prompts if they don't exist yet."""
prompts = self.db.getRecordset("prompts") try:
logger.debug(f"Found {len(prompts)} existing prompts") # Check if any prompts exist
existingPrompts = self.db.getRecordset("prompts")
if existingPrompts:
logger.info("Prompts already exist, skipping initialization")
return
if not prompts: # Get the root interface to access the initial mandate ID
logger.debug("Creating standard prompts") 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 # Define standard prompts
standardPrompts = [ standardPrompts = [
{ Prompt(
"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="Market Research",
"name": "Web Research: 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
{ ),
"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.", Prompt(
"name": "Analysis: Data Analysis" 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
"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" 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.",
"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
"name": "Design: UI/UX Design" ),
}, Prompt(
{ name="UI/UX Design",
"content": "Gib mir die ersten 1000 Primzahlen", 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": "Code: Primzahlen" mandateId=mandateId
}, ),
{ Prompt(
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.", name="Primzahlen",
"name": "Mail: Vorbereitung" 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 # Create prompts
for promptData in standardPrompts: for prompt in standardPrompts:
createdPrompt = self.db.recordCreate("prompts", promptData) self.db.recordCreate("prompts", prompt.to_dict())
logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('mandateId')}, user={createdPrompt.get('_createdBy')}") logger.info(f"Created standard prompt: {prompt.name}")
else:
logger.debug("Prompts already exist, skipping creation") # Restore original user context if it existed
if currentUser:
self.setUserContext(currentUser)
else:
self.currentUser = None
self.userId = None
self.access = None
self.db.updateContext("") # Reset database context
except Exception as e:
logger.error(f"Error initializing standard prompts: {str(e)}")
# Ensure we restore user context even if there's an error
if 'currentUser' in locals() and currentUser:
self.setUserContext(currentUser)
else:
self.currentUser = None
self.userId = None
self.access = None
self.db.updateContext("") # Reset database context
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module.""" """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: def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module.""" """Delegate to access control module."""
@ -236,9 +297,16 @@ class ServiceManagement:
def getAllPrompts(self) -> List[Prompt]: def getAllPrompts(self) -> List[Prompt]:
"""Returns prompts based on user access level.""" """Returns prompts based on user access level."""
allPrompts = self.db.getRecordset("prompts") try:
filteredPrompts = self._uam("prompts", allPrompts) allPrompts = self.db.getRecordset("prompts")
return [Prompt.from_dict(prompt) for prompt in filteredPrompts] 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]: def getPrompt(self, promptId: str) -> Optional[Prompt]:
"""Returns a prompt by ID if user has access.""" """Returns a prompt by ID if user has access."""
@ -269,20 +337,15 @@ class ServiceManagement:
if not prompt: if not prompt:
raise ValueError(f"Prompt {promptId} not found") raise ValueError(f"Prompt {promptId} not found")
# Update prompt data using model # Update prompt record directly with the update data
updatedData = prompt.to_dict() self.db.recordModify("prompts", promptId, updateData)
updatedData.update(updateData)
updatedPrompt = Prompt.from_dict(updatedData)
# Update prompt record
self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
# Get updated prompt # Get updated prompt
updatedPrompt = self.getPrompt(promptId) updatedPrompt = self.getPrompt(promptId)
if not updatedPrompt: if not updatedPrompt:
raise ValueError("Failed to retrieve updated prompt") raise ValueError("Failed to retrieve updated prompt")
return updatedPrompt return updatedPrompt.to_dict()
except Exception as e: except Exception as e:
logger.error(f"Error updating prompt: {str(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 from datetime import datetime
import uuid import uuid
from modules.shared.attributeUtils import Label, BaseModelWithUI # Import for label registration
from modules.shared.attributeUtils import register_model_labels, ModelMixin
# CORE MODELS # CORE MODELS
class FileItem(BaseModelWithUI): class FileItem(BaseModel, ModelMixin):
"""Data model for a file item""" """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") mandateId: str = Field(description="ID of the mandate this file belongs to")
filename: str = Field(description="Name of the file") filename: str = Field(description="Name of the file")
mimeType: str = Field(description="MIME type 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") fileHash: str = Field(description="Hash of the file")
fileSize: int = Field(description="Size of the file in bytes") fileSize: int = Field(description="Size of the file in bytes")
label: Label = Field( # Register labels for FileItem
default=Label(default="File Item", translations={"en": "File Item", "fr": "Élément de fichier"}), register_model_labels(
description="Label for the class" "FileItem",
) {"en": "File Item", "fr": "Élément de fichier"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}), "filename": {"en": "Filename", "fr": "Nom de fichier"},
"filename": Label(default="Filename", translations={"en": "Filename", "fr": "Nom de fichier"}), "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
"mimeType": Label(default="MIME Type", translations={"en": "MIME Type", "fr": "Type MIME"}), "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du flux de travail"}), "fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
"fileHash": Label(default="File Hash", translations={"en": "File Hash", "fr": "Hash du fichier"}), "fileSize": {"en": "File Size", "fr": "Taille du fichier"}
"fileSize": Label(default="File Size", translations={"en": "File Size", "fr": "Taille du fichier"})
} }
)
class FileData(BaseModelWithUI): class FileData(BaseModel, ModelMixin):
"""Data model for file data""" """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") data: str = Field(description="File data content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded") base64Encoded: bool = Field(description="Whether the data is base64 encoded")
label: Label = Field( # Register labels for FileData
default=Label(default="File Data", translations={"en": "File Data", "fr": "Données de fichier"}), register_model_labels(
description="Label for the class" "FileData",
) {"en": "File Data", "fr": "Données de fichier"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "data": {"en": "Data", "fr": "Données"},
"data": Label(default="Data", translations={"en": "Data", "fr": "Données"}), "base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"}
"base64Encoded": Label(default="Base64 Encoded", translations={"en": "Base64 Encoded", "fr": "Encodé en Base64"})
} }
)
class Prompt(BaseModelWithUI): class Prompt(BaseModel, ModelMixin):
"""Data model for a prompt""" """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") mandateId: str = Field(description="ID of the mandate this prompt belongs to")
content: str = Field(description="Content of the prompt") content: str = Field(description="Content of the prompt")
name: str = Field(description="Name of the prompt") name: str = Field(description="Name of the prompt")
label: Label = Field( # Register labels for Prompt
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}), register_model_labels(
description="Label for the class" "Prompt",
) {"en": "Prompt", "fr": "Invite"},
{
fieldLabels: Dict[str, Label] = { "id": {"en": "ID", "fr": "ID"},
"id": Label(default="ID", translations={}), "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}), "content": {"en": "Content", "fr": "Contenu"},
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}), "name": {"en": "Name", "fr": "Nom"}
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"})
} }
)

View file

@ -11,35 +11,12 @@ import logging
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.serviceAppModel import AttributeDefinition, User from modules.interfaces.serviceAppModel import User
from modules.shared.attributeUtils import getModelClasses from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Configure logger # Configure logger
logger = logging.getLogger(__name__) 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 # Create a router for the attribute endpoints
router = APIRouter( router = APIRouter(
prefix="/api/attributes", prefix="/api/attributes",
@ -75,11 +52,11 @@ async def get_entity_attributes(
# Get model class and derive attributes from it # Get model class and derive attributes from it
modelClass = modelClasses[entityType] modelClass = modelClasses[entityType]
attributes = modelClass.getModelAttributeDefinitions() attribute_defs = getModelAttributeDefinitions(modelClass)
# Convert dictionary attributes to AttributeDefinition objects # Convert dictionary attributes to AttributeDefinition objects
attribute_definitions = [] attribute_definitions = []
for attr in attributes: for attr in attribute_defs["attributes"]:
if isinstance(attr, dict) and attr.get('visible', True): if isinstance(attr, dict) and attr.get('visible', True):
attribute_definitions.append(AttributeDefinition(**attr)) attribute_definitions.append(AttributeDefinition(**attr))
elif hasattr(attr, 'visible') and attr.visible: 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 datetime import datetime, timezone
from dataclasses import dataclass from dataclasses import dataclass
import io import io
import inspect
import importlib
import os
from pydantic import BaseModel
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
@ -12,8 +16,8 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import FileItem from modules.interfaces.serviceManagementModel import FileItem
from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
from modules.interfaces.serviceAppModel import AttributeDefinition, User from modules.interfaces.serviceAppModel import User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -199,50 +203,33 @@ async def update_file(
detail=str(e) 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") @limiter.limit("10/minute")
async def delete_file( async def delete_file(
request: Request, request: Request,
fileId: str, fileId: str = Path(..., description="ID of the file to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> JSONResponse: ) -> Dict[str, Any]:
"""Delete a file""" """Delete a file"""
try: managementInterface = serviceManagementClass.getInterface(currentUser)
managementInterface = serviceManagementClass.getInterface(currentUser)
# Delete file via LucyDOM interface # Check if the file exists
managementInterface.deleteFile(fileId) existingFile = managementInterface.getFile(fileId)
if not existingFile:
# 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)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, 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)}") success = managementInterface.deleteFile(fileId)
raise HTTPException( if not success:
status_code=status.HTTP_403_FORBIDDEN,
detail=str(e)
)
except serviceManagementClass.FileDeletionError as e:
logger.error(f"Error deleting file: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e) detail="Error deleting the file"
)
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)}"
) )
return {"message": f"File with ID {fileId} successfully deleted"}
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file_stats( async def get_file_stats(
@ -281,18 +268,3 @@ async def get_file_stats(
detail=f"Error retrieving file statistics: {str(e)}" 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 fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Response
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
from datetime import datetime
import logging import logging
import inspect
import importlib
import os
from pydantic import BaseModel
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
# Import the model classes # Import the model classes
from modules.interfaces.serviceAppModel import AttributeDefinition, Mandate, User from modules.interfaces.serviceAppModel import Mandate, User
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -189,19 +194,3 @@ async def delete_mandate(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete mandate: {str(e)}" 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 typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
from datetime import datetime from datetime import datetime
import logging import logging
import inspect
import importlib
import os
from pydantic import BaseModel
# Import auth module # Import auth module
from modules.security.auth import limiter, getCurrentUser from modules.security.auth import limiter, getCurrentUser
@ -10,7 +14,8 @@ from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.serviceManagementClass as serviceManagementClass import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.serviceManagementModel import Prompt 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 # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,7 +36,7 @@ async def get_prompts(
"""Get all prompts""" """Get all prompts"""
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
prompts = managementInterface.getAllPrompts() prompts = managementInterface.getAllPrompts()
return [Prompt.from_dict(prompt) for prompt in prompts] return prompts
@router.post("", response_model=Prompt) @router.post("", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")
@ -44,13 +49,14 @@ async def create_prompt(
managementInterface = serviceManagementClass.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Convert Prompt to dict for interface # Convert Prompt to dict for interface
prompt_data = prompt.to_dict() prompt_data = prompt.dict()
# Create prompt # Create prompt
newPrompt = managementInterface.createPrompt(prompt_data) newPrompt = managementInterface.createPrompt(prompt_data)
# Set current time for createdAt if it exists in the model # 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() newPrompt["createdAt"] = datetime.now().isoformat()
return Prompt.from_dict(newPrompt) return Prompt.from_dict(newPrompt)
@ -95,7 +101,7 @@ async def update_prompt(
) )
# Convert Prompt to dict for interface # Convert Prompt to dict for interface
update_data = promptData.to_dict() update_data = promptData.dict()
# Update prompt # Update prompt
updatedPrompt = managementInterface.updatePrompt(promptId, update_data) updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
@ -134,19 +140,3 @@ async def delete_prompt(
) )
return {"message": f"Prompt with ID {promptId} successfully deleted"} 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 from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.interfaces.serviceAppModel import User, AttributeDefinition as ServiceAppAttributeDefinition from modules.interfaces.serviceAppModel import User, AttributeDefinition
from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -93,7 +93,8 @@ async def create_user(
newUser = managementInterface.createUser(user_data) newUser = managementInterface.createUser(user_data)
# Set current time for createdAt if it exists in the model # 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() newUser["createdAt"] = datetime.now().isoformat()
return User.from_dict(newUser) return User.from_dict(newUser)
@ -139,34 +140,22 @@ async def delete_user(
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a user""" """Delete a user"""
try: appInterface = serviceManagementClass.getInterface(currentUser)
appInterface = serviceManagementClass.getInterface(currentUser)
appInterface.deleteUser(userId) # Check if the user exists
return {"message": f"User {userId} deleted successfully"} existingUser = appInterface.getUser(userId)
except ValueError as e: if not existingUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_404_NOT_FOUND,
detail=str(e) 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( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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]) return {"message": f"User with ID {userId} successfully deleted"}
@limiter.limit("30/minute")
async def get_user_attributes(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> List[ServiceAppAttributeDefinition]:
"""
Retrieves the attribute definitions for users.
This can be used for dynamic form generation.
Returns:
- A list of attribute definitions that can be used to generate forms
"""
# Get attributes from the User model class
return User.getModelAttributeDefinitions()

View file

@ -2,7 +2,7 @@
Routes for Google authentication. 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 from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging import logging
import json import json
@ -11,12 +11,13 @@ from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request as GoogleRequest from google.auth.transport.requests import Request as GoogleRequest
from googleapiclient.discovery import build
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import AuthAuthority, User from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
from modules.interfaces.serviceAppTokens import GoogleToken
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.shared.attributeUtils import ModelMixin
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +47,11 @@ SCOPES = [
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @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""" """Initiate Google login"""
try: try:
# Create OAuth flow # Create OAuth flow
@ -63,10 +68,14 @@ async def login(request: Request) -> RedirectResponse:
scopes=SCOPES scopes=SCOPES
) )
# Generate auth URL # Generate auth URL with state
auth_url, _ = flow.authorization_url( auth_url, _ = flow.authorization_url(
access_type="offline", access_type="offline",
include_granted_scopes="true" include_granted_scopes="true",
state=json.dumps({
"type": state,
"connectionId": connectionId
})
) )
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
@ -79,9 +88,14 @@ async def login(request: Request) -> RedirectResponse:
) )
@router.get("/auth/callback") @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""" """Handle Google OAuth callback"""
try: try:
# Parse state
state_data = json.loads(state)
state_type = state_data.get("type", "login")
connection_id = state_data.get("connectionId")
# Create OAuth flow # Create OAuth flow
flow = Flow.from_client_config( flow = Flow.from_client_config(
{ {
@ -93,46 +107,127 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
"redirect_uris": [REDIRECT_URI] "redirect_uris": [REDIRECT_URI]
} }
}, },
scopes=SCOPES, scopes=SCOPES
redirect_uri=REDIRECT_URI
) )
# Exchange code for token # Exchange code for credentials
flow.fetch_token(code=code) flow.fetch_token(code=code)
credentials = flow.credentials credentials = flow.credentials
# Create token data # Get user info
token_data = { user_info_response = flow.oauth2session.get("https://www.googleapis.com/oauth2/v2/userinfo")
"access_token": credentials.token, user_info = user_info_response.json()
"refresh_token": credentials.refresh_token,
"token_type": credentials.token_type,
"expires_at": credentials.expiry.timestamp()
}
# Save token data if state_type == "login":
appInterface = getInterface() # Handle login flow
appInterface.saveToken("Google", token_data) rootInterface = getRootInterface()
user = rootInterface.getUserByUsername(user_info.get("email"))
# Return success page with token data if not user:
return HTMLResponse( # Create new user if doesn't exist
content=f""" user = rootInterface.createUser(
<html> username=user_info.get("email"),
<head><title>Authentication Successful</title></head> email=user_info.get("email"),
<body> fullName=user_info.get("name"),
<script> authenticationAuthority=AuthAuthority.GOOGLE,
if (window.opener) {{ externalId=user_info.get("id"),
window.opener.postMessage({{ externalUsername=user_info.get("email"),
type: 'google_auth_success', externalEmail=user_info.get("email")
access_token: {json.dumps(credentials.token)}, )
token_data: {json.dumps(token_data)}
}}, '*'); # Create token
}} token = Token(
setTimeout(() => window.close(), 1000); userId=user.id,
</script> authority=AuthAuthority.GOOGLE,
</body> tokenAccess=credentials.token,
</html> tokenRefresh=credentials.refresh_token,
""" tokenType=credentials.token_type,
) expiresAt=credentials.expiry.timestamp() if credentials.expiry else None
)
# Save token
appInterface = getInterface(user)
appInterface.saveToken(token)
# Return success page with token data
return HTMLResponse(
content=f"""
<html>
<head><title>Authentication Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_auth_success',
access_token: {json.dumps(credentials.token)},
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);
</script>
</body>
</html>
"""
)
except Exception as e: except Exception as e:
logger.error(f"Error in auth callback: {str(e)}") logger.error(f"Error in auth callback: {str(e)}")
@ -154,7 +249,7 @@ async def get_current_user(
logger.error(f"Error getting current user: {str(e)}") logger.error(f"Error getting current user: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get current user: {str(e)}" detail=str(e)
) )
@router.post("/logout") @router.post("/logout")

View file

@ -13,8 +13,8 @@ from pydantic import BaseModel
# Import auth modules # Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter from modules.security.auth import createAccessToken, getCurrentUser, limiter
from modules.interfaces.serviceAppClass import getInterface, getRootInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege from modules.interfaces.serviceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token
from modules.interfaces.serviceAppTokens import LocalToken from modules.shared.attributeUtils import ModelMixin
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -94,19 +94,23 @@ async def login(
# Get user-specific interface for token operations # Get user-specific interface for token operations
userInterface = getInterface(user) userInterface = getInterface(user)
# Save token data # Create token
token_data = { token = Token(
"access_token": access_token, userId=user.id,
"token_type": "bearer", authority=AuthAuthority.LOCAL,
"expires_at": expires_at.timestamp() tokenAccess=access_token,
} tokenType="bearer",
userInterface.saveToken("Local", token_data) expiresAt=expires_at.timestamp()
)
# Save token
userInterface.saveToken(token)
# Create response data # Create response data
response_data = { response_data = {
"type": "local_auth_success", "type": "local_auth_success",
"access_token": access_token, "access_token": access_token,
"token_data": token_data "token_data": token.dict()
} }
return response_data return response_data

View file

@ -2,7 +2,7 @@
Routes for Microsoft authentication. 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 from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging import logging
import json import json
@ -12,9 +12,9 @@ import msal
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import AuthAuthority, User from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
from modules.interfaces.serviceAppTokens import MsftToken
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
from modules.shared.attributeUtils import ModelMixin
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -42,7 +42,11 @@ SCOPES = ["Mail.ReadWrite", "User.Read"]
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @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""" """Initiate Microsoft login"""
try: try:
# Create MSAL app # Create MSAL app
@ -52,10 +56,14 @@ async def login(request: Request) -> RedirectResponse:
client_credential=CLIENT_SECRET client_credential=CLIENT_SECRET
) )
# Generate auth URL # Generate auth URL with state
auth_url = msal_app.get_authorization_request_url( auth_url = msal_app.get_authorization_request_url(
scopes=SCOPES, scopes=SCOPES,
redirect_uri=REDIRECT_URI redirect_uri=REDIRECT_URI,
state=json.dumps({
"type": state,
"connectionId": connectionId
})
) )
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
@ -68,9 +76,14 @@ async def login(request: Request) -> RedirectResponse:
) )
@router.get("/auth/callback") @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""" """Handle Microsoft OAuth callback"""
try: try:
# Parse state
state_data = json.loads(state)
state_type = state_data.get("type", "login")
connection_id = state_data.get("connectionId")
# Create MSAL app # Create MSAL app
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
CLIENT_ID, CLIENT_ID,
@ -91,38 +104,124 @@ async def auth_callback(code: str, request: Request) -> HTMLResponse:
status_code=400 status_code=400
) )
# Create token data # Get user info from Microsoft
token_data = { user_info = msal_app.acquire_token_for_client(scopes=["User.Read"])
"access_token": token_response["access_token"], if "error" in user_info:
"refresh_token": token_response.get("refresh_token", ""), raise HTTPException(
"token_type": token_response.get("token_type", "bearer"), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
"expires_at": datetime.now().timestamp() + token_response.get("expires_in", 0) detail="Failed to get user info from Microsoft"
} )
# Save token data if state_type == "login":
appInterface = getInterface() # Handle login flow
appInterface.saveToken("Msft", token_data) rootInterface = getRootInterface()
user = rootInterface.getUserByUsername(user_info.get("preferred_username"))
# Return success page with token data if not user:
return HTMLResponse( # Create new user if doesn't exist
content=f""" user = rootInterface.createUser(
<html> username=user_info.get("preferred_username"),
<head><title>Authentication Successful</title></head> email=user_info.get("email"),
<body> fullName=user_info.get("name"),
<script> authenticationAuthority=AuthAuthority.MSFT,
if (window.opener) {{ externalId=user_info.get("id"),
window.opener.postMessage({{ externalUsername=user_info.get("preferred_username"),
type: 'msft_auth_success', externalEmail=user_info.get("email")
access_token: {json.dumps(token_response["access_token"])}, )
token_data: {json.dumps(token_data)}
}}, '*'); # Create token
}} token = Token(
setTimeout(() => window.close(), 1000); userId=user.id,
</script> authority=AuthAuthority.MSFT,
</body> tokenAccess=token_response["access_token"],
</html> tokenRefresh=token_response.get("refresh_token", ""),
""" tokenType=token_response.get("token_type", "bearer"),
) expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0)
)
# Save token
appInterface = getInterface(user)
appInterface.saveToken(token)
# Return success page with token data
return HTMLResponse(
content=f"""
<html>
<head><title>Authentication Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_auth_success',
access_token: {json.dumps(token_response["access_token"])},
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);
</script>
</body>
</html>
"""
)
except Exception as e: except Exception as e:
logger.error(f"Error in auth callback: {str(e)}") logger.error(f"Error in auth callback: {str(e)}")

View file

@ -30,7 +30,7 @@ from modules.interfaces.serviceChatModel import (
ChatDocument, ChatDocument,
UserInputRequest UserInputRequest
) )
from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse
from modules.interfaces.serviceAppModel import User from modules.interfaces.serviceAppModel import User
# Configure logger # Configure logger

View file

@ -129,7 +129,11 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
# Ensure the user has the correct context # Ensure the user has the correct context
if str(user.mandateId) != str(mandateId) or str(user.id) != str(userId): 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})") 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 return user

View file

@ -3,129 +3,187 @@ Shared utilities for model attributes and labels.
""" """
from pydantic import BaseModel, Field 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 inspect
import importlib import importlib
import os import os
from datetime import datetime
class BaseModelWithUI(BaseModel): class ModelMixin:
"""Base model class with UI support and common functionality""" """Mixin class that provides serialization methods for Pydantic models."""
@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 {}
}
def to_dict(self) -> Dict[str, Any]: 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'): if hasattr(self, 'model_dump'):
return self.model_dump() # Pydantic v2 return self.model_dump() # Pydantic v2
return self.dict() # Pydantic v1 return self.dict() # Pydantic v1
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI': def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
"""Create instance from dictionary with validation"""
return cls(**data)
@classmethod
def getModelAttributeDefinitions(cls) -> List[Dict[str, Any]]:
""" """
Get attribute definitions for this model class. Create a Pydantic model instance from a dictionary.
Override this method in model classes to provide custom attribute definitions.
Returns:
List[Dict[str, Any]]: List of attribute definitions
"""
attributes = []
# Handle both Pydantic v1 and v2
if hasattr(cls, 'model_fields'): # Pydantic v2
fields = cls.model_fields
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
"required": field.is_required() if hasattr(field, "is_required") else True,
"description": field.description if hasattr(field, "description") else "",
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
"placeholder": f"Please enter {name}",
"editable": True,
"visible": True,
"order": len(attributes)
})
else: # Pydantic v1
fields = cls.__fields__
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
"required": field.required,
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
"label": cls.fieldLabels.get(name, Label(default=name)).getLabel() if hasattr(cls, "fieldLabels") else name,
"placeholder": f"Please enter {name}",
"editable": True,
"visible": True,
"order": len(attributes)
})
return attributes
def getModelAttributes(modelClass):
"""
Get all attributes of a model class.
Args:
modelClass: The model class to get attributes from
Returns:
List[str]: List of attribute names
"""
return [attr for attr in dir(modelClass)
if not callable(getattr(modelClass, attr))
and not attr.startswith('_')
and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')]
class Label(BaseModel):
"""
Label for an attribute or a class with support for multiple languages.
Attributes:
default: Default label text
translations: Dictionary of translations for different languages
"""
default: str = Field(..., description="Default label text")
translations: Dict[str, str] = Field(default_factory=dict, description="Translations for different languages")
class Config:
title = "Label"
description = "A label with support for multiple languages"
schema_extra = {
"example": {
"default": "Document",
"translations": {
"en": "Document",
"fr": "Document"
}
}
}
def getLabel(self, language: str = None) -> str:
"""
Returns the label in the specified language, or the default value if not available.
Args: Args:
language: Language code to get the label for data: Dictionary containing the model data
Returns: Returns:
str: Label text in the specified language or default ModelMixin: New instance of the model class
""" """
if language and language in self.translations: return cls(**data)
return self.translations[language]
return self.default # Define the AttributeDefinition class here instead of importing it
class AttributeDefinition(BaseModel, ModelMixin):
"""Definition of a model attribute with its metadata."""
name: str
type: str
label: str
description: Optional[str] = None
required: bool = False
default: Any = None
options: Optional[List[Any]] = None
validation: Optional[Dict[str, Any]] = None
ui: Optional[Dict[str, Any]] = None
# Global registry for model labels
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
def to_dict(model: BaseModel) -> Dict[str, Any]:
"""
Convert a Pydantic model to a dictionary.
Handles both Pydantic v1 and v2.
Args:
model: The Pydantic model instance to convert
Returns:
Dict[str, Any]: Dictionary representation of the model
"""
if hasattr(model, 'model_dump'):
return model.model_dump() # Pydantic v2
return model.dict() # Pydantic v1
def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
"""
Create a Pydantic model instance from a dictionary.
Args:
model_class: The Pydantic model class to instantiate
data: Dictionary containing the model data
Returns:
BaseModel: New instance of the model class
"""
return model_class(**data)
def register_model_labels(model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]):
"""
Register labels for a model's attributes and the model itself.
Args:
model_name: Name of the model class
model_label: Dictionary mapping language codes to model labels
e.g. {"en": "Prompt", "fr": "Invite"}
labels: Dictionary mapping attribute names to their translations
e.g. {"name": {"en": "Name", "fr": "Nom"}}
"""
MODEL_LABELS[model_name] = {
"model": model_label,
"attributes": labels
}
def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
"""
Get labels for a model's attributes in the specified language.
Args:
model_name: Name of the model class
language: Language code (default: "en")
Returns:
Dictionary mapping attribute names to their labels in the specified language
"""
model_data = MODEL_LABELS.get(model_name, {})
attribute_labels = model_data.get("attributes", {})
return {
attr: translations.get(language, translations.get("en", attr))
for attr, translations in attribute_labels.items()
}
def get_model_label(model_name: str, language: str = "en") -> str:
"""
Get the label for a model in the specified language.
Args:
model_name: Name of the model class
language: Language code (default: "en")
Returns:
Model label in the specified language, or model name if no label exists
"""
model_data = MODEL_LABELS.get(model_name, {})
model_label = model_data.get("model", {})
return model_label.get(language, model_label.get("en", model_name))
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
"""
Get attribute definitions for a model class.
Args:
modelClass: The model class to get attributes for
userLanguage: Language code for translations (default: "en")
Returns:
Dictionary containing model label and attribute definitions
"""
if not modelClass:
return {}
attributes = []
model_name = modelClass.__name__
labels = get_model_labels(model_name, userLanguage)
model_label = get_model_label(model_name, userLanguage)
# Handle both Pydantic v1 and v2
if hasattr(modelClass, 'model_fields'): # Pydantic v2
fields = modelClass.model_fields
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation),
"required": field.is_required() if hasattr(field, "is_required") else True,
"description": field.description if hasattr(field, "description") else "",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}",
"editable": True,
"visible": True,
"order": len(attributes)
})
else: # Pydantic v1
fields = modelClass.__fields__
for name, field in fields.items():
attributes.append({
"name": name,
"type": field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_),
"required": field.required,
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
"label": labels.get(name, name),
"placeholder": f"Please enter {labels.get(name, name)}",
"editable": True,
"visible": True,
"order": len(attributes)
})
return {
"model": model_label,
"attributes": attributes
}
def getModelClasses() -> Dict[str, Type[BaseModel]]: def getModelClasses() -> Dict[str, Type[BaseModel]]:
""" """
@ -155,27 +213,24 @@ def getModelClasses() -> Dict[str, Type[BaseModel]]:
return modelClasses return modelClasses
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]: class AttributeResponse(BaseModel):
""" """Response model for entity attributes"""
Get attribute definitions for model classes. attributes: List[AttributeDefinition]
If modelClass is provided, returns attributes for that specific class.
If no modelClass is provided, returns attributes for all model classes.
Args: class Config:
modelClass: Optional specific model class to get attributes for schema_extra = {
userLanguage: Language code for translations (default: "en") "example": {
"attributes": [
Returns: {
Dict[str, Any]: Dictionary of model class names to their attribute definitions "name": "username",
""" "label": "Username",
if modelClass: "type": "string",
return getModelAttributes(modelClass) "required": True,
"placeholder": "Please enter username",
# Get all model classes "editable": True,
modelClasses = getModelClasses() "visible": True,
"order": 0
# Create dictionary of model class names to their attribute definitions }
return { ]
name: getModelAttributes(cls) }
for name, cls in modelClasses.items() }
}

View file

@ -7,6 +7,7 @@ from typing import Dict, Any, List, Optional
from datetime import datetime from datetime import datetime
from modules.interfaces.serviceChatModel import ChatDocument, ChatContent from modules.interfaces.serviceChatModel import ChatDocument, ChatContent
from modules.workflow.documentProcessor import getDocumentContents from modules.workflow.documentProcessor import getDocumentContents
import uuid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,43 +47,33 @@ class DocumentManager:
self.service = service self.service = service
return True return True
async def extractContent(self, fileId: int) -> Optional[ChatDocument]: async def extractContent(self, fileId: str) -> Optional[ChatDocument]:
""" """Extracts content from a file and creates a chat document."""
Extract content from a file.
Args:
fileId: ID of the file to process
Returns:
ChatDocument object with extracted content or None if processing failed
"""
try: try:
# Get file metadata and content from service # Get file content
fileMetadata = await self.service.base.getFileMetadata(fileId) fileContent = await self.getFileContent(fileId)
fileContent = await self.service.base.getFileContent(fileId) if not fileContent:
if not fileMetadata or not fileContent:
logger.error(f"Could not retrieve file data for fileId {fileId}")
return None return None
# Extract content using documentProcessor # Get file metadata
contents = getDocumentContents(fileMetadata, fileContent) fileMetadata = await self.getFileMetadata(fileId)
if not fileMetadata:
return None
# Create ChatDocument # Create chat document
return ChatDocument( return ChatDocument(
id=str(fileId), # Using fileId as document id id=str(uuid.uuid4()),
fileId=fileId, fileId=fileId,
filename=fileMetadata.get("name", "unknown"), filename=fileMetadata.get("name", "Unknown"),
fileSize=fileMetadata.get("size", 0), fileSize=fileMetadata.get("size", 0),
mimeType=fileMetadata.get("mimeType", "application/octet-stream"), content=fileContent.decode('utf-8', errors='ignore'),
contents=contents mimeType=fileMetadata.get("mimeType", "text/plain")
) )
except Exception as e: 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 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. Process multiple files and extract their contents.
@ -103,34 +94,18 @@ class DocumentManager:
continue continue
return documents return documents
async def getFileContent(self, fileId: int) -> Optional[bytes]: async def getFileContent(self, fileId: str) -> Optional[bytes]:
""" """Gets the content of a file."""
Get raw file content.
Args:
fileId: ID of the file
Returns:
File content as bytes or None if not found
"""
try: try:
return await self.service.base.getFileContent(fileId) return self.service.functions.getFileData(fileId)
except Exception as e: except Exception as e:
logger.error(f"Error getting file content for {fileId}: {str(e)}") logger.error(f"Error getting file content for {fileId}: {str(e)}")
return None return None
async def getFileMetadata(self, fileId: int) -> Optional[Dict[str, Any]]: async def getFileMetadata(self, fileId: str) -> Optional[Dict[str, Any]]:
""" """Gets the metadata of a file."""
Get file metadata.
Args:
fileId: ID of the file
Returns:
File metadata dictionary or None if not found
"""
try: try:
return await self.service.base.getFileMetadata(fileId) return self.service.functions.getFile(fileId)
except Exception as e: except Exception as e:
logger.error(f"Error getting file metadata for {fileId}: {str(e)}") logger.error(f"Error getting file metadata for {fileId}: {str(e)}")
return None return None
@ -153,18 +128,10 @@ class DocumentManager:
logger.error(f"Error saving file {filename}: {str(e)}") logger.error(f"Error saving file {filename}: {str(e)}")
return None return None
async def deleteFile(self, fileId: int) -> bool: async def deleteFile(self, fileId: str) -> bool:
""" """Deletes a file."""
Delete a file.
Args:
fileId: ID of the file to delete
Returns:
True if successful, False otherwise
"""
try: try:
return await self.service.base.deleteFile(fileId) return self.service.functions.deleteFile(fileId)
except Exception as e: except Exception as e:
logger.error(f"Error deleting file {fileId}: {str(e)}") logger.error(f"Error deleting file {fileId}: {str(e)}")
return False return False

View file

@ -1259,18 +1259,15 @@ filesDelivered = {self.parseJson2text(matchingDocuments)}
"stats": workflow.stats.to_dict() "stats": workflow.stats.to_dict()
} }
def _checkFileAccess(self, fileId: int) -> bool: def _checkFileAccess(self, fileId: str) -> bool:
"""Checks if the current user has access to a file.""" """Checks if the current user has access to the file."""
file = self.service.functions.getFile(fileId) try:
if not file: file = self.service.functions.getFile(fileId)
return file is not None
except Exception as e:
logger.error(f"Error checking file access: {str(e)}")
return False 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 # Singleton factory for the WorkflowManager
_workflowManagers = {} _workflowManagers = {}

View file

@ -12,6 +12,7 @@ passlib==1.7.4
argon2-cffi>=21.3.0 # Für Passwort-Hashing in gateway_interface.py 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-oauthlib==1.2.0 # Für Google OAuth
google-auth==2.27.0 # Für Google Authentication 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 bcrypt==4.0.1 # For password hashing
python-jose[cryptography]==3.3.0 # For JWT tokens python-jose[cryptography]==3.3.0 # For JWT tokens