This commit is contained in:
ValueOn AG 2025-05-26 07:04:30 +02:00
parent 628cca0ed4
commit cf94b1115b
50 changed files with 3504 additions and 3903 deletions

42
app.py
View file

@ -17,17 +17,41 @@ def initLogging():
logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING") logLevelName = APP_CONFIG.get("APP_LOGGING_LOG_LEVEL", "WARNING")
logLevel = getattr(logging, logLevelName) logLevel = getattr(logging, logLevelName)
# Create formatters
consoleFormatter = logging.Formatter(
fmt=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"),
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
)
# File formatter with more detailed error information
fileFormatter = logging.Formatter(
fmt="%(asctime)s - %(levelname)s - %(name)s - %(message)s - %(pathname)s:%(lineno)d\n%(funcName)s\n%(exc_info)s",
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
)
# Configure handlers based on config # Configure handlers based on config
handlers = [] handlers = []
# Add console handler if enabled # Add console handler if enabled
if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True): if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True):
consoleHandler = logging.StreamHandler() consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(consoleFormatter)
handlers.append(consoleHandler) handlers.append(consoleHandler)
# Add file handler if enabled # Add file handler if enabled
if APP_CONFIG.get("APP_LOGGING_FILE_ENABLED", True): if APP_CONFIG.get("APP_LOGGING_FILE_ENABLED", True):
# Get log file path and ensure it's absolute
logFile = APP_CONFIG.get("APP_LOGGING_LOG_FILE", "app.log") logFile = APP_CONFIG.get("APP_LOGGING_LOG_FILE", "app.log")
if not os.path.isabs(logFile):
# If relative path, make it relative to the gateway directory
gatewayDir = os.path.dirname(os.path.abspath(__file__))
logFile = os.path.join(gatewayDir, logFile)
# Ensure log directory exists
logDir = os.path.dirname(logFile)
if logDir:
os.makedirs(logDir, exist_ok=True)
rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB rotationSize = int(APP_CONFIG.get("APP_LOGGING_ROTATION_SIZE", 10485760)) # Default: 10MB
backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5)) backupCount = int(APP_CONFIG.get("APP_LOGGING_BACKUP_COUNT", 5))
@ -36,9 +60,10 @@ def initLogging():
maxBytes=rotationSize, maxBytes=rotationSize,
backupCount=backupCount backupCount=backupCount
) )
fileHandler.setFormatter(fileFormatter)
handlers.append(fileHandler) handlers.append(fileHandler)
# Configure the logger # Configure the root logger
logging.basicConfig( logging.basicConfig(
level=logLevel, level=logLevel,
format=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"), format=APP_CONFIG.get("APP_LOGGING_FORMAT", "%(asctime)s - %(levelname)s - %(name)s - %(message)s"),
@ -46,6 +71,7 @@ def initLogging():
handlers=handlers handlers=handlers
) )
# Silence noisy third-party libraries - use the same level as the root logger # Silence noisy third-party libraries - use the same level as the root logger
noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"] noisyLoggers = ["httpx", "urllib3", "asyncio", "fastapi.security.oauth2"]
for loggerName in noisyLoggers: for loggerName in noisyLoggers:
@ -96,29 +122,29 @@ app.add_middleware(
) )
# Include all routers # Include all routers
from modules.routes.routeGeneral import router as generalRouter from modules.routes.routeAdmin import router as generalRouter
app.include_router(generalRouter) app.include_router(generalRouter)
from modules.routes.routeAttributes import router as attributesRouter from modules.routes.routeAttributes import router as attributesRouter
app.include_router(attributesRouter) app.include_router(attributesRouter)
from modules.routes.routeMandates import router as mandateRouter from modules.routes.routeDataMandates import router as mandateRouter
app.include_router(mandateRouter) app.include_router(mandateRouter)
from modules.routes.routeUsers import router as userRouter from modules.routes.routeDataUsers import router as userRouter
app.include_router(userRouter) app.include_router(userRouter)
from modules.routes.routeFiles import router as fileRouter from modules.routes.routeDataFiles import router as fileRouter
app.include_router(fileRouter) app.include_router(fileRouter)
from modules.routes.routePrompts import router as promptRouter from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(promptRouter) app.include_router(promptRouter)
from modules.routes.routeWorkflows import router as workflowRouter from modules.routes.routeWorkflows import router as workflowRouter
app.include_router(workflowRouter) app.include_router(workflowRouter)
from modules.routes.routeMsft import router as msftRouter from modules.routes.routeSecurityMsft import router as msftRouter
app.include_router(msftRouter) app.include_router(msftRouter)
from modules.routes.routeGoogle import router as googleRouter from modules.routes.routeSecurityGoogle import router as googleRouter
app.include_router(googleRouter) app.include_router(googleRouter)

View file

@ -5,23 +5,23 @@ APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8000 APP_API_URL = http://localhost:8000
# Database Configuration Gateway # Database Configuration for Application
DB_GATEWAY_HOST=D:/Temp/_powerondb DB_APP_HOST=D:/Temp/_powerondb
DB_GATEWAY_DATABASE=gateway DB_APP_DATABASE=app
DB_GATEWAY_USER=dev_user DB_APPY_USER=dev_user
DB_GATEWAY_PASSWORD_SECRET=dev_password DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration LucyDOM # Database Configuration Chat
DB_LUCYDOM_HOST=D:/Temp/_powerondb DB_CHAT_HOST=D:/Temp/_powerondb
DB_LUCYDOM_DATABASE=lucydom DB_CHAT_DATABASE=chat
DB_LUCYDOM_USER=dev_user DB_CHAT_USER=dev_user
DB_LUCYDOM_PASSWORD_SECRET=dev_password DB_CHAT_PASSWORD_SECRET=dev_password
# Database Configuration MSFT # Database Configuration Management
DB_MSFT_HOST=D:/Temp/_powerondb DB_MANAGEMENT_HOST=D:/Temp/_powerondb
DB_MSFT_DATABASE=msft DB_MANAGEMENT_DATABASE=management
DB_MSFT_USER=dev_user DB_MANAGEMENT_USER=dev_user
DB_MSFT_PASSWORD_SECRET=dev_password DB_MANAGEMENT_PASSWORD_SECRET=dev_password
# Security Configuration # Security Configuration
APP_JWT_SECRET_SECRET=dev_jwt_secret_token APP_JWT_SECRET_SECRET=dev_jwt_secret_token

View file

@ -5,23 +5,23 @@ APP_ENV_TYPE = prod
APP_ENV_LABEL = Production Instance APP_ENV_LABEL = Production Instance
APP_API_URL = https://gateway.poweron-center.net APP_API_URL = https://gateway.poweron-center.net
# Database Configuration Gateway # Database Configuration Application
DB_GATEWAY_HOST=/home/_powerondb DB_APP_HOST=/home/_powerondb
DB_GATEWAY_DATABASE=gateway DB_APP_DATABASE=app
DB_GATEWAY_USER=dev_user DB_APPY_USER=dev_user
DB_GATEWAY_PASSWORD_SECRET=prod_password DB_APP_PASSWORD_SECRET=dev_password
# Database Configuration LucyDOM # Database Configuration Chat
DB_LUCYDOM_HOST=/home/_powerondb DB_CHAT_HOST=/home/_powerondb
DB_LUCYDOM_DATABASE=lucydom DB_CHAT_DATABASE=chat
DB_LUCYDOM_USER=dev_user DB_CHAT_USER=dev_user
DB_LUCYDOM_PASSWORD_SECRET=prod_password DB_CHAT_PASSWORD_SECRET=dev_password
# Database Configuration MSFT # Database Configuration Management
DB_MSFT_HOST=/home/_powerondb DB_MANAGEMENT_HOST=/home/_powerondb
DB_MSFT_DATABASE=msft DB_MANAGEMENT_DATABASE=management
DB_MSFT_USER=dev_user DB_MANAGEMENT_USER=dev_user
DB_MSFT_PASSWORD_SECRET=dev_password DB_MANAGEMENT_PASSWORD_SECRET=dev_password
# Security Configuration # Security Configuration
APP_JWT_SECRET_SECRET=dev_jwt_secret_token APP_JWT_SECRET_SECRET=dev_jwt_secret_token

View file

@ -13,9 +13,20 @@ from typing import Dict, Any, List, Optional
import pandas as pd import pandas as pd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import seaborn as sns import seaborn as sns
from datetime import datetime
import hashlib
import uuid
import re
import shutil
from pathlib import Path
import traceback
import sys
import importlib.util
import inspect
from pydantic import BaseModel
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.interfaces.lucydomModel import ChatContent from modules.interfaces.serviceChatModel import ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -68,7 +79,7 @@ class AgentAnalyst(AgentBase):
# Create analysis plan # Create analysis plan
if workflow: if workflow:
self.workflowManager.logAdd(workflow, "Extracting data from documents...", level="info", progress=35) self.service.logAdd(workflow, "Extracting data from documents...", level="info", progress=35)
analysisPlan = await self._createAnalysisPlan(prompt) analysisPlan = await self._createAnalysisPlan(prompt)
# Check if this is truly an analysis task # Check if this is truly an analysis task
@ -80,15 +91,14 @@ class AgentAnalyst(AgentBase):
# Analyze data # Analyze data
if workflow: if workflow:
self.workflowManager.logAdd(workflow, "Analyzing task requirements...", level="info", progress=45) self.service.logAdd(workflow, "Analyzing task requirements...", level="info", progress=45)
analysisResults = await self._analyzeData(task, analysisPlan) analysisResults = await self._analyzeData(task, analysisPlan)
# Format results into requested output documents # Format results into requested output documents
totalSpecs = len(outputSpecs) totalSpecs = len(outputSpecs)
for i, spec in enumerate(outputSpecs): for i, spec in enumerate(outputSpecs):
progress = 50 + int((i / totalSpecs) * 40) # Progress from 50% to 90% progress = 50 + int((i / totalSpecs) * 40) # Progress from 50% to 90%
if self.workflowManager: self.service.logAdd(workflow, f"Creating output {i+1}/{totalSpecs}...", level="info", progress=progress)
self.workflowManager.logAdd(workflow, f"Creating output {i+1}/{totalSpecs}...", level="info", progress=progress)
documents = await self._createOutputDocuments( documents = await self._createOutputDocuments(
prompt, prompt,

View file

@ -4,14 +4,24 @@ Provides comprehensive documentation generation capabilities.
""" """
import logging import logging
from typing import Dict, Any, List from typing import Dict, Any, List, Optional
import json import json
import re import re
from datetime import datetime from datetime import datetime
import os import os
import hashlib
import base64
import uuid
import shutil
from pathlib import Path
import traceback
import sys
import importlib.util
import inspect
from pydantic import BaseModel
from modules.workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
from modules.interfaces.lucydomModel import ChatContent from modules.interfaces.serviceChatModel import ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -6,7 +6,7 @@ Handles email-related tasks using Microsoft Graph API.
import logging import logging
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from ..workflow.agentBase import AgentBase from modules.workflow.agentBase import AgentBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -0,0 +1,348 @@
"""
SharePoint Agent Module.
Handles SharePoint document search and data extraction using Microsoft Graph API.
"""
import logging
import json
from typing import Dict, Any, List, Optional
from modules.workflow.agentBase import AgentBase
logger = logging.getLogger(__name__)
class AgentSharepoint(AgentBase):
"""Agent for handling SharePoint document operations."""
def __init__(self):
"""Initialize the SharePoint agent."""
super().__init__()
self.name = "sharepoint"
self.label = "SharePoint Agent"
self.description = "Searches and extracts data from SharePoint documents using Microsoft Graph API"
self.capabilities = [
"document_search",
"content_extraction",
"metadata_analysis",
"document_processing"
]
async def processTask(self, task: Dict[str, Any]) -> Dict[str, Any]:
"""
Process a SharePoint-related task.
Args:
task: Task object containing:
- prompt: Instructions for the agent
- inputDocuments: List of documents to process
- outputSpecifications: List of required output documents
- context: Additional context including workflow info
Returns:
Dictionary containing:
- feedback: Text response explaining what was done
- documents: List of created documents
"""
try:
# Extract task information
prompt = task.get("prompt", "")
inputDocuments = task.get("inputDocuments", [])
outputSpecs = task.get("outputSpecifications", [])
# Check AI service
if not self.service.base:
return {
"feedback": "The SharePoint agent requires an AI service to function.",
"documents": []
}
# Check if Microsoft connector is available
if not hasattr(self.service, 'msft'):
return {
"feedback": "Microsoft connector not available. Please ensure Microsoft integration is properly configured.",
"documents": []
}
# Get Microsoft token
token_data = self.service.msft.getMsftToken()
if not token_data:
# Create authentication trigger document
auth_doc = self._createFrontendAuthTriggerDocument()
return {
"feedback": "Microsoft authentication required. Please authenticate to continue.",
"documents": [auth_doc]
}
# Parse the search query from the prompt
searchQuery = await self._parseSearchQuery(prompt)
# Search SharePoint documents
searchResults = await self._searchSharePointDocuments(searchQuery)
# Process search results
documents = []
for spec in outputSpecs:
label = spec.get("label", "")
description = spec.get("description", "")
if label.endswith(".json"):
# Create JSON summary of search results
summaryDoc = self._createSearchSummaryJson(searchResults, description)
documents.append(summaryDoc)
elif label.endswith(".csv"):
# Create CSV summary of search results
summaryDoc = self._createSearchSummaryCsv(searchResults, description)
documents.append(summaryDoc)
else:
# Create text summary of search results
summaryDoc = self._createSearchSummaryText(searchResults, description)
documents.append(summaryDoc)
# Prepare feedback message
feedback = f"Found {len(searchResults)} documents matching your search criteria. "
if searchResults:
feedback += "The results have been saved as documents."
else:
feedback += "No matching documents were found."
return {
"feedback": feedback,
"documents": documents
}
except Exception as e:
logger.error(f"Error in SharePoint agent: {str(e)}")
return {
"feedback": f"Error processing SharePoint task: {str(e)}",
"documents": []
}
def _createFrontendAuthTriggerDocument(self) -> Dict[str, Any]:
"""Create a document that triggers Microsoft authentication in the frontend."""
return self.formatAgentDocumentOutput(
"microsoft_auth.html",
"""
<div>
<h2>Microsoft Authentication Required</h2>
<p>Please click the button below to authenticate with Microsoft:</p>
<button onclick="window.location.href='/api/auth/microsoft'">Authenticate with Microsoft</button>
</div>
""",
"text/html"
)
async def _parseSearchQuery(self, prompt: str) -> Dict[str, Any]:
"""
Parse the search query from the prompt using AI.
Args:
prompt: The task prompt
Returns:
Dictionary containing search parameters
"""
try:
# Use AI to parse the search query
response = await self.service.base.callAi([
{"role": "system", "content": "You are a SharePoint search query parser. Extract search parameters from the user's request."},
{"role": "user", "content": f"""
Parse the following SharePoint search request into structured parameters:
{prompt}
Return a JSON object with these fields:
- query: The main search query
- site: Optional SharePoint site name
- folder: Optional folder path
- fileTypes: List of file types to search for
- dateRange: Optional date range for filtering
- maxResults: Maximum number of results to return
Only return valid JSON. No preamble or explanations.
"""}
])
# Extract JSON from response
jsonStart = response.find('{')
jsonEnd = response.rfind('}') + 1
if jsonStart >= 0 and jsonEnd > jsonStart:
return json.loads(response[jsonStart:jsonEnd])
else:
# Fallback to simple query
return {
"query": prompt,
"maxResults": 10
}
except Exception as e:
logger.warning(f"Error parsing search query: {str(e)}")
return {
"query": prompt,
"maxResults": 10
}
async def _searchSharePointDocuments(self, searchParams: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Search SharePoint documents using Microsoft Graph API.
Args:
searchParams: Search parameters
Returns:
List of search results
"""
try:
# Get Microsoft token
token = self.service.msft.getMsftToken()
if not token:
return []
# Prepare search query
query = searchParams.get("query", "")
site = searchParams.get("site", "")
folder = searchParams.get("folder", "")
fileTypes = searchParams.get("fileTypes", [])
maxResults = searchParams.get("maxResults", 10)
# Build search URL
searchUrl = "https://graph.microsoft.com/v1.0/sites/root/drives"
if site:
searchUrl = f"https://graph.microsoft.com/v1.0/sites/{site}/drives"
# Get drives (document libraries)
response = self.service.msft.makeGraphRequest("GET", searchUrl)
if not response or "value" not in response:
return []
results = []
for drive in response["value"]:
# Search in each drive
driveId = drive["id"]
searchEndpoint = f"https://graph.microsoft.com/v1.0/drives/{driveId}/root/search(q='{query}')"
# Add file type filters if specified
if fileTypes:
typeFilter = " or ".join([f"fileType eq '{ft}'" for ft in fileTypes])
searchEndpoint += f"&filter={typeFilter}"
# Add folder filter if specified
if folder:
searchEndpoint += f"&filter=parentReference/path eq '/{folder}'"
# Add result limit
searchEndpoint += f"&top={maxResults}"
# Make the search request
searchResponse = self.service.msft.makeGraphRequest("GET", searchEndpoint)
if searchResponse and "value" in searchResponse:
for item in searchResponse["value"]:
# Get file content
fileContent = await self._getFileContent(driveId, item["id"])
results.append({
"name": item["name"],
"id": item["id"],
"driveId": driveId,
"webUrl": item["webUrl"],
"lastModified": item["lastModifiedDateTime"],
"size": item["size"],
"content": fileContent
})
return results
except Exception as e:
logger.error(f"Error searching SharePoint: {str(e)}")
return []
async def _getFileContent(self, driveId: str, fileId: str) -> str:
"""
Get file content from SharePoint.
Args:
driveId: Drive ID
fileId: File ID
Returns:
File content as string
"""
try:
# Get file content URL
contentUrl = f"https://graph.microsoft.com/v1.0/drives/{driveId}/items/{fileId}/content"
# Download file content
response = self.service.msft.makeGraphRequest("GET", contentUrl, raw=True)
if response:
return response.decode('utf-8')
return ""
except Exception as e:
logger.error(f"Error getting file content: {str(e)}")
return ""
def _createSearchSummaryJson(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
"""Create a JSON summary of search results."""
summary = {
"description": description,
"totalResults": len(results),
"results": []
}
for result in results:
summary["results"].append({
"name": result["name"],
"url": result["webUrl"],
"lastModified": result["lastModified"],
"size": result["size"]
})
return self.formatAgentDocumentOutput(
"sharepoint_search_results.json",
json.dumps(summary, indent=2),
"application/json"
)
def _createSearchSummaryCsv(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
"""Create a CSV summary of search results."""
csvLines = ["Name,URL,Last Modified,Size (bytes)"]
for result in results:
name = result["name"].replace('"', '""')
url = result["webUrl"].replace('"', '""')
lastModified = result["lastModified"].replace('"', '""')
size = str(result["size"])
csvLines.append(f'"{name}","{url}","{lastModified}",{size}')
return self.formatAgentDocumentOutput(
"sharepoint_search_results.csv",
"\n".join(csvLines),
"text/csv"
)
def _createSearchSummaryText(self, results: List[Dict[str, Any]], description: str) -> Dict[str, Any]:
"""Create a text summary of search results."""
textLines = [
f"SharePoint Search Results",
f"Description: {description}",
f"Total Results: {len(results)}",
"\nResults:"
]
for result in results:
textLines.extend([
f"\nName: {result['name']}",
f"URL: {result['webUrl']}",
f"Last Modified: {result['lastModified']}",
f"Size: {result['size']} bytes"
])
return self.formatAgentDocumentOutput(
"sharepoint_search_results.txt",
"\n".join(textLines),
"text/plain"
)
def getAgentSharepoint() -> AgentSharepoint:
"""Factory function to create and return a SharePointAgent instance."""
return AgentSharepoint()

View file

@ -80,7 +80,7 @@ class AgentWebcrawler(AgentBase):
# Create research plan # Create research plan
if workflow: if workflow:
self.workflowManager.logAdd(workflow, "Creating research plan...", level="info", progress=35) self.service.logAdd(workflow, "Creating research plan...", level="info", progress=35)
researchPlan = await self._createResearchPlan(prompt) researchPlan = await self._createResearchPlan(prompt)
# Check if this is truly a web research task # Check if this is truly a web research task
@ -92,12 +92,12 @@ class AgentWebcrawler(AgentBase):
# Gather raw material through web research # Gather raw material through web research
if workflow: if workflow:
self.workflowManager.logAdd(workflow, "Gathering research material...", level="info", progress=45) self.service.logAdd(workflow, "Gathering research material...", level="info", progress=45)
rawResults = await self._gatherResearchMaterial(researchPlan, workflow) rawResults = await self._gatherResearchMaterial(researchPlan, workflow)
# Format results into requested output documents # Format results into requested output documents
if workflow: if workflow:
self.workflowManager.logAdd(workflow, "Creating output documents...", level="info", progress=55) self.service.logAdd(workflow, "Creating output documents...", level="info", progress=55)
documents = await self._createOutputDocuments( documents = await self._createOutputDocuments(
prompt, prompt,
rawResults, rawResults,
@ -213,8 +213,7 @@ class AgentWebcrawler(AgentBase):
directUrls = researchPlan.get("directUrls", [])[:self.maxUrl] directUrls = researchPlan.get("directUrls", [])[:self.maxUrl]
for i, url in enumerate(directUrls): for i, url in enumerate(directUrls):
progress = 45 + int((i / len(directUrls)) * 5) # Progress from 45% to 50% progress = 45 + int((i / len(directUrls)) * 5) # Progress from 45% to 50%
if hasattr(self, 'workflowManager') and self.workflowManager: self.service.logAdd(workflow, f"Processing direct URL {i+1}/{len(directUrls)}...", level="info", progress=progress)
self.workflowManager.logAdd(workflow, f"Processing direct URL {i+1}/{len(directUrls)}...", level="info", progress=progress)
logger.info(f"Processing direct URL: {url}") logger.info(f"Processing direct URL: {url}")
try: try:
# Fetch and extract content # Fetch and extract content
@ -240,8 +239,7 @@ class AgentWebcrawler(AgentBase):
searchTerms = researchPlan.get("searchTerms", [])[:self.maxSearchTerms] searchTerms = researchPlan.get("searchTerms", [])[:self.maxSearchTerms]
for i, term in enumerate(searchTerms): for i, term in enumerate(searchTerms):
progress = 50 + int((i / len(searchTerms)) * 5) # Progress from 50% to 55% progress = 50 + int((i / len(searchTerms)) * 5) # Progress from 50% to 55%
if hasattr(self, 'workflowManager') and self.workflowManager: self.service.logAdd(workflow, f"Searching term {i+1}/{len(searchTerms)}...", level="info", progress=progress)
self.workflowManager.logAdd(workflow, f"Searching term {i+1}/{len(searchTerms)}...", level="info", progress=progress)
logger.info(f"Searching for: {term}") logger.info(f"Searching for: {term}")
try: try:
# Perform search # Perform search

View file

@ -1,151 +0,0 @@
"""
Data models for the gateway system.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
# Get all attributes of the model
def getModelAttributes(modelClass):
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"""
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": "User",
"translations": {
"en": "User",
"fr": "Utilisateur"
}
}
}
def getLabel(self, language: str = None):
"""Returns the label in the specified language, or the default value if not available"""
if language and language in self.translations:
return self.translations[language]
return self.default
class Mandate(BaseModel):
"""Data model for a mandate"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate")
language: str = Field(description="Default language of the mandate")
label: Label = Field(
default=Label(default="Mandate", translations={"en": "Mandate", "fr": "Mandat"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"name": Label(default="Name of the mandate", translations={"en": "Mandate name", "fr": "Nom du mandat"}),
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"})
}
class UserConnection(BaseModel):
"""Data model for a user's connection to an external service"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
authority: str = Field(description="Authentication authority (microsoft, google, etc.)")
externalId: str = Field(description="User ID in the external system")
externalUsername: str = Field(description="Username in the external system")
externalEmail: Optional[str] = Field(None, description="Email in the external system")
connectedAt: datetime = Field(default_factory=datetime.now, description="When the connection was established")
label: Label = Field(
default=Label(default="User Connection", translations={"en": "User Connection", "fr": "Connexion utilisateur"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"authority": Label(default="Authority", translations={"en": "Authority", "fr": "Autorité"}),
"externalId": Label(default="External ID", translations={"en": "External ID", "fr": "ID externe"}),
"externalUsername": Label(default="External Username", translations={"en": "External Username", "fr": "Nom d'utilisateur externe"}),
"externalEmail": Label(default="External Email", translations={"en": "External Email", "fr": "Email externe"}),
"connectedAt": Label(default="Connected At", translations={"en": "Connected At", "fr": "Connecté le"})
}
class User(BaseModel):
"""Data model for a user"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login")
email: Optional[str] = Field(None, description="Email address of the user")
fullName: Optional[str] = Field(None, description="Full name of the user")
language: str = Field(description="Preferred language of the user")
disabled: Optional[bool] = Field(False, description="Indicates whether the user is disabled")
privilege: str = Field(description="Permission level") #sysadmin,admin,user
authenticationAuthority: str = Field(default="local", description="Primary authentication authority (local, microsoft)")
mandateId: str = Field(description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
label: Label = Field(
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"username": Label(default="Username", translations={"en": "Username", "fr": "Nom d'utilisateur"}),
"email": Label(default="Email", translations={"en": "Email", "fr": "E-mail"}),
"fullName": Label(default="Full name", translations={"en": "Full name", "fr": "Nom complet"}),
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
"privilege": Label(default="Permission level", translations={"en": "Access level", "fr": "Niveau d'accès"}),
"authenticationAuthority": Label(default="Authentication Authority", translations={"en": "Authentication Authority", "fr": "Autorité d'authentification"}),
"connections": Label(default="External Connections", translations={"en": "External Connections", "fr": "Connexions externes"})
}
class UserInDB(User):
"""Extended user class with password hash"""
hashedPassword: str = Field(description="Hash of the user password")
label: Label = Field(
default=Label(default="User Access", translations={"en": "User Access", "fr": "Accès de l'utilisateur"}),
description="Label for the class"
)
# Additional label for the password field
fieldLabels: Dict[str, Label] = {
"hashedPassword": Label(default="Password hash", translations={"en": "Password hash", "fr": "Hachage de mot de passe"})
}
class Token(BaseModel):
"""Data model for an authentication token"""
accessToken: str = Field(description="The issued access token")
tokenType: str = Field(description="Type of token (usually 'bearer')")
label: Label = Field(
default=Label(default="Token", translations={"en": "Token", "fr": "Jeton"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"accessToken": Label(default="Access token", translations={"en": "Access token", "fr": "Jeton d'accès"}),
"tokenType": Label(default="Token type", translations={"en": "Token type", "fr": "Type de jeton"})
}
class TokenData(BaseModel):
"""Data for token decoding and validation"""
username: Optional[str] = None
mandateId: Optional[str] = None
exp: Optional[datetime] = None

View file

@ -1,113 +0,0 @@
"""
Access control module for Google interface.
Handles user access management and permission checks for Google tokens.
"""
from typing import Dict, Any, List, Optional
class GoogleAccess:
"""
Access control class for Google interface.
Handles user access management and permission checks for Google tokens.
"""
def __init__(self, currentUser: Dict[str, Any], db):
"""Initialize with user context."""
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
self.db = db
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
userPrivilege = self.currentUser.get("privilege", "user")
filtered_records = []
# Apply filtering based on privilege
if userPrivilege == "sysadmin":
filtered_records = recordset # System admins see all records
elif userPrivilege == "admin":
# Admins see records in their mandate
filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
else: # Regular users
# Users only see their own Google tokens
filtered_records = [r for r in recordset
if r.get("_mandateId") == self._mandateId and r.get("_userId") == self._userId]
# Add access control attributes to each record
for record in filtered_records:
record_id = record.get("id")
# Set access control flags based on user permissions
if table == "googleTokens":
record["_hideView"] = False # Everyone can view their own tokens
record["_hideEdit"] = not self.canModify("googleTokens", record_id)
record["_hideDelete"] = not self.canModify("googleTokens", record_id)
else:
# Default access control for other tables
record["_hideView"] = False
record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records
def canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
userPrivilege = self.currentUser.get("privilege", "user")
# System admins can modify anything
if userPrivilege == "sysadmin":
return True
# Check specific record permissions
if recordId is not None:
# Get the record to check ownership
records = self.db.getRecordset(table, recordFilter={"id": recordId})
if not records:
return False
record = records[0]
# Admins can modify anything in their mandate
if userPrivilege == "admin" and record.get("_mandateId") == self._mandateId:
return True
# Users can only modify their own Google tokens
if (record.get("_mandateId") == self._mandateId and
record.get("_userId") == self._userId):
return True
return False
else:
# For general table modify permission (e.g., create)
# Admins can create anything in their mandate
if userPrivilege == "admin":
return True
# Regular users can create their own Google tokens
if table == "googleTokens":
return True
return False

View file

@ -1,287 +0,0 @@
"""
Google interface for handling Google authentication and API operations.
"""
import logging
import requests
from typing import Dict, Any, Optional, Tuple
from datetime import datetime
import secrets
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request
import os
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.googleModel import GoogleToken, GoogleUserInfo, GoogleConfig
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.interfaces.googleAccess import GoogleAccess
from modules.interfaces.gatewayInterface import getRootUser
logger = logging.getLogger(__name__)
# Singleton factory for GoogleInterface instances per context
_googleInterfaces = {}
# Root interface instance
_rootGoogleInterface = None
class GoogleInterface:
"""Interface for Google authentication and API operations"""
def __init__(self, currentUser: Dict[str, Any] = None):
"""Initialize the Google interface"""
# Initialize variables
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") if currentUser else None
self.userId = currentUser.get("id") if currentUser else None
self.access = None # Will be set when user context is provided
# Initialize configuration
self.clientId = APP_CONFIG.get("Service_GOOGLE_CLIENT_ID")
self.clientSecret = APP_CONFIG.get("Service_GOOGLE_CLIENT_SECRET")
self.redirectUri = APP_CONFIG.get("Service_GOOGLE_REDIRECT_URI")
self.authorityUrl = "https://accounts.google.com"
self.tokenUrl = "https://oauth2.googleapis.com/token"
self.userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo"
self.scopes = ["openid", "profile", "email"]
# Initialize database
self._initializeDatabase()
# Initialize OAuth2 flow
self.flow = Flow.from_client_config(
{
"web": {
"client_id": self.clientId,
"client_secret": self.clientSecret,
"auth_uri": f"{self.authorityUrl}/o/oauth2/auth",
"token_uri": self.tokenUrl,
"redirect_uris": [self.redirectUri]
}
},
scopes=self.scopes
)
# Set user context if provided
if currentUser:
self.setUserContext(currentUser)
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_GOOGLE_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_GOOGLE_DATABASE", "google")
dbUser = APP_CONFIG.get("DB_GOOGLE_USER")
dbPassword = APP_CONFIG.get("DB_GOOGLE_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
mandateId=self.mandateId,
userId=self.userId
)
# Set context
self.db.updateContext(self.mandateId, self.userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def initiateLogin(self) -> str:
"""Initiate Google login flow"""
try:
# Generate auth URL
auth_url, _ = self.flow.authorization_url(
access_type="offline",
include_granted_scopes="true",
state=self._generateState()
)
return auth_url
except Exception as e:
logger.error(f"Error initiating Google login: {str(e)}")
return None
def handleAuthCallback(self, code: str) -> Optional[GoogleToken]:
"""Handle Google OAuth callback"""
try:
# Exchange code for token
self.flow.fetch_token(code=code)
credentials = self.flow.credentials
# Get user info
user_info = self.getUserInfoFromToken(credentials.token)
if not user_info:
return None
# Create token model
token = GoogleToken(
access_token=credentials.token,
refresh_token=credentials.refresh_token,
expires_in=credentials.expiry.timestamp() - datetime.now().timestamp(),
token_type=credentials.token_type,
expires_at=credentials.expiry.timestamp(),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error handling auth callback: {str(e)}")
return None
def verifyToken(self, token: str) -> bool:
"""Verify Google token"""
try:
# Get user info from token
user_info = self.getUserInfoFromToken(token)
if not user_info:
return False
# Get current user's Google connection
user = self.db.getRecordset("users", recordFilter={"id": self.userId})[0]
google_connection = next((conn for conn in user.get("connections", [])
if conn.get("authority") == "google"), None)
if not google_connection:
return False
# Verify the token belongs to this user
return user_info.id == google_connection.get("externalId")
except Exception as e:
logger.error(f"Error verifying Google token: {str(e)}")
return False
def getUserInfoFromToken(self, token: str) -> Optional[GoogleUserInfo]:
"""Get user info from Google API"""
try:
# Call Google API
response = requests.get(
self.userInfoUrl,
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code != 200:
logger.error(f"Failed to get user info: {response.text}")
return None
data = response.json()
# Create user info model
return GoogleUserInfo(
id=data["sub"], # Google uses 'sub' as the unique identifier
email=data["email"],
name=data.get("name", ""),
picture=data.get("picture") # Google provides profile picture URL
)
except Exception as e:
logger.error(f"Error getting user info: {str(e)}")
return None
def refreshToken(self, refresh_token: str) -> Optional[GoogleToken]:
"""Refresh Google token"""
try:
# Create credentials object
credentials = Credentials(
None, # No access token
refresh_token=refresh_token,
token_uri=self.tokenUrl,
client_id=self.clientId,
client_secret=self.clientSecret
)
# Refresh token
credentials.refresh(Request())
# Get user info
user_info = self.getUserInfoFromToken(credentials.token)
if not user_info:
return None
# Create token model
token = GoogleToken(
access_token=credentials.token,
refresh_token=credentials.refresh_token or refresh_token,
expires_in=credentials.expiry.timestamp() - datetime.now().timestamp(),
token_type=credentials.token_type,
expires_at=credentials.expiry.timestamp(),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return None
def _generateState(self) -> str:
"""Generate secure state token"""
return secrets.token_urlsafe(32)
def setUserContext(self, currentUser: Dict[str, Any]):
"""Set user context for the interface"""
if not currentUser:
logger.info("Initializing interface without user context")
return
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId")
self.userId = currentUser.get("id")
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and id are required")
# Initialize access control with user context
self.access = GoogleAccess(self.currentUser, self.db)
# Update database context
self.db.updateContext(self.mandateId, self.userId)
logger.debug(f"User context set: userId={self.userId}")
def getRootInterface() -> GoogleInterface:
"""
Returns a GoogleInterface instance with root privileges.
This is used for initial setup and user creation.
"""
global _rootGoogleInterface
if _rootGoogleInterface is None:
# Get root user from gateway
rootUser = getRootUser()
_rootGoogleInterface = GoogleInterface(rootUser)
return _rootGoogleInterface
def getInterface(currentUser: Dict[str, Any] = None) -> GoogleInterface:
"""
Returns a GoogleInterface instance.
If currentUser is provided, initializes with user context.
Otherwise, returns an instance with only database access.
"""
# Create new instance if not exists
if "default" not in _googleInterfaces:
_googleInterfaces["default"] = GoogleInterface(currentUser or {})
interface = _googleInterfaces["default"]
if currentUser:
interface.setUserContext(currentUser)
else:
logger.info("Returning interface without user context")
return interface

View file

@ -1,35 +0,0 @@
"""
Models for Google authentication and API operations.
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from datetime import datetime
class GoogleToken(BaseModel):
"""Model for Google OAuth tokens"""
access_token: str
refresh_token: Optional[str] = None
expires_in: int
token_type: str = "bearer"
expires_at: float
user_info: Dict[str, Any]
mandateId: str
userId: str
class GoogleUserInfo(BaseModel):
"""Model for Google user information"""
id: str # Google uses 'sub' as the unique identifier
email: str
name: str
picture: Optional[str] = None # Google provides profile picture URL
class GoogleConfig(BaseModel):
"""Configuration for Google authentication service"""
client_id: str
client_secret: str
redirect_uri: str
scopes: list[str]
authority_url: str = "https://accounts.google.com"
token_url: str = "https://oauth2.googleapis.com/token"
user_info_url: str = "https://www.googleapis.com/oauth2/v3/userinfo"

View file

@ -1,246 +0,0 @@
"""
LucyDOM model classes for the workflow and document system.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
# Get all attributes of the model
def getModelAttributes(modelClass):
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')]
# CORE MODELS
class Label(BaseModel):
"""Label for an attribute or a class with support for multiple 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):
"""Returns the label in the specified language, or the default value if not available"""
if language and language in self.translations:
return self.translations[language]
return self.default
class Prompt(BaseModel):
"""Data model for a prompt"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the prompt")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Display name of the prompt")
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
label: Label = Field(
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
"name": Label(default="Name", translations={"en": "Label", "fr": "Nom"}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"})
}
class FileItem(BaseModel):
"""Data model for a file"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
mimeType: str = Field(description="Type of the file MIME type")
fileName: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file in bytes")
fileHash: str = Field(description="Hash code for deduplication")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
mandateId: str = Field(description="ID of the mandate this file belongs to")
label: Label = Field(
default=Label(default="Data Object", translations={"en": "Data Object", "fr": "Objet de données"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mimeType": Label(default="Type", translations={"en": "Type", "fr": "Type"}),
"fileName": Label(default="Filename", translations={"en": "fileName", "fr": "Nom de fichier"}),
"fileSize": Label(default="Size", translations={"en": "Size", "fr": "Taille"}),
"fileHash": Label(default="File Hash", translations={"en": "Hash", "fr": "Hash"}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"})
}
class FileData(BaseModel):
"""Data model for file content"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the data object")
data: str = Field(description="content of the file, text or base64 encoded based on base64Encoded flag")
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
workflowId: Optional[str] = Field(None, description="ID of the associated workflow, if any")
# WORKFLOW MODELS
class ChatContent(BaseModel):
"""Content of a document in the chat"""
sequenceNr: int = Field(1, description="Sequence number of the content in the source document")
name: str = Field(description="Designation")
mimeType: str = Field(description="MIME type")
data: str = Field(description="Actual content")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata about the content")
class ChatDocument(BaseModel):
"""Document in the chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the document")
fileId: str = Field(description="ID of the referenced file in the database")
fileName: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file in bytes")
mimeType: str = Field(description="MIME type")
contents: List[ChatContent] = Field(default=[], description="Document contents")
class ChatStat(BaseModel):
"""Statistics for performance and data usage"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the stats")
workflowId: str = Field(description="ID of the associated workflow")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Token count (for AI models)")
bytesSent: Optional[int] = Field(None, description="Bytes sent")
bytesReceived: Optional[int] = Field(None, description="Bytes received")
label: Label = Field(
default=Label(default="Chat Statistics", translations={"en": "Chat Statistics", "fr": "Statistiques de chat"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du workflow"}),
"processingTime": Label(default="Processing Time", translations={"en": "Processing Time", "fr": "Temps de traitement"}),
"tokenCount": Label(default="Token Count", translations={"en": "Token Count", "fr": "Nombre de tokens"}),
"bytesSent": Label(default="Bytes Sent", translations={"en": "Bytes Sent", "fr": "Octets envoyés"}),
"bytesReceived": Label(default="Bytes Received", translations={"en": "Bytes Received", "fr": "Octets reçus"})
}
class ChatMessage(BaseModel):
"""Message object in the chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the message")
workflowId: str = Field(description="Reference to the parent workflow")
parentMessageId: Optional[str] = Field(None, description="Reference to the replied message")
agentName: Optional[str] = Field(None, description="Name of the agent used")
documents: Optional[List[ChatDocument]] = Field(None, description="Documents in this message")
message: Optional[str] = Field(None, description="Text content of the message")
role: str = Field(description="Role of the sender ('system', 'user', 'assistant')")
status: str = Field(description="Status of the message ('first', 'step', 'last')")
sequenceNr: int = Field(description="Sequence number for sorting")
startedAt: datetime = Field(description="Timestamp for message creation")
finishedAt: Optional[datetime] = Field(None, description="Timestamp for message completion")
stats: Optional[ChatStat] = Field(None, description="Statistics")
class ChatLog(BaseModel):
"""Log entry for a chat workflow"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the log entry")
workflowId: str = Field(description="ID of the associated workflow")
message: str = Field(description="Log message content")
type: str = Field(description="Type of log ('info', 'warning', 'error')")
timestamp: str = Field(description="Timestamp of the log entry")
agentName: str = Field(description="Name of the agent that created the log")
status: str = Field(description="Status of the workflow at log time")
progress: Optional[int] = Field(None, description="Progress value (0-100)")
class ChatWorkflow(BaseModel):
"""Chat workflow object for multi-agent system"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the chat workflow")
status: str = Field(description="Status of the chat workflow")
name: Optional[str] = Field(None, description="Name of the chat workflow")
currentRound: int = Field(default=1, description="Current round/iteration")
lastActivity: str = Field(description="Timestamp of the last activity")
startedAt: str = Field(description="Start timestamp")
logs: List[ChatLog] = Field(default=[], description="Log entries")
messages: List[ChatMessage] = Field(default=[], description="Message history")
stats: Optional[ChatStat] = Field(None, description="Statistics")
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
label: Label = Field(
default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Workflow de chat"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"currentRound": Label(default="Current Round", translations={"en": "Current Round", "fr": "Tour actuel"}),
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
"startedAt": Label(default="Started At", translations={"en": "Started At", "fr": "Démarré à"}),
"logs": Label(default="Logs", translations={"en": "Logs", "fr": "Journaux"}),
"messages": Label(default="Messages", translations={"en": "Messages", "fr": "Messages"}),
"stats": Label(default="Statistics", translations={"en": "Statistics", "fr": "Statistiques"}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"})
}
# AGENT AND TASK MODELS
class Agent(BaseModel):
"""Data model for an agent"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the agent")
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent's functionality")
capabilities: List[str] = Field(default=[], description="List of agent capabilities")
class AgentResponse(BaseModel):
"""Response structure returned by agent processing"""
response: str = Field(description="Text response from the agent")
documents: List[ChatDocument] = Field(default=[], description="List of document objects created by the agent")
class TaskItem(BaseModel):
"""Individual task in the workplan"""
sequenceNr: int = Field(description="Sequence number of the task")
agentName: str = Field(description="Name of an available agent")
prompt: str = Field(description="Specific instructions to the agent")
userLanguage: str = Field(description="Language code of the user's request")
filesInput: List[str] = Field(default=[], description="List of input files in format 'fileName[;documentId]'")
filesOutput: List[str] = Field(default=[], description="List of output files in format 'fileName'")
class TaskPlan(BaseModel):
"""Work plan created by project manager"""
fileList: List[str] = Field(default=[], description="List of required result documents in format 'fileName'")
taskItems: List[TaskItem] = Field(default=[], description="Plan for executing agents")
userResponse: str = Field(description="Response to the user explaining the plan")
userLanguage: str = Field(default="en", description="Language code of the user's request")
class UserInputRequest(BaseModel):
"""Request for user input to a running workflow"""
prompt: str = Field(description="Message from the user")
listFileId: List[str] = Field(default=[], description="List of FileItem IDs")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the request")

View file

@ -1,113 +0,0 @@
"""
Access control module for Microsoft interface.
Handles user access management and permission checks for Microsoft tokens.
"""
from typing import Dict, Any, List, Optional
class MsftAccess:
"""
Access control class for Microsoft interface.
Handles user access management and permission checks for Microsoft tokens.
"""
def __init__(self, currentUser: Dict[str, Any], db):
"""Initialize with user context."""
self.currentUser = currentUser
self._mandateId = currentUser.get("_mandateId")
self._userId = currentUser.get("id")
if not self._mandateId or not self._userId:
raise ValueError("Invalid user context: _mandateId and id are required")
self.db = db
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
userPrivilege = self.currentUser.get("privilege", "user")
filtered_records = []
# Apply filtering based on privilege
if userPrivilege == "sysadmin":
filtered_records = recordset # System admins see all records
elif userPrivilege == "admin":
# Admins see records in their mandate
filtered_records = [r for r in recordset if r.get("_mandateId") == self._mandateId]
else: # Regular users
# Users only see their own Microsoft tokens
filtered_records = [r for r in recordset
if r.get("_mandateId") == self._mandateId and r.get("_userId") == self._userId]
# Add access control attributes to each record
for record in filtered_records:
record_id = record.get("id")
# Set access control flags based on user permissions
if table == "msftTokens":
record["_hideView"] = False # Everyone can view their own tokens
record["_hideEdit"] = not self.canModify("msftTokens", record_id)
record["_hideDelete"] = not self.canModify("msftTokens", record_id)
else:
# Default access control for other tables
record["_hideView"] = False
record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records
def canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
userPrivilege = self.currentUser.get("privilege", "user")
# System admins can modify anything
if userPrivilege == "sysadmin":
return True
# Check specific record permissions
if recordId is not None:
# Get the record to check ownership
records = self.db.getRecordset(table, recordFilter={"id": recordId})
if not records:
return False
record = records[0]
# Admins can modify anything in their mandate
if userPrivilege == "admin" and record.get("_mandateId") == self._mandateId:
return True
# Users can only modify their own Microsoft tokens
if (record.get("_mandateId") == self._mandateId and
record.get("_userId") == self._userId):
return True
return False
else:
# For general table modify permission (e.g., create)
# Admins can create anything in their mandate
if userPrivilege == "admin":
return True
# Regular users can create their own Microsoft tokens
if table == "msftTokens":
return True
return False

View file

@ -1,520 +0,0 @@
"""
Microsoft interface for handling Microsoft authentication and Graph API operations.
"""
import logging
import json
import requests
import base64
import msal
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime, timedelta
import secrets
import os
from modules.shared.configuration import APP_CONFIG
from .msftModel import MsftToken, MsftUserInfo, MsftConfig
from modules.connectors.connectorDbJson import DatabaseConnector
from .msftAccess import MsftAccess
from modules.interfaces.gatewayInterface import getRootUser
logger = logging.getLogger(__name__)
# Singleton factory for MsftInterface instances per context
_msftInterfaces = {}
# Root interface instance
_rootMsftInterface = None
class MsftInterface:
"""Interface for Microsoft authentication and Graph API operations"""
def __init__(self, currentUser: Dict[str, Any] = None):
"""Initialize the Microsoft interface"""
# Initialize variables
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") if currentUser else None
self.userId = currentUser.get("id") if currentUser else None
self.access = None # Will be set when user context is provided
# Initialize configuration
self.clientId = APP_CONFIG.get("Service_MSFT_CLIENT_ID")
self.clientSecret = APP_CONFIG.get("Service_MSFT_CLIENT_SECRET")
self.tenantId = APP_CONFIG.get("Service_MSFT_TENANT_ID", "common")
self.redirectUri = APP_CONFIG.get("Service_MSFT_REDIRECT_URI")
self.authority = f"https://login.microsoftonline.com/{self.tenantId}"
self.scopes = ["Mail.ReadWrite", "User.Read"]
# Initialize database
self._initializeDatabase()
# Initialize MSAL application
self.msal_app = msal.ConfidentialClientApplication(
self.clientId,
authority=self.authority,
client_credential=self.clientSecret
)
# Set user context if provided
if currentUser:
self.setUserContext(currentUser)
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_MSFT_HOST", "data")
dbDatabase = APP_CONFIG.get("DB_MSFT_DATABASE", "msft")
dbUser = APP_CONFIG.get("DB_MSFT_USER")
dbPassword = APP_CONFIG.get("DB_MSFT_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword,
mandateId=self.mandateId,
userId=self.userId
)
# Set context
self.db.updateContext(self.mandateId, self.userId)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
return self.access.canModify(table, recordId)
def initiateLogin(self) -> str:
"""Initiate Microsoft login flow"""
try:
# Generate auth URL
auth_url = self.msal_app.get_authorization_request_url(
scopes=self.scopes,
redirect_uri=self.redirectUri,
state=self._generateState()
)
return auth_url
except Exception as e:
logger.error(f"Error initiating Microsoft login: {str(e)}")
return None
def handleAuthCallback(self, code: str) -> Optional[MsftToken]:
"""Handle Microsoft OAuth callback"""
try:
# Get token from code
token_response = self.msal_app.acquire_token_by_authorization_code(
code,
scopes=self.scopes,
redirect_uri=self.redirectUri
)
if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return None
# Get user info
user_info = self.getUserInfoFromToken(token_response["access_token"])
if not user_info:
return None
# Create token model
token = MsftToken(
access_token=token_response["access_token"],
refresh_token=token_response.get("refresh_token", ""),
expires_in=token_response.get("expires_in", 0),
token_type=token_response.get("token_type", "bearer"),
expires_at=datetime.now().timestamp() + token_response.get("expires_in", 0),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error handling auth callback: {str(e)}")
return None
def verifyToken(self, token: str) -> bool:
"""Verify Microsoft token"""
try:
# Get user info from token
user_info = self.getUserInfoFromToken(token)
if not user_info:
return False
# Get current user's Microsoft connection
user = self.db.getRecordset("users", recordFilter={"id": self.userId})[0]
msft_connection = next((conn for conn in user.get("connections", [])
if conn.get("authority") == "microsoft"), None)
if not msft_connection:
return False
# Verify the token belongs to this user
return user_info.id == msft_connection.get("externalId")
except Exception as e:
logger.error(f"Error verifying Microsoft token: {str(e)}")
return False
def getUserInfoFromToken(self, token: str) -> Optional[MsftUserInfo]:
"""Get user info from Microsoft Graph"""
try:
# Call Microsoft Graph API
response = requests.get(
"https://graph.microsoft.com/v1.0/me",
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code != 200:
logger.error(f"Failed to get user info: {response.text}")
return None
data = response.json()
# Create user info model
return MsftUserInfo(
id=data["id"],
email=data.get("mail") or data.get("userPrincipalName"),
name=data.get("displayName", ""),
picture=None # Microsoft Graph doesn't provide profile picture by default
)
except Exception as e:
logger.error(f"Error getting user info: {str(e)}")
return None
def refreshToken(self, refresh_token: str) -> Optional[MsftToken]:
"""Refresh Microsoft token"""
try:
# Refresh token
token_response = self.msal_app.acquire_token_by_refresh_token(
refresh_token,
scopes=self.scopes
)
if "error" in token_response:
logger.error(f"Token refresh failed: {token_response['error']}")
return None
# Get user info
user_info = self.getUserInfoFromToken(token_response["access_token"])
if not user_info:
return None
# Create token model
token = MsftToken(
access_token=token_response["access_token"],
refresh_token=token_response.get("refresh_token", refresh_token),
expires_in=token_response.get("expires_in", 0),
token_type=token_response.get("token_type", "bearer"),
expires_at=datetime.now().timestamp() + token_response.get("expires_in", 0),
user_info=user_info.model_dump(),
mandateId=self.mandateId,
userId=self.userId
)
return token
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
return None
def _generateState(self) -> str:
"""Generate secure state token"""
return secrets.token_urlsafe(32)
def createDraftEmail(self, recipient: str, subject: str, body: str, attachments: List[Dict[str, Any]] = None) -> bool:
"""Create a draft email using Microsoft Graph API"""
try:
user_info, access_token = self.getCurrentUserToken()
if not user_info or not access_token:
return False
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
email_data = {
'subject': subject,
'body': {
'contentType': 'HTML',
'content': body
},
'toRecipients': [
{
'emailAddress': {
'address': recipient
}
}
]
}
if attachments:
email_data['attachments'] = []
for attachment in attachments:
doc = attachment.get('document', {})
file_name = attachment.get('name', 'attachment.file')
file_content = doc.get('data')
if not file_content:
continue
mime_type = doc.get('mimeType', 'application/octet-stream')
is_base64 = doc.get('base64Encoded', False)
try:
if is_base64:
content_bytes = file_content
else:
if isinstance(file_content, str):
content_bytes = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
elif isinstance(file_content, bytes):
content_bytes = base64.b64encode(file_content).decode('utf-8')
else:
continue
decoded_size = len(base64.b64decode(content_bytes))
attachment_data = {
'@odata.type': '#microsoft.graph.fileAttachment',
'name': file_name,
'contentType': mime_type,
'contentBytes': content_bytes,
'isInline': False,
'size': decoded_size
}
email_data['attachments'].append(attachment_data)
except Exception as e:
logger.error(f"Error processing attachment {file_name}: {str(e)}")
continue
response = requests.post(
'https://graph.microsoft.com/v1.0/me/messages',
headers=headers,
json=email_data
)
return response.status_code >= 200 and response.status_code < 300
except Exception as e:
logger.error(f"Error creating draft email: {str(e)}")
return False
def saveMsftToken(self, token_data: Dict[str, Any]) -> bool:
"""
Save Microsoft token data to the database.
Args:
token_data: Token data to save
Returns:
bool: True if successful, False otherwise
"""
try:
# Get existing token if any
existing_tokens = self.db.getRecordset(
"msftTokens",
recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
}
)
if existing_tokens:
# Update existing token
token_id = existing_tokens[0]["id"]
success = self.db.updateRecord(
"msftTokens",
token_id,
token_data
)
else:
# Create new token record
success = self.db.createRecord(
"msftTokens",
token_data
)
return success
except Exception as e:
logger.error(f"Error saving Microsoft token: {str(e)}")
return False
def getMsftToken(self) -> Optional[Dict[str, Any]]:
"""
Get Microsoft token data for current user.
Returns:
Optional[Dict[str, Any]]: Token data if found, None otherwise
"""
try:
tokens = self.db.getRecordset(
"msftTokens",
recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
}
)
if not tokens:
return None
return tokens[0]
except Exception as e:
logger.error(f"Error getting Microsoft token: {str(e)}")
return None
def getCurrentUserToken(self) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
"""
Get current user's Microsoft token and user info.
Returns:
Tuple[Optional[Dict[str, Any]], Optional[str]]: User info and access token
"""
try:
token_data = self.getMsftToken()
if not token_data:
return None, None
# Check if token needs refresh
if datetime.now().timestamp() >= token_data["expires_at"]:
if not token_data.get("refresh_token"):
return None, None
# Refresh token
new_token = self.refreshToken(token_data["refresh_token"])
if not new_token:
return None, None
# Save new token
self.saveMsftToken(new_token.model_dump())
token_data = new_token.model_dump()
return token_data["user_info"], token_data["access_token"]
except Exception as e:
logger.error(f"Error getting current user token: {str(e)}")
return None, None
def deleteMsftToken(self) -> bool:
"""
Delete Microsoft token for current user.
Returns:
bool: True if successful, False otherwise
"""
try:
# Get existing token
existing_tokens = self.db.getRecordset(
"msftTokens",
recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
}
)
if not existing_tokens:
return True # No token to delete
# Delete token
success = self.db.deleteRecord(
"msftTokens",
existing_tokens[0]["id"]
)
return success
except Exception as e:
logger.error(f"Error deleting Microsoft token: {str(e)}")
return False
def setUserContext(self, currentUser: Dict[str, Any]):
"""Set user context for the interface"""
if not currentUser:
logger.info("Initializing interface without user context")
return
self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId")
self.userId = currentUser.get("id")
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and id are required")
# Initialize access control with user context
self.access = MsftAccess(self.currentUser, self.db)
# Update database context
self.db.updateContext(self.mandateId, self.userId)
logger.debug(f"User context set: userId={self.userId}")
def getRootInterface() -> MsftInterface:
"""
Returns a MsftInterface instance with root privileges.
This is used for initial setup and user creation.
"""
global _rootMsftInterface
if _rootMsftInterface is None:
# Get root user from gateway
rootUser = getRootUser()
_rootMsftInterface = MsftInterface(rootUser)
return _rootMsftInterface
def getInterface(currentUser: Dict[str, Any] = None) -> MsftInterface:
"""
Returns a MsftInterface instance.
If currentUser is provided, initializes with user context.
Otherwise, returns an instance with only database access.
"""
# Create new instance if not exists
if "default" not in _msftInterfaces:
_msftInterfaces["default"] = MsftInterface(currentUser or {})
interface = _msftInterfaces["default"]
if currentUser:
interface.setUserContext(currentUser)
else:
logger.info("Returning interface without user context")
return interface

View file

@ -1,70 +0,0 @@
"""
Models for Microsoft authentication and Graph API operations.
"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
from datetime import datetime
class MsftToken(BaseModel):
"""Model for Microsoft OAuth tokens"""
access_token: str
refresh_token: Optional[str] = None
expires_in: int
token_type: str = "bearer"
expires_at: float
user_info: Dict[str, Any]
mandateId: str
userId: str
class MsftUserInfo(BaseModel):
"""Model for Microsoft user information"""
id: str
email: str
name: str
picture: Optional[str] = None # Microsoft Graph doesn't provide profile picture by default
class MsftConfig(BaseModel):
"""Configuration for Microsoft authentication service"""
client_id: str
client_secret: str
redirect_uri: str
scopes: list[str]
authority_url: str = "https://login.microsoftonline.com/common"
token_url: str = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
user_info_url: str = "https://graph.microsoft.com/v1.0/me"
# Get all attributes of the model
def getModelAttributes(modelClass):
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"""
default: str
translations: Dict[str, str] = {}
def getLabel(self, language: str = None):
"""Returns the label in the specified language, or the default value if not available"""
if language and language in self.translations:
return self.translations[language]
return self.default
# Response models for Microsoft routes
class MsftAuthStatus(BaseModel):
"""Response model for Microsoft authentication status"""
authenticated: bool
message: Optional[str] = None
user: Optional[MsftUserInfo] = None
class MsftTokenResponse(BaseModel):
"""Response model for Microsoft token"""
token: MsftToken
class MsftSaveTokenResponse(BaseModel):
"""Response model for saving Microsoft token"""
success: bool
message: str
token: Optional[MsftToken] = None

View file

@ -1,13 +1,14 @@
""" """
Access control module for Gateway interface. Access control for the Application.
Handles user access management and permission checks.
""" """
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from datetime import datetime
from modules.interfaces.serviceAppModel import UserPrivilege, Session
class GatewayAccess: class AppAccess:
""" """
Access control class for Gateway interface. Access control class for Application interface.
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
@ -16,6 +17,7 @@ class GatewayAccess:
self.currentUser = currentUser self.currentUser = currentUser
self.mandateId = currentUser.get("mandateId") self.mandateId = currentUser.get("mandateId")
self.userId = currentUser.get("id") self.userId = currentUser.get("id")
self.privilege = currentUser.get("privilege", UserPrivilege.USER)
if not self.mandateId or not self.userId: if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required") raise ValueError("Invalid user context: mandateId and userId are required")
@ -34,19 +36,18 @@ class GatewayAccess:
Returns: Returns:
Filtered recordset with access control attributes Filtered recordset with access control attributes
""" """
userPrivilege = self.currentUser.get("privilege", "user")
filtered_records = [] filtered_records = []
# Apply filtering based on privilege # Apply filtering based on privilege
if userPrivilege == "sysadmin": if self.privilege == UserPrivilege.SYSADMIN:
filtered_records = recordset # System admins see all records filtered_records = recordset # System admins see all records
elif userPrivilege == "admin": elif self.privilege == 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
# Users only see records they own within their mandate # Users only see records they own within their mandate
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:
@ -61,6 +62,22 @@ class GatewayAccess:
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("users", record_id) record["_hideEdit"] = not self.canModify("users", record_id)
record["_hideDelete"] = not self.canModify("users", record_id) record["_hideDelete"] = not self.canModify("users", record_id)
elif table == "sessions":
# Only show sessions for the current user or if admin
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
record["_hideView"] = False
else:
record["_hideView"] = record.get("userId") != self.userId
record["_hideEdit"] = True # Sessions can't be edited
record["_hideDelete"] = not self.canModify("sessions", record_id)
elif table == "auth_events":
# Only show auth events for the current user or if admin
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
record["_hideView"] = False
else:
record["_hideView"] = record.get("userId") != self.userId
record["_hideEdit"] = True # Auth events can't be edited
record["_hideDelete"] = not self.canModify("auth_events", record_id)
else: else:
# Default access control for other tables # Default access control for other tables
record["_hideView"] = False record["_hideView"] = False
@ -80,10 +97,8 @@ class GatewayAccess:
Returns: Returns:
Boolean indicating permission Boolean indicating permission
""" """
userPrivilege = self.currentUser.get("privilege", "user")
# System admins can modify anything # System admins can modify anything
if userPrivilege == "sysadmin": if self.privilege == UserPrivilege.SYSADMIN:
return True return True
# Check specific record permissions # Check specific record permissions
@ -96,25 +111,79 @@ class GatewayAccess:
record = records[0] record = records[0]
# Admins can modify anything in their mandate # Admins can modify anything in their mandate
if userPrivilege == "admin" and record.get("mandateId","-") == self.mandateId: if self.privilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId:
# Exception: Can't modify Root mandate unless you are a sysadmin # Exception: Can't modify Root mandate unless you are a sysadmin
if table == "mandates" and record.get("initialid") and userPrivilege != "sysadmin": if table == "mandates" and record.get("initialid") and self.privilege != UserPrivilege.SYSADMIN:
return False return False
return True return True
# Users can only modify their own records # Users can only modify their own records
if (record.get("mandateId","-") == self.mandateId and if (record.get("mandateId","-") == self.mandateId and
record.get("_createdBy") == self.userId): record.get("createdBy") == self.userId):
return True return True
return False return False
else: else:
# For general table modify permission (e.g., create) # For general table modify permission (e.g., create)
# Admins can create anything in their mandate # Admins can create anything in their mandate
if userPrivilege == "admin": if self.privilege == UserPrivilege.ADMIN:
return True return True
# Regular users can create most entities # Regular users can create most entities
if table == "mandates": if table == "mandates":
return False # Regular users can't create mandates return False # Regular users can't create mandates
return True return True
def validateSession(self, sessionId: str) -> bool:
"""
Validates a user session.
Args:
sessionId: ID of the session to validate
Returns:
Boolean indicating if session is valid
"""
try:
# Get session
sessions = self.db.getRecordset("sessions", recordFilter={"id": sessionId})
if not sessions:
return False
session = sessions[0]
# Check if session is expired
if datetime.now() > session["expiresAt"]:
return False
# Check if user has permission to access this session
if session["userId"] != self.userId and self.privilege not in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
return False
# Update last activity
self.db.recordModify("sessions", sessionId, {
"lastActivity": datetime.now()
})
return True
except Exception as e:
logger.error(f"Error validating session: {str(e)}")
return False
def canAccessAuthEvents(self, userId: str) -> bool:
"""
Checks if the current user can access auth events for a specific user.
Args:
userId: ID of the user whose auth events to check
Returns:
Boolean indicating permission
"""
# System admins and admins can access all auth events
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
return True
# Regular users can only access their own auth events
return userId == self.userId

View file

@ -3,7 +3,7 @@ Interface to the Gateway system.
Manages users and mandates for authentication. Manages users and mandates for authentication.
""" """
from datetime import datetime from datetime import datetime, timedelta
import os import os
import logging import logging
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
@ -13,12 +13,15 @@ from passlib.context import CryptContext
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
from modules.interfaces.gatewayAccess import GatewayAccess from modules.interfaces.serviceAppAccess import AppAccess
from modules.interfaces.gatewayModel import User, Mandate, UserInDB, UserConnection from modules.interfaces.serviceAppModel import (
User, Mandate, UserInDB, UserConnection,
Session, AuthEvent, AuthAuthority, UserPrivilege,
ConnectionStatus
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Singleton factory for GatewayInterface instances per context # Singleton factory for GatewayInterface instances per context
_gatewayInterfaces = {} _gatewayInterfaces = {}
@ -28,7 +31,6 @@ _rootGatewayInterface = None
# Password-Hashing # Password-Hashing
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto") pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
class GatewayInterface: class GatewayInterface:
""" """
Interface to the Gateway system. Interface to the Gateway system.
@ -40,6 +42,7 @@ class GatewayInterface:
# Initialize variables # Initialize variables
self.currentUser = currentUser self.currentUser = currentUser
self.userId = currentUser.get("id") if currentUser else None self.userId = currentUser.get("id") if currentUser else None
self.mandateId = currentUser.get("mandateId") if currentUser else None
self.access = None # Will be set when user context is provided self.access = None # Will be set when user context is provided
# Initialize database # Initialize database
@ -60,26 +63,27 @@ class GatewayInterface:
self.currentUser = currentUser self.currentUser = currentUser
self.userId = currentUser.get("id") self.userId = currentUser.get("id")
self.mandateId = currentUser.get("mandateId")
if not self.userId: if not self.userId or not self.mandateId:
raise ValueError("Invalid user context: id is required") raise ValueError("Invalid user context: id and mandateId are required")
# Add language settings # Add language settings
self.userLanguage = currentUser.get("language", "en") # Default user language self.userLanguage = currentUser.get("language", "en") # Default user language
# Initialize access control with user context # Initialize access control with user context
self.access = GatewayAccess(self.currentUser, self.db) self.access = AppAccess(self.currentUser, self.db)
logger.debug(f"User context set: userId={self.userId}") logger.debug(f"User context set: userId={self.userId}, mandateId={self.mandateId}")
def _initializeDatabase(self): def _initializeDatabase(self):
"""Initializes the database connection.""" """Initializes the database connection."""
try: try:
# Get configuration values with defaults # Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_GATEWAY_HOST", "data") dbHost = APP_CONFIG.get("DB_APP_HOST", "_no_config_default_data")
dbDatabase = APP_CONFIG.get("DB_GATEWAY_DATABASE", "gateway") dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "app")
dbUser = APP_CONFIG.get("DB_GATEWAY_USER") dbUser = APP_CONFIG.get("DB_APP_USER")
dbPassword = APP_CONFIG.get("DB_GATEWAY_PASSWORD_SECRET") dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
# Ensure the database directory exists # Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True) os.makedirs(dbHost, exist_ok=True)
@ -97,6 +101,7 @@ class GatewayInterface:
raise raise
def _initRecords(self): def _initRecords(self):
"""Initialize standard records if they don't exist."""
self._initRootMandate() self._initRootMandate()
self._initAdminUser() self._initAdminUser()
@ -106,18 +111,18 @@ class GatewayInterface:
mandates = self.db.getRecordset("mandates") mandates = self.db.getRecordset("mandates")
if existingMandateId is None or not mandates: if existingMandateId is None or not mandates:
logger.info("Creating Root mandate") logger.info("Creating Root mandate")
rootMandate = { rootMandate = Mandate(
"name": "Root", name="Root",
"language": "en" language="en"
} )
createdMandate = self.db.recordCreate("mandates", rootMandate) createdMandate = self.db.recordCreate("mandates", rootMandate.model_dump())
logger.info(f"Root mandate created with ID {createdMandate['id']}") logger.info(f"Root mandate created with ID {createdMandate['id']}")
# Register the initial ID # Register the initial ID
self.db._registerInitialId("mandates", createdMandate['id']) self.db._registerInitialId("mandates", createdMandate['id'])
# Update mandate context # Update mandate context
self.currentUser["mandateId"] = createdMandate['id'] self.mandateId = createdMandate['id']
def _initAdminUser(self): def _initAdminUser(self):
"""Creates the Admin user if it doesn't exist.""" """Creates the Admin user if it doesn't exist."""
@ -125,18 +130,19 @@ class GatewayInterface:
users = self.db.getRecordset("users") users = self.db.getRecordset("users")
if existingUserId is None or not users: if existingUserId is None or not users:
logger.info("Creating Admin user") logger.info("Creating Admin user")
adminUser = { adminUser = UserInDB(
"mandateId": self.getInitialId("mandates"), mandateId=self.getInitialId("mandates"),
"username": "admin", username="admin",
"email": "admin@example.com", email="admin@example.com",
"fullName": "Administrator", fullName="Administrator",
"disabled": False, disabled=False,
"language": "en", language="en",
"privilege": "sysadmin", privilege=UserPrivilege.SYSADMIN,
"authenticationAuthority": "local", authenticationAuthority=AuthAuthority.LOCAL,
"hashedPassword": self._getPasswordHash("The 1st Poweron Admin") # Use a secure password in production! hashedPassword=self._getPasswordHash("The 1st Poweron Admin"), # Use a secure password in production!
} connections=[]
createdUser = self.db.recordCreate("users", adminUser) )
createdUser = self.db.recordCreate("users", adminUser.model_dump())
logger.info(f"Admin user created with ID {createdUser['id']}") logger.info(f"Admin user created with ID {createdUser['id']}")
# Register the initial ID # Register the initial ID
@ -185,97 +191,6 @@ class GatewayInterface:
"""Checks if the password matches the hash.""" """Checks if the password matches the hash."""
return pwdContext.verify(plainPassword, hashedPassword) return pwdContext.verify(plainPassword, hashedPassword)
# Mandate methods
def getAllMandates(self) -> List[Mandate]:
"""Returns mandates based on user access level."""
allMandates = self.db.getRecordset("mandates")
filteredMandates = self._uam("mandates", allMandates)
return [Mandate(**mandate) for mandate in filteredMandates]
def getMandate(self, mandateId: str) -> Optional[Mandate]:
"""Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates:
return None
filteredMandates = self._uam("mandates", mandates)
if not filteredMandates:
return None
return Mandate(**filteredMandates[0])
def createMandate(self, name: str, language: str = "en") -> Mandate:
"""Creates a new mandate if user has permission."""
if not self._canModify("mandates"):
raise PermissionError("No permission to create mandates")
# Create and validate mandate data using Pydantic model
mandateData = Mandate(
name=name,
language=language
)
# Convert to dict for database storage
created = self.db.recordCreate("mandates", mandateData.model_dump())
return Mandate(**created)
def updateMandate(self, mandateId: str, mandateData: Dict[str, Any]) -> Mandate:
"""Updates a mandate if user has access."""
# Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
raise ValueError(f"Mandate with ID {mandateId} not found")
if not self._canModify("mandates", mandateId):
raise PermissionError(f"No permission to update mandate {mandateId}")
# Validate update data using Pydantic model
try:
# Create a new Mandate instance with existing data plus updates
updatedMandate = Mandate(**{**mandate.model_dump(), **mandateData})
except Exception as e:
raise ValueError(f"Invalid mandate data: {str(e)}")
# Update the mandate
updated = self.db.recordModify("mandates", mandateId, updatedMandate.model_dump())
return Mandate(**updated)
def deleteMandate(self, mandateId: str) -> bool:
"""
Deletes a mandate and all associated users and data if user has permission.
"""
# Check if the mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
return False
if not self._canModify("mandates", mandateId):
raise PermissionError(f"No permission to delete mandate {mandateId}")
# Check if it's the initial mandate
initialMandateId = self.getInitialId("mandates")
if initialMandateId is not None and mandateId == initialMandateId:
logger.warning(f"Attempt to delete the Root mandate was prevented")
return False
# Find all users of the mandate
users = self.getUsersByMandate(mandateId)
# Delete all users of the mandate and their associated data
for user in users:
self.deleteUser(user["id"])
# Delete the mandate
success = self.db.recordDelete("mandates", mandateId)
if success:
logger.info(f"Mandate with ID {mandateId} was successfully deleted")
else:
logger.error(f"Error deleting mandate with ID {mandateId}")
return success
# User methods # User methods
def getAllUsers(self) -> List[User]: def getAllUsers(self) -> List[User]:
@ -283,12 +198,8 @@ class GatewayInterface:
allUsers = self.db.getRecordset("users") allUsers = self.db.getRecordset("users")
filteredUsers = self._uam("users", allUsers) filteredUsers = self._uam("users", allUsers)
# Remove password hashes # Convert to User models
for user in filteredUsers: return [User.from_dict(user) for user in filteredUsers]
if "hashedPassword" in user:
del user["hashedPassword"]
return [User(**user) for user in filteredUsers]
def getUsersByMandate(self, mandateId: str) -> List[User]: def getUsersByMandate(self, mandateId: str) -> List[User]:
"""Returns users for a specific mandate if user has access.""" """Returns users for a specific mandate if user has access."""
@ -296,12 +207,8 @@ class GatewayInterface:
users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId}) users = self.db.getRecordset("users", recordFilter={"mandateId": mandateId})
filteredUsers = self._uam("users", users) filteredUsers = self._uam("users", users)
# Remove password hashes # Convert to User models
for user in filteredUsers: return [User.from_dict(user) for user in filteredUsers]
if "hashedPassword" in user:
del user["hashedPassword"]
return [User(**user) for user in filteredUsers]
def getUserByUsername(self, username: str) -> Optional[User]: def getUserByUsername(self, username: str) -> Optional[User]:
"""Returns a user by username.""" """Returns a user by username."""
@ -315,8 +222,7 @@ class GatewayInterface:
for user in users: for user in users:
if user.get("username") == username: if user.get("username") == username:
logger.info(f"Found user with username {username}") logger.info(f"Found user with username {username}")
logger.debug(f"User fields: {list(user.keys())}") return User.from_dict(user)
return User(**user)
logger.info(f"No user found with username {username}") logger.info(f"No user found with username {username}")
return None return None
@ -335,17 +241,10 @@ class GatewayInterface:
if not filteredUsers: if not filteredUsers:
return None return None
user = filteredUsers[0] return User.from_dict(filteredUsers[0])
# Remove password hash def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
if "hashedPassword" in user: externalUsername: str, externalEmail: Optional[str] = None) -> UserConnection:
userCopy = user.copy()
del userCopy["hashedPassword"]
return User(**userCopy)
return User(**user)
def addUserConnection(self, userId: str, authority: str, externalId: str, externalUsername: str, externalEmail: Optional[str] = None) -> UserConnection:
"""Add a new connection to an external service for a user""" """Add a new connection to an external service for a user"""
try: try:
# Get user # Get user
@ -363,7 +262,8 @@ class GatewayInterface:
authority=authority, authority=authority,
externalId=externalId, externalId=externalId,
externalUsername=externalUsername, externalUsername=externalUsername,
externalEmail=externalEmail externalEmail=externalEmail,
status=ConnectionStatus.ACTIVE
) )
# Add connection to user # Add connection to user
@ -396,8 +296,8 @@ class GatewayInterface:
logger.error(f"Error removing user connection: {str(e)}") logger.error(f"Error removing user connection: {str(e)}")
raise ValueError(f"Failed to remove user connection: {str(e)}") raise ValueError(f"Failed to remove user connection: {str(e)}")
def authenticateUser(self, username: str, password: str = None, authority: str = "local", external_token: str = None) -> Optional[User]: def authenticateLocalUser(self, username: str, password: str) -> Optional[User]:
"""Authenticates a user by username and password or external authority.""" """Authenticates a user by username and password using local authentication."""
# Clear the users table from cache and reload it # Clear the users table from cache and reload it
if "users" in self.db._tablesCache: if "users" in self.db._tablesCache:
del self.db._tablesCache["users"] del self.db._tablesCache["users"]
@ -412,83 +312,41 @@ class GatewayInterface:
if user.disabled: if user.disabled:
raise ValueError("User is disabled") raise ValueError("User is disabled")
# Handle authentication based on authority # Verify that the user has local authentication enabled
if authority == "local": if user.authenticationAuthority != AuthAuthority.LOCAL:
if not password: raise ValueError("User does not have local authentication enabled")
raise ValueError("Password is required for local authentication")
# Get the full user record with password hash for verification # Get the full user record with password hash for verification
userWithPassword = UserInDB(**self.db.getRecordset("users", recordFilter={"id": user.id})[0]) userWithPassword = UserInDB(**self.db.getRecordset("users", recordFilter={"id": user.id})[0])
if not self._verifyPassword(password, userWithPassword.hashedPassword): if not self._verifyPassword(password, userWithPassword.hashedPassword):
raise ValueError("Invalid password") raise ValueError("Invalid password")
elif authority in ["microsoft", "google"]: # Support for multiple external auth providers
# Verify that the user has the correct authentication authority
if user.authenticationAuthority != authority:
raise ValueError(f"User does not have {authority} authentication enabled")
# Verify that the user has a valid connection for this authority
if not any(conn.authority == authority for conn in user.connections):
raise ValueError(f"User does not have a valid {authority} connection")
# Verify the external token
if not external_token:
raise ValueError(f"External token is required for {authority} authentication")
# Get the appropriate auth service
if authority == "microsoft":
from .msftInterface import getInterface as getMsftInterface
auth_service = getMsftInterface({"_mandateId": user._mandateId, "id": user.id})
elif authority == "google":
from .googleInterface import getInterface as getGoogleInterface
auth_service = getGoogleInterface({"_mandateId": user._mandateId, "id": user.id})
else:
raise ValueError(f"Unsupported authentication authority: {authority}")
# Verify the token
if not auth_service.verifyToken(external_token):
raise ValueError(f"Invalid or expired {authority} token")
else:
raise ValueError(f"Unknown authentication authority: {authority}")
return user return user
def createUser(self, username: str, password: str = None, email: str = None, fullName: str = None, def createUser(self, username: str, password: str = None, email: str = None,
language: str = "en", disabled: bool = False, fullName: str = None, language: str = "en", disabled: bool = False,
privilege: str = "user", authenticationAuthority: str = "local", privilege: UserPrivilege = UserPrivilege.USER,
externalId: str = None, externalUsername: str = None, externalEmail: str = None) -> User: authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
externalId: str = None, externalUsername: str = None,
externalEmail: str = None) -> User:
"""Create a new user with optional external connection""" """Create a new user with optional external connection"""
try: try:
# Validate username
if not username:
raise ValueError("Username is required")
# Check if user already exists with the same authentication authority
existingUser = self.getUserByUsername(username)
if existingUser and existingUser.authenticationAuthority == authenticationAuthority:
raise ValueError(f"Username '{username}' already exists with {authenticationAuthority} authentication")
# Validate password for local authentication
if authenticationAuthority == "local":
if not password:
raise ValueError("Password is required for local authentication")
if len(password) < 8:
raise ValueError("Password must be at least 8 characters long")
# Create user data using UserInDB model # Create user data using UserInDB model
userData = UserInDB( userData = UserInDB(
username=username, username=username,
email=email, email=email,
fullName=fullName, fullName=fullName,
language=language, language=language,
mandateId=self.currentUser.get("mandateId"), mandateId=self.mandateId,
disabled=disabled, disabled=disabled,
privilege=privilege, privilege=privilege,
authenticationAuthority=authenticationAuthority, authenticationAuthority=authenticationAuthority,
hashedPassword=self._getPasswordHash(password) if authenticationAuthority == "local" else None, hashedPassword=self._getPasswordHash(password) if password else None,
connections=[] connections=[]
) )
# Create user record # Create user record
createdRecord = self.db.recordCreate("users", userData.model_dump(exclude_none=True)) createdRecord = self.db.recordCreate("users", userData.to_dict())
if not createdRecord or not createdRecord.get("id"): if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create user record") raise ValueError("Failed to create user record")
@ -504,9 +362,6 @@ class GatewayInterface:
# Get created user using the returned ID # Get created user using the returned ID
createdUser = self.db.getRecordset("users", recordFilter={"id": createdRecord["id"]}) createdUser = self.db.getRecordset("users", recordFilter={"id": createdRecord["id"]})
if not createdUser or len(createdUser) == 0:
# Try to get user by username as fallback
createdUser = self.db.getRecordset("users", recordFilter={"username": userData.username})
if not createdUser or len(createdUser) == 0: if not createdUser or len(createdUser) == 0:
raise ValueError("Failed to retrieve created user") raise ValueError("Failed to retrieve created user")
@ -514,7 +369,7 @@ class GatewayInterface:
if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache: if hasattr(self.db, '_tablesCache') and "users" in self.db._tablesCache:
del self.db._tablesCache["users"] del self.db._tablesCache["users"]
return User(**createdUser[0]) return User.from_dict(createdUser[0])
except ValueError as e: except ValueError as e:
logger.error(f"Error creating user: {str(e)}") logger.error(f"Error creating user: {str(e)}")
@ -523,47 +378,32 @@ class GatewayInterface:
logger.error(f"Unexpected error creating user: {str(e)}") logger.error(f"Unexpected error creating user: {str(e)}")
raise ValueError(f"Failed to create user: {str(e)}") raise ValueError(f"Failed to create user: {str(e)}")
def updateUser(self, userId: str, userData: Dict[str, Any]) -> User: def updateUser(self, userId: str, updateData: Dict[str, Any]) -> User:
"""Updates a user if current user has permission.""" """Update a user's information"""
# Check if the user exists and current user has access try:
# Get user
user = self.getUser(userId) user = self.getUser(userId)
if not user: if not user:
# Try to get the raw user record for admin access check raise ValueError(f"User {userId} not found")
users = self.db.getRecordset("users", recordFilter={"id": userId})
if not users:
raise ValueError(f"User with ID {userId} not found")
# Check if current user is admin/sysadmin # Update user data using model
if not self._canModify("users", userId): updatedData = user.model_dump()
raise PermissionError(f"No permission to update user {userId}") updatedData.update(updateData)
updatedUser = User.from_dict(updatedData)
user = users[0] # Update user record
self.db.recordModify("users", userId, updatedUser.to_dict())
# Check privilege escalation # Get updated user
if "privilege" in userData: updatedUser = self.getUser(userId)
currentPrivilege = self.currentUser.get("privilege") if not updatedUser:
targetPrivilege = userData["privilege"] raise ValueError("Failed to retrieve updated user")
if (targetPrivilege == "sysadmin" and currentPrivilege != "sysadmin") or ( return updatedUser
targetPrivilege == "admin" and currentPrivilege == "user"):
raise PermissionError(f"Cannot escalate privilege to {targetPrivilege}")
# If the password is being changed, hash it
if "password" in userData:
userData["hashedPassword"] = self._getPasswordHash(userData["password"])
del userData["password"]
try:
# Create a new UserInDB instance with existing data plus updates
updatedUser = UserInDB(**{**user.model_dump(), **userData})
except Exception as e: except Exception as e:
raise ValueError(f"Invalid user data: {str(e)}") logger.error(f"Error updating user: {str(e)}")
raise ValueError(f"Failed to update user: {str(e)}")
# Update the user
updated = self.db.recordModify("users", userId, updatedUser.model_dump(exclude_none=True))
# Return User model without password hash
return User(**updated)
def disableUser(self, userId: str) -> User: def disableUser(self, userId: str) -> User:
"""Disables a user if current user has permission.""" """Disables a user if current user has permission."""
@ -575,72 +415,121 @@ class GatewayInterface:
def _deleteUserReferencedData(self, userId: str) -> None: def _deleteUserReferencedData(self, userId: str) -> None:
"""Deletes all data associated with a user.""" """Deletes all data associated with a user."""
# Delete user attributes
try: try:
attributes = self.db.getRecordset("attributes", recordFilter={"createdBy": userId}) # Delete user sessions
for attribute in attributes: sessions = self.db.getRecordset("sessions", recordFilter={"userId": userId})
self.db.recordDelete("attributes", attribute["id"]) for session in sessions:
except Exception as e: self.db.recordDelete("sessions", session["id"])
logger.error(f"Error deleting attributes for user {userId}: {e}") logger.debug(f"Deleted session {session['id']} for user {userId}")
# Delete user auth events
events = self.db.getRecordset("auth_events", recordFilter={"userId": userId})
for event in events:
self.db.recordDelete("auth_events", event["id"])
logger.debug(f"Deleted auth event {event['id']} for user {userId}")
# Delete user connections
user = self.getUser(userId)
if user and user.connections:
for conn in user.connections:
self.removeUserConnection(userId, conn.id)
logger.debug(f"Deleted connection {conn.id} for user {userId}")
logger.info(f"All referenced data for user {userId} has been deleted") logger.info(f"All referenced data for user {userId} has been deleted")
def deleteUser(self, userId: str) -> bool: except Exception as e:
"""Deletes a user and all associated data if current user has permission.""" logger.error(f"Error deleting referenced data for user {userId}: {str(e)}")
# Check if the user exists raise
users = self.db.getRecordset("users", recordFilter={"id": userId})
if not users:
return False
# Check if current user has permission # Mandate methods
if not self._canModify("users", userId):
raise PermissionError(f"No permission to delete user {userId}")
# Check if it's the initial user def getAllMandates(self) -> List[Mandate]:
initialUserId = self.getInitialId("users") """Returns all mandates based on user access level."""
if initialUserId is not None and userId == initialUserId: allMandates = self.db.getRecordset("mandates")
logger.warning("Attempt to delete the Root Admin was prevented") filteredMandates = self._uam("mandates", allMandates)
return False return [Mandate.from_dict(mandate) for mandate in filteredMandates]
# Delete all data associated with the user def getMandate(self, mandateId: str) -> Optional[Mandate]:
self._deleteUserReferencedData(userId) """Returns a mandate by ID if user has access."""
mandates = self.db.getRecordset("mandates", recordFilter={"id": mandateId})
if not mandates:
return None
# Delete the user filteredMandates = self._uam("mandates", mandates)
success = self.db.recordDelete("users", userId) if not filteredMandates:
return None
if success: return Mandate.from_dict(filteredMandates[0])
logger.info(f"User with ID {userId} was successfully deleted")
else:
logger.error(f"Error deleting user with ID {userId}")
return success def createMandate(self, name: str, language: str = "en") -> Mandate:
"""Creates a new mandate if user has permission."""
if not self._canModify("mandates"):
raise PermissionError("No permission to create mandates")
def setupLocalAuth(self, userId: str, password: str) -> User: # Create mandate data using model
"""Set up local authentication for a user who registered with Microsoft""" mandateData = Mandate(
name=name,
language=language
)
# Create mandate record
createdRecord = self.db.recordCreate("mandates", mandateData.to_dict())
if not createdRecord or not createdRecord.get("id"):
raise ValueError("Failed to create mandate record")
return Mandate.from_dict(createdRecord)
def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate:
"""Updates a mandate if user has access."""
try: try:
# Get user # Get mandate
user = self.getUser(userId) mandate = self.getMandate(mandateId)
if not user: if not mandate:
raise ValueError(f"User {userId} not found") raise ValueError(f"Mandate {mandateId} not found")
# Validate password # Update mandate data using model
if not password: updatedData = mandate.model_dump()
raise ValueError("Password is required") updatedData.update(updateData)
if len(password) < 8: updatedMandate = Mandate.from_dict(updatedData)
raise ValueError("Password must be at least 8 characters long")
# Update user with local password # Update mandate record
userData = { self.db.recordModify("mandates", mandateId, updatedMandate.to_dict())
"hashedPassword": self._getPasswordHash(password),
"authenticationAuthority": "local" # Change to local auth
}
return self.updateUser(userId, userData) # Get updated mandate
updatedMandate = self.getMandate(mandateId)
if not updatedMandate:
raise ValueError("Failed to retrieve updated mandate")
return updatedMandate
except Exception as e: except Exception as e:
logger.error(f"Error setting up local authentication: {str(e)}") logger.error(f"Error updating mandate: {str(e)}")
raise ValueError(f"Failed to set up local authentication: {str(e)}") raise ValueError(f"Failed to update mandate: {str(e)}")
def deleteMandate(self, mandateId: str) -> bool:
"""Deletes a mandate if user has access."""
try:
# Check if mandate exists and user has access
mandate = self.getMandate(mandateId)
if not mandate:
return False
if not self._canModify("mandates", mandateId):
raise PermissionError(f"No permission to delete mandate {mandateId}")
# Check if mandate has users
users = self.getUsersByMandate(mandateId)
if users:
raise ValueError(f"Cannot delete mandate {mandateId} with existing users")
# Delete mandate
return self.db.recordDelete("mandates", mandateId)
except Exception as e:
logger.error(f"Error deleting mandate: {str(e)}")
raise ValueError(f"Failed to delete mandate: {str(e)}")
# Public Methods
def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface: def getInterface(currentUser: Dict[str, Any]) -> GatewayInterface:
""" """

View file

@ -0,0 +1,211 @@
"""
Models for User Service
"""
import uuid
from pydantic import BaseModel, Field, EmailStr
from typing import List, Dict, Any, Optional
from datetime import datetime
from enum import Enum
from modules.shared.attributeUtils import Label, BaseModelWithUI
class AuthAuthority(str, Enum):
"""Authentication authorities"""
LOCAL = "local"
MICROSOFT = "microsoft"
GOOGLE = "google"
EXTERNAL = "external"
class UserPrivilege(str, Enum):
"""User privilege levels"""
SYSADMIN = "sysadmin"
ADMIN = "admin"
USER = "user"
class ConnectionStatus(str, Enum):
"""Connection status"""
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
PENDING = "pending"
class Mandate(BaseModelWithUI):
"""Data model for a mandate"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the mandate")
name: str = Field(description="Name of the mandate")
language: str = Field(default="en", description="Default language of the mandate")
label: Label = Field(
default=Label(default="Mandate", translations={"en": "Mandate", "fr": "Mandat"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"name": Label(default="Name of the mandate", translations={"en": "Mandate name", "fr": "Nom du mandat"}),
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"})
}
@classmethod
def get_validations(cls) -> Dict[str, Any]:
"""Get validation rules for frontend"""
return {
"name": {
"required": True,
"minLength": 2,
"maxLength": 100
},
"language": {
"required": True,
"pattern": "^[a-z]{2}$"
}
}
class UserConnection(BaseModelWithUI):
"""Data model for a user's connection to an external service"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
authority: AuthAuthority = Field(description="Authentication authority")
externalId: str = Field(description="User ID in the external system")
externalUsername: str = Field(description="Username in the external system")
externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system")
status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status")
connectedAt: datetime = Field(default_factory=datetime.now, description="When the connection was established")
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
label: Label = Field(
default=Label(default="User Connection", translations={"en": "User Connection", "fr": "Connexion utilisateur"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"authority": Label(default="Authority", translations={"en": "Authority", "fr": "Autorité"}),
"externalId": Label(default="External ID", translations={"en": "External ID", "fr": "ID externe"}),
"externalUsername": Label(default="External Username", translations={"en": "External Username", "fr": "Nom d'utilisateur externe"}),
"externalEmail": Label(default="External Email", translations={"en": "External Email", "fr": "Email externe"}),
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
"connectedAt": Label(default="Connected At", translations={"en": "Connected At", "fr": "Connecté le"}),
"lastChecked": Label(default="Last Checked", translations={"en": "Last Checked", "fr": "Dernière vérification"}),
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"})
}
class Session(BaseModelWithUI):
"""Data model for user sessions"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique session ID")
userId: str = Field(description="ID of the user")
tokenId: str = Field(description="ID of the associated token")
lastActivity: datetime = Field(default_factory=datetime.now, description="Last activity timestamp")
expiresAt: datetime = Field(description="When the session expires")
ipAddress: Optional[str] = Field(None, description="IP address of the session")
userAgent: Optional[str] = Field(None, description="User agent of the session")
label: Label = Field(
default=Label(default="Session", translations={"en": "Session", "fr": "Session"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
"tokenId": Label(default="Token ID", translations={"en": "Token ID", "fr": "ID du token"}),
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
"expiresAt": Label(default="Expires At", translations={"en": "Expires At", "fr": "Expire le"}),
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
}
class AuthEvent(BaseModelWithUI):
"""Data model for authentication events"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique event ID")
userId: str = Field(description="ID of the user")
eventType: str = Field(description="Type of event (login, logout, etc.)")
details: Dict[str, Any] = Field(description="Event details")
timestamp: datetime = Field(default_factory=datetime.now, description="When the event occurred")
ipAddress: Optional[str] = Field(None, description="IP address of the event")
userAgent: Optional[str] = Field(None, description="User agent of the event")
label: Label = Field(
default=Label(default="Auth Event", translations={"en": "Auth Event", "fr": "Événement d'authentification"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID utilisateur"}),
"eventType": Label(default="Event Type", translations={"en": "Event Type", "fr": "Type d'événement"}),
"details": Label(default="Details", translations={"en": "Details", "fr": "Détails"}),
"timestamp": Label(default="Timestamp", translations={"en": "Timestamp", "fr": "Horodatage"}),
"ipAddress": Label(default="IP Address", translations={"en": "IP Address", "fr": "Adresse IP"}),
"userAgent": Label(default="User Agent", translations={"en": "User Agent", "fr": "User Agent"})
}
class User(BaseModelWithUI):
"""Data model for a user"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the user")
username: str = Field(description="Username for login")
email: Optional[EmailStr] = Field(None, description="Email address of the user")
fullName: Optional[str] = Field(None, description="Full name of the user")
language: str = Field(default="en", description="Preferred language of the user")
disabled: bool = Field(default=False, description="Indicates whether the user is disabled")
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: str = Field(description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
label: Label = Field(
default=Label(default="User", translations={"en": "User", "fr": "Utilisateur"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"username": Label(default="Username", translations={"en": "Username", "fr": "Nom d'utilisateur"}),
"email": Label(default="Email", translations={"en": "Email", "fr": "Email"}),
"fullName": Label(default="Full Name", translations={"en": "Full Name", "fr": "Nom complet"}),
"language": Label(default="Language", translations={"en": "Language", "fr": "Langue"}),
"disabled": Label(default="Disabled", translations={"en": "Disabled", "fr": "Désactivé"}),
"privilege": Label(default="Privilege", translations={"en": "Privilege", "fr": "Privilège"}),
"authenticationAuthority": Label(default="Auth Authority", translations={"en": "Auth Authority", "fr": "Autorité d'authentification"}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"connections": Label(default="Connections", translations={"en": "Connections", "fr": "Connexions"})
}
@classmethod
def get_validations(cls) -> Dict[str, Any]:
"""Get validation rules for frontend"""
return {
"username": {
"required": True,
"minLength": 3,
"maxLength": 50,
"pattern": "^[a-zA-Z0-9_-]+$"
},
"email": {
"required": False,
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"fullName": {
"required": False,
"maxLength": 100
},
"language": {
"required": True,
"pattern": "^[a-z]{2}$"
}
}
class UserInDB(User):
"""Extended user class with password hash"""
hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
label: Label = Field(
default=Label(default="User Access", translations={"en": "User Access", "fr": "Accès de l'utilisateur"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"hashedPassword": Label(default="Password hash", translations={"en": "Password hash", "fr": "Hachage de mot de passe"})
}

View file

@ -0,0 +1,52 @@
"""
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
# Token management functions
def saveToken(interface, tokenType: str, tokenData: dict) -> bool:
"""Save token data for a specific service"""
try:
return interface.saveToken(f"tokens{tokenType}", tokenData)
except Exception as e:
logger.error(f"Error saving {tokenType} token: {str(e)}")
return False
def getToken(interface, tokenType: str) -> Optional[dict]:
"""Get token data for a specific service"""
try:
return interface.getToken(f"tokens{tokenType}")
except Exception as e:
logger.error(f"Error getting {tokenType} token: {str(e)}")
return None
def deleteToken(interface, tokenType: str) -> bool:
"""Delete token data for a specific service"""
try:
return interface.deleteToken(f"tokens{tokenType}")
except Exception as e:
logger.error(f"Error deleting {tokenType} token: {str(e)}")
return False

View file

@ -0,0 +1,133 @@
"""
Access control module for Chat interface.
Handles user access management and permission checks.
"""
from typing import Dict, Any, List, Optional
from modules.interfaces.serviceAppModel import User, UserPrivilege
class ChatAccess:
"""
Access control class for Chat interface.
Handles user access management and permission checks.
"""
def __init__(self, currentUser: User, db):
"""Initialize with user context."""
self.currentUser = currentUser
self.mandateId = currentUser.mandateId
self.userId = currentUser.id
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required")
self.db = db
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
and adds access control attributes.
Args:
table: Name of the table
recordset: Recordset to filter based on access rules
Returns:
Filtered recordset with access control attributes
"""
userPrivilege = self.currentUser.privilege
filtered_records = []
# Apply filtering based on privilege
if userPrivilege == UserPrivilege.SYSADMIN:
filtered_records = recordset # System admins see all records
elif userPrivilege == UserPrivilege.ADMIN:
# Admins see records in their mandate
filtered_records = [r for r in recordset if r.get("mandateId","-") == self.mandateId]
else: # Regular users
# For prompts, users can see all prompts from their mandate
if table == "prompts":
filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
else:
# Users see only their records for other tables
filtered_records = [r for r in recordset
if r.get("mandateId","-") == self.mandateId and r.get("_createdBy") == self.userId]
# Add access control attributes to each record
for record in filtered_records:
record_id = record.get("id")
# Set access control flags based on user permissions
if table == "prompts":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("prompts", record_id)
record["_hideDelete"] = not self.canModify("prompts", record_id)
elif table == "files":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("files", record_id)
record["_hideDelete"] = not self.canModify("files", record_id)
record["_hideDownload"] = not self.canModify("files", record_id)
elif table == "workflows":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("workflows", record_id)
record["_hideDelete"] = not self.canModify("workflows", record_id)
elif table == "workflowMessages":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
elif table == "workflowLogs":
record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
else:
# Default access control for other tables
record["_hideView"] = False
record["_hideEdit"] = not self.canModify(table, record_id)
record["_hideDelete"] = not self.canModify(table, record_id)
return filtered_records
def canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""
Checks if the current user can modify (create/update/delete) records in a table.
Args:
table: Name of the table
recordId: Optional record ID for specific record check
Returns:
Boolean indicating permission
"""
userPrivilege = self.currentUser.privilege
# System admins can modify anything
if userPrivilege == UserPrivilege.SYSADMIN:
return True
# For regular users and admins, check specific cases
if recordId is not None:
# Get the record to check ownership
records = self.db.getRecordset(table, recordFilter={"id": recordId})
if not records:
return False
record = records[0]
# Admins can modify anything in their mandate, if mandate is specified for a record
if userPrivilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId:
return True
# Regular users can only modify their own records
if (record.get("mandateId","-") == self.mandateId and
record.get("_createdBy") == self.userId):
return True
return False
else:
# For general modification permission (e.g., create)
# Admins can create anything in their mandate
if userPrivilege == UserPrivilege.ADMIN:
return True
# Regular users can create in most tables
return True

View file

@ -12,10 +12,11 @@ from typing import Dict, Any, List, Optional, Union
import hashlib import hashlib
from modules.shared.mimeUtils import isTextMimeType from modules.shared.mimeUtils import isTextMimeType
from modules.interfaces.lucydomAccess import LucydomAccess from modules.interfaces.serviceChatAccess import ChatAccess
from modules.interfaces.lucydomModel import ( from modules.interfaces.serviceChatModel import (
ChatWorkflow, ChatMessage, ChatLog, ChatStat, ChatContent, ChatDocument, ChatStat, ChatMessage,
ChatDocument, UserInputRequest ChatLog, ChatWorkflow, Agent, AgentResponse,
TaskItem, TaskPlan, UserInputRequest
) )
# DYNAMIC PART: Connectors to the Interface # DYNAMIC PART: Connectors to the Interface
@ -26,8 +27,8 @@ from modules.connectors.connectorAiOpenai import ChatService
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Singleton factory for Lucydom instances with AI service per context # Singleton factory for Chat instances with AI service per context
_lucydomInterfaces = {} _chatInterfaces = {}
# Custom exceptions for file handling # Custom exceptions for file handling
class FileError(Exception): class FileError(Exception):
@ -50,14 +51,14 @@ class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file.""" """Exception raised when there's an error deleting a file."""
pass pass
class LucydomInterface: class ChatInterface:
""" """
Interface to LucyDOM database and AI Connectors. Interface to Chat database and AI Connectors.
Uses the JSON connector for data access with added language support. Uses the JSON connector for data access with added language support.
""" """
def __init__(self): def __init__(self):
"""Initializes the Lucydom Interface.""" """Initializes the Chat Interface."""
# Initialize database # Initialize database
self._initializeDatabase() self._initializeDatabase()
@ -67,6 +68,7 @@ class LucydomInterface:
# Initialize variables # Initialize variables
self.currentUser = None self.currentUser = None
self.userId = None self.userId = None
self.mandateId = None
self.access = None # Will be set when user context is provided self.access = None # Will be set when user context is provided
self.aiService = None # Will be set when user context is provided self.aiService = None # Will be set when user context is provided
@ -78,7 +80,7 @@ class LucydomInterface:
self.currentUser = currentUser self.currentUser = currentUser
self.userId = currentUser.get("id") self.userId = currentUser.get("id")
self.mandateId = currentUser.get("mandateId")
if not self.userId: if not self.userId:
raise ValueError("Invalid user context: id is required") raise ValueError("Invalid user context: id is required")
@ -86,7 +88,7 @@ class LucydomInterface:
self.userLanguage = currentUser.get("language", "en") # Default user language self.userLanguage = currentUser.get("language", "en") # Default user language
# Initialize access control with user context # Initialize access control with user context
self.access = LucydomAccess(self.currentUser, self.db) self.access = ChatAccess(self.currentUser, self.db)
# Initialize AI service # Initialize AI service
self.aiService = ChatService() self.aiService = ChatService()
@ -97,10 +99,10 @@ class LucydomInterface:
"""Initializes the database connection.""" """Initializes the database connection."""
try: try:
# Get configuration values with defaults # Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_LUCYDOM_HOST", "data") dbHost = APP_CONFIG.get("DB_CHAT_HOST", "_no_config_default_data")
dbDatabase = APP_CONFIG.get("DB_LUCYDOM_DATABASE", "lucydom") dbDatabase = APP_CONFIG.get("DB_CHAT_DATABASE", "chat")
dbUser = APP_CONFIG.get("DB_LUCYDOM_USER") dbUser = APP_CONFIG.get("DB_CHAT_USER")
dbPassword = APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET") dbPassword = APP_CONFIG.get("DB_CHAT_PASSWORD_SECRET")
# Ensure the database directory exists # Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True) os.makedirs(dbHost, exist_ok=True)
@ -232,447 +234,6 @@ class LucydomInterface:
"""Returns the current timestamp in ISO format""" """Returns the current timestamp in ISO format"""
return datetime.now().isoformat() return datetime.now().isoformat()
# Prompt methods
def getAllPrompts(self) -> List[Dict[str, Any]]:
"""Returns prompts based on user access level."""
allPrompts = self.db.getRecordset("prompts")
return self._uam("prompts", allPrompts)
def getPrompt(self, promptId: str) -> Optional[Dict[str, Any]]:
"""Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts:
return None
filteredPrompts = self._uam("prompts", prompts)
return filteredPrompts[0] if filteredPrompts else None
def createPrompt(self, content: str, name: str) -> Dict[str, Any]:
"""Creates a new prompt if user has permission."""
if not self._canModify("prompts"):
raise PermissionError("No permission to create prompts")
promptData = {
"content": content,
"name": name,
"createdAt": self._getCurrentTimestamp()
}
return self.db.recordCreate("prompts", promptData)
def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Dict[str, Any]:
"""Updates a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
return None
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to update prompt {promptId}")
# Prepare data for update
promptData = {}
if content is not None:
promptData["content"] = content
if name is not None:
promptData["name"] = name
# Update prompt
return self.db.recordModify("prompts", promptId, promptData)
def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
return False
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to delete prompt {promptId}")
return self.db.recordDelete("prompts", promptId)
# File Utilities
def calculateFileHash(self, fileContent: bytes) -> str:
"""Calculates a SHA-256 hash for the file content"""
return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]:
"""Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash,
"mandateId": self.currentUser.get("mandateId"),
"_createdBy": self.currentUser.get("id")
})
if files:
return files[0]
return None
def getMimeType(self, filename: str) -> str:
"""Determines the MIME type based on the file extension."""
import os
ext = os.path.splitext(filename)[1].lower()[1:]
extensionToMime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml",
"py": "text/x-python",
"js": "application/javascript",
"css": "text/css"
}
return extensionToMime.get(ext.lower(), "application/octet-stream")
# File methods - metadata-based operations
def getAllFiles(self) -> List[Dict[str, Any]]:
"""Returns files based on user access level."""
allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles)
def getFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files:
return None
filteredFiles = self._uam("files", files)
return filteredFiles[0] if filteredFiles else None
def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> Dict[str, Any]:
"""Creates a new file entry if user has permission."""
if not self._canModify("files"):
raise PermissionError("No permission to create files")
fileData = {
"mandateId": self.currentUser.get("mandateId"),
"name": name,
"mimeType": mimeType,
"size": size,
"fileHash": fileHash,
"creationDate": self._getCurrentTimestamp()
}
return self.db.recordCreate("files", fileData)
def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access."""
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to update file {fileId}")
# Update file
return self.db.recordModify("files", fileId, updateData)
def deleteFile(self, fileId: str) -> bool:
"""Deletes a file if user has access."""
try:
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to delete file {fileId}")
# Check for other references to this file (by hash)
fileHash = file.get("fileHash")
if fileHash:
otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
if f.get("id") != fileId]
# Only delete associated fileData if no other references exist
if not otherReferences:
try:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
self.db.recordDelete("fileData", fileId)
logger.debug(f"FileData for file {fileId} deleted")
except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
# Delete the FileItem entry
return self.db.recordDelete("files", fileId)
except FileNotFoundError as e:
raise
except FilePermissionError as e:
raise
except Exception as e:
logger.error(f"Error deleting file {fileId}: {str(e)}")
raise FileDeletionError(f"Error deleting file: {str(e)}")
# FileData methods - data operations
def createFileData(self, fileId: str, data: bytes) -> bool:
"""Stores the binary data of a file in the database."""
try:
import base64
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when storing data")
return False
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
if isTextFormat:
# Try to decode as text
try:
textContent = data.decode('utf-8')
fileData = textContent
base64Encoded = False
logger.debug(f"Stored file {fileId} as text")
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.warning(f"Failed to decode text file {fileId}, falling back to base64")
else:
# Binary format - always use base64
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.debug(f"Stored file {fileId} as base64")
# Create the fileData record with data and encoding flag
fileDataObj = {
"id": fileId,
"data": fileData,
"base64Encoded": base64Encoded
}
self.db.recordCreate("fileData", fileDataObj)
logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False
def getFileData(self, fileId: str) -> Optional[bytes]:
"""Returns the binary data of a file if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.warning(f"No access to file ID {fileId}")
return None
import base64
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if not fileDataEntries:
logger.warning(f"No data found for file ID {fileId}")
return None
fileDataEntry = fileDataEntries[0]
if "data" not in fileDataEntry:
logger.warning(f"No data field in file data for ID {fileId}")
return None
data = fileDataEntry["data"]
base64Encoded = fileDataEntry.get("base64Encoded", False)
try:
if base64Encoded:
# Decode base64 to bytes
return base64.b64decode(data)
else:
# Convert text to bytes
return data.encode('utf-8')
except Exception as e:
logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None
def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when updating data")
return False
if not self._canModify("files", fileId):
logger.error(f"No permission to update file data for {fileId}")
return False
try:
import base64
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
# Convert input data to the right format
if isinstance(data, bytes):
if isTextFormat:
try:
# Try to convert bytes to text
fileData = data.decode('utf-8')
base64Encoded = False
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
else:
# Binary format - use base64
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
elif isinstance(data, str):
if isTextFormat:
# Text format - store as text
fileData = data
base64Encoded = False
else:
# Check if it's already base64 encoded
try:
# Try to decode as base64 to validate
base64.b64decode(data)
fileData = data
base64Encoded = True
except:
# Not valid base64, encode the string
fileData = base64.b64encode(data.encode('utf-8')).decode('utf-8')
base64Encoded = True
else:
# Convert to string first
stringData = str(data)
if isTextFormat:
fileData = stringData
base64Encoded = False
else:
fileData = base64.b64encode(stringData.encode('utf-8')).decode('utf-8')
base64Encoded = True
# Check if a record already exists
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
dataUpdate = {
"data": fileData,
"base64Encoded": base64Encoded
}
if fileDataEntries:
# Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate)
logger.debug(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
else:
# Create a new record
dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate)
logger.debug(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error updating data for file {fileId}: {str(e)}")
return False
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> Dict[str, Any]:
"""Saves an uploaded file if user has permission."""
try:
# Check file creation permission
if not self._canModify("files"):
raise PermissionError("No permission to upload files")
logger.debug(f"Starting upload process for file: {fileName}")
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
# Calculate file hash for deduplication
fileHash = self.calculateFileHash(fileContent)
logger.debug(f"Calculated file hash: {fileHash}")
# Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile
# Determine MIME type and size
mimeType = self.getMimeType(fileName)
fileSize = len(fileContent)
# Save metadata
logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
mimeType=mimeType,
size=fileSize,
fileHash=fileHash
)
# Save binary data
logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
logger.debug(f"File upload process completed for: {fileName}")
return dbFile
except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}")
def downloadFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file for download if user has access."""
try:
# Check file access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
# Get binary data
fileContent = self.getFileData(fileId)
if fileContent is None:
raise FileNotFoundError(f"Binary data for file with ID {fileId} not found")
return {
"id": fileId,
"name": file.get("name", f"file_{fileId}"),
"contentType": file.get("mimeType", "application/octet-stream"),
"size": file.get("size", len(fileContent)),
"content": fileContent
}
except FileNotFoundError as e:
raise
except Exception as e:
logger.error(f"Error downloading file {fileId}: {str(e)}")
raise FileError(f"Error downloading file: {str(e)}")
# Workflow methods # Workflow methods
def getAllWorkflows(self) -> List[Dict[str, Any]]: def getAllWorkflows(self) -> List[Dict[str, Any]]:
@ -1291,17 +852,17 @@ class LucydomInterface:
return None return None
def getInterface(currentUser: Dict[str, Any] = None) -> 'LucydomInterface': def getInterface(currentUser: Dict[str, Any] = None) -> 'ChatInterface':
""" """
Returns a LucydomInterface instance. Returns a ChatInterface instance.
If currentUser is provided, initializes with user context. If currentUser is provided, initializes with user context.
Otherwise, returns an instance with only database access. Otherwise, returns an instance with only database access.
""" """
# Create new instance if not exists # Create new instance if not exists
if "default" not in _lucydomInterfaces: if "default" not in _chatInterfaces:
_lucydomInterfaces["default"] = LucydomInterface() _chatInterfaces["default"] = ChatInterface()
interface = _lucydomInterfaces["default"] interface = _chatInterfaces["default"]
if currentUser: if currentUser:
interface.setUserContext(currentUser) interface.setUserContext(currentUser)

View file

@ -0,0 +1,130 @@
"""
Chat model classes for the chat system.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
from modules.shared.attributeUtils import Label, BaseModelWithUI
# WORKFLOW MODELS
class ChatContent(BaseModelWithUI):
"""Data model for chat content"""
sequenceNr: int = Field(description="Sequence number of the content")
name: str = Field(description="Name of the content")
data: str = Field(description="The actual content data")
mimeType: str = Field(description="MIME type of the content")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
class ChatDocument(BaseModelWithUI):
"""Data model for a chat document"""
id: str = Field(description="Primary key")
fileId: int = Field(description="Foreign key to file")
filename: str = Field(description="Name of the file")
fileSize: int = Field(description="Size of the file")
mimeType: str = Field(description="MIME type of the file")
contents: List[ChatContent] = Field(default_factory=list, description="List of chat contents")
class ChatStat(BaseModelWithUI):
"""Data model for chat statistics"""
id: str = Field(description="Primary key")
processingTime: Optional[float] = Field(None, description="Processing time in seconds")
tokenCount: Optional[int] = Field(None, description="Number of tokens processed")
bytesSent: Optional[int] = Field(None, description="Number of bytes sent")
bytesReceived: Optional[int] = Field(None, description="Number of bytes received")
class ChatLog(BaseModelWithUI):
"""Data model for a chat log"""
id: str = Field(description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
message: str = Field(description="Log message")
type: str = Field(description="Type of log entry")
timestamp: str = Field(description="Timestamp of the log entry")
agentName: str = Field(description="Name of the agent")
status: str = Field(description="Status of the log entry")
progress: Optional[int] = Field(None, description="Progress percentage")
class ChatMessage(BaseModelWithUI):
"""Data model for a chat message"""
id: str = Field(description="Primary key")
workflowId: str = Field(description="Foreign key to workflow")
parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading")
agentName: Optional[str] = Field(None, description="Name of the agent")
documents: List[ChatDocument] = Field(default_factory=list, description="Associated documents")
message: Optional[str] = Field(None, description="Message content")
role: str = Field(description="Role of the message sender")
status: str = Field(description="Status of the message")
sequenceNr: int = Field(description="Sequence number of the message")
startedAt: str = Field(description="When the message processing started")
finishedAt: Optional[str] = Field(None, description="When the message processing finished")
stats: Optional[ChatStat] = Field(None, description="Statistics for this message")
class ChatWorkflow(BaseModelWithUI):
"""Data model for a chat workflow"""
id: str = Field(description="Primary key")
mandateId: str = Field(description="ID of the mandate this workflow belongs to")
status: str = Field(description="Current status of the workflow")
name: Optional[str] = Field(None, description="Name of the workflow")
currentRound: int = Field(description="Current round number")
lastActivity: str = Field(description="Timestamp of last activity")
startedAt: str = Field(description="When the workflow started")
logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs")
messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow")
stats: Optional[ChatStat] = Field(None, description="Workflow statistics")
label: Label = Field(
default=Label(default="Chat Workflow", translations={"en": "Chat Workflow", "fr": "Flux de travail de chat"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
"status": Label(default="Status", translations={"en": "Status", "fr": "Statut"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"currentRound": Label(default="Current Round", translations={"en": "Current Round", "fr": "Tour actuel"}),
"lastActivity": Label(default="Last Activity", translations={"en": "Last Activity", "fr": "Dernière activité"}),
"startedAt": Label(default="Started At", translations={"en": "Started At", "fr": "Démarré le"}),
"logs": Label(default="Logs", translations={"en": "Logs", "fr": "Journaux"}),
"messages": Label(default="Messages", translations={"en": "Messages", "fr": "Messages"}),
"stats": Label(default="Statistics", translations={"en": "Statistics", "fr": "Statistiques"})
}
# AGENT AND TASK MODELS
class Agent(BaseModelWithUI):
"""Data model for an agent"""
id: str = Field(description="Primary key")
name: str = Field(description="Name of the agent")
description: str = Field(description="Description of the agent")
capabilities: List[str] = Field(default_factory=list, description="List of agent capabilities")
class AgentResponse(BaseModelWithUI):
"""Data model for an agent response"""
response: str = Field(description="Response content from the agent")
documents: List[ChatDocument] = Field(default_factory=list, description="Documents associated with the response")
class TaskItem(BaseModelWithUI):
"""Data model for a task item"""
sequenceNr: int = Field(description="Sequence number of the task")
agentName: str = Field(description="Name of the agent assigned to this task")
prompt: str = Field(description="Prompt for the task")
userLanguage: str = Field(description="User's preferred language")
filesInput: List[str] = Field(default_factory=list, description="Input files (format: filename;[documentId])")
filesOutput: List[str] = Field(default_factory=list, description="Output files (format: filename)")
class TaskPlan(BaseModelWithUI):
"""Data model for a task plan"""
fileList: List[str] = Field(default_factory=list, description="List of files (format: filename)")
taskItems: List[TaskItem] = Field(default_factory=list, description="List of task items in the plan")
userLanguage: str = Field(description="User's preferred language")
userResponse: str = Field(description="User's response or feedback")
class UserInputRequest(BaseModelWithUI):
"""Data model for a user input request"""
prompt: str = Field(description="Prompt for the user")
listFileId: List[int] = Field(default_factory=list, description="List of file IDs")

View file

@ -1,13 +1,13 @@
""" """
Access control module for LucyDOM interface. Access control module for Management interface.
Handles user access management and permission checks. Handles user access management and permission checks.
""" """
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
class LucydomAccess: class ManagementAccess:
""" """
Access control class for LucyDOM interface. Access control class for Management interface.
Handles user access management and permission checks. Handles user access management and permission checks.
""" """

View file

@ -0,0 +1,704 @@
"""
Interface to Management database and AI Connectors.
Uses the JSON connector for data access with added language support.
"""
import os
import logging
import uuid
from datetime import datetime
from typing import Dict, Any, List, Optional, Union
import hashlib
from modules.shared.mimeUtils import isTextMimeType
from modules.interfaces.serviceManagementAccess import ManagementAccess
from modules.interfaces.serviceManagementModel import (
Prompt, FileItem, FileData
)
from modules.interfaces.serviceAppModel import User, Mandate, UserPrivilege
# DYNAMIC PART: Connectors to the Interface
from modules.connectors.connectorDbJson import DatabaseConnector
from modules.connectors.connectorAiOpenai import ChatService
# Basic Configurations
from modules.shared.configuration import APP_CONFIG
logger = logging.getLogger(__name__)
# Singleton factory for Management instances with AI service per context
_instancesManagement = {}
# Custom exceptions for file handling
class FileError(Exception):
"""Base class for file handling exceptions."""
pass
class FileNotFoundError(FileError):
"""Exception raised when a file is not found."""
pass
class FileStorageError(FileError):
"""Exception raised when there's an error storing a file."""
pass
class FilePermissionError(FileError):
"""Exception raised when there's a permission issue with a file."""
pass
class FileDeletionError(FileError):
"""Exception raised when there's an error deleting a file."""
pass
class ServiceManagement:
"""
Interface to Management database and AI Connectors.
Uses the JSON connector for data access with added language support.
"""
def __init__(self):
"""Initializes the Management Interface."""
# Initialize database
self._initializeDatabase()
# Initialize standard records if needed
self._initRecords()
# Initialize variables
self.currentUser: Optional[User] = None
self.userId: Optional[str] = None
self.access: Optional[ManagementAccess] = None # Will be set when user context is provided
self.aiService: Optional[ChatService] = None # Will be set when user context is provided
def setUserContext(self, currentUser: User):
"""Sets the user context for the interface."""
if not currentUser:
logger.info("Initializing interface without user context")
return
self.currentUser = currentUser
self.userId = currentUser.id
if not self.userId:
raise ValueError("Invalid user context: id is required")
# Add language settings
self.userLanguage = currentUser.language # Default user language
# Initialize access control with user context
self.access = ManagementAccess(self.currentUser, self.db)
# Initialize AI service
self.aiService = ChatService()
logger.debug(f"User context set: userId={self.userId}")
def _initializeDatabase(self):
"""Initializes the database connection."""
try:
# Get configuration values with defaults
dbHost = APP_CONFIG.get("DB_MANAGEMENT_HOST", "_no_config_default_data")
dbDatabase = APP_CONFIG.get("DB_MANAGEMENT_DATABASE", "management")
dbUser = APP_CONFIG.get("DB_MANAGEMENT_USER")
dbPassword = APP_CONFIG.get("DB_MANAGEMENT_PASSWORD_SECRET")
# Ensure the database directory exists
os.makedirs(dbHost, exist_ok=True)
self.db = DatabaseConnector(
dbHost=dbHost,
dbDatabase=dbDatabase,
dbUser=dbUser,
dbPassword=dbPassword
)
logger.info("Database initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize database: {str(e)}")
raise
def _initRecords(self):
"""Initializes standard records in the database if they don't exist."""
try:
# Initialize standard prompts
self._initializeStandardPrompts()
# Add other record initializations here
logger.info("Standard records initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize standard records: {str(e)}")
raise
def _initializeStandardPrompts(self):
"""Creates standard prompts if they don't exist."""
prompts = self.db.getRecordset("prompts")
logger.debug(f"Found {len(prompts)} existing prompts")
if not prompts:
logger.debug("Creating standard prompts")
# Define standard prompts
standardPrompts = [
{
"content": "Research the current market trends and developments in [TOPIC]. Collect information about leading companies, innovative products or services, and current challenges. Present the results in a structured overview with relevant data and sources.",
"name": "Web Research: Market Research"
},
{
"content": "Analyze the attached dataset on [TOPIC] and identify the most important trends, patterns, and anomalies. Perform statistical calculations to support your findings. Present the results in a clearly structured analysis and draw relevant conclusions.",
"name": "Analysis: Data Analysis"
},
{
"content": "Create a detailed protocol of our meeting on [TOPIC]. Capture all discussed points, decisions made, and agreed measures. Structure the protocol clearly with agenda items, participant list, and clear responsibilities for follow-up actions.",
"name": "Protocol: Meeting Minutes"
},
{
"content": "Develop a UI/UX design concept for [APPLICATION/WEBSITE]. Consider the target audience, main functions, and brand identity. Describe the visual design, navigation, interaction patterns, and information architecture. Explain how the design optimizes user-friendliness and user experience.",
"name": "Design: UI/UX Design"
},
{
"content": "Gib mir die ersten 1000 Primzahlen",
"name": "Code: Primzahlen"
},
{
"content": "Bereite mir eine formelle E-Mail an peter.muster@domain.com vor, um meinen Termin von 10 Uhr auf Freitag zu scheiben.",
"name": "Mail: Vorbereitung"
},
]
# Create prompts
for promptData in standardPrompts:
createdPrompt = self.db.recordCreate("prompts", promptData)
logger.debug(f"Prompt '{promptData.get('name', 'Standard')}' was created with ID {createdPrompt['id']} and context mandate={createdPrompt.get('mandateId')}, user={createdPrompt.get('_createdBy')}")
else:
logger.debug("Prompts already exist, skipping creation")
def _uam(self, table: str, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Delegate to access control module."""
return self.access.uam(table, recordset)
def _canModify(self, table: str, recordId: Optional[str] = None) -> bool:
"""Delegate to access control module."""
return self.access.canModify(table, recordId)
# Language support method
def setUserLanguage(self, languageCode: str):
"""Set the user's preferred language"""
self.userLanguage = languageCode
logger.debug(f"User language set to: {languageCode}")
# AI Call Root Function
async def callAi(self, messages: List[Dict[str, str]], produceUserAnswer: bool = False, temperature: float = None) -> str:
"""Enhanced AI service call with language support."""
if not self.aiService:
logger.error("AI service not set in ServiceManagement")
return "Error: AI service not available"
# Add language instruction for user-facing responses
if produceUserAnswer and self.userLanguage:
ltext= f"Please respond in '{self.userLanguage}' language."
if messages and messages[0]["role"] == "system":
if "language" not in messages[0]["content"].lower():
messages[0]["content"] = f"{ltext} {messages[0]['content']}"
else:
# Insert a system message with language instruction
messages.insert(0, {
"role": "system",
"content": ltext
})
# Call the AI service
if temperature is not None:
return await self.aiService.callApi(messages, temperature=temperature)
else:
return await self.aiService.callApi(messages)
async def callAi4Image(self, imageData: Union[str, bytes], mimeType: str = None, prompt: str = "Describe this image") -> str:
"""Enhanced AI service call with language support."""
if not self.aiService:
logger.error("AI service not set in ServiceManagement")
return "Error: AI service not available"
return await self.aiService.analyzeImage(imageData, mimeType, prompt)
# Utilities
def getInitialId(self, table: str) -> Optional[str]:
"""Returns the initial ID for a table."""
return self.db.getInitialId(table)
def _getCurrentTimestamp(self) -> str:
"""Returns the current timestamp in ISO format"""
return datetime.now().isoformat()
# Prompt methods
def getAllPrompts(self) -> List[Prompt]:
"""Returns prompts based on user access level."""
allPrompts = self.db.getRecordset("prompts")
filteredPrompts = self._uam("prompts", allPrompts)
return [Prompt.from_dict(prompt) for prompt in filteredPrompts]
def getPrompt(self, promptId: str) -> Optional[Prompt]:
"""Returns a prompt by ID if user has access."""
prompts = self.db.getRecordset("prompts", recordFilter={"id": promptId})
if not prompts:
return None
filteredPrompts = self._uam("prompts", prompts)
return Prompt.from_dict(filteredPrompts[0]) if filteredPrompts else None
def createPrompt(self, content: str, name: str) -> Prompt:
"""Creates a new prompt if user has permission."""
if not self._canModify("prompts"):
raise PermissionError("No permission to create prompts")
promptData = Prompt(
content=content,
name=name,
createdAt=self._getCurrentTimestamp()
)
createdRecord = self.db.recordCreate("prompts", promptData.to_dict())
return Prompt.from_dict(createdRecord)
def updatePrompt(self, promptId: str, content: str = None, name: str = None) -> Prompt:
"""Updates a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
raise ValueError(f"Prompt {promptId} not found")
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to update prompt {promptId}")
# Update prompt data using model
updatedData = prompt.model_dump()
if content is not None:
updatedData["content"] = content
if name is not None:
updatedData["name"] = name
updatedPrompt = Prompt.from_dict(updatedData)
# Update prompt
self.db.recordModify("prompts", promptId, updatedPrompt.to_dict())
# Get updated prompt
updatedPrompt = self.getPrompt(promptId)
if not updatedPrompt:
raise ValueError("Failed to retrieve updated prompt")
return updatedPrompt
def deletePrompt(self, promptId: str) -> bool:
"""Deletes a prompt if user has access."""
# Check if the prompt exists and user has access
prompt = self.getPrompt(promptId)
if not prompt:
return False
if not self._canModify("prompts", promptId):
raise PermissionError(f"No permission to delete prompt {promptId}")
return self.db.recordDelete("prompts", promptId)
# File Utilities
def calculateFileHash(self, fileContent: bytes) -> str:
"""Calculates a SHA-256 hash for the file content"""
return hashlib.sha256(fileContent).hexdigest()
def checkForDuplicateFile(self, fileHash: str) -> Optional[Dict[str, Any]]:
"""Checks if a file with the same hash already exists for the current user and mandate."""
files = self.db.getRecordset("files", recordFilter={
"fileHash": fileHash,
"mandateId": self.currentUser.get("mandateId"),
"_createdBy": self.currentUser.get("id")
})
if files:
return files[0]
return None
def getMimeType(self, filename: str) -> str:
"""Determines the MIME type based on the file extension."""
import os
ext = os.path.splitext(filename)[1].lower()[1:]
extensionToMime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml",
"py": "text/x-python",
"js": "application/javascript",
"css": "text/css"
}
return extensionToMime.get(ext.lower(), "application/octet-stream")
# File methods - metadata-based operations
def getAllFiles(self) -> List[Dict[str, Any]]:
"""Returns files based on user access level."""
allFiles = self.db.getRecordset("files")
return self._uam("files", allFiles)
def getFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file by ID if user has access."""
files = self.db.getRecordset("files", recordFilter={"id": fileId})
if not files:
return None
filteredFiles = self._uam("files", files)
return filteredFiles[0] if filteredFiles else None
def createFile(self, name: str, mimeType: str, size: int = None, fileHash: str = None) -> Dict[str, Any]:
"""Creates a new file entry if user has permission."""
if not self._canModify("files"):
raise PermissionError("No permission to create files")
fileData = {
"mandateId": self.currentUser.get("mandateId"),
"name": name,
"mimeType": mimeType,
"size": size,
"fileHash": fileHash,
"creationDate": self._getCurrentTimestamp()
}
return self.db.recordCreate("files", fileData)
def updateFile(self, fileId: str, updateData: Dict[str, Any]) -> Dict[str, Any]:
"""Updates file metadata if user has access."""
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to update file {fileId}")
# Update file
return self.db.recordModify("files", fileId, updateData)
def deleteFile(self, fileId: str) -> bool:
"""Deletes a file if user has access."""
try:
# Check if the file exists and user has access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
if not self._canModify("files", fileId):
raise PermissionError(f"No permission to delete file {fileId}")
# Check for other references to this file (by hash)
fileHash = file.get("fileHash")
if fileHash:
otherReferences = [f for f in self.db.getRecordset("files", recordFilter={"fileHash": fileHash})
if f.get("id") != fileId]
# Only delete associated fileData if no other references exist
if not otherReferences:
try:
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if fileDataEntries:
self.db.recordDelete("fileData", fileId)
logger.debug(f"FileData for file {fileId} deleted")
except Exception as e:
logger.warning(f"Error deleting FileData for file {fileId}: {str(e)}")
# Delete the FileItem entry
return self.db.recordDelete("files", fileId)
except FileNotFoundError as e:
raise
except FilePermissionError as e:
raise
except Exception as e:
logger.error(f"Error deleting file {fileId}: {str(e)}")
raise FileDeletionError(f"Error deleting file: {str(e)}")
# FileData methods - data operations
def createFileData(self, fileId: str, data: bytes) -> bool:
"""Stores the binary data of a file in the database."""
try:
import base64
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when storing data")
return False
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
if isTextFormat:
# Try to decode as text
try:
textContent = data.decode('utf-8')
fileData = textContent
base64Encoded = False
logger.debug(f"Stored file {fileId} as text")
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.warning(f"Failed to decode text file {fileId}, falling back to base64")
else:
# Binary format - always use base64
encodedData = base64.b64encode(data).decode('utf-8')
fileData = encodedData
base64Encoded = True
logger.debug(f"Stored file {fileId} as base64")
# Create the fileData record with data and encoding flag
fileDataObj = {
"id": fileId,
"data": fileData,
"base64Encoded": base64Encoded
}
self.db.recordCreate("fileData", fileDataObj)
logger.debug(f"Successfully stored data for file {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error storing data for file {fileId}: {str(e)}")
return False
def getFileData(self, fileId: str) -> Optional[bytes]:
"""Returns the binary data of a file if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.warning(f"No access to file ID {fileId}")
return None
import base64
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
if not fileDataEntries:
logger.warning(f"No data found for file ID {fileId}")
return None
fileDataEntry = fileDataEntries[0]
if "data" not in fileDataEntry:
logger.warning(f"No data field in file data for ID {fileId}")
return None
data = fileDataEntry["data"]
base64Encoded = fileDataEntry.get("base64Encoded", False)
try:
if base64Encoded:
# Decode base64 to bytes
return base64.b64decode(data)
else:
# Convert text to bytes
return data.encode('utf-8')
except Exception as e:
logger.error(f"Error processing file data for {fileId}: {str(e)}")
return None
def updateFileData(self, fileId: str, data: Union[bytes, str]) -> bool:
"""Updates file data if user has access."""
# Check file access
file = self.getFile(fileId)
if not file:
logger.error(f"File with ID {fileId} not found when updating data")
return False
if not self._canModify("files", fileId):
logger.error(f"No permission to update file data for {fileId}")
return False
try:
import base64
# Determine if this is a text-based format
mimeType = file.get("mimeType", "application/octet-stream")
isTextFormat = isTextMimeType(mimeType)
base64Encoded = False
fileData = None
# Convert input data to the right format
if isinstance(data, bytes):
if isTextFormat:
try:
# Try to convert bytes to text
fileData = data.decode('utf-8')
base64Encoded = False
except UnicodeDecodeError:
# Fallback to base64 if text decoding fails
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
else:
# Binary format - use base64
fileData = base64.b64encode(data).decode('utf-8')
base64Encoded = True
elif isinstance(data, str):
if isTextFormat:
# Text format - store as text
fileData = data
base64Encoded = False
else:
# Check if it's already base64 encoded
try:
# Try to decode as base64 to validate
base64.b64decode(data)
fileData = data
base64Encoded = True
except:
# Not valid base64, encode the string
fileData = base64.b64encode(data.encode('utf-8')).decode('utf-8')
base64Encoded = True
else:
# Convert to string first
stringData = str(data)
if isTextFormat:
fileData = stringData
base64Encoded = False
else:
fileData = base64.b64encode(stringData.encode('utf-8')).decode('utf-8')
base64Encoded = True
# Check if a record already exists
fileDataEntries = self.db.getRecordset("fileData", recordFilter={"id": fileId})
dataUpdate = {
"data": fileData,
"base64Encoded": base64Encoded
}
if fileDataEntries:
# Update the existing record
self.db.recordModify("fileData", fileId, dataUpdate)
logger.debug(f"Updated file data for file ID {fileId} (base64Encoded: {base64Encoded})")
else:
# Create a new record
dataUpdate["id"] = fileId
self.db.recordCreate("fileData", dataUpdate)
logger.debug(f"Created new file data for file ID {fileId} (base64Encoded: {base64Encoded})")
return True
except Exception as e:
logger.error(f"Error updating data for file {fileId}: {str(e)}")
return False
def saveUploadedFile(self, fileContent: bytes, fileName: str) -> Dict[str, Any]:
"""Saves an uploaded file if user has permission."""
try:
# Check file creation permission
if not self._canModify("files"):
raise PermissionError("No permission to upload files")
logger.debug(f"Starting upload process for file: {fileName}")
if not isinstance(fileContent, bytes):
logger.error(f"Invalid fileContent type: {type(fileContent)}")
raise ValueError(f"fileContent must be bytes, got {type(fileContent)}")
# Calculate file hash for deduplication
fileHash = self.calculateFileHash(fileContent)
logger.debug(f"Calculated file hash: {fileHash}")
# Check for duplicate within same user/mandate
existingFile = self.checkForDuplicateFile(fileHash)
if existingFile:
logger.debug(f"Duplicate found for {fileName}: {existingFile['id']}")
return existingFile
# Determine MIME type and size
mimeType = self.getMimeType(fileName)
fileSize = len(fileContent)
# Save metadata
logger.debug(f"Saving file metadata to database for file: {fileName}")
dbFile = self.createFile(
name=fileName,
mimeType=mimeType,
size=fileSize,
fileHash=fileHash
)
# Save binary data
logger.debug(f"Saving file content to database for file: {fileName}")
self.createFileData(dbFile["id"], fileContent)
logger.debug(f"File upload process completed for: {fileName}")
return dbFile
except Exception as e:
logger.error(f"Error in saveUploadedFile for {fileName}: {str(e)}", exc_info=True)
raise FileStorageError(f"Error saving file: {str(e)}")
def downloadFile(self, fileId: str) -> Optional[Dict[str, Any]]:
"""Returns a file for download if user has access."""
try:
# Check file access
file = self.getFile(fileId)
if not file:
raise FileNotFoundError(f"File with ID {fileId} not found")
# Get binary data
fileContent = self.getFileData(fileId)
if fileContent is None:
raise FileNotFoundError(f"Binary data for file with ID {fileId} not found")
return {
"id": fileId,
"name": file.get("name", f"file_{fileId}"),
"contentType": file.get("mimeType", "application/octet-stream"),
"size": file.get("size", len(fileContent)),
"content": fileContent
}
except FileNotFoundError as e:
raise
except Exception as e:
logger.error(f"Error downloading file {fileId}: {str(e)}")
raise FileError(f"Error downloading file: {str(e)}")
def getInterface(currentUser: Optional[User] = None) -> 'ServiceManagement':
"""
Returns a ServiceManagement instance.
If currentUser is provided, initializes with user context.
Otherwise, returns an instance with only database access.
"""
# Create new instance if not exists
if "default" not in _instancesManagement:
_instancesManagement["default"] = ServiceManagement()
interface = _instancesManagement["default"]
if currentUser:
interface.setUserContext(currentUser)
else:
logger.info("Returning interface without user context")
return interface

View file

@ -0,0 +1,75 @@
"""
Service Management model classes for the service management system.
Updated to match the Entity Relation Diagram structure.
"""
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
import uuid
from modules.shared.attributeUtils import Label, BaseModelWithUI
# CORE MODELS
class FileItem(BaseModelWithUI):
"""Data model for a file item"""
id: int = Field(description="Primary key")
mandateId: str = Field(description="ID of the mandate this file belongs to")
filename: str = Field(description="Name of the file")
mimeType: str = Field(description="MIME type of the file")
workflowId: Optional[str] = Field(None, description="Foreign key to workflow")
fileHash: str = Field(description="Hash of the file")
fileSize: int = Field(description="Size of the file in bytes")
label: Label = Field(
default=Label(default="File Item", translations={"en": "File Item", "fr": "Élément de fichier"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
"filename": Label(default="Filename", translations={"en": "Filename", "fr": "Nom de fichier"}),
"mimeType": Label(default="MIME Type", translations={"en": "MIME Type", "fr": "Type MIME"}),
"workflowId": Label(default="Workflow ID", translations={"en": "Workflow ID", "fr": "ID du flux de travail"}),
"fileHash": Label(default="File Hash", translations={"en": "File Hash", "fr": "Hash du fichier"}),
"fileSize": Label(default="File Size", translations={"en": "File Size", "fr": "Taille du fichier"})
}
class FileData(BaseModelWithUI):
"""Data model for file data"""
id: int = Field(description="Primary key")
data: str = Field(description="File data content")
base64Encoded: bool = Field(description="Whether the data is base64 encoded")
label: Label = Field(
default=Label(default="File Data", translations={"en": "File Data", "fr": "Données de fichier"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"data": Label(default="Data", translations={"en": "Data", "fr": "Données"}),
"base64Encoded": Label(default="Base64 Encoded", translations={"en": "Base64 Encoded", "fr": "Encodé en Base64"})
}
class Prompt(BaseModelWithUI):
"""Data model for a prompt"""
id: int = Field(description="Primary key")
mandateId: str = Field(description="ID of the mandate this prompt belongs to")
content: str = Field(description="Content of the prompt")
name: str = Field(description="Name of the prompt")
label: Label = Field(
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
description="Label for the class"
)
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID du mandat"}),
"content": Label(default="Content", translations={"en": "Content", "fr": "Contenu"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"})
}

View file

@ -0,0 +1,63 @@
from fastapi import APIRouter, Response, Depends
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import os
import logging
from pathlib import Path as FilePath
from typing import Dict, Any
from modules.shared.configuration import APP_CONFIG
from modules.security.auth import limiter, getCurrentUser
router = APIRouter(
prefix="",
tags=["General"],
responses={404: {"description": "Not found"}}
)
# Static folder setup - using absolute path from app root
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
staticFolder = baseDir / "static"
os.makedirs(staticFolder, exist_ok=True)
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="",
tags=["Administration"],
responses={404: {"description": "Not found"}}
)
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
@router.get("/")
@limiter.limit("30/minute")
async def root():
"""API status endpoint"""
return {
"status": "online",
"message": "Data Platform API is active",
"allowedOrigins": f"Allowed origins are {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
}
@router.get("/api/environment")
@limiter.limit("30/minute")
async def get_environment(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Get environment configuration for frontend"""
return {
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
"environment": APP_CONFIG.get("APP_ENV", "development"),
"instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"),
# Add other environment variables the frontend might need
}
@router.options("/{fullPath:path}")
@limiter.limit("60/minute")
async def options_route(fullPath: str):
return Response(status_code=200)
@router.get("/favicon.ico")
@limiter.limit("30/minute")
async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")

View file

@ -8,10 +8,11 @@ from pydantic import BaseModel
import logging import logging
# Import auth module # Import auth module
import modules.security.auth as auth from modules.security.auth import limiter, getCurrentUser
# Import the attribute definition and helper functions # Import the attribute definition and helper functions
from modules.shared.defAttributes import AttributeDefinition, getModelAttributes from modules.interfaces.serviceAppModel import AttributeDefinition
from modules.shared.attributeUtils import getModelClasses
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -39,30 +40,6 @@ class AttributeResponse(BaseModel):
} }
} }
def getModelClasses() -> Dict[str, Any]:
"""Dynamically get all model classes from all model modules"""
modelClasses = {}
# Get the interfaces directory path
# Since we're in modules/routes/, we need to go up one level to modules/ then into interfaces/
interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces')
# Find all model files
for filename in os.listdir(interfaces_dir):
if filename.endswith('Model.py'):
# Convert filename to module name (e.g., gatewayModel.py -> gatewayModel)
module_name = filename[:-3]
# Import the module dynamically
module = importlib.import_module(f'modules.interfaces.{module_name}')
# Get all classes from the module
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel:
modelClasses[name.lower()] = obj
return modelClasses
# Create a router for the attribute endpoints # Create a router for the attribute endpoints
router = APIRouter( router = APIRouter(
prefix="/api/attributes", prefix="/api/attributes",
@ -71,9 +48,10 @@ router = APIRouter(
) )
@router.get("/{entityType}", response_model=AttributeResponse) @router.get("/{entityType}", response_model=AttributeResponse)
@limiter.limit("30/minute")
async def get_entity_attributes( async def get_entity_attributes(
entityType: str = Path(..., description="Type of entity (e.g. prompt)"), entityType: str = Path(..., description="Type of entity (e.g. prompt)"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
""" """
Retrieves the attribute definitions for a specific entity. Retrieves the attribute definitions for a specific entity.
@ -100,12 +78,13 @@ 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 = getModelAttributes(modelClass, userLanguage) attributes = modelClass.getModelAttributeDefinitions()
# Return only visible attributes # Return only visible attributes
return AttributeResponse(attributes=[attr for attr in attributes if attr.visible]) return AttributeResponse(attributes=[attr for attr in attributes if attr.visible])
@router.options("/{entityType}") @router.options("/{entityType}")
@limiter.limit("60/minute")
async def options_entity_attributes( async def options_entity_attributes(
entityType: str = Path(..., description="Type of entity (e.g. prompt)") entityType: str = Path(..., description="Type of entity (e.g. prompt)")
): ):

View file

@ -7,22 +7,23 @@ from dataclasses import dataclass
import io import io
# Import auth module # Import auth module
import modules.security.auth as auth from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.lucydomInterface as lucydomInterface import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.lucydomModel import FileItem from modules.interfaces.serviceManagementModel import FileItem, getModelAttributeDefinitions
from modules.interfaces.serviceAppModel import AttributeDefinition
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Model attributes for FileItem # Model attributes for FileItem
fileAttributes = lucydomInterface.getModelAttributes(FileItem) fileAttributes = getModelAttributeDefinitions(FileItem)
# Create router for file endpoints # Create router for file endpoints
router = APIRouter( router = APIRouter(
prefix="/api/files", prefix="/api/files",
tags=["Files"], tags=["Manage Files"],
responses={ responses={
404: {"description": "Not found"}, 404: {"description": "Not found"},
400: {"description": "Bad request"}, 400: {"description": "Bad request"},
@ -33,13 +34,14 @@ router = APIRouter(
) )
@router.get("", response_model=List[FileItem]) @router.get("", response_model=List[FileItem])
async def get_files(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)): @limiter.limit("30/minute")
async def get_files(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Get all available files""" """Get all available files"""
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get all files generically - only metadata, no binary data # Get all files generically - only metadata, no binary data
files = interfaceLucydom.getAllFiles() files = managementInterface.getAllFiles()
return [FileItem(**file) for file in files] return [FileItem(**file) for file in files]
except Exception as e: except Exception as e:
logger.error(f"Error retrieving files: {str(e)}") logger.error(f"Error retrieving files: {str(e)}")
@ -49,39 +51,40 @@ async def get_files(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveU
) )
@router.post("/upload", status_code=status.HTTP_201_CREATED) @router.post("/upload", status_code=status.HTTP_201_CREATED)
@limiter.limit("10/minute")
async def upload_file( async def upload_file(
file: UploadFile = File(...), file: UploadFile = File(...),
workflowId: Optional[str] = Form(None), workflowId: Optional[str] = Form(None),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Upload a file""" """Upload a file"""
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Read file # Read file
fileContent = await file.read() fileContent = await file.read()
# Check size limits # Check size limits
maxSize = int(lucydomInterface.APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in bytes maxSize = int(serviceManagementClass.APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in bytes
if len(fileContent) > maxSize: if len(fileContent) > maxSize:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size: {lucydomInterface.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" detail=f"File too large. Maximum size: {serviceManagementClass.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB"
) )
# Save file via LucyDOM interface in the database # Save file via LucyDOM interface in the database
fileMeta = interfaceLucydom.saveUploadedFile(fileContent, file.filename) fileMeta = managementInterface.saveUploadedFile(fileContent, file.filename)
# If workflowId is provided, update the file information # If workflowId is provided, update the file information
if workflowId: if workflowId:
updateData = {"workflowId": workflowId} updateData = {"workflowId": workflowId}
interfaceLucydom.updateFile(fileMeta["id"], updateData) managementInterface.updateFile(fileMeta["id"], updateData)
fileMeta["workflowId"] = workflowId fileMeta["workflowId"] = workflowId
# Successful response # Successful response
return fileMeta return fileMeta
except lucydomInterface.FileStorageError as e: except serviceManagementClass.FileStorageError as e:
logger.error(f"Error during file upload (storage): {str(e)}") logger.error(f"Error during file upload (storage): {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -95,16 +98,17 @@ async def upload_file(
) )
@router.get("/{fileId}") @router.get("/{fileId}")
@limiter.limit("30/minute")
async def get_file( async def get_file(
fileId: str, fileId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Returns a file by its ID for download""" """Returns a file by its ID for download"""
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get file via LucyDOM interface from the database # Get file via LucyDOM interface from the database
fileData = interfaceLucydom.downloadFile(fileId) fileData = managementInterface.downloadFile(fileId)
# Return file # Return file
headers = { headers = {
@ -116,19 +120,19 @@ async def get_file(
headers=headers headers=headers
) )
except lucydomInterface.FileNotFoundError as e: except serviceManagementClass.FileNotFoundError as e:
logger.warning(f"File not found: {str(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=str(e)
) )
except lucydomInterface.FilePermissionError as e: except serviceManagementClass.FilePermissionError as e:
logger.warning(f"No permission for file: {str(e)}") logger.warning(f"No permission for file: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail=str(e) detail=str(e)
) )
except lucydomInterface.FileError as e: except serviceManagementClass.FileError as e:
logger.error(f"Error retrieving file: {str(e)}") logger.error(f"Error retrieving file: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -142,19 +146,20 @@ async def get_file(
) )
@router.put("/{file_id}", response_model=FileItem) @router.put("/{file_id}", response_model=FileItem)
@limiter.limit("10/minute")
async def update_file( async def update_file(
file_id: str, file_id: str,
file_data: FileItem, file_data: FileItem,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
""" """
Update file metadata Update file metadata
""" """
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get the file from the database # Get the file from the database
file = interfaceLucydom.getFile(file_id) file = managementInterface.getFile(file_id)
if not file: if not file:
raise HTTPException(status_code=404, detail="File not found") raise HTTPException(status_code=404, detail="File not found")
@ -166,12 +171,12 @@ async def update_file(
update_data = file_data.model_dump() update_data = file_data.model_dump()
# Update the file # Update the file
result = interfaceLucydom.updateFile(file_id, update_data) result = managementInterface.updateFile(file_id, update_data)
if not result: if not result:
raise HTTPException(status_code=500, detail="Failed to update file") raise HTTPException(status_code=500, detail="Failed to update file")
# Get updated file and convert to FileItem # Get updated file and convert to FileItem
updatedFile = interfaceLucydom.getFile(file_id) updatedFile = managementInterface.getFile(file_id)
return FileItem(**updatedFile) return FileItem(**updatedFile)
except HTTPException as he: except HTTPException as he:
@ -181,33 +186,34 @@ async def update_file(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit("10/minute")
async def delete_file( async def delete_file(
fileId: str, fileId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Deletes a file by its ID from the database""" """Deletes a file by its ID from the database"""
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Delete file via LucyDOM interface # Delete file via LucyDOM interface
interfaceLucydom.deleteFile(fileId) managementInterface.deleteFile(fileId)
# Return successful deletion without content (204 No Content) # Return successful deletion without content (204 No Content)
return Response(status_code=status.HTTP_204_NO_CONTENT) return Response(status_code=status.HTTP_204_NO_CONTENT)
except lucydomInterface.FileNotFoundError as e: except serviceManagementClass.FileNotFoundError as e:
logger.warning(f"File not found: {str(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=str(e)
) )
except lucydomInterface.FilePermissionError as e: except serviceManagementClass.FilePermissionError as e:
logger.warning(f"No permission to delete file: {str(e)}") logger.warning(f"No permission to delete file: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail=str(e) detail=str(e)
) )
except lucydomInterface.FileDeletionError as e: except serviceManagementClass.FileDeletionError as e:
logger.error(f"Error deleting file: {str(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,
@ -221,15 +227,16 @@ async def delete_file(
) )
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_file_stats( async def get_file_stats(
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Returns statistics about the stored files""" """Returns statistics about the stored files"""
try: try:
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get all files - metadata only # Get all files - metadata only
allFiles = interfaceLucydom.getAllFiles() allFiles = managementInterface.getAllFiles()
# Calculate statistics # Calculate statistics
totalFiles = len(allFiles) totalFiles = len(allFiles)
@ -255,3 +262,18 @@ async def get_file_stats(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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(
currentUser: Dict[str, Any] = Depends(getCurrentUser)
):
"""
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

@ -1,33 +1,38 @@
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request 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
import logging import logging
# Import auth module # Import auth module
import modules.security.auth as auth from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.gatewayInterface as gatewayInterface import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.gatewayModel import Mandate, getModelAttributes from modules.interfaces.serviceManagementModel import Mandate, getModelAttributeDefinitions
# Import the model classes
from modules.interfaces.serviceAppModel import AttributeDefinition
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Model attributes for Mandate # Model attributes for Mandate
mandateAttributes = getModelAttributes(Mandate) mandateAttributes = getModelAttributeDefinitions(Mandate)
# Create a router for the mandate endpoints
router = APIRouter( router = APIRouter(
prefix="/api/mandates", prefix="/api/mandates",
tags=["Mandates"], tags=["Manage Mandates"],
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[Dict[str, Any]], tags=["Mandates"]) @router.get("/", response_model=List[Dict[str, Any]], tags=["Mandates"])
async def get_mandates(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)): @limiter.limit("30/minute")
async def get_mandates(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Get all mandates""" """Get all mandates"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
return interfaceGateway.getMandates() return appInterface.getMandates()
except Exception as e: except Exception as e:
logger.error(f"Error getting mandates: {str(e)}") logger.error(f"Error getting mandates: {str(e)}")
raise HTTPException( raise HTTPException(
@ -36,14 +41,15 @@ async def get_mandates(currentUser: Dict[str, Any] = Depends(auth.getCurrentActi
) )
@router.get("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"]) @router.get("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
@limiter.limit("30/minute")
async def get_mandate( async def get_mandate(
mandateId: str, mandateId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get a specific mandate by ID""" """Get a specific mandate by ID"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
mandate = interfaceGateway.getMandateById(mandateId) mandate = appInterface.getMandateById(mandateId)
if not mandate: if not mandate:
raise HTTPException( raise HTTPException(
@ -62,16 +68,17 @@ async def get_mandate(
) )
@router.post("/", response_model=Mandate, tags=["Mandates"]) @router.post("/", response_model=Mandate, tags=["Mandates"])
@limiter.limit("10/minute")
async def create_mandate( async def create_mandate(
mandateData: Mandate, mandateData: Mandate,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Create a new mandate""" """Create a new mandate"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
try: try:
createdMandate = interfaceGateway.createMandate(mandateData) createdMandate = appInterface.createMandate(mandateData)
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -95,17 +102,18 @@ async def create_mandate(
) )
@router.put("/{mandateId}", response_model=Mandate, tags=["Mandates"]) @router.put("/{mandateId}", response_model=Mandate, tags=["Mandates"])
@limiter.limit("10/minute")
async def update_mandate( async def update_mandate(
mandateId: str, mandateId: str,
mandateData: Mandate, mandateData: Mandate,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Update an existing mandate""" """Update an existing mandate"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
# Check if mandate exists # Check if mandate exists
existingMandate = interfaceGateway.getMandateById(mandateId) existingMandate = appInterface.getMandateById(mandateId)
if not existingMandate: if not existingMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -114,7 +122,7 @@ async def update_mandate(
# Update mandate data # Update mandate data
try: try:
updatedMandate = interfaceGateway.updateMandate(mandateId, mandateData) updatedMandate = appInterface.updateMandate(mandateId, mandateData)
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -138,16 +146,17 @@ async def update_mandate(
) )
@router.delete("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"]) @router.delete("/{mandateId}", response_model=Dict[str, Any], tags=["Mandates"])
@limiter.limit("10/minute")
async def delete_mandate( async def delete_mandate(
mandateId: str, mandateId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Delete a mandate""" """Delete a mandate"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
# Check if mandate exists # Check if mandate exists
existingMandate = interfaceGateway.getMandateById(mandateId) existingMandate = appInterface.getMandateById(mandateId)
if not existingMandate: if not existingMandate:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -156,7 +165,7 @@ async def delete_mandate(
# Delete mandate # Delete mandate
try: try:
interfaceGateway.deleteMandate(mandateId) appInterface.deleteMandate(mandateId)
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -172,3 +181,18 @@ 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(
currentUser: Dict[str, Any] = Depends(getCurrentUser)
):
"""
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

@ -5,64 +5,65 @@ from datetime import datetime
import logging import logging
# Import auth module # Import auth module
import modules.security.auth as auth from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.lucydomInterface as lucydomInterface import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.interfaces.lucydomModel import Prompt, getModelAttributes from modules.interfaces.serviceManagementModel import Prompt
from modules.interfaces.serviceAppModel import AttributeDefinition
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Model attributes for Prompt
promptAttributes = getModelAttributes(Prompt)
# Create router for prompt endpoints # Create router for prompt endpoints
router = APIRouter( router = APIRouter(
prefix="/api/prompts", prefix="/api/prompts",
tags=["Prompts"], tags=["Manage Prompts"],
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("", response_model=List[Prompt]) @router.get("", response_model=List[Prompt])
@limiter.limit("30/minute")
async def get_prompts( async def get_prompts(
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get all prompts""" """Get all prompts"""
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
prompts = interfaceLucydom.getAllPrompts() prompts = managementInterface.getAllPrompts()
return [Prompt(**prompt) for prompt in prompts] return [Prompt(**prompt) for prompt in prompts]
@router.post("", response_model=Prompt) @router.post("", response_model=Prompt)
@limiter.limit("10/minute")
async def create_prompt( async def create_prompt(
prompt: Prompt, prompt: Prompt,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Create a new prompt""" """Create a new prompt"""
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Convert Prompt to dict for interface # Convert Prompt to dict for interface
prompt_data = prompt.model_dump() prompt_data = prompt.model_dump()
# Create prompt # Create prompt
newPrompt = interfaceLucydom.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 promptAttributes and hasattr(newPrompt, "createdAt"): if "createdAt" in Prompt.getModelAttributeDefinitions() and hasattr(newPrompt, "createdAt"):
newPrompt["createdAt"] = datetime.now().isoformat() newPrompt["createdAt"] = datetime.now().isoformat()
return Prompt(**newPrompt) return Prompt(**newPrompt)
@router.get("/{promptId}", response_model=Prompt) @router.get("/{promptId}", response_model=Prompt)
@limiter.limit("30/minute")
async def get_prompt( async def get_prompt(
promptId: str = Path(..., description="ID of the prompt"), promptId: str = Path(..., description="ID of the prompt"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get a specific prompt""" """Get a specific prompt"""
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Get prompt # Get prompt
prompt = interfaceLucydom.getPrompt(promptId) prompt = managementInterface.getPrompt(promptId)
if not prompt: if not prompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -72,16 +73,17 @@ async def get_prompt(
return Prompt(**prompt) return Prompt(**prompt)
@router.put("/{promptId}", response_model=Prompt) @router.put("/{promptId}", response_model=Prompt)
@limiter.limit("10/minute")
async def update_prompt( async def update_prompt(
promptId: str = Path(..., description="ID of the prompt to update"), promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Prompt = Body(...), promptData: Prompt = Body(...),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Update an existing prompt""" """Update an existing prompt"""
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Check if the prompt exists # Check if the prompt exists
existingPrompt = interfaceLucydom.getPrompt(promptId) existingPrompt = managementInterface.getPrompt(promptId)
if not existingPrompt: if not existingPrompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -92,7 +94,7 @@ async def update_prompt(
update_data = promptData.model_dump() update_data = promptData.model_dump()
# Update prompt # Update prompt
updatedPrompt = interfaceLucydom.updatePrompt(promptId, update_data) updatedPrompt = managementInterface.updatePrompt(promptId, update_data)
if not updatedPrompt: if not updatedPrompt:
raise HTTPException( raise HTTPException(
@ -103,22 +105,23 @@ async def update_prompt(
return Prompt(**updatedPrompt) return Prompt(**updatedPrompt)
@router.delete("/{promptId}", response_model=Dict[str, Any]) @router.delete("/{promptId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_prompt( async def delete_prompt(
promptId: str = Path(..., description="ID of the prompt to delete"), promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Delete a prompt""" """Delete a prompt"""
interfaceLucydom = lucydomInterface.getInterface(currentUser) managementInterface = serviceManagementClass.getInterface(currentUser)
# Check if the prompt exists # Check if the prompt exists
existingPrompt = interfaceLucydom.getPrompt(promptId) existingPrompt = managementInterface.getPrompt(promptId)
if not existingPrompt: if not existingPrompt:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prompt with ID {promptId} not found" detail=f"Prompt with ID {promptId} not found"
) )
success = interfaceLucydom.deletePrompt(promptId) success = managementInterface.deletePrompt(promptId)
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -126,3 +129,18 @@ 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(
currentUser: Dict[str, Any] = Depends(getCurrentUser)
):
"""
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

@ -1,34 +1,36 @@
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request 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 from datetime import datetime
import logging import logging
import inspect
import importlib
import os
from pydantic import BaseModel
# Import auth module # Import interfaces and models
import modules.security.auth as auth import modules.interfaces.serviceManagementClass as serviceManagementClass
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
# Import interfaces # Import the attribute definition and helper functions
import modules.interfaces.gatewayInterface as gatewayInterface from modules.interfaces.serviceManagementModel import User, AttributeDefinition, getModelAttributeDefinitions
import modules.interfaces.gatewayModel as gatewayModel from modules.interfaces.serviceAppModel import AttributeDefinition as ServiceAppAttributeDefinition
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Model attributes for User
userAttributes = gatewayModel.getModelAttributes(gatewayModel.User)
router = APIRouter( router = APIRouter(
prefix="/api/users", prefix="/api/users",
tags=["Users"], tags=["Manage Users"],
responses={404: {"description": "Not found"}} responses={404: {"description": "Not found"}}
) )
@router.get("/", response_model=List[Dict[str, Any]], tags=["Users"]) @router.get("/", response_model=List[Dict[str, Any]], tags=["Users"])
async def get_users(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)): async def get_users(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Get all users in the current mandate""" """Get all users in the current mandate"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
return interfaceGateway.getUsers() return appInterface.getUsers()
except Exception as e: except Exception as e:
logger.error(f"Error getting users: {str(e)}") logger.error(f"Error getting users: {str(e)}")
raise HTTPException( raise HTTPException(
@ -39,12 +41,12 @@ async def get_users(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveU
@router.get("/{userId}", response_model=Dict[str, Any], tags=["Users"]) @router.get("/{userId}", response_model=Dict[str, Any], tags=["Users"])
async def get_user( async def get_user(
userId: str, userId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get a specific user by ID""" """Get a specific user by ID"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
user = interfaceGateway.getUserById(userId) user = appInterface.getUserById(userId)
if not user: if not user:
raise HTTPException( raise HTTPException(
@ -62,19 +64,19 @@ async def get_user(
detail=f"Failed to get user: {str(e)}" detail=f"Failed to get user: {str(e)}"
) )
@router.post("/", response_model=gatewayModel.User, tags=["Users"]) @router.post("/", response_model=User, tags=["Users"])
async def create_user( async def create_user(
userData: gatewayModel.User, userData: User,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Create a new user""" """Create a new user"""
try: try:
# Get admin user for user creation # Get interface for user creation
interfaceRoot = gatewayInterface.getRootInterface() appInterface = serviceManagementClass.getInterface(currentUser)
try: try:
# Convert User model to dict and pass to createUser # Convert User model to dict and pass to createUser
createdUser = interfaceRoot.createUser( createdUser = appInterface.createUser(
username=userData.username, username=userData.username,
email=userData.email, email=userData.email,
fullName=userData.fullName, fullName=userData.fullName,
@ -105,19 +107,19 @@ async def create_user(
detail=f"Failed to create user: {str(e)}" detail=f"Failed to create user: {str(e)}"
) )
@router.put("/{userId}", response_model=gatewayModel.User, tags=["Users"]) @router.put("/{userId}", response_model=User, tags=["Users"])
async def update_user( async def update_user(
userId: str, userId: str,
userData: gatewayModel.User, userData: User,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Update an existing user""" """Update an existing user"""
try: try:
# Get admin user for user updates # Get interface for user updates
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
# Check if user exists # Check if user exists
existingUser = interfaceGateway.getUserById(userId) existingUser = appInterface.getUserById(userId)
if not existingUser: if not existingUser:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -126,7 +128,7 @@ async def update_user(
# Update user data # Update user data
try: try:
updatedUser = interfaceGateway.updateUser(userId, userData) updatedUser = appInterface.updateUser(userId, userData)
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -152,12 +154,12 @@ async def update_user(
@router.delete("/{userId}", response_model=Dict[str, Any], tags=["Users"]) @router.delete("/{userId}", response_model=Dict[str, Any], tags=["Users"])
async def delete_user( async def delete_user(
userId: str, userId: str,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Delete a user""" """Delete a user"""
try: try:
interfaceGateway = gatewayInterface.getInterface(currentUser) appInterface = serviceManagementClass.getInterface(currentUser)
interfaceGateway.deleteUser(userId) appInterface.deleteUser(userId)
return {"message": f"User {userId} deleted successfully"} return {"message": f"User {userId} deleted successfully"}
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
@ -170,3 +172,18 @@ async def delete_user(
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=f"Failed to delete user: {str(e)}"
) )
@router.get("/attributes", response_model=List[ServiceAppAttributeDefinition])
@limiter.limit("30/minute")
async def get_user_attributes(
currentUser: Dict[str, Any] = Depends(getCurrentUser)
):
"""
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

@ -1,132 +0,0 @@
from fastapi import APIRouter, HTTPException, Depends, Body, status, Response
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.staticfiles import StaticFiles
from typing import Dict, Any, Optional
from datetime import timedelta
import pathlib
import os
import logging
from pathlib import Path as FilePath
from modules.shared.configuration import APP_CONFIG
import modules.security.auth as auth
import modules.interfaces.gatewayInterface as gatewayInterface
router = APIRouter(
prefix="",
tags=["General"],
responses={404: {"description": "Not found"}}
)
# Static folder setup - using absolute path from app root
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
staticFolder = baseDir / "static"
os.makedirs(staticFolder, exist_ok=True)
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
logger = logging.getLogger(__name__)
@router.get("/", tags=["General"])
async def root():
"""API status endpoint"""
return {
"status": "online",
"message": "Data Platform API is active",
"allowedOrigins": f"Allowed origins are {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
}
@router.get("/api/environment", tags=["General"])
async def get_environment():
"""Get environment configuration for frontend"""
return {
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
"environment": APP_CONFIG.get("APP_ENV", "development"),
"instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"),
# Add other environment variables the frontend might need
}
@router.options("/{fullPath:path}", tags=["General"])
async def options_route(fullPath: str):
return Response(status_code=200)
@router.post("/api/token", response_model=gatewayModel.Token, tags=["General"])
async def login_for_access_token(
formData: OAuth2PasswordRequestForm = Depends(),
authority: str = "local",
external_token: Optional[str] = None
):
"""Get access token for user authentication"""
# Create a new gateway interface instance with admin context
interfaceRoot = gatewayInterface.getRootInterface()
try:
# Get token directly
token = interfaceRoot.authenticateAndGetToken(
username=formData.username,
password=formData.password,
authority=authority,
external_token=external_token
)
return token
except ValueError as e:
# Handle authentication errors
error_msg = str(e)
logger.warning(f"Authentication failed for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_msg,
headers={"WWW-Authenticate": "Bearer"},
)
except Exception as e:
# Handle other errors
error_msg = f"Login failed: {str(e)}"
logger.error(f"Unexpected error during login for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_msg
)
@router.get("/api/user/me", response_model=Dict[str, Any], tags=["General"])
async def read_user_me(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
return currentUser
@router.post("/api/user/register", response_model=gatewayModel.User, tags=["General"])
async def register_user(userData: gatewayModel.User):
"""Register a new user."""
try:
interfaceRoot = gatewayInterface.getRootInterface()
return interfaceRoot.registerUser(userData.model_dump())
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Error registering user: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to register user: {str(e)}"
)
@router.get("/api/user/available", response_model=Dict[str, Any], tags=["General"])
async def check_username_availability(
username: str,
authenticationAuthority: str = "local"
):
"""Check if a username is available for registration"""
try:
interfaceRoot = gatewayInterface.getRootInterface()
return interfaceRoot.checkUsernameAvailability(username, authenticationAuthority)
except Exception as e:
logger.error(f"Error checking username availability: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to check username availability: {str(e)}"
)
@router.get("/favicon.ico", tags=["General"])
async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")

View file

@ -1,322 +0,0 @@
"""
Routes for Google authentication.
"""
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie, Body
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
# Import auth module
import modules.security.auth as auth
# Import interfaces
import modules.interfaces.googleInterface as googleInterface
import modules.interfaces.gatewayInterface as gatewayInterface
from modules.interfaces.googleModel import (
GoogleToken,
GoogleUserInfo,
GoogleAuthStatus,
GoogleTokenResponse,
GoogleSaveTokenResponse
)
# Configure logger
logger = logging.getLogger(__name__)
# Create router for Google Auth endpoints
router = APIRouter(
prefix="/api/google",
tags=["Google"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
@router.get("/login")
async def login():
"""Initiate Google login for the current user"""
try:
# Get Google interface with root context for initial setup
google = googleInterface.getRootInterface()
# Get login URL
auth_url = google.initiateLogin()
if not auth_url:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to initiate Google login"
)
logger.info("Redirecting to Google login")
return RedirectResponse(auth_url)
except Exception as e:
logger.error(f"Error initiating Google login: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initiate Google login: {str(e)}"
)
@router.get("/auth/callback")
async def auth_callback(code: str, state: str, request: Request):
"""Handle Google OAuth callback"""
try:
# Get Google interface with root context for initial setup
google = googleInterface.getRootInterface()
# Handle auth callback
token_response = google.handleAuthCallback(code)
if not token_response:
return HTMLResponse(
content="""
<html>
<head>
<title>Authentication Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Authentication Failed</h1>
<p>Could not acquire access token.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Get gateway interface for user operations
gateway = gatewayInterface.getRootInterface()
# Check if user exists
user = gateway.getUserByUsername(token_response.user_info["email"])
# If user doesn't exist, create a new user in the default mandate
if not user:
try:
# Get the root mandate ID
rootMandateId = gateway.getInitialId("mandates")
if not rootMandateId:
raise ValueError("Root mandate not found")
# Create new user with Google authentication
user = gateway.createUser(
username=token_response.user_info["email"],
email=token_response.user_info["email"],
fullName=token_response.user_info.get("name", token_response.user_info["email"]),
mandateId=rootMandateId,
authenticationAuthority="google"
)
logger.info(f"Created new user for Google account: {token_response.user_info['email']}")
# Verify user was created by retrieving it
user = gateway.getUserByUsername(token_response.user_info["email"])
if not user:
raise ValueError("Failed to retrieve created user")
except Exception as e:
logger.error(f"Failed to create user for Google account: {str(e)}")
return HTMLResponse(
content="""
<html>
<head>
<title>Registration Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Registration Failed</h1>
<p>Could not create user account.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Create backend token
access_token_expires = timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = auth.createAccessToken(
data={
"sub": user["username"],
"mandateId": str(user["mandateId"]),
"userId": str(user["id"]),
"authenticationAuthority": "google"
},
expiresDelta=access_token_expires
)
# Store tokens in session storage for the frontend to pick up
response = HTMLResponse(
content=f"""
<html>
<head>
<title>Authentication Successful</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }}
.success {{ color: green; }}
</style>
</head>
<body>
<h1 class="success">Authentication Successful</h1>
<p>Welcome, {token_response.user_info.get('name', 'User')}!</p>
<p>This window will close automatically.</p>
<script>
// Store token data in session storage
sessionStorage.setItem('google_token_data', JSON.stringify({json.dumps(token_response.model_dump())}));
// Notify parent window of success
if (window.opener) {{
window.opener.postMessage({{
type: 'google_auth_success',
user: {json.dumps(token_response.user_info)},
token_data: {json.dumps(token_response.model_dump())},
access_token: "{access_token}"
}}, '*');
}}
// Close window after 3 seconds
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
"""
)
return response
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
)
@router.get("/status", response_model=GoogleAuthStatus)
async def auth_status(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Check Google authentication status"""
try:
# For authenticated endpoints, use the current user's context
google = googleInterface.getInterface(currentUser)
# Get current user token and info
user_info, access_token = google.getCurrentUserToken()
if not user_info or not access_token:
return GoogleAuthStatus(
authenticated=False,
message="Not authenticated with Google"
)
# Convert user_info to GoogleUserInfo model
user_info_model = GoogleUserInfo(**user_info)
return GoogleAuthStatus(
authenticated=True,
user=user_info_model
)
except Exception as e:
logger.error(f"Error checking authentication status: {str(e)}")
return GoogleAuthStatus(
authenticated=False,
message=f"Error checking authentication status: {str(e)}"
)
@router.get("/token", response_model=GoogleTokenResponse)
async def get_token(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Get Google token for current user."""
try:
# For authenticated endpoints, use the current user's context
google = googleInterface.getInterface(currentUser)
# Get token
token_data = google.getGoogleToken()
if not token_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No token found"
)
# Convert to GoogleToken model
token = GoogleToken(**token_data)
return GoogleTokenResponse(token=token)
except Exception as e:
logger.error(f"Error getting token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.post("/save-token", response_model=GoogleSaveTokenResponse)
async def save_token(
token_data: GoogleToken,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)
):
"""Save Google token data from frontend"""
try:
# For authenticated endpoints, use the current user's context
google = googleInterface.getInterface(currentUser)
# Save token
success = google.saveGoogleToken(token_data.model_dump())
if success:
return GoogleSaveTokenResponse(
success=True,
message="Token saved successfully",
token=token_data
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to save token"
)
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error saving token: {str(e)}"
)
@router.post("/logout")
async def logout(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Logout from Google"""
try:
# For authenticated endpoints, use the current user's context
google = googleInterface.getInterface(currentUser)
# Delete token
success = google.deleteGoogleToken()
if success:
return JSONResponse({
"message": "Successfully logged out from Google"
})
else:
return JSONResponse({
"message": "Failed to logout from Google"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)

View file

@ -1,322 +0,0 @@
"""
Routes for Microsoft authentication.
"""
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie, Body
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
# Import auth module
import modules.security.auth as auth
# Import interfaces
import modules.interfaces.msftInterface as msftInterface
import modules.interfaces.gatewayInterface as gatewayInterface
from modules.interfaces.msftModel import (
MsftToken,
MsftUserInfo,
MsftAuthStatus,
MsftTokenResponse,
MsftSaveTokenResponse
)
# Configure logger
logger = logging.getLogger(__name__)
# Create router for Microsoft Auth endpoints
router = APIRouter(
prefix="/api/msft",
tags=["Microsoft"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
@router.get("/login")
async def login():
"""Initiate Microsoft login for the current user"""
try:
# Get Microsoft interface with root context for initial setup
msft = msftInterface.getRootInterface()
# Get login URL
auth_url = msft.initiateLogin()
if not auth_url:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to initiate Microsoft login"
)
logger.info("Redirecting to Microsoft login")
return RedirectResponse(auth_url)
except Exception as e:
logger.error(f"Error initiating Microsoft login: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initiate Microsoft login: {str(e)}"
)
@router.get("/auth/callback")
async def auth_callback(code: str, state: str, request: Request):
"""Handle Microsoft OAuth callback"""
try:
# Get Microsoft interface with root context for initial setup
msft = msftInterface.getRootInterface()
# Handle auth callback
token_response = msft.handleAuthCallback(code)
if not token_response:
return HTMLResponse(
content="""
<html>
<head>
<title>Authentication Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Authentication Failed</h1>
<p>Could not acquire access token.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Get gateway interface for user operations
gateway = gatewayInterface.getRootInterface()
# Check if user exists
user = gateway.getUserByUsername(token_response.user_info["email"])
# If user doesn't exist, create a new user in the default mandate
if not user:
try:
# Get the root mandate ID
rootMandateId = gateway.getInitialId("mandates")
if not rootMandateId:
raise ValueError("Root mandate not found")
# Create new user with Microsoft authentication
user = gateway.createUser(
username=token_response.user_info["email"],
email=token_response.user_info["email"],
fullName=token_response.user_info.get("name", token_response.user_info["email"]),
mandateId=rootMandateId,
authenticationAuthority="microsoft"
)
logger.info(f"Created new user for Microsoft account: {token_response.user_info['email']}")
# Verify user was created by retrieving it
user = gateway.getUserByUsername(token_response.user_info["email"])
if not user:
raise ValueError("Failed to retrieve created user")
except Exception as e:
logger.error(f"Failed to create user for Microsoft account: {str(e)}")
return HTMLResponse(
content="""
<html>
<head>
<title>Registration Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Registration Failed</h1>
<p>Could not create user account.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Create backend token
access_token_expires = timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = auth.createAccessToken(
data={
"sub": user["username"],
"mandateId": str(user["mandateId"]),
"userId": str(user["id"]),
"authenticationAuthority": "microsoft"
},
expiresDelta=access_token_expires
)
# Store tokens in session storage for the frontend to pick up
response = HTMLResponse(
content=f"""
<html>
<head>
<title>Authentication Successful</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }}
.success {{ color: green; }}
</style>
</head>
<body>
<h1 class="success">Authentication Successful</h1>
<p>Welcome, {token_response.user_info.get('name', 'User')}!</p>
<p>This window will close automatically.</p>
<script>
// Store token data in session storage
sessionStorage.setItem('msft_token_data', JSON.stringify({json.dumps(token_response.model_dump())}));
// Notify parent window of success
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_auth_success',
user: {json.dumps(token_response.user_info)},
token_data: {json.dumps(token_response.model_dump())},
access_token: "{access_token}"
}}, '*');
}}
// Close window after 3 seconds
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
"""
)
return response
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
)
@router.get("/status", response_model=MsftAuthStatus)
async def auth_status(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Check Microsoft authentication status"""
try:
# For authenticated endpoints, use the current user's context
msft = msftInterface.getInterface(currentUser)
# Get current user token and info
user_info, access_token = msft.getCurrentUserToken()
if not user_info or not access_token:
return MsftAuthStatus(
authenticated=False,
message="Not authenticated with Microsoft"
)
# Convert user_info to MsftUserInfo model
user_info_model = MsftUserInfo(**user_info)
return MsftAuthStatus(
authenticated=True,
user=user_info_model
)
except Exception as e:
logger.error(f"Error checking authentication status: {str(e)}")
return MsftAuthStatus(
authenticated=False,
message=f"Error checking authentication status: {str(e)}"
)
@router.get("/token", response_model=MsftTokenResponse)
async def get_token(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Get Microsoft token for current user."""
try:
# For authenticated endpoints, use the current user's context
msft = msftInterface.getInterface(currentUser)
# Get token
token_data = msft.getMsftToken()
if not token_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No token found"
)
# Convert to MsftToken model
token = MsftToken(**token_data)
return MsftTokenResponse(token=token)
except Exception as e:
logger.error(f"Error getting token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.post("/save-token", response_model=MsftSaveTokenResponse)
async def save_token(
token_data: MsftToken,
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)
):
"""Save Microsoft token data from frontend"""
try:
# For authenticated endpoints, use the current user's context
msft = msftInterface.getInterface(currentUser)
# Save token
success = msft.saveMsftToken(token_data.model_dump())
if success:
return MsftSaveTokenResponse(
success=True,
message="Token saved successfully",
token=token_data
)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to save token"
)
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error saving token: {str(e)}"
)
@router.post("/logout")
async def logout(currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser)):
"""Logout from Microsoft"""
try:
# For authenticated endpoints, use the current user's context
msft = msftInterface.getInterface(currentUser)
# Delete token
success = msft.deleteMsftToken()
if success:
return JSONResponse({
"message": "Successfully logged out from Microsoft"
})
else:
return JSONResponse({
"message": "Failed to logout from Microsoft"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)

View file

@ -0,0 +1,164 @@
"""
Routes for Google authentication.
"""
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from google.auth.transport.requests import Request
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface
from modules.interfaces.serviceAppModel import AuthAuthority
from modules.interfaces.serviceAppTokens import GoogleToken, saveToken
from modules.security.auth import getCurrentUser, limiter
# Configure logger
logger = logging.getLogger(__name__)
# Create router
router = APIRouter(
prefix="/api/google",
tags=["Security Google"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
# Google OAuth configuration
CLIENT_ID = APP_CONFIG.get("Service_GOOGLE_CLIENT_ID")
CLIENT_SECRET = APP_CONFIG.get("Service_GOOGLE_CLIENT_SECRET")
REDIRECT_URI = APP_CONFIG.get("Service_GOOGLE_REDIRECT_URI")
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
]
@router.get("/login")
@limiter.limit("5/minute")
async def login():
"""Initiate Google login"""
try:
# Create OAuth flow
flow = Flow.from_client_config(
{
"web": {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [REDIRECT_URI]
}
},
scopes=SCOPES
)
# Generate auth URL
auth_url, _ = flow.authorization_url(
access_type="offline",
include_granted_scopes="true"
)
return RedirectResponse(auth_url)
except Exception as e:
logger.error(f"Error initiating Google login: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initiate Google login: {str(e)}"
)
@router.get("/auth/callback")
async def auth_callback(code: str, request: Request):
"""Handle Google OAuth callback"""
try:
# Create OAuth flow
flow = Flow.from_client_config(
{
"web": {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": [REDIRECT_URI]
}
},
scopes=SCOPES,
redirect_uri=REDIRECT_URI
)
# Exchange code for token
flow.fetch_token(code=code)
credentials = flow.credentials
# Create token data
token_data = {
"access_token": credentials.token,
"refresh_token": credentials.refresh_token,
"token_type": credentials.token_type,
"expires_at": credentials.expiry.timestamp()
}
# Save token data
appInterface = getInterface()
saveToken(appInterface, "Google", token_data)
# 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_data)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
)
@router.post("/logout")
@limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Logout from Google"""
try:
# Get user interface
appInterface = getInterface()
# Revoke all sessions for the user
appInterface.revokeAllUserSessions(currentUser.get("id"))
return JSONResponse({
"message": "Successfully logged out from Google"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)

View file

@ -0,0 +1,195 @@
"""
Routes for local security and authentication.
"""
from fastapi import APIRouter, HTTPException, status, Depends, Request
from fastapi.security import OAuth2PasswordRequestForm
import logging
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from fastapi.responses import JSONResponse
# Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter
from modules.interfaces.serviceAppClass import getInterface
from modules.interfaces.serviceAppModel import User, AuthAuthority
from modules.interfaces.serviceAppTokens import LocalToken, saveToken
# Configure logger
logger = logging.getLogger(__name__)
# Create router for Local Security endpoints
router = APIRouter(
prefix="/api/local",
tags=["Security Local"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
@router.post("/login")
@limiter.limit("5/minute")
async def login(
request: Request,
formData: OAuth2PasswordRequestForm = Depends(),
):
"""Get access token for local user authentication"""
try:
# Validate CSRF token
csrf_token = request.headers.get("X-CSRF-Token")
if not csrf_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF token missing"
)
# Get gateway interface
appInterface = getInterface()
# Authenticate user
user = appInterface.authenticateLocalUser(
username=formData.username,
password=formData.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create token data
token_data = {
"sub": user.username,
"mandateId": str(user.mandateId),
"userId": str(user.id),
"authenticationAuthority": AuthAuthority.LOCAL
}
# Create access token
access_token, expires_at = createAccessToken(token_data)
if not access_token:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create access token"
)
# Save token data
token_data = {
"access_token": access_token,
"token_type": "bearer",
"expires_at": expires_at.timestamp()
}
saveToken(appInterface, "Local", token_data)
# Create response data
response_data = {
"type": "local_auth_success",
"access_token": access_token,
"token_data": token_data
}
return response_data
except ValueError as e:
# Handle authentication errors
error_msg = str(e)
logger.warning(f"Authentication failed for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_msg,
headers={"WWW-Authenticate": "Bearer"},
)
except Exception as e:
# Handle other errors
error_msg = f"Login failed: {str(e)}"
logger.error(f"Unexpected error during login for user {formData.username}: {error_msg}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_msg
)
@router.post("/register", response_model=User)
async def register_user(userData: User):
"""Register a new local user."""
try:
# Get gateway interface
appInterface = getInterface()
# Create user
user = appInterface.createUser(
username=userData.username,
password=userData.password,
email=userData.email,
mandateId=userData.mandateId,
authenticationAuthority=AuthAuthority.LOCAL
)
if not user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to register user"
)
return user
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error(f"Error registering user: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to register user: {str(e)}"
)
@router.get("/me", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def read_user_me(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Get current user information"""
return currentUser
@router.post("/logout")
@limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Logout from local authentication"""
try:
# Get user interface
appInterface = getInterface()
# Revoke all sessions for the user
appInterface.revokeAllUserSessions(currentUser.get("id"))
return JSONResponse({
"message": "Successfully logged out"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)
@router.get("/available", response_model=Dict[str, Any])
async def check_username_availability(
username: str,
authenticationAuthority: str = "local"
):
"""Check if a username is available for registration"""
try:
interfaceRoot = getInterface()
return interfaceRoot.checkUsernameAvailability(username, authenticationAuthority)
except Exception as e:
logger.error(f"Error checking username availability: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to check username availability: {str(e)}"
)

View file

@ -0,0 +1,154 @@
"""
Routes for Microsoft authentication.
"""
from fastapi import APIRouter, HTTPException, Request, Response, status, Depends
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
import msal
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface
from modules.interfaces.serviceAppModel import AuthAuthority
from modules.interfaces.serviceAppTokens import MsftToken, saveToken
from modules.security.auth import getCurrentUser, limiter
# Configure logger
logger = logging.getLogger(__name__)
# Create router
router = APIRouter(
prefix="/api/msft",
tags=["Security Microsoft"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
# Microsoft OAuth configuration
CLIENT_ID = APP_CONFIG.get("Service_MSFT_CLIENT_ID")
CLIENT_SECRET = APP_CONFIG.get("Service_MSFT_CLIENT_SECRET")
TENANT_ID = APP_CONFIG.get("Service_MSFT_TENANT_ID", "common")
REDIRECT_URI = APP_CONFIG.get("Service_MSFT_REDIRECT_URI")
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["Mail.ReadWrite", "User.Read"]
@router.get("/login")
@limiter.limit("5/minute")
async def login():
"""Initiate Microsoft login"""
try:
# Create MSAL app
msal_app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET
)
# Generate auth URL
auth_url = msal_app.get_authorization_request_url(
scopes=SCOPES,
redirect_uri=REDIRECT_URI
)
return RedirectResponse(auth_url)
except Exception as e:
logger.error(f"Error initiating Microsoft login: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initiate Microsoft login: {str(e)}"
)
@router.get("/auth/callback")
async def auth_callback(code: str, request: Request):
"""Handle Microsoft OAuth callback"""
try:
# Create MSAL app
msal_app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET
)
# Get token from code
token_response = msal_app.acquire_token_by_authorization_code(
code,
scopes=SCOPES,
redirect_uri=REDIRECT_URI
)
if "error" in token_response:
return HTMLResponse(
content="<html><body><h1>Authentication Failed</h1><p>Could not acquire token.</p></body></html>",
status_code=400
)
# Create token data
token_data = {
"access_token": token_response["access_token"],
"refresh_token": token_response.get("refresh_token", ""),
"token_type": token_response.get("token_type", "bearer"),
"expires_at": datetime.now().timestamp() + token_response.get("expires_in", 0)
}
# Save token data
appInterface = getInterface()
saveToken(appInterface, "Msft", token_data)
# 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_data)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
)
@router.post("/logout")
@limiter.limit("30/minute")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentUser)):
"""Logout from Microsoft"""
try:
# Get user interface
appInterface = getInterface()
# Revoke all sessions for the user
appInterface.revokeAllUserSessions(currentUser.get("id"))
return JSONResponse({
"message": "Successfully logged out from Microsoft"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)

View file

@ -10,33 +10,32 @@ from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status
from datetime import datetime from datetime import datetime
# Import auth module # Import auth modules
import modules.security.auth as auth from modules.security.auth import limiter, getCurrentUser
# Import interfaces # Import interfaces
import modules.interfaces.lucydomInterface as lucydomInterface import modules.interfaces.serviceChatClass as serviceChatClass
import modules.interfaces.msftInterface as msftInterface
import modules.interfaces.googleInterface as googleInterface
# Import workflow manager # Import workflow manager
from modules.workflow.workflowManager import getWorkflowManager from modules.workflow.workflowManager import getWorkflowManager
# Import models # Import models
from modules.interfaces.lucydomModel import ( from modules.interfaces.serviceChatModel import (
ChatWorkflow, ChatWorkflow,
ChatMessage, ChatMessage,
ChatLog, ChatLog,
ChatStat, ChatStat,
ChatDocument, ChatDocument,
UserInputRequest, UserInputRequest,
getModelAttributes Workflow,
getModelAttributeDefinitions
) )
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Model attributes for ChatWorkflow # Model attributes for ChatWorkflow
workflowAttributes = getModelAttributes(ChatWorkflow) workflowAttributes = getModelAttributeDefinitions(ChatWorkflow)
# Create router for workflow endpoints # Create router for workflow endpoints
router = APIRouter( router = APIRouter(
@ -48,23 +47,21 @@ router = APIRouter(
def createServiceContainer(currentUser: Dict[str, Any]): def createServiceContainer(currentUser: Dict[str, Any]):
"""Create a service container with all required interfaces.""" """Create a service container with all required interfaces."""
# Get all interfaces # Get all interfaces
interfaceBase = lucydomInterface.getInterface(currentUser) chatInterface = serviceChatClass.getInterface(currentUser)
interfaceMsft = msftInterface.getInterface(currentUser)
interfaceGoogle = googleInterface.getInterface(currentUser)
# Create service container # Create service container
service = type('ServiceContainer', (), { service = type('ServiceContainer', (), {
'base': interfaceBase, 'user': currentUser,
'msft': interfaceMsft, 'functions': chatInterface
'google': interfaceGoogle
}) })
return service return service
# API Endpoint for getting all workflows # API Endpoint for getting all workflows
@router.get("", response_model=List[ChatWorkflow]) @router.get("", response_model=List[ChatWorkflow])
@limiter.limit("30/minute")
async def list_workflows( async def list_workflows(
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""List all workflows for the current user.""" """List all workflows for the current user."""
try: try:
@ -83,10 +80,11 @@ async def list_workflows(
# State 1: Workflow Initialization endpoint # State 1: Workflow Initialization endpoint
@router.post("/start", response_model=ChatWorkflow) @router.post("/start", response_model=ChatWorkflow)
@limiter.limit("10/minute")
async def start_workflow( async def start_workflow(
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"), workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
userInput: UserInputRequest = Body(...), userInput: UserInputRequest = Body(...),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
""" """
Starts a new workflow or continues an existing one. Starts a new workflow or continues an existing one.
@ -110,9 +108,10 @@ async def start_workflow(
# State 8: Workflow Stopped endpoint # State 8: Workflow Stopped endpoint
@router.post("/{workflowId}/stop", response_model=ChatWorkflow) @router.post("/{workflowId}/stop", response_model=ChatWorkflow)
@limiter.limit("10/minute")
async def stop_workflow( async def stop_workflow(
workflowId: str = Path(..., description="ID of the workflow to stop"), workflowId: str = Path(..., description="ID of the workflow to stop"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Stops a running workflow.""" """Stops a running workflow."""
try: try:
@ -133,9 +132,10 @@ async def stop_workflow(
# State 11: Workflow Reset/Deletion endpoint # State 11: Workflow Reset/Deletion endpoint
@router.delete("/{workflowId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_workflow( async def delete_workflow(
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Deletes a workflow and its associated data.""" """Deletes a workflow and its associated data."""
try: try:
@ -181,9 +181,10 @@ async def delete_workflow(
# API Endpoint for workflow status # API Endpoint for workflow status
@router.get("/{workflowId}/status", response_model=ChatWorkflow) @router.get("/{workflowId}/status", response_model=ChatWorkflow)
@limiter.limit("30/minute")
async def get_workflow_status( async def get_workflow_status(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get the current status of a workflow.""" """Get the current status of a workflow."""
try: try:
@ -210,10 +211,11 @@ async def get_workflow_status(
# API Endpoint for workflow logs with selective data transfer # API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=List[ChatLog]) @router.get("/{workflowId}/logs", response_model=List[ChatLog])
@limiter.limit("30/minute")
async def get_workflow_logs( async def get_workflow_logs(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get logs for a workflow with support for selective data transfer.""" """Get logs for a workflow with support for selective data transfer."""
try: try:
@ -251,10 +253,11 @@ async def get_workflow_logs(
# API Endpoint for workflow messages with selective data transfer # API Endpoint for workflow messages with selective data transfer
@router.get("/{workflowId}/messages", response_model=List[ChatMessage]) @router.get("/{workflowId}/messages", response_model=List[ChatMessage])
@limiter.limit("30/minute")
async def get_workflow_messages( async def get_workflow_messages(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get messages for a workflow with support for selective data transfer.""" """Get messages for a workflow with support for selective data transfer."""
try: try:
@ -301,10 +304,11 @@ async def get_workflow_messages(
# Document Management Endpoints # Document Management Endpoints
@router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_workflow_message( async def delete_workflow_message(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message to delete"), messageId: str = Path(..., description="ID of the message to delete"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Delete a message from a workflow.""" """Delete a message from a workflow."""
try: try:
@ -349,11 +353,12 @@ async def delete_workflow_message(
) )
@router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_file_from_message( async def delete_file_from_message(
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"), messageId: str = Path(..., description="ID of the message"),
fileId: str = Path(..., description="ID of the file to delete"), fileId: str = Path(..., description="ID of the file to delete"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Delete a file reference from a message in a workflow.""" """Delete a file reference from a message in a workflow."""
try: try:
@ -395,9 +400,10 @@ async def delete_file_from_message(
# File preview and download routes # File preview and download routes
@router.get("/files/{fileId}/preview", response_model=ChatDocument) @router.get("/files/{fileId}/preview", response_model=ChatDocument)
@limiter.limit("30/minute")
async def preview_file( async def preview_file(
fileId: str = Path(..., description="ID of the file to preview"), fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Get file metadata and a preview of the file content.""" """Get file metadata and a preview of the file content."""
try: try:
@ -489,9 +495,10 @@ async def preview_file(
) )
@router.get("/files/{fileId}/download") @router.get("/files/{fileId}/download")
@limiter.limit("30/minute")
async def download_file( async def download_file(
fileId: str = Path(..., description="ID of the file to download"), fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) currentUser: Dict[str, Any] = Depends(getCurrentUser)
): ):
"""Download a file.""" """Download a file."""
try: try:

View file

@ -5,25 +5,33 @@ Handles JWT-based authentication, token generation, and user context.
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any, Tuple from typing import Optional, Dict, Any, Tuple
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt from jose import JWTError, jwt
import logging import logging
from slowapi import Limiter
from slowapi.util import get_remote_address
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getRootInterface
from modules.interfaces.serviceAppModel import Session, AuthEvent, UserPrivilege
# Get Config Data # Get Config Data
SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET") SECRET_KEY = APP_CONFIG.get("APP_JWT_SECRET_SECRET")
ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM") ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY")) ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
# OAuth2 Setup # OAuth2 Setup
oauth2Scheme = OAuth2PasswordBearer(tokenUrl="token") oauth2Scheme = OAuth2PasswordBearer(tokenUrl="token")
# Rate Limiter
limiter = Limiter(key_func=get_remote_address)
# Logger # Logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> str: def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, datetime]:
""" """
Creates a JWT Access Token. Creates a JWT Access Token.
@ -32,7 +40,7 @@ def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> s
expiresDelta: Validity duration of the token (optional) expiresDelta: Validity duration of the token (optional)
Returns: Returns:
JWT Token as string Tuple of (JWT Token as string, expiration datetime)
""" """
toEncode = data.copy() toEncode = data.copy()
@ -44,9 +52,27 @@ def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> s
toEncode.update({"exp": expire}) toEncode.update({"exp": expire})
encodedJwt = jwt.encode(toEncode, SECRET_KEY, algorithm=ALGORITHM) encodedJwt = jwt.encode(toEncode, SECRET_KEY, algorithm=ALGORITHM)
return encodedJwt return encodedJwt, expire
def _getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]: def createRefreshToken(data: dict) -> Tuple[str, datetime]:
"""
Creates a JWT Refresh Token.
Args:
data: Data to encode (usually user ID or username)
Returns:
Tuple of (JWT Token as string, expiration datetime)
"""
toEncode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
toEncode.update({"exp": expire, "type": "refresh"})
encodedJwt = jwt.encode(toEncode, SECRET_KEY, algorithm=ALGORITHM)
return encodedJwt, expire
def _getUserBase(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
""" """
Extracts and validates the current user from the JWT token. Extracts and validates the current user from the JWT token.
@ -75,11 +101,11 @@ def _getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
raise credentialsException raise credentialsException
# Extract mandate ID and user ID from token # Extract mandate ID and user ID from token
_mandateId: str = payload.get("_mandateId") mandateId: str = payload.get("mandateId")
_userId: str = payload.get("_userId") userId: str = payload.get("userId")
if not _mandateId or not _userId: if not mandateId or not userId:
logger.error(f"Missing context in token: _mandateId={_mandateId}, _userId={_userId}") logger.error(f"Missing context in token: mandateId={mandateId}, userId={userId}")
raise credentialsException raise credentialsException
except JWTError: except JWTError:
@ -87,10 +113,10 @@ def _getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
raise credentialsException raise credentialsException
# Initialize Gateway Interface with context # Initialize Gateway Interface with context
gateway = getRootInterface() appInterface = getRootInterface()
# Retrieve user from database # Retrieve user from database
user = gateway.getUserByUsername(username) user = appInterface.getUserByUsername(username)
if user is None: if user is None:
logger.warning(f"User {username} not found") logger.warning(f"User {username} not found")
@ -101,27 +127,86 @@ def _getCurrentUser(token: str = Depends(oauth2Scheme)) -> Dict[str, Any]:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
# Ensure the user has the correct context # Ensure the user has the correct context
if str(user.get("_mandateId")) != str(_mandateId) or str(user.get("id")) != str(_userId): if str(user.get("mandateId")) != str(mandateId) or str(user.get("id")) != str(userId):
logger.error(f"User context mismatch: token(_mandateId={_mandateId}, _userId={_userId}) vs user(_mandateId={user.get('_mandateId')}, id={user.get('id')})") logger.error(f"User context mismatch: token(mandateId={mandateId}, userId={userId}) vs user(mandateId={user.get('mandateId')}, id={user.get('id')})")
raise credentialsException raise credentialsException
# Add authentication authority to user data
user["authenticationAuthority"] = user.get("authenticationAuthority", "local")
return user return user
def getCurrentActiveUser(currentUser: Dict[str, Any] = Depends(_getCurrentUser)) -> Dict[str, Any]: def getCurrentUser(currentUser: Dict[str, Any] = Depends(_getUserBase)) -> Dict[str, Any]:
"""Get current active user with additional validation."""
if currentUser.get("disabled", False): if currentUser.get("disabled", False):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="User is disabled" detail="User is disabled"
) )
return currentUser
auth_authority = currentUser.get("authenticationAuthority", "local") def createUserSession(userId: str, tokenId: str, request: Request) -> Session:
if auth_authority not in ["local", "microsoft"]: """Create a new user session."""
raise HTTPException( appInterface = getRootInterface()
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Invalid authentication authority: {auth_authority}" session = Session(
userId=userId,
tokenId=tokenId,
expiresAt=datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
ipAddress=request.client.host if request.client else None,
userAgent=request.headers.get("user-agent")
) )
return currentUser # Save session to database
appInterface.db.recordCreate("sessions", session.model_dump())
return session
def logAuthEvent(userId: str, eventType: str, details: Dict[str, Any], request: Request) -> None:
"""Log an authentication event."""
appInterface = getRootInterface()
event = AuthEvent(
userId=userId,
eventType=eventType,
details=details,
ipAddress=request.client.host if request.client else None,
userAgent=request.headers.get("user-agent")
)
# Save event to database
appInterface.db.recordCreate("auth_events", event.model_dump())
def validateSession(sessionId: str) -> bool:
"""Validate a user session."""
appInterface = getRootInterface()
session = appInterface.db.getRecordset("sessions", recordFilter={"id": sessionId})
if not session:
return False
session = session[0]
if datetime.now(timezone.utc) > session["expiresAt"]:
return False
# Update last activity
appInterface.db.recordModify("sessions", sessionId, {
"lastActivity": datetime.now(timezone.utc)
})
return True
def revokeSession(sessionId: str) -> None:
"""Revoke a user session."""
appInterface = getRootInterface()
# Delete session
appInterface.db.recordDelete("sessions", sessionId)
def revokeAllUserSessions(userId: str) -> None:
"""Revoke all sessions for a user."""
appInterface = getRootInterface()
# Get all sessions for user
sessions = appInterface.db.getRecordset("sessions", recordFilter={"userId": userId})
# Delete each session
for session in sessions:
appInterface.db.recordDelete("sessions", session["id"])

View file

@ -0,0 +1,154 @@
"""
Shared utilities for model attributes and labels.
"""
from pydantic import BaseModel, Field
from typing import Dict, Any, List, Type, ClassVar
import inspect
import importlib
import os
class BaseModelWithUI(BaseModel):
"""Base model class with UI support and common functionality"""
@classmethod
def get_ui_schema(cls) -> Dict[str, Any]:
"""Get UI schema for frontend"""
return {
"fields": cls.fieldLabels if hasattr(cls, 'fieldLabels') else {},
"validations": cls.get_validations() if hasattr(cls, 'get_validations') else {}
}
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary with proper validation"""
return self.model_dump()
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'BaseModelWithUI':
"""Create instance from dictionary with validation"""
return cls(**data)
@classmethod
def getModelAttributeDefinitions(cls) -> Dict[str, Any]:
"""
Get attribute definitions for this model class.
Override this method in model classes to provide custom attribute definitions.
Returns:
Dict[str, Any]: Dictionary of attribute definitions
"""
return {
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
}
for name, field in cls.model_fields.items()
}
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:
language: Language code to get the label for
Returns:
str: Label text in the specified language or default
"""
if language and language in self.translations:
return self.translations[language]
return self.default
def getModelClasses() -> Dict[str, Type[BaseModel]]:
"""
Dynamically get all model classes from all model modules.
Returns:
Dict[str, Type[BaseModel]]: Dictionary of model class names to their classes
"""
modelClasses = {}
# Get the interfaces directory path
interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces')
# Find all model files
for filename in os.listdir(interfaces_dir):
if filename.endswith('Model.py'):
# Convert filename to module name (e.g., gatewayModel.py -> gatewayModel)
module_name = filename[:-3]
# Import the module dynamically
module = importlib.import_module(f'modules.interfaces.{module_name}')
# Get all classes from the module
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel:
modelClasses[name] = obj
return modelClasses
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
"""
Get attribute definitions for model classes.
If modelClass is provided, returns attributes for that specific class.
If no modelClass is provided, returns attributes for all model classes.
Args:
modelClass: Optional specific model class to get attributes for
userLanguage: Language code for translations (default: "en")
Returns:
Dict[str, Any]: Dictionary of model class names to their attribute definitions
"""
if modelClass:
return getModelAttributes(modelClass)
# Get all model classes
modelClasses = getModelClasses()
# Create dictionary of model class names to their attribute definitions
return {
name: getModelAttributes(cls)
for name, cls in modelClasses.items()
}

View file

@ -1,123 +0,0 @@
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
# Define the model for attribute definitions
class AttributeDefinition(BaseModel):
name: str
label: str
type: str
required: bool = False
placeholder: Optional[str] = None
defaultValue: Optional[Any] = None
options: Optional[List[Dict[str, Any]]] = None
editable: bool = True
visible: bool = True
order: int = 0
validation: Optional[Dict[str, Any]] = None
helpText: Optional[str] = None
# Helper classes for type mapping
typeMappings = {
"int": "number",
"str": "string",
"float": "number",
"bool": "boolean",
"List[int]": "array",
"List[str]": "array",
"Dict[str, Any]": "object",
"Optional[str]": "string",
"Optional[int]": "number",
"Optional[Dict[str, Any]]": "object"
}
# Special field types based on naming conventions
specialFieldTypes = {
"content": "textarea",
"description": "textarea",
"instructions": "textarea",
"password": "password",
"email": "email",
"workspaceId": "select",
"agentId": "select",
"type": "select"
}
# Function to convert a Pydantic model into attribute definitions
def getModelAttributes(modelClass, userLanguage="de"):
"""
Converts a Pydantic model into a list of AttributeDefinition objects
"""
attributes = []
# Go through all fields in the model
for i, (fieldName, field) in enumerate(modelClass.__fields__.items()):
# Skip internal fields
if fieldName.startswith('_') or fieldName in ["label", "fieldLabels"]:
continue
# Determine the field type
fieldType = typeMappings.get(str(field.type_), "string")
# Check for special field types
if fieldName in specialFieldTypes:
fieldType = specialFieldTypes[fieldName]
# Get the label (if available)
fieldLabel = fieldName.replace('_', ' ').capitalize()
if hasattr(modelClass, 'fieldLabels') and fieldName in modelClass.fieldLabels:
labelObj = modelClass.fieldLabels[fieldName]
fieldLabel = labelObj.getLabel(userLanguage)
# Determine default values and required status
required = field.required
defaultValue = field.default if not field.required else None
# Check for validation rules
validation = None
if field.validators:
validation = {"hasValidators": True}
# Placeholder text
placeholder = f"Please enter {fieldLabel}"
# Special options for Select fields
options = None
if fieldType == "select":
if fieldName == "type" and modelClass.__name__ == "Agent":
options = [
{"value": "Analysis", "label": "Analysis"},
{"value": "Transformation", "label": "Transformation"},
{"value": "Generation", "label": "Generation"},
{"value": "Classification", "label": "Classification"},
{"value": "Custom", "label": "Custom"}
]
# Extract description from Field object
description = None
# Try to get description from various possible sources
if hasattr(field, 'field_info') and hasattr(field.field_info, 'description'):
description = field.field_info.description
elif hasattr(field, 'description'):
description = field.description
elif hasattr(field, 'schema') and hasattr(field.schema, 'description'):
description = field.schema.description
# Create attribute definition
attrDef = AttributeDefinition(
name=fieldName,
label=fieldLabel,
type=fieldType,
required=required,
placeholder=placeholder,
defaultValue=defaultValue,
options=options,
editable=fieldName not in ["id", "_mandateId", "_userId", "uploadDate", "_createdAt", "_modifiedAt"],
visible=fieldName not in ["hashedPassword", "_mandateId", "_userId"],
order=i,
validation=validation,
helpText=description or "" # Set empty string as default value if no description found
)
attributes.append(attrDef)
return attributes

View file

@ -10,7 +10,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.shared.mimeUtils import isTextMimeType, determineContentEncoding from modules.shared.mimeUtils import isTextMimeType, determineContentEncoding
from modules.interfaces.lucydomModel import ChatContent from modules.interfaces.serviceChatModel import ChatContent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,27 +26,8 @@ class AgentBase:
self.label = "Base Agent" self.label = "Base Agent"
self.description = "Base agent functionality" self.description = "Base agent functionality"
self.capabilities = [] self.capabilities = []
self.workflowManager = None
self.service = None self.service = None
def setWorkflowManager(self, workflowManager):
"""
Set the workflow manager reference and validate service container.
Args:
workflowManager: The workflow manager instance
"""
if not workflowManager:
logger.warning("Attempted to set null workflow manager")
return False
self.workflowManager = workflowManager
# Set service reference from workflow manager if available
if hasattr(workflowManager, 'service'):
return self.setService(workflowManager.service)
return False
def setService(self, service): def setService(self, service):
""" """
Set the service container reference and validate required interfaces. Set the service container reference and validate required interfaces.
@ -111,7 +92,7 @@ class AgentBase:
- documents: List of document objects created by the agent, - documents: List of document objects created by the agent,
each containing a "base64Encoded" flag in addition to "label" and "content" each containing a "base64Encoded" flag in addition to "label" and "content"
""" """
# Validate service and workflow manager # Validate service manager
if not self.service: if not self.service:
logger.error("Service container not initialized") logger.error("Service container not initialized")
return { return {
@ -119,13 +100,6 @@ class AgentBase:
"documents": [] "documents": []
} }
if not self.workflowManager:
logger.error("Workflow manager not initialized")
return {
"feedback": "Error: Workflow manager not initialized",
"documents": []
}
# Base implementation - should be overridden by specialized agents # Base implementation - should be overridden by specialized agents
logger.warning(f"Agent {self.name} is using the default implementation of processTask") logger.warning(f"Agent {self.name} is using the default implementation of processTask")
return { return {

View file

@ -6,7 +6,7 @@ import os
import logging import logging
import importlib import importlib
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .agentBase import AgentBase from modules.workflow.agentBase import AgentBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -27,10 +27,10 @@ class AgentRegistry:
if AgentRegistry._instance is not None: if AgentRegistry._instance is not None:
raise RuntimeError("Singleton instance already exists - use getInstance()") raise RuntimeError("Singleton instance already exists - use getInstance()")
self.agents = {} self.agents: Dict[str, AgentBase] = {}
self._loadAgents() self._loadAgents()
def initialize(self, service=None, workflowManager=None): def initialize(self, service=None):
"""Initialize or update the registry with workflow manager and service references.""" """Initialize or update the registry with workflow manager and service references."""
if service: if service:
# Validate required interfaces # Validate required interfaces
@ -44,10 +44,8 @@ class AgentRegistry:
logger.warning(f"Service container missing required interfaces: {', '.join(missing_interfaces)}") logger.warning(f"Service container missing required interfaces: {', '.join(missing_interfaces)}")
return False return False
# Initialize agents with service and workflow manager # Initialize agents with service
for agent in self.agents.values(): for agent in self.agents.values():
if workflowManager and hasattr(agent, 'setWorkflowManager'):
agent.setWorkflowManager(workflowManager)
if service and hasattr(agent, 'setService'): if service and hasattr(agent, 'setService'):
agent.setService(service) agent.setService(service)
@ -132,9 +130,14 @@ class AgentRegistry:
logger.error(f"Agent with identifier '{agentIdentifier}' not found") logger.error(f"Agent with identifier '{agentIdentifier}' not found")
return None return None
def getAllAgents(self) -> Dict[str, Any]: def getAllAgents(self) -> Dict[str, AgentBase]:
"""Return all registered agents.""" """
return self.agents Get all registered agents.
Returns:
Dictionary mapping agent names to agent instances
"""
return self.agents.copy()
def getAgentInfos(self) -> List[Dict[str, Any]]: def getAgentInfos(self) -> List[Dict[str, Any]]:
"""Return information about all registered agents.""" """Return information about all registered agents."""

View file

@ -8,7 +8,7 @@ import os
import io import io
from typing import Dict, Any, List, Optional, Union, Tuple from typing import Dict, Any, List, Optional, Union, Tuple
import base64 import base64
from modules.interfaces.lucydomModel import ChatContent from modules.interfaces.serviceChatModel import ChatContent
# Configure logger # Configure logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,51 @@
....................... TASKS ....................... TASKS
TODO: Frontend to adapt
##################### WORKFLOW TO ENHANCE WITH self.service container --> let AI define it, then to initialize it for a workflow class
! function callAI() to ask with userPrompt,systemPrompt optional), not with json WORKFLOW: To create model environment: The BUILDING BLOCKS
! in the taskplan to refer files always in context of user/mandate
! userinput to handle with object AgentQuery --> when received in frontend to enhance for full object
! user prompt to handle as directive AND content
! database to work with files per record, not files per table
! database to serialize list[] objects and replace by id-list
we need to adapt following things according to data objects in lucydomModel.py: self.service:
- All file handling in whole code to be with correct file objects FileItem and FileData object - user
- Everywhere to use datamodel specification by lucydomModel.py - attributes (items)
- connection (list)
- functions (serviceManagementClass instance)
- operator:
- for each (list of references)
- aiCall
- extract(file) -> content
- fileref agent 2 fileid
- fileid 2 fileref agent
- convert(data, format)
- create agent input file list
- save agent output files
- workflow
- active task (reference)
- id
- progress
- status
- tasks (list of tasks)
- id
- input data?
- output data?
-
Walkthroughs:
- register
- login local
- login msft
- management pages
- workflow
----------------------- OPEN
TODO: DOCUMENT handling in the workflow !!!!!!!!!!!!!!!!!!
- the workflow in "workflowManager.py" to run with pure documents and no content extraction from documents. To use revised Document model everywhere - the workflow in "workflowManager.py" to run with pure documents and no content extraction from documents. To use revised Document model everywhere
- Prompts for tasklist to revise accordingly and to make clear, that the prompting for data extraction will be a job for each agent, not to be topic of the taskplan. - Prompts for tasklist to revise accordingly and to make clear, that the prompting for data extraction will be a job for each agent, not to be topic of the taskplan.
- task to the agent to include the prompt for his job to do and also no data extraction. also here to make clear, that data extraction will be done by the agent. - task to the agent to include the prompt for his job to do and also no data extraction. also here to make clear, that data extraction will be done by the agent.
@ -22,7 +54,6 @@ Implemented agents: they use following tools depending on their job:
- extract content using global function getContent(document list) --> define prompt for each document of to extract data based on agent's task. it creates an ai call to specify the prompt per document to extract relevant data in the required format using the global function documentProcessor() and stores extracted data in the content object to use - extract content using global function getContent(document list) --> define prompt for each document of to extract data based on agent's task. it creates an ai call to specify the prompt per document to extract relevant data in the required format using the global function documentProcessor() and stores extracted data in the content object to use
- produce message object (feedback prompt, document list) - produce message object (feedback prompt, document list)
function documentProcessor(): function documentProcessor():
- return one content per document using ai call - return one content per document using ai call
- if there are many content objects in a document it uses one ai call per content to be specified, that if no relevant content is in the content object, an empty string is returned, otherwise the text in the required format - if there are many content objects in a document it uses one ai call per content to be specified, that if no relevant content is in the content object, an empty string is returned, otherwise the text in the required format
@ -34,25 +65,25 @@ Other topics:
NEXT:
! function callAI() to ask with userPrompt,systemPrompt optional), not with json
! in the taskplan to refer files always in context of user/mandate
! userinput to handle with object AgentQuery --> when received in frontend to enhance for full object
! user prompt to handle as directive AND file
! database to serialize list[] objects and replace by id-list -> already done in workflow?
! Prompts pro Agent mit prägnantem system prompt ergänzen. erfasse alle kontext-themen, regeln, anweisungen bei nichtwissen, format der antwort (generische stati)
----------------------- OPEN
PRIO1:
agentDocumentation delivers a ".docx" file, but the content is a ".md" text markup file agentDocumentation delivers a ".docx" file, but the content is a ".md" text markup file
sharepoint connector with document search, content search, content extraction
PRIO2:
Integrate NDA Text as modal form - Data governance agreement by login with checkbox
PRIO3:
Tools to transfer incl funds: Tools to transfer incl funds:
- Google SERPAPI (shelly) - Google SERPAPI (shelly)
- Anthropic Claude (valueon + shelly) - Anthropic Claude (valueon + shelly)
@ -64,6 +95,56 @@ Tools to transfer incl funds:
----------------------- DONE ----------------------- DONE
FRONTEND
- the application initiation gets userdata with the token over apiCall.js:/api/local/me --> object:
username
fullName
email
language
list of connections with attributes:
id
authority
externalUsername
Backend
in the backend to handle the routes as follows:
- routeSecurityLocal.py to handle all local endpoints, to include token generation from local authority in auth.py
- routeSecurityMsft.py and routeSecurityGoogle.py to handle all their endpoints
- all routeSecurity*.py to use the same interface to manage tokens and userdata: serviceUserClass.py. This class to have following
logic:
- all tokens are stored in one tabel, where each token has the attribute of the according authenticationAuthority
- login and logout endoints for "local" use a function "getUseridFromToken" to identify the user context. If user does not exist, error message
- login and logout endoints for "msft" and "google" use a function "getUseridFromToken" to identify the user context. If user does not exist for login, to register a new "local" user with the external user data and to attach the external connection. within the identified user context and the connection in its list to send back user context as tokenLocal and connection as tokenExt
- the important thing is, that login endpoint serves for two different actions:
a) without user context (no tokenLocal), it makes login for a user by external authority and sets user context
b) with user context (a tokenLocal provided), it does NOT set a nwe user context, but manipulate a connection in the connection list of a local user
- illustrative example of token data to send to UI (attributes):
connect and
{
"token_type": "Bearer",
"expires_in": ,
"access_token": ,
"id_token": ,
"client_info": ,
"user_info": {
"name": "Patrick Motsch",
"email": "p.motsch@valueon.ch",
"id": "xxx"
},
"mandateId": "",
"userId": "",
"id": "tokenid",
}
We have to correct the following wrong user access management. We have to correct the following wrong user access management.
Issue is: when user logs in with "local" managed account and then logs in to msft account with "msft" authority, the userid is switched to the microsoft instance in the workflow. this must not happen. Issue is: when user logs in with "local" managed account and then logs in to msft account with "msft" authority, the userid is switched to the microsoft instance in the workflow. this must not happen.

View file

@ -1,37 +0,0 @@
DATA PROCESSING AND AI USAGE CONSENT AGREEMENT
By indicating your acceptance selecting "I agree", you as the user of this application acknowledge, consent to, and agree to the following terms regarding the processing of your data through artificial intelligence services:
1. CONSENT TO DATA PROCESSING
1.1 You expressly authorize the collection, processing, transmission, and storage of any and all data you provide or generate while using our services ("User Data").
1.2 You understand and agree that User Data may be transmitted to and processed by third-party artificial intelligence providers, including but not limited to OpenAI and similar AI service providers.
1.3 This consent extends to all content, including but not limited to text, images, documents, conversation histories, preferences, activity logs, and any derivative data generated through your interaction with our services.
2. ACKNOWLEDGMENT OF AI PROCESSING RISKS
2.1 You acknowledge that artificial intelligence systems process data differently than human operators and may produce unexpected, inaccurate, or inappropriate outputs.
2.2 You understand that AI services may retain, learn from, or use submitted data for improving their systems in accordance with their own terms of service.
2.3 You recognize that despite reasonable security measures, data transmitted to third-party AI services may be vulnerable to interception, unauthorized access, or breach.
3. WAIVER OF LIABILITY
3.1 To the fullest extent permitted by applicable law, you hereby irrevocably and unconditionally waive and release any and all claims, liabilities, damages, losses, expenses, demands, and causes of action against us arising from or related to:
a) The processing, transmission, storage, or usage of User Data by AI services;
b) Any outputs, recommendations, or decisions generated by AI systems based on User Data;
c) Any data breach, unauthorized access, or security incident that occurs after User Data is transmitted to third-party AI providers;
d) Any unintended disclosure of confidential information processed through AI services;
e) Any direct, indirect, incidental, special, consequential, or punitive damages, including but not limited to loss of profits, goodwill, data, or other intangible losses.
3.2 This waiver applies regardless of whether such damages arise from breach of contract, tort (including negligence), or any other legal theory.
4. USER REPRESENTATIONS AND WARRANTIES
4.1 You represent and warrant that:
a) You have the legal right to provide all User Data submitted;
b) User Data does not infringe upon any intellectual property rights, privacy rights, or other rights of any third party;
c) You have obtained all necessary consents from any third parties whose information may be included in User Data;
d) User Data does not contain any information that is unlawful, harmful, threatening, abusive, harassing, defamatory, or otherwise objectionable.
4.2 You agree to indemnify and hold harmless our organization from any third-party claims arising from breach of these representations.
5. LIMITATIONS AND SEVERABILITY
5.1 This waiver does not apply to any liability that cannot be excluded or limited by law, including liability for fraud, gross negligence, or willful misconduct.
5.2 If any provision of this agreement is found to be unenforceable, the remaining provisions shall remain in full force and effect.
5.3 This waiver shall be governed by and construed in accordance with applicable laws, without regard to conflict of law principles.
By selection "I agree", you acknowledge that you have read, understood, and agree to be bound by all the terms and conditions set forth in this agreement.

8
notes/releasenotes.txt Normal file
View file

@ -0,0 +1,8 @@
New features
- Limiter and tracking of ip adress access
- Sessions improved
- user and connection consequently separated
- seamless local and external authorities integration
- audit trail
- nda disclaimer in login window
- CSRF Tokens included in forms

View file

@ -4,6 +4,7 @@ uvicorn==0.23.2
python-multipart==0.0.6 python-multipart==0.0.6
httpx==0.25.0 httpx==0.25.0
pydantic==1.10.13 # Ältere Version ohne Rust-Abhängigkeit pydantic==1.10.13 # Ältere Version ohne Rust-Abhängigkeit
slowapi==0.1.8 # For rate limiting
## Authentication & Security ## Authentication & Security
python-jose==3.3.0 python-jose==3.3.0
@ -11,6 +12,8 @@ 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
bcrypt==4.0.1 # For password hashing
python-jose[cryptography]==3.3.0 # For JWT tokens
## Database ## Database
mysql-connector-python==8.1.0 mysql-connector-python==8.1.0