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