feat: add langgraph first tool; pydantic v2
This commit is contained in:
parent
68d6ab9890
commit
98b258ae53
7 changed files with 718 additions and 432 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
"""Security models: Token and AuthEvent."""
|
"""Security models: Token and AuthEvent."""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from modules.shared.attributeUtils import register_model_labels, ModelMixin
|
from modules.shared.attributeUtils import register_model_labels, ModelMixin
|
||||||
from modules.shared.timezoneUtils import get_utc_timestamp
|
from modules.shared.timezoneUtils import get_utc_timestamp
|
||||||
from .datamodelUam import AuthAuthority
|
from .datamodelUam import AuthAuthority
|
||||||
|
|
@ -18,21 +18,36 @@ class Token(BaseModel, ModelMixin):
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
userId: str
|
userId: str
|
||||||
authority: AuthAuthority
|
authority: AuthAuthority
|
||||||
connectionId: Optional[str] = Field(None, description="ID of the connection this token belongs to")
|
connectionId: Optional[str] = Field(
|
||||||
|
None, description="ID of the connection this token belongs to"
|
||||||
|
)
|
||||||
tokenAccess: str
|
tokenAccess: str
|
||||||
tokenType: str = "bearer"
|
tokenType: str = "bearer"
|
||||||
expiresAt: float = Field(description="When the token expires (UTC timestamp in seconds)")
|
expiresAt: float = Field(
|
||||||
|
description="When the token expires (UTC timestamp in seconds)"
|
||||||
|
)
|
||||||
tokenRefresh: Optional[str] = None
|
tokenRefresh: Optional[str] = None
|
||||||
createdAt: Optional[float] = Field(None, description="When the token was created (UTC timestamp in seconds)")
|
createdAt: Optional[float] = Field(
|
||||||
status: TokenStatus = Field(default=TokenStatus.ACTIVE, description="Token status: active/revoked")
|
None, description="When the token was created (UTC timestamp in seconds)"
|
||||||
revokedAt: Optional[float] = Field(None, description="When the token was revoked (UTC timestamp in seconds)")
|
)
|
||||||
revokedBy: Optional[str] = Field(None, description="User ID who revoked the token (admin/self)")
|
status: TokenStatus = Field(
|
||||||
|
default=TokenStatus.ACTIVE, description="Token status: active/revoked"
|
||||||
|
)
|
||||||
|
revokedAt: Optional[float] = Field(
|
||||||
|
None, description="When the token was revoked (UTC timestamp in seconds)"
|
||||||
|
)
|
||||||
|
revokedBy: Optional[str] = Field(
|
||||||
|
None, description="User ID who revoked the token (admin/self)"
|
||||||
|
)
|
||||||
reason: Optional[str] = Field(None, description="Optional revocation reason")
|
reason: Optional[str] = Field(None, description="Optional revocation reason")
|
||||||
sessionId: Optional[str] = Field(None, description="Logical session grouping for logout revocation")
|
sessionId: Optional[str] = Field(
|
||||||
mandateId: Optional[str] = Field(None, description="Mandate ID for tenant scoping of the token")
|
None, description="Logical session grouping for logout revocation"
|
||||||
|
)
|
||||||
|
mandateId: Optional[str] = Field(
|
||||||
|
None, description="Mandate ID for tenant scoping of the token"
|
||||||
|
)
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(use_enum_values=True)
|
||||||
use_enum_values = True
|
|
||||||
|
|
||||||
|
|
||||||
register_model_labels(
|
register_model_labels(
|
||||||
|
|
@ -59,14 +74,60 @@ register_model_labels(
|
||||||
|
|
||||||
|
|
||||||
class AuthEvent(BaseModel, ModelMixin):
|
class AuthEvent(BaseModel, ModelMixin):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the auth event", frontend_type="text", frontend_readonly=True, frontend_required=False)
|
id: str = Field(
|
||||||
userId: str = Field(description="ID of the user this event belongs to", frontend_type="text", frontend_readonly=True, frontend_required=True)
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
eventType: str = Field(description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')", frontend_type="text", frontend_readonly=True, frontend_required=True)
|
description="Unique ID of the auth event",
|
||||||
timestamp: float = Field(default_factory=get_utc_timestamp, description="Unix timestamp when the event occurred", frontend_type="datetime", frontend_readonly=True, frontend_required=True)
|
frontend_type="text",
|
||||||
ipAddress: Optional[str] = Field(default=None, description="IP address from which the event originated", frontend_type="text", frontend_readonly=True, frontend_required=False)
|
frontend_readonly=True,
|
||||||
userAgent: Optional[str] = Field(default=None, description="User agent string from the request", frontend_type="text", frontend_readonly=True, frontend_required=False)
|
frontend_required=False,
|
||||||
success: bool = Field(default=True, description="Whether the authentication event was successful", frontend_type="boolean", frontend_readonly=True, frontend_required=True)
|
)
|
||||||
details: Optional[str] = Field(default=None, description="Additional details about the event", frontend_type="text", frontend_readonly=True, frontend_required=False)
|
userId: str = Field(
|
||||||
|
description="ID of the user this event belongs to",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=True,
|
||||||
|
)
|
||||||
|
eventType: str = Field(
|
||||||
|
description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=True,
|
||||||
|
)
|
||||||
|
timestamp: float = Field(
|
||||||
|
default_factory=get_utc_timestamp,
|
||||||
|
description="Unix timestamp when the event occurred",
|
||||||
|
frontend_type="datetime",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=True,
|
||||||
|
)
|
||||||
|
ipAddress: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="IP address from which the event originated",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
|
userAgent: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="User agent string from the request",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
|
success: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether the authentication event was successful",
|
||||||
|
frontend_type="boolean",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=True,
|
||||||
|
)
|
||||||
|
details: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Additional details about the event",
|
||||||
|
frontend_type="text",
|
||||||
|
frontend_readonly=True,
|
||||||
|
frontend_required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
register_model_labels(
|
register_model_labels(
|
||||||
|
|
@ -83,5 +144,3 @@ register_model_labels(
|
||||||
"details": {"en": "Details", "fr": "Détails"},
|
"details": {"en": "Details", "fr": "Détails"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
1
modules/features/chatBot/chatbotTools/__init__.py
Normal file
1
modules/features/chatBot/chatbotTools/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Contains all tools available for the chatbot to use."""
|
||||||
|
|
@ -1 +1,7 @@
|
||||||
"""Tools that are custom to a specific customer go here."""
|
"""Shared tools available across all chatbot implementations."""
|
||||||
|
|
||||||
|
from modules.features.chatBot.chatbotTools.sharedTools.toolTavilySearch import (
|
||||||
|
tavily_search,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["tavily_search"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""Tavily Search Tool for LangGraph.
|
||||||
|
|
||||||
|
This tool provides web search capabilities using the Tavily API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from modules.connectors.connectorAiTavily import ConnectorWeb
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
async def tavily_search(
|
||||||
|
query: Annotated[str, "The search query to look up on the web"],
|
||||||
|
) -> str:
|
||||||
|
"""Search the web using Tavily API.
|
||||||
|
|
||||||
|
Use this tool to search for current information, news, or any web content.
|
||||||
|
The tool returns relevant search results including titles and URLs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The search query string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A formatted string containing search results with titles and URLs
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create connector instance
|
||||||
|
connector = await ConnectorWeb.create()
|
||||||
|
|
||||||
|
# Perform search with default parameters
|
||||||
|
results = await connector._search(
|
||||||
|
query=query,
|
||||||
|
max_results=5,
|
||||||
|
search_depth="basic",
|
||||||
|
include_answer=True,
|
||||||
|
include_raw_content=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format results
|
||||||
|
if not results:
|
||||||
|
return f"No results found for query: {query}"
|
||||||
|
|
||||||
|
formatted_results = [f"Search results for '{query}':\n"]
|
||||||
|
for i, result in enumerate(results, 1):
|
||||||
|
formatted_results.append(f"{i}. {result.title}")
|
||||||
|
formatted_results.append(f" URL: {result.url}\n")
|
||||||
|
|
||||||
|
return "\n".join(formatted_results)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in tavily_search tool: {str(e)}")
|
||||||
|
return f"Error performing search: {str(e)}"
|
||||||
|
|
@ -18,11 +18,19 @@ from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp
|
from modules.shared.timezoneUtils import get_utc_now, get_utc_timestamp
|
||||||
from modules.interfaces.interfaceDbAppAccess import AppAccess
|
from modules.interfaces.interfaceDbAppAccess import AppAccess
|
||||||
from modules.datamodels.datamodelUam import (
|
from modules.datamodels.datamodelUam import (
|
||||||
User, Mandate, UserInDB, UserConnection,
|
User,
|
||||||
AuthAuthority, UserPrivilege, ConnectionStatus,
|
Mandate,
|
||||||
|
UserInDB,
|
||||||
|
UserConnection,
|
||||||
|
AuthAuthority,
|
||||||
|
UserPrivilege,
|
||||||
|
ConnectionStatus,
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelSecurity import Token, AuthEvent, TokenStatus
|
from modules.datamodels.datamodelSecurity import Token, AuthEvent, TokenStatus
|
||||||
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
|
from modules.datamodels.datamodelNeutralizer import (
|
||||||
|
DataNeutraliserConfig,
|
||||||
|
DataNeutralizerAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -35,6 +43,7 @@ _rootAppObjects = None
|
||||||
# Password-Hashing
|
# Password-Hashing
|
||||||
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
pwdContext = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
class AppObjects:
|
class AppObjects:
|
||||||
"""
|
"""
|
||||||
Interface to the Gateway system.
|
Interface to the Gateway system.
|
||||||
|
|
@ -76,14 +85,16 @@ class AppObjects:
|
||||||
self.userLanguage = currentUser.language # Default user language
|
self.userLanguage = currentUser.language # Default user language
|
||||||
|
|
||||||
# Initialize access control with user context
|
# Initialize access control with user context
|
||||||
self.access = AppAccess(self.currentUser, self.db) # Convert to dict only when needed
|
self.access = AppAccess(
|
||||||
|
self.currentUser, self.db
|
||||||
|
) # Convert to dict only when needed
|
||||||
|
|
||||||
# Update database context
|
# Update database context
|
||||||
self.db.updateContext(self.userId)
|
self.db.updateContext(self.userId)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Cleanup method to close database connection."""
|
"""Cleanup method to close database connection."""
|
||||||
if hasattr(self, 'db') and self.db is not None:
|
if hasattr(self, "db") and self.db is not None:
|
||||||
try:
|
try:
|
||||||
self.db.close()
|
self.db.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -106,7 +117,7 @@ class AppObjects:
|
||||||
dbUser=dbUser,
|
dbUser=dbUser,
|
||||||
dbPassword=dbPassword,
|
dbPassword=dbPassword,
|
||||||
dbPort=dbPort,
|
dbPort=dbPort,
|
||||||
userId=self.userId
|
userId=self.userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize database system
|
# Initialize database system
|
||||||
|
|
@ -129,16 +140,12 @@ class AppObjects:
|
||||||
mandates = self.db.getRecordset(Mandate)
|
mandates = self.db.getRecordset(Mandate)
|
||||||
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 = Mandate(
|
rootMandate = Mandate(name="Root", language="en", enabled=True)
|
||||||
name="Root",
|
|
||||||
language="en",
|
|
||||||
enabled=True
|
|
||||||
)
|
|
||||||
createdMandate = self.db.recordCreate(Mandate, rootMandate)
|
createdMandate = self.db.recordCreate(Mandate, rootMandate)
|
||||||
logger.info(f"Root mandate created with ID {createdMandate['id']}")
|
logger.info(f"Root mandate created with ID {createdMandate['id']}")
|
||||||
|
|
||||||
# Update mandate context
|
# Update mandate context
|
||||||
self.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."""
|
||||||
|
|
@ -155,8 +162,10 @@ class AppObjects:
|
||||||
language="en",
|
language="en",
|
||||||
privilege=UserPrivilege.SYSADMIN,
|
privilege=UserPrivilege.SYSADMIN,
|
||||||
authenticationAuthority="local", # Using lowercase value directly
|
authenticationAuthority="local", # Using lowercase value directly
|
||||||
hashedPassword=self._getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")),
|
hashedPassword=self._getPasswordHash(
|
||||||
connections=[]
|
APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")
|
||||||
|
),
|
||||||
|
connections=[],
|
||||||
)
|
)
|
||||||
createdUser = self.db.recordCreate(UserInDB, adminUser)
|
createdUser = self.db.recordCreate(UserInDB, adminUser)
|
||||||
logger.info(f"Admin user created with ID {createdUser['id']}")
|
logger.info(f"Admin user created with ID {createdUser['id']}")
|
||||||
|
|
@ -168,7 +177,9 @@ class AppObjects:
|
||||||
def _initEventUser(self):
|
def _initEventUser(self):
|
||||||
"""Creates the Event user if it doesn't exist."""
|
"""Creates the Event user if it doesn't exist."""
|
||||||
# Check if event user already exists
|
# Check if event user already exists
|
||||||
existingUsers = self.db.getRecordset(UserInDB, recordFilter={"username": "event"})
|
existingUsers = self.db.getRecordset(
|
||||||
|
UserInDB, recordFilter={"username": "event"}
|
||||||
|
)
|
||||||
if not existingUsers:
|
if not existingUsers:
|
||||||
logger.info("Creating Event user")
|
logger.info("Creating Event user")
|
||||||
eventUser = UserInDB(
|
eventUser = UserInDB(
|
||||||
|
|
@ -180,13 +191,17 @@ class AppObjects:
|
||||||
language="en",
|
language="en",
|
||||||
privilege=UserPrivilege.SYSADMIN,
|
privilege=UserPrivilege.SYSADMIN,
|
||||||
authenticationAuthority="local", # Using lowercase value directly
|
authenticationAuthority="local", # Using lowercase value directly
|
||||||
hashedPassword=self._getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")),
|
hashedPassword=self._getPasswordHash(
|
||||||
connections=[]
|
APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")
|
||||||
|
),
|
||||||
|
connections=[],
|
||||||
)
|
)
|
||||||
createdUser = self.db.recordCreate(UserInDB, eventUser)
|
createdUser = self.db.recordCreate(UserInDB, eventUser)
|
||||||
logger.info(f"Event user created with ID {createdUser['id']}")
|
logger.info(f"Event user created with ID {createdUser['id']}")
|
||||||
|
|
||||||
def _uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def _uam(
|
||||||
|
self, model_class: type, recordset: List[Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Unified user access management function that filters data based on user privileges
|
Unified user access management function that filters data based on user privileges
|
||||||
and adds access control attributes.
|
and adds access control attributes.
|
||||||
|
|
@ -205,7 +220,7 @@ class AppObjects:
|
||||||
cleanedRecords = []
|
cleanedRecords = []
|
||||||
for record in filteredRecords:
|
for record in filteredRecords:
|
||||||
# Create a new dict with only non-database fields
|
# Create a new dict with only non-database fields
|
||||||
cleanedRecord = {k: v for k, v in record.items() if not k.startswith('_')}
|
cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
|
||||||
cleanedRecords.append(cleanedRecord)
|
cleanedRecords.append(cleanedRecord)
|
||||||
|
|
||||||
return cleanedRecords
|
return cleanedRecords
|
||||||
|
|
@ -317,12 +332,20 @@ class AppObjects:
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def createUser(self, username: str, password: str = None, email: str = None,
|
def createUser(
|
||||||
fullName: str = None, language: str = "en", enabled: bool = True,
|
self,
|
||||||
|
username: str,
|
||||||
|
password: str = None,
|
||||||
|
email: str = None,
|
||||||
|
fullName: str = None,
|
||||||
|
language: str = "en",
|
||||||
|
enabled: bool = True,
|
||||||
privilege: UserPrivilege = UserPrivilege.USER,
|
privilege: UserPrivilege = UserPrivilege.USER,
|
||||||
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
|
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
|
||||||
externalId: str = None, externalUsername: str = None,
|
externalId: str = None,
|
||||||
externalEmail: str = None) -> User:
|
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:
|
||||||
# Ensure username is a string
|
# Ensure username is a string
|
||||||
|
|
@ -348,7 +371,7 @@ class AppObjects:
|
||||||
privilege=privilege,
|
privilege=privilege,
|
||||||
authenticationAuthority=authenticationAuthority,
|
authenticationAuthority=authenticationAuthority,
|
||||||
hashedPassword=self._getPasswordHash(password) if password else None,
|
hashedPassword=self._getPasswordHash(password) if password else None,
|
||||||
connections=[]
|
connections=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create user record
|
# Create user record
|
||||||
|
|
@ -356,7 +379,6 @@ class AppObjects:
|
||||||
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")
|
||||||
|
|
||||||
|
|
||||||
# Add external connection if provided
|
# Add external connection if provided
|
||||||
if externalId and externalUsername:
|
if externalId and externalUsername:
|
||||||
self.addUserConnection(
|
self.addUserConnection(
|
||||||
|
|
@ -364,11 +386,13 @@ class AppObjects:
|
||||||
authenticationAuthority,
|
authenticationAuthority,
|
||||||
externalId,
|
externalId,
|
||||||
externalUsername,
|
externalUsername,
|
||||||
externalEmail
|
externalEmail,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get created user using the returned ID
|
# Get created user using the returned ID
|
||||||
createdUser = self.db.getRecordset(UserInDB, recordFilter={"id": createdRecord["id"]})
|
createdUser = self.db.getRecordset(
|
||||||
|
UserInDB, recordFilter={"id": createdRecord["id"]}
|
||||||
|
)
|
||||||
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")
|
||||||
|
|
||||||
|
|
@ -399,7 +423,6 @@ class AppObjects:
|
||||||
# Update user record
|
# Update user record
|
||||||
self.db.recordModify(UserInDB, userId, updatedUser)
|
self.db.recordModify(UserInDB, userId, updatedUser)
|
||||||
|
|
||||||
|
|
||||||
# Get updated user
|
# Get updated user
|
||||||
updatedUser = self.getUser(userId)
|
updatedUser = self.getUser(userId)
|
||||||
if not updatedUser:
|
if not updatedUser:
|
||||||
|
|
@ -422,8 +445,6 @@ class AppObjects:
|
||||||
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."""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|
||||||
# Delete user auth events
|
# Delete user auth events
|
||||||
events = self.db.getRecordset(AuthEvent, recordFilter={"userId": userId})
|
events = self.db.getRecordset(AuthEvent, recordFilter={"userId": userId})
|
||||||
for event in events:
|
for event in events:
|
||||||
|
|
@ -434,9 +455,10 @@ class AppObjects:
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
self.db.recordDelete(Token, token["id"])
|
self.db.recordDelete(Token, token["id"])
|
||||||
|
|
||||||
|
|
||||||
# Delete user connections
|
# Delete user connections
|
||||||
connections = self.db.getRecordset(UserConnection, recordFilter={"userId": userId})
|
connections = self.db.getRecordset(
|
||||||
|
UserConnection, recordFilter={"userId": userId}
|
||||||
|
)
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
self.db.recordDelete(UserConnection, conn["id"])
|
self.db.recordDelete(UserConnection, conn["id"])
|
||||||
|
|
||||||
|
|
@ -465,7 +487,6 @@ class AppObjects:
|
||||||
if not success:
|
if not success:
|
||||||
raise ValueError(f"Failed to delete user {userId}")
|
raise ValueError(f"Failed to delete user {userId}")
|
||||||
|
|
||||||
|
|
||||||
logger.info(f"User {userId} successfully deleted")
|
logger.info(f"User {userId} successfully deleted")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -493,31 +514,22 @@ class AppObjects:
|
||||||
authenticationAuthority = checkData.get("authenticationAuthority", "local")
|
authenticationAuthority = checkData.get("authenticationAuthority", "local")
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
return {
|
return {"available": False, "message": "Username is required"}
|
||||||
"available": False,
|
|
||||||
"message": "Username is required"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get user by username
|
# Get user by username
|
||||||
user = self.getUserByUsername(username)
|
user = self.getUserByUsername(username)
|
||||||
|
|
||||||
# Check if user exists (User model instance)
|
# Check if user exists (User model instance)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return {
|
return {"available": False, "message": "Username is already taken"}
|
||||||
"available": False,
|
|
||||||
"message": "Username is already taken"
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {"available": True, "message": "Username is available"}
|
||||||
"available": True,
|
|
||||||
"message": "Username is available"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking username availability: {str(e)}")
|
logger.error(f"Error checking username availability: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"available": False,
|
"available": False,
|
||||||
"message": f"Error checking username availability: {str(e)}"
|
"message": f"Error checking username availability: {str(e)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Connection methods
|
# Connection methods
|
||||||
|
|
@ -526,7 +538,9 @@ class AppObjects:
|
||||||
"""Returns all connections for a user."""
|
"""Returns all connections for a user."""
|
||||||
try:
|
try:
|
||||||
# Get connections for this user
|
# Get connections for this user
|
||||||
connections = self.db.getRecordset(UserConnection, recordFilter={"userId": userId})
|
connections = self.db.getRecordset(
|
||||||
|
UserConnection, recordFilter={"userId": userId}
|
||||||
|
)
|
||||||
|
|
||||||
# Convert to UserConnection objects
|
# Convert to UserConnection objects
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -543,11 +557,13 @@ class AppObjects:
|
||||||
status=conn_dict.get("status", "pending"),
|
status=conn_dict.get("status", "pending"),
|
||||||
connectedAt=conn_dict.get("connectedAt"),
|
connectedAt=conn_dict.get("connectedAt"),
|
||||||
lastChecked=conn_dict.get("lastChecked"),
|
lastChecked=conn_dict.get("lastChecked"),
|
||||||
expiresAt=conn_dict.get("expiresAt")
|
expiresAt=conn_dict.get("expiresAt"),
|
||||||
)
|
)
|
||||||
result.append(connection)
|
result.append(connection)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error converting connection dict to object: {str(e)}")
|
logger.error(
|
||||||
|
f"Error converting connection dict to object: {str(e)}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -555,9 +571,15 @@ class AppObjects:
|
||||||
logger.error(f"Error getting user connections: {str(e)}")
|
logger.error(f"Error getting user connections: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
|
def addUserConnection(
|
||||||
externalUsername: str, externalEmail: Optional[str] = None,
|
self,
|
||||||
status: ConnectionStatus = ConnectionStatus.PENDING) -> UserConnection:
|
userId: str,
|
||||||
|
authority: AuthAuthority,
|
||||||
|
externalId: str,
|
||||||
|
externalUsername: str,
|
||||||
|
externalEmail: Optional[str] = None,
|
||||||
|
status: ConnectionStatus = ConnectionStatus.PENDING,
|
||||||
|
) -> UserConnection:
|
||||||
"""
|
"""
|
||||||
Adds a new connection for a user.
|
Adds a new connection for a user.
|
||||||
|
|
||||||
|
|
@ -589,13 +611,12 @@ class AppObjects:
|
||||||
status=status,
|
status=status,
|
||||||
connectedAt=get_utc_timestamp(),
|
connectedAt=get_utc_timestamp(),
|
||||||
lastChecked=get_utc_timestamp(),
|
lastChecked=get_utc_timestamp(),
|
||||||
expiresAt=None # Optional field, set to None by default
|
expiresAt=None, # Optional field, set to None by default
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save to connections table
|
# Save to connections table
|
||||||
self.db.recordCreate(UserConnection, connection)
|
self.db.recordCreate(UserConnection, connection)
|
||||||
|
|
||||||
|
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -606,9 +627,9 @@ class AppObjects:
|
||||||
"""Remove a connection to an external service"""
|
"""Remove a connection to an external service"""
|
||||||
try:
|
try:
|
||||||
# Get connection
|
# Get connection
|
||||||
connections = self.db.getRecordset(UserConnection, recordFilter={
|
connections = self.db.getRecordset(
|
||||||
"id": connectionId
|
UserConnection, recordFilter={"id": connectionId}
|
||||||
})
|
)
|
||||||
|
|
||||||
if not connections:
|
if not connections:
|
||||||
raise ValueError(f"Connection {connectionId} not found")
|
raise ValueError(f"Connection {connectionId} not found")
|
||||||
|
|
@ -616,7 +637,6 @@ class AppObjects:
|
||||||
# Delete connection
|
# Delete connection
|
||||||
self.db.recordDelete(UserConnection, connectionId)
|
self.db.recordDelete(UserConnection, connectionId)
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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)}")
|
||||||
|
|
@ -647,17 +667,13 @@ class AppObjects:
|
||||||
raise PermissionError("No permission to create mandates")
|
raise PermissionError("No permission to create mandates")
|
||||||
|
|
||||||
# Create mandate data using model
|
# Create mandate data using model
|
||||||
mandateData = Mandate(
|
mandateData = Mandate(name=name, language=language)
|
||||||
name=name,
|
|
||||||
language=language
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create mandate record
|
# Create mandate record
|
||||||
createdRecord = self.db.recordCreate(Mandate, mandateData)
|
createdRecord = self.db.recordCreate(Mandate, mandateData)
|
||||||
if not createdRecord or not createdRecord.get("id"):
|
if not createdRecord or not createdRecord.get("id"):
|
||||||
raise ValueError("Failed to create mandate record")
|
raise ValueError("Failed to create mandate record")
|
||||||
|
|
||||||
|
|
||||||
return Mandate.from_dict(createdRecord)
|
return Mandate.from_dict(createdRecord)
|
||||||
|
|
||||||
def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate:
|
def updateMandate(self, mandateId: str, updateData: Dict[str, Any]) -> Mandate:
|
||||||
|
|
@ -707,7 +723,9 @@ class AppObjects:
|
||||||
# Check if mandate has users
|
# Check if mandate has users
|
||||||
users = self.getUsersByMandate(mandateId)
|
users = self.getUsersByMandate(mandateId)
|
||||||
if users:
|
if users:
|
||||||
raise ValueError(f"Cannot delete mandate {mandateId} with existing users")
|
raise ValueError(
|
||||||
|
f"Cannot delete mandate {mandateId} with existing users"
|
||||||
|
)
|
||||||
|
|
||||||
# Delete mandate
|
# Delete mandate
|
||||||
success = self.db.recordDelete(Mandate, mandateId)
|
success = self.db.recordDelete(Mandate, mandateId)
|
||||||
|
|
@ -727,7 +745,9 @@ class AppObjects:
|
||||||
try:
|
try:
|
||||||
# Validate that this is NOT a connection token
|
# Validate that this is NOT a connection token
|
||||||
if token.connectionId:
|
if token.connectionId:
|
||||||
raise ValueError("Access tokens cannot have connectionId - use saveConnectionToken instead")
|
raise ValueError(
|
||||||
|
"Access tokens cannot have connectionId - use saveConnectionToken instead"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate user context
|
# Validate user context
|
||||||
if not self.currentUser or not self.currentUser.id:
|
if not self.currentUser or not self.currentUser.id:
|
||||||
|
|
@ -745,33 +765,44 @@ class AppObjects:
|
||||||
# If replace_existing is True, delete old access tokens for this user and authority first
|
# If replace_existing is True, delete old access tokens for this user and authority first
|
||||||
if replace_existing:
|
if replace_existing:
|
||||||
try:
|
try:
|
||||||
old_tokens = self.db.getRecordset(Token, recordFilter={
|
old_tokens = self.db.getRecordset(
|
||||||
|
Token,
|
||||||
|
recordFilter={
|
||||||
"userId": self.currentUser.id,
|
"userId": self.currentUser.id,
|
||||||
"authority": token.authority,
|
"authority": token.authority,
|
||||||
"connectionId": None # Ensure we only delete access tokens
|
"connectionId": None, # Ensure we only delete access tokens
|
||||||
})
|
},
|
||||||
|
)
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
for old_token in old_tokens:
|
for old_token in old_tokens:
|
||||||
if old_token["id"] != token.id: # Don't delete the new token if it already exists
|
if (
|
||||||
|
old_token["id"] != token.id
|
||||||
|
): # Don't delete the new token if it already exists
|
||||||
self.db.recordDelete(Token, old_token["id"])
|
self.db.recordDelete(Token, old_token["id"])
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
|
|
||||||
if deleted_count > 0:
|
if deleted_count > 0:
|
||||||
logger.info(f"Replaced {deleted_count} old access tokens for user {self.currentUser.id} and authority {token.authority}")
|
logger.info(
|
||||||
|
f"Replaced {deleted_count} old access tokens for user {self.currentUser.id} and authority {token.authority}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete old access tokens for user {self.currentUser.id} and authority {token.authority}: {str(e)}")
|
logger.warning(
|
||||||
|
f"Failed to delete old access tokens for user {self.currentUser.id} and authority {token.authority}: {str(e)}"
|
||||||
|
)
|
||||||
# Continue with saving the new token even if deletion fails
|
# Continue with saving the new token even if deletion fails
|
||||||
|
|
||||||
# Convert to dict and ensure all fields are properly set
|
# Convert to dict and ensure all fields are properly set
|
||||||
token_dict = token.dict()
|
token_dict = token.model_dump()
|
||||||
|
# Ensure userId is set to current user
|
||||||
|
# Convert to dict and ensure all fields are properly set
|
||||||
|
token_dict = token.model_dump()
|
||||||
# Ensure userId is set to current user
|
# Ensure userId is set to current user
|
||||||
token_dict["userId"] = self.currentUser.id
|
token_dict["userId"] = self.currentUser.id
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
self.db.recordCreate(Token, token_dict)
|
self.db.recordCreate(Token, token_dict)
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving access token: {str(e)}")
|
logger.error(f"Error saving access token: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
@ -781,7 +812,9 @@ class AppObjects:
|
||||||
try:
|
try:
|
||||||
# Validate that this IS a connection token
|
# Validate that this IS a connection token
|
||||||
if not token.connectionId:
|
if not token.connectionId:
|
||||||
raise ValueError("Connection tokens must have connectionId - use saveAccessToken instead")
|
raise ValueError(
|
||||||
|
"Connection tokens must have connectionId - use saveAccessToken instead"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate user context
|
# Validate user context
|
||||||
if not self.currentUser or not self.currentUser.id:
|
if not self.currentUser or not self.currentUser.id:
|
||||||
|
|
@ -799,31 +832,36 @@ class AppObjects:
|
||||||
# If replace_existing is True, delete old tokens for this connectionId first
|
# If replace_existing is True, delete old tokens for this connectionId first
|
||||||
if replace_existing:
|
if replace_existing:
|
||||||
try:
|
try:
|
||||||
old_tokens = self.db.getRecordset(Token, recordFilter={
|
old_tokens = self.db.getRecordset(
|
||||||
"connectionId": token.connectionId
|
Token, recordFilter={"connectionId": token.connectionId}
|
||||||
})
|
)
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
for old_token in old_tokens:
|
for old_token in old_tokens:
|
||||||
if old_token["id"] != token.id: # Don't delete the new token if it already exists
|
if (
|
||||||
|
old_token["id"] != token.id
|
||||||
|
): # Don't delete the new token if it already exists
|
||||||
self.db.recordDelete(Token, old_token["id"])
|
self.db.recordDelete(Token, old_token["id"])
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
|
|
||||||
if deleted_count > 0:
|
if deleted_count > 0:
|
||||||
logger.info(f"Replaced {deleted_count} old tokens for connectionId {token.connectionId}")
|
logger.info(
|
||||||
|
f"Replaced {deleted_count} old tokens for connectionId {token.connectionId}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete old tokens for connectionId {token.connectionId}: {str(e)}")
|
logger.warning(
|
||||||
|
f"Failed to delete old tokens for connectionId {token.connectionId}: {str(e)}"
|
||||||
|
)
|
||||||
# Continue with saving the new token even if deletion fails
|
# Continue with saving the new token even if deletion fails
|
||||||
|
|
||||||
# Convert to dict and ensure all fields are properly set
|
# Convert to dict and ensure all fields are properly set
|
||||||
token_dict = token.dict()
|
token_dict = token.model_dump()
|
||||||
# Ensure userId is set to current user
|
# Ensure userId is set to current user
|
||||||
token_dict["userId"] = self.currentUser.id
|
token_dict["userId"] = self.currentUser.id
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
self.db.recordCreate(Token, token_dict)
|
self.db.recordCreate(Token, token_dict)
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving connection token: {str(e)}")
|
logger.error(f"Error saving connection token: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
@ -837,13 +875,14 @@ class AppObjects:
|
||||||
|
|
||||||
# Get token for this specific connection
|
# Get token for this specific connection
|
||||||
# Query for specific connection
|
# Query for specific connection
|
||||||
tokens = self.db.getRecordset(Token, recordFilter={
|
tokens = self.db.getRecordset(
|
||||||
"connectionId": connectionId
|
Token, recordFilter={"connectionId": connectionId}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
if not tokens:
|
if not tokens:
|
||||||
logger.warning(f"No connection token found for connectionId: {connectionId}")
|
logger.warning(
|
||||||
|
f"No connection token found for connectionId: {connectionId}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Sort by expiration date and get the latest (most recent expiration)
|
# Sort by expiration date and get the latest (most recent expiration)
|
||||||
|
|
@ -855,16 +894,27 @@ class AppObjects:
|
||||||
return latest_token
|
return latest_token
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting connection token for connectionId {connectionId}: {str(e)}")
|
logger.error(
|
||||||
|
f"Error getting connection token for connectionId {connectionId}: {str(e)}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def findActiveTokenById(self, tokenId: str, userId: str, authority: AuthAuthority, sessionId: str = None, mandateId: str = None) -> Optional[Token]:
|
def findActiveTokenById(
|
||||||
|
self,
|
||||||
|
tokenId: str,
|
||||||
|
userId: str,
|
||||||
|
authority: AuthAuthority,
|
||||||
|
sessionId: str = None,
|
||||||
|
mandateId: str = None,
|
||||||
|
) -> Optional[Token]:
|
||||||
"""Find an active access token by its id (jti) with optional session/tenant scoping."""
|
"""Find an active access token by its id (jti) with optional session/tenant scoping."""
|
||||||
try:
|
try:
|
||||||
recordFilter = {
|
recordFilter = {
|
||||||
"id": tokenId,
|
"id": tokenId,
|
||||||
"userId": userId,
|
"userId": userId,
|
||||||
"authority": authority.value if hasattr(authority, 'value') else str(authority),
|
"authority": authority.value
|
||||||
|
if hasattr(authority, "value")
|
||||||
|
else str(authority),
|
||||||
"status": TokenStatus.ACTIVE,
|
"status": TokenStatus.ACTIVE,
|
||||||
}
|
}
|
||||||
if sessionId is not None:
|
if sessionId is not None:
|
||||||
|
|
@ -892,7 +942,7 @@ class AppObjects:
|
||||||
"status": TokenStatus.REVOKED,
|
"status": TokenStatus.REVOKED,
|
||||||
"revokedAt": get_utc_timestamp(),
|
"revokedAt": get_utc_timestamp(),
|
||||||
"revokedBy": revokedBy,
|
"revokedBy": revokedBy,
|
||||||
"reason": reason or "revoked"
|
"reason": reason or "revoked",
|
||||||
}
|
}
|
||||||
self.db.recordModify(Token, tokenId, tokenUpdate)
|
self.db.recordModify(Token, tokenId, tokenUpdate)
|
||||||
return True
|
return True
|
||||||
|
|
@ -900,30 +950,53 @@ class AppObjects:
|
||||||
logger.error(f"Error revoking token {tokenId}: {str(e)}")
|
logger.error(f"Error revoking token {tokenId}: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def revokeTokensBySessionId(self, sessionId: str, userId: str, authority: AuthAuthority, revokedBy: str, reason: str = None) -> int:
|
def revokeTokensBySessionId(
|
||||||
|
self,
|
||||||
|
sessionId: str,
|
||||||
|
userId: str,
|
||||||
|
authority: AuthAuthority,
|
||||||
|
revokedBy: str,
|
||||||
|
reason: str = None,
|
||||||
|
) -> int:
|
||||||
"""Revoke all tokens of a session for a user/authority."""
|
"""Revoke all tokens of a session for a user/authority."""
|
||||||
try:
|
try:
|
||||||
tokens = self.db.getRecordset(Token, recordFilter={
|
tokens = self.db.getRecordset(
|
||||||
|
Token,
|
||||||
|
recordFilter={
|
||||||
"userId": userId,
|
"userId": userId,
|
||||||
"authority": authority.value if hasattr(authority, 'value') else str(authority),
|
"authority": authority.value
|
||||||
|
if hasattr(authority, "value")
|
||||||
|
else str(authority),
|
||||||
"sessionId": sessionId,
|
"sessionId": sessionId,
|
||||||
"status": TokenStatus.ACTIVE
|
"status": TokenStatus.ACTIVE,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
count = 0
|
count = 0
|
||||||
for t in tokens:
|
for t in tokens:
|
||||||
self.db.recordModify(Token, t["id"], {
|
self.db.recordModify(
|
||||||
|
Token,
|
||||||
|
t["id"],
|
||||||
|
{
|
||||||
"status": TokenStatus.REVOKED,
|
"status": TokenStatus.REVOKED,
|
||||||
"revokedAt": get_utc_timestamp(),
|
"revokedAt": get_utc_timestamp(),
|
||||||
"revokedBy": revokedBy,
|
"revokedBy": revokedBy,
|
||||||
"reason": reason or "session logout"
|
"reason": reason or "session logout",
|
||||||
})
|
},
|
||||||
|
)
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error revoking tokens for session {sessionId}: {str(e)}")
|
logger.error(f"Error revoking tokens for session {sessionId}: {str(e)}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def revokeTokensByUser(self, userId: str, authority: AuthAuthority = None, mandateId: str = None, revokedBy: str = None, reason: str = None) -> int:
|
def revokeTokensByUser(
|
||||||
|
self,
|
||||||
|
userId: str,
|
||||||
|
authority: AuthAuthority = None,
|
||||||
|
mandateId: str = None,
|
||||||
|
revokedBy: str = None,
|
||||||
|
reason: str = None,
|
||||||
|
) -> int:
|
||||||
"""Revoke all active tokens for a user, optionally filtered by authority/mandate."""
|
"""Revoke all active tokens for a user, optionally filtered by authority/mandate."""
|
||||||
try:
|
try:
|
||||||
# Fetch all active tokens for user (optionally filtered by authority)
|
# Fetch all active tokens for user (optionally filtered by authority)
|
||||||
|
|
@ -932,16 +1005,22 @@ class AppObjects:
|
||||||
"status": TokenStatus.ACTIVE,
|
"status": TokenStatus.ACTIVE,
|
||||||
}
|
}
|
||||||
if authority is not None:
|
if authority is not None:
|
||||||
recordFilter["authority"] = authority.value if hasattr(authority, 'value') else str(authority)
|
recordFilter["authority"] = (
|
||||||
|
authority.value if hasattr(authority, "value") else str(authority)
|
||||||
|
)
|
||||||
tokens = self.db.getRecordset(Token, recordFilter=recordFilter)
|
tokens = self.db.getRecordset(Token, recordFilter=recordFilter)
|
||||||
count = 0
|
count = 0
|
||||||
for t in tokens:
|
for t in tokens:
|
||||||
self.db.recordModify(Token, t["id"], {
|
self.db.recordModify(
|
||||||
|
Token,
|
||||||
|
t["id"],
|
||||||
|
{
|
||||||
"status": TokenStatus.REVOKED,
|
"status": TokenStatus.REVOKED,
|
||||||
"revokedAt": get_utc_timestamp(),
|
"revokedAt": get_utc_timestamp(),
|
||||||
"revokedBy": revokedBy,
|
"revokedBy": revokedBy,
|
||||||
"reason": reason or "admin revoke"
|
"reason": reason or "admin revoke",
|
||||||
})
|
},
|
||||||
|
)
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -958,7 +1037,10 @@ class AppObjects:
|
||||||
all_tokens = self.db.getRecordset(Token, recordFilter={})
|
all_tokens = self.db.getRecordset(Token, recordFilter={})
|
||||||
|
|
||||||
for token_data in all_tokens:
|
for token_data in all_tokens:
|
||||||
if token_data.get("expiresAt") and token_data.get("expiresAt") < current_time:
|
if (
|
||||||
|
token_data.get("expiresAt")
|
||||||
|
and token_data.get("expiresAt") < current_time
|
||||||
|
):
|
||||||
# Token is expired, delete it
|
# Token is expired, delete it
|
||||||
self.db.recordDelete(Token, token_data["id"])
|
self.db.recordDelete(Token, token_data["id"])
|
||||||
cleaned_count += 1
|
cleaned_count += 1
|
||||||
|
|
@ -983,7 +1065,7 @@ class AppObjects:
|
||||||
self.access = None
|
self.access = None
|
||||||
|
|
||||||
# Clear database context
|
# Clear database context
|
||||||
if hasattr(self, 'db'):
|
if hasattr(self, "db"):
|
||||||
self.db.updateContext("")
|
self.db.updateContext("")
|
||||||
|
|
||||||
logger.info("User logged out successfully")
|
logger.info("User logged out successfully")
|
||||||
|
|
@ -997,7 +1079,9 @@ class AppObjects:
|
||||||
def getNeutralizationConfig(self) -> Optional[DataNeutraliserConfig]:
|
def getNeutralizationConfig(self) -> Optional[DataNeutraliserConfig]:
|
||||||
"""Get the data neutralization configuration for the current user's mandate"""
|
"""Get the data neutralization configuration for the current user's mandate"""
|
||||||
try:
|
try:
|
||||||
configs = self.db.getRecordset(DataNeutraliserConfig, recordFilter={"mandateId": self.mandateId})
|
configs = self.db.getRecordset(
|
||||||
|
DataNeutraliserConfig, recordFilter={"mandateId": self.mandateId}
|
||||||
|
)
|
||||||
if not configs:
|
if not configs:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -1012,7 +1096,9 @@ class AppObjects:
|
||||||
logger.error(f"Error getting neutralization config: {str(e)}")
|
logger.error(f"Error getting neutralization config: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def createOrUpdateNeutralizationConfig(self, config_data: Dict[str, Any]) -> DataNeutraliserConfig:
|
def createOrUpdateNeutralizationConfig(
|
||||||
|
self, config_data: Dict[str, Any]
|
||||||
|
) -> DataNeutraliserConfig:
|
||||||
"""Create or update the data neutralization configuration"""
|
"""Create or update the data neutralization configuration"""
|
||||||
try:
|
try:
|
||||||
# Check if config already exists
|
# Check if config already exists
|
||||||
|
|
@ -1025,7 +1111,9 @@ class AppObjects:
|
||||||
update_data["updatedAt"] = get_utc_timestamp()
|
update_data["updatedAt"] = get_utc_timestamp()
|
||||||
|
|
||||||
updated_config = DataNeutraliserConfig.from_dict(update_data)
|
updated_config = DataNeutraliserConfig.from_dict(update_data)
|
||||||
self.db.recordModify(DataNeutraliserConfig, existing_config.id, updated_config)
|
self.db.recordModify(
|
||||||
|
DataNeutraliserConfig, existing_config.id, updated_config
|
||||||
|
)
|
||||||
|
|
||||||
return updated_config
|
return updated_config
|
||||||
else:
|
else:
|
||||||
|
|
@ -1042,17 +1130,24 @@ class AppObjects:
|
||||||
logger.error(f"Error creating/updating neutralization config: {str(e)}")
|
logger.error(f"Error creating/updating neutralization config: {str(e)}")
|
||||||
raise ValueError(f"Failed to create/update neutralization config: {str(e)}")
|
raise ValueError(f"Failed to create/update neutralization config: {str(e)}")
|
||||||
|
|
||||||
def getNeutralizationAttributes(self, file_id: Optional[str] = None) -> List[DataNeutralizerAttributes]:
|
def getNeutralizationAttributes(
|
||||||
|
self, file_id: Optional[str] = None
|
||||||
|
) -> List[DataNeutralizerAttributes]:
|
||||||
"""Get neutralization attributes, optionally filtered by file ID"""
|
"""Get neutralization attributes, optionally filtered by file ID"""
|
||||||
try:
|
try:
|
||||||
filter_dict = {"mandateId": self.mandateId}
|
filter_dict = {"mandateId": self.mandateId}
|
||||||
if file_id:
|
if file_id:
|
||||||
filter_dict["fileId"] = file_id
|
filter_dict["fileId"] = file_id
|
||||||
|
|
||||||
attributes = self.db.getRecordset(DataNeutralizerAttributes, recordFilter=filter_dict)
|
attributes = self.db.getRecordset(
|
||||||
|
DataNeutralizerAttributes, recordFilter=filter_dict
|
||||||
|
)
|
||||||
filtered_attributes = self._uam(DataNeutralizerAttributes, attributes)
|
filtered_attributes = self._uam(DataNeutralizerAttributes, attributes)
|
||||||
|
|
||||||
return [DataNeutralizerAttributes.from_dict(attr) for attr in filtered_attributes]
|
return [
|
||||||
|
DataNeutralizerAttributes.from_dict(attr)
|
||||||
|
for attr in filtered_attributes
|
||||||
|
]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting neutralization attributes: {str(e)}")
|
logger.error(f"Error getting neutralization attributes: {str(e)}")
|
||||||
|
|
@ -1061,23 +1156,27 @@ class AppObjects:
|
||||||
def deleteNeutralizationAttributes(self, file_id: str) -> bool:
|
def deleteNeutralizationAttributes(self, file_id: str) -> bool:
|
||||||
"""Delete all neutralization attributes for a specific file"""
|
"""Delete all neutralization attributes for a specific file"""
|
||||||
try:
|
try:
|
||||||
attributes = self.db.getRecordset(DataNeutralizerAttributes, recordFilter={
|
attributes = self.db.getRecordset(
|
||||||
"mandateId": self.mandateId,
|
DataNeutralizerAttributes,
|
||||||
"fileId": file_id
|
recordFilter={"mandateId": self.mandateId, "fileId": file_id},
|
||||||
})
|
)
|
||||||
|
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
self.db.recordDelete(DataNeutralizerAttributes, attribute["id"])
|
self.db.recordDelete(DataNeutralizerAttributes, attribute["id"])
|
||||||
|
|
||||||
logger.info(f"Deleted {len(attributes)} neutralization attributes for file {file_id}")
|
logger.info(
|
||||||
|
f"Deleted {len(attributes)} neutralization attributes for file {file_id}"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting neutralization attributes: {str(e)}")
|
logger.error(f"Error deleting neutralization attributes: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Public Methods
|
# Public Methods
|
||||||
|
|
||||||
|
|
||||||
def getInterface(currentUser: User) -> AppObjects:
|
def getInterface(currentUser: User) -> AppObjects:
|
||||||
"""
|
"""
|
||||||
Returns a AppObjects instance for the current user.
|
Returns a AppObjects instance for the current user.
|
||||||
|
|
@ -1095,6 +1194,7 @@ def getInterface(currentUser: User) -> AppObjects:
|
||||||
|
|
||||||
return _gatewayInterfaces[contextKey]
|
return _gatewayInterfaces[contextKey]
|
||||||
|
|
||||||
|
|
||||||
def getRootInterface() -> AppObjects:
|
def getRootInterface() -> AppObjects:
|
||||||
"""
|
"""
|
||||||
Returns a AppObjects instance with root privileges.
|
Returns a AppObjects instance with root privileges.
|
||||||
|
|
@ -1112,13 +1212,15 @@ def getRootInterface() -> AppObjects:
|
||||||
if not initialUserId:
|
if not initialUserId:
|
||||||
raise ValueError("No initial user ID found in database")
|
raise ValueError("No initial user ID found in database")
|
||||||
|
|
||||||
users = tempInterface.db.getRecordset(UserInDB, recordFilter={"id": initialUserId})
|
users = tempInterface.db.getRecordset(
|
||||||
|
UserInDB, recordFilter={"id": initialUserId}
|
||||||
|
)
|
||||||
if not users:
|
if not users:
|
||||||
raise ValueError("Initial user not found in database")
|
raise ValueError("Initial user not found in database")
|
||||||
|
|
||||||
# Convert to User model
|
# Convert to User model
|
||||||
user_data = users[0]
|
user_data = users[0]
|
||||||
rootUser = User.parse_obj(user_data)
|
rootUser = User.model_validate(user_data)
|
||||||
|
|
||||||
# Create root interface with the root user
|
# Create root interface with the root user
|
||||||
_rootAppObjects = AppObjects(rootUser)
|
_rootAppObjects = AppObjects(rootUser)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
Shared utilities for model attributes and labels.
|
Shared utilities for model attributes and labels.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from typing import Dict, Any, List, Type, Optional, Union
|
from typing import Dict, Any, List, Type, Optional, Union
|
||||||
import inspect
|
import inspect
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class ModelMixin:
|
class ModelMixin:
|
||||||
"""Mixin class that provides serialization methods for Pydantic models."""
|
"""Mixin class that provides serialization methods for Pydantic models."""
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ class ModelMixin:
|
||||||
Dict[str, Any]: Dictionary representation of the model
|
Dict[str, Any]: Dictionary representation of the model
|
||||||
"""
|
"""
|
||||||
# Get the raw dictionary
|
# Get the raw dictionary
|
||||||
if hasattr(self, 'model_dump'):
|
if hasattr(self, "model_dump"):
|
||||||
data: Dict[str, Any] = self.model_dump() # Pydantic v2
|
data: Dict[str, Any] = self.model_dump() # Pydantic v2
|
||||||
else:
|
else:
|
||||||
data: Dict[str, Any] = self.dict() # Pydantic v1
|
data: Dict[str, Any] = self.dict() # Pydantic v1
|
||||||
|
|
@ -33,7 +34,7 @@ class ModelMixin:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
|
def from_dict(cls, data: Dict[str, Any]) -> "ModelMixin":
|
||||||
"""
|
"""
|
||||||
Create a Pydantic model instance from a dictionary.
|
Create a Pydantic model instance from a dictionary.
|
||||||
|
|
||||||
|
|
@ -45,9 +46,11 @@ class ModelMixin:
|
||||||
"""
|
"""
|
||||||
return cls(**data)
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
# Define the AttributeDefinition class here instead of importing it
|
# Define the AttributeDefinition class here instead of importing it
|
||||||
class AttributeDefinition(BaseModel, ModelMixin):
|
class AttributeDefinition(BaseModel, ModelMixin):
|
||||||
"""Definition of a model attribute with its metadata."""
|
"""Definition of a model attribute with its metadata."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
label: str
|
label: str
|
||||||
|
|
@ -64,9 +67,11 @@ class AttributeDefinition(BaseModel, ModelMixin):
|
||||||
order: int = 0
|
order: int = 0
|
||||||
placeholder: Optional[str] = None
|
placeholder: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Global registry for model labels
|
# Global registry for model labels
|
||||||
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
|
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
|
||||||
|
|
||||||
|
|
||||||
def to_dict(model: BaseModel) -> Dict[str, Any]:
|
def to_dict(model: BaseModel) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Convert a Pydantic model to a dictionary.
|
Convert a Pydantic model to a dictionary.
|
||||||
|
|
@ -78,10 +83,11 @@ def to_dict(model: BaseModel) -> Dict[str, Any]:
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: Dictionary representation of the model
|
Dict[str, Any]: Dictionary representation of the model
|
||||||
"""
|
"""
|
||||||
if hasattr(model, 'model_dump'):
|
if hasattr(model, "model_dump"):
|
||||||
return model.model_dump() # Pydantic v2
|
return model.model_dump() # Pydantic v2
|
||||||
return model.dict() # Pydantic v1
|
return model.dict() # Pydantic v1
|
||||||
|
|
||||||
|
|
||||||
def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
|
def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
|
||||||
"""
|
"""
|
||||||
Create a Pydantic model instance from a dictionary.
|
Create a Pydantic model instance from a dictionary.
|
||||||
|
|
@ -95,7 +101,10 @@ def from_dict(model_class: Type[BaseModel], data: Dict[str, Any]) -> BaseModel:
|
||||||
"""
|
"""
|
||||||
return model_class(**data)
|
return model_class(**data)
|
||||||
|
|
||||||
def register_model_labels(model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]):
|
|
||||||
|
def register_model_labels(
|
||||||
|
model_name: str, model_label: Dict[str, str], labels: Dict[str, Dict[str, str]]
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Register labels for a model's attributes and the model itself.
|
Register labels for a model's attributes and the model itself.
|
||||||
|
|
||||||
|
|
@ -106,10 +115,8 @@ def register_model_labels(model_name: str, model_label: Dict[str, str], labels:
|
||||||
labels: Dictionary mapping attribute names to their translations
|
labels: Dictionary mapping attribute names to their translations
|
||||||
e.g. {"name": {"en": "Name", "fr": "Nom"}}
|
e.g. {"name": {"en": "Name", "fr": "Nom"}}
|
||||||
"""
|
"""
|
||||||
MODEL_LABELS[model_name] = {
|
MODEL_LABELS[model_name] = {"model": model_label, "attributes": labels}
|
||||||
"model": model_label,
|
|
||||||
"attributes": labels
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
|
def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -130,6 +137,7 @@ def get_model_labels(model_name: str, language: str = "en") -> Dict[str, str]:
|
||||||
for attr, translations in attribute_labels.items()
|
for attr, translations in attribute_labels.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_model_label(model_name: str, language: str = "en") -> str:
|
def get_model_label(model_name: str, language: str = "en") -> str:
|
||||||
"""
|
"""
|
||||||
Get the label for a model in the specified language.
|
Get the label for a model in the specified language.
|
||||||
|
|
@ -145,7 +153,10 @@ def get_model_label(model_name: str, language: str = "en") -> str:
|
||||||
model_label = model_data.get("model", {})
|
model_label = model_data.get("model", {})
|
||||||
return model_label.get(language, model_label.get("en", model_name))
|
return model_label.get(language, model_label.get("en", model_name))
|
||||||
|
|
||||||
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
|
|
||||||
|
def getModelAttributeDefinitions(
|
||||||
|
modelClass: Type[BaseModel] = None, userLanguage: str = "en"
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get attribute definitions for a model class.
|
Get attribute definitions for a model class.
|
||||||
|
|
||||||
|
|
@ -165,11 +176,11 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
||||||
model_label = get_model_label(model_name, userLanguage)
|
model_label = get_model_label(model_name, userLanguage)
|
||||||
|
|
||||||
# Handle both Pydantic v1 and v2
|
# Handle both Pydantic v1 and v2
|
||||||
if hasattr(modelClass, 'model_fields'): # Pydantic v2
|
if hasattr(modelClass, "model_fields"): # Pydantic v2
|
||||||
fields = modelClass.model_fields
|
fields = modelClass.model_fields
|
||||||
for name, field in fields.items():
|
for name, field in fields.items():
|
||||||
# Extract frontend metadata from field info
|
# Extract frontend metadata from field info
|
||||||
field_info = field.field_info if hasattr(field, 'field_info') else None
|
field_info = field.field_info if hasattr(field, "field_info") else None
|
||||||
# Check both direct attributes and extra field for frontend metadata
|
# Check both direct attributes and extra field for frontend metadata
|
||||||
frontend_type = None
|
frontend_type = None
|
||||||
frontend_readonly = False
|
frontend_readonly = False
|
||||||
|
|
@ -178,43 +189,63 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
||||||
|
|
||||||
if field_info:
|
if field_info:
|
||||||
# Try direct attributes first
|
# Try direct attributes first
|
||||||
frontend_type = getattr(field_info, 'frontend_type', None)
|
frontend_type = getattr(field_info, "frontend_type", None)
|
||||||
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
|
frontend_readonly = getattr(field_info, "frontend_readonly", False)
|
||||||
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
|
frontend_required = getattr(
|
||||||
frontend_options = getattr(field_info, 'frontend_options', None)
|
field_info, "frontend_required", frontend_required
|
||||||
|
)
|
||||||
|
frontend_options = getattr(field_info, "frontend_options", None)
|
||||||
|
|
||||||
# If not found, check extra field
|
# If not found, check extra field
|
||||||
if hasattr(field_info, 'extra') and field_info.extra:
|
if hasattr(field_info, "extra") and field_info.extra:
|
||||||
if frontend_type is None:
|
if frontend_type is None:
|
||||||
frontend_type = field_info.extra.get('frontend_type')
|
frontend_type = field_info.extra.get("frontend_type")
|
||||||
if not frontend_readonly:
|
if not frontend_readonly:
|
||||||
frontend_readonly = field_info.extra.get('frontend_readonly', False)
|
frontend_readonly = field_info.extra.get(
|
||||||
if frontend_required == field.is_required(): # Only override if we didn't get it from direct attribute
|
"frontend_readonly", False
|
||||||
frontend_required = field_info.extra.get('frontend_required', frontend_required)
|
)
|
||||||
|
if (
|
||||||
|
frontend_required == field.is_required()
|
||||||
|
): # Only override if we didn't get it from direct attribute
|
||||||
|
frontend_required = field_info.extra.get(
|
||||||
|
"frontend_required", frontend_required
|
||||||
|
)
|
||||||
if frontend_options is None:
|
if frontend_options is None:
|
||||||
frontend_options = field_info.extra.get('frontend_options')
|
frontend_options = field_info.extra.get("frontend_options")
|
||||||
|
|
||||||
# Use frontend type if available, otherwise fall back to Python type
|
# Use frontend type if available, otherwise fall back to Python type
|
||||||
field_type = frontend_type if frontend_type else (field.annotation.__name__ if hasattr(field.annotation, "__name__") else str(field.annotation))
|
field_type = (
|
||||||
|
frontend_type
|
||||||
|
if frontend_type
|
||||||
|
else (
|
||||||
|
field.annotation.__name__
|
||||||
|
if hasattr(field.annotation, "__name__")
|
||||||
|
else str(field.annotation)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
attributes.append({
|
attributes.append(
|
||||||
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": field_type,
|
"type": field_type,
|
||||||
"required": frontend_required,
|
"required": frontend_required,
|
||||||
"description": field.description if hasattr(field, "description") else "",
|
"description": field.description
|
||||||
|
if hasattr(field, "description")
|
||||||
|
else "",
|
||||||
"label": labels.get(name, name),
|
"label": labels.get(name, name),
|
||||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||||
"editable": not frontend_readonly,
|
"editable": not frontend_readonly,
|
||||||
"visible": True,
|
"visible": True,
|
||||||
"order": len(attributes),
|
"order": len(attributes),
|
||||||
"readonly": frontend_readonly,
|
"readonly": frontend_readonly,
|
||||||
"options": frontend_options
|
"options": frontend_options,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
else: # Pydantic v1
|
else: # Pydantic v1
|
||||||
fields = modelClass.__fields__
|
fields = modelClass.__fields__
|
||||||
for name, field in fields.items():
|
for name, field in fields.items():
|
||||||
# Extract frontend metadata from field info
|
# Extract frontend metadata from field info
|
||||||
field_info = field.field_info if hasattr(field, 'field_info') else None
|
field_info = field.field_info if hasattr(field, "field_info") else None
|
||||||
# Check both direct attributes and extra field for frontend metadata
|
# Check both direct attributes and extra field for frontend metadata
|
||||||
frontend_type = None
|
frontend_type = None
|
||||||
frontend_readonly = False
|
frontend_readonly = False
|
||||||
|
|
@ -223,43 +254,61 @@ def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguag
|
||||||
|
|
||||||
if field_info:
|
if field_info:
|
||||||
# Try direct attributes first
|
# Try direct attributes first
|
||||||
frontend_type = getattr(field_info, 'frontend_type', None)
|
frontend_type = getattr(field_info, "frontend_type", None)
|
||||||
frontend_readonly = getattr(field_info, 'frontend_readonly', False)
|
frontend_readonly = getattr(field_info, "frontend_readonly", False)
|
||||||
frontend_required = getattr(field_info, 'frontend_required', frontend_required)
|
frontend_required = getattr(
|
||||||
frontend_options = getattr(field_info, 'frontend_options', None)
|
field_info, "frontend_required", frontend_required
|
||||||
|
)
|
||||||
|
frontend_options = getattr(field_info, "frontend_options", None)
|
||||||
|
|
||||||
# If not found, check extra field
|
# If not found, check extra field
|
||||||
if hasattr(field_info, 'extra') and field_info.extra:
|
if hasattr(field_info, "extra") and field_info.extra:
|
||||||
if frontend_type is None:
|
if frontend_type is None:
|
||||||
frontend_type = field_info.extra.get('frontend_type')
|
frontend_type = field_info.extra.get("frontend_type")
|
||||||
if not frontend_readonly:
|
if not frontend_readonly:
|
||||||
frontend_readonly = field_info.extra.get('frontend_readonly', False)
|
frontend_readonly = field_info.extra.get(
|
||||||
if frontend_required == field.required: # Only override if we didn't get it from direct attribute
|
"frontend_readonly", False
|
||||||
frontend_required = field_info.extra.get('frontend_required', frontend_required)
|
)
|
||||||
|
if (
|
||||||
|
frontend_required == field.required
|
||||||
|
): # Only override if we didn't get it from direct attribute
|
||||||
|
frontend_required = field_info.extra.get(
|
||||||
|
"frontend_required", frontend_required
|
||||||
|
)
|
||||||
if frontend_options is None:
|
if frontend_options is None:
|
||||||
frontend_options = field_info.extra.get('frontend_options')
|
frontend_options = field_info.extra.get("frontend_options")
|
||||||
|
|
||||||
# Use frontend type if available, otherwise fall back to Python type
|
# Use frontend type if available, otherwise fall back to Python type
|
||||||
field_type = frontend_type if frontend_type else (field.type_.__name__ if hasattr(field.type_, "__name__") else str(field.type_))
|
field_type = (
|
||||||
|
frontend_type
|
||||||
|
if frontend_type
|
||||||
|
else (
|
||||||
|
field.type_.__name__
|
||||||
|
if hasattr(field.type_, "__name__")
|
||||||
|
else str(field.type_)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
attributes.append({
|
attributes.append(
|
||||||
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": field_type,
|
"type": field_type,
|
||||||
"required": frontend_required,
|
"required": frontend_required,
|
||||||
"description": field.field_info.description if hasattr(field.field_info, "description") else "",
|
"description": field.field_info.description
|
||||||
|
if hasattr(field.field_info, "description")
|
||||||
|
else "",
|
||||||
"label": labels.get(name, name),
|
"label": labels.get(name, name),
|
||||||
"placeholder": f"Please enter {labels.get(name, name)}",
|
"placeholder": f"Please enter {labels.get(name, name)}",
|
||||||
"editable": not frontend_readonly,
|
"editable": not frontend_readonly,
|
||||||
"visible": True,
|
"visible": True,
|
||||||
"order": len(attributes),
|
"order": len(attributes),
|
||||||
"readonly": frontend_readonly,
|
"readonly": frontend_readonly,
|
||||||
"options": frontend_options
|
"options": frontend_options,
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"model": model_label,
|
|
||||||
"attributes": attributes
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"model": model_label, "attributes": attributes}
|
||||||
|
|
||||||
|
|
||||||
def getModelClasses() -> Dict[str, Type[BaseModel]]:
|
def getModelClasses() -> Dict[str, Type[BaseModel]]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -271,30 +320,38 @@ def getModelClasses() -> Dict[str, Type[BaseModel]]:
|
||||||
modelClasses = {}
|
modelClasses = {}
|
||||||
|
|
||||||
# Get the interfaces directory path
|
# Get the interfaces directory path
|
||||||
interfaces_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'interfaces')
|
interfaces_dir = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(__file__)), "interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
# Find all model files
|
# Find all model files
|
||||||
for fileName in os.listdir(interfaces_dir):
|
for fileName in os.listdir(interfaces_dir):
|
||||||
if fileName.endswith('Model.py'):
|
if fileName.endswith("Model.py"):
|
||||||
# Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel)
|
# Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel)
|
||||||
module_name = fileName[:-3]
|
module_name = fileName[:-3]
|
||||||
|
|
||||||
# Import the module dynamically
|
# Import the module dynamically
|
||||||
module = importlib.import_module(f'modules.interfaces.{module_name}')
|
module = importlib.import_module(f"modules.interfaces.{module_name}")
|
||||||
|
|
||||||
# Get all classes from the module
|
# Get all classes from the module
|
||||||
for name, obj in inspect.getmembers(module):
|
for name, obj in inspect.getmembers(module):
|
||||||
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj != BaseModel:
|
if (
|
||||||
|
inspect.isclass(obj)
|
||||||
|
and issubclass(obj, BaseModel)
|
||||||
|
and obj != BaseModel
|
||||||
|
):
|
||||||
modelClasses[name] = obj
|
modelClasses[name] = obj
|
||||||
|
|
||||||
return modelClasses
|
return modelClasses
|
||||||
|
|
||||||
|
|
||||||
class AttributeResponse(BaseModel):
|
class AttributeResponse(BaseModel):
|
||||||
"""Response model for entity attributes"""
|
"""Response model for entity attributes"""
|
||||||
|
|
||||||
attributes: List[AttributeDefinition]
|
attributes: List[AttributeDefinition]
|
||||||
|
|
||||||
class Config:
|
model_config = ConfigDict(
|
||||||
schema_extra = {
|
json_schema_extra={
|
||||||
"example": {
|
"example": {
|
||||||
"attributes": [
|
"attributes": [
|
||||||
{
|
{
|
||||||
|
|
@ -305,8 +362,9 @@ class AttributeResponse(BaseModel):
|
||||||
"placeholder": "Please enter username",
|
"placeholder": "Please enter username",
|
||||||
"editable": True,
|
"editable": True,
|
||||||
"visible": True,
|
"visible": True,
|
||||||
"order": 0
|
"order": 0,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ websockets==12.0
|
||||||
uvicorn==0.23.2
|
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>=2.0.0 # Upgraded to v2 for LangChain compatibility
|
||||||
email-validator==2.0.0 # Required by Pydantic for email validation
|
email-validator==2.0.0 # Required by Pydantic for email validation
|
||||||
slowapi==0.1.8 # For rate limiting
|
slowapi==0.1.8 # For rate limiting
|
||||||
|
|
||||||
|
|
@ -108,3 +108,8 @@ xyzservices>=2021.09.1
|
||||||
|
|
||||||
# PostgreSQL connector dependencies
|
# PostgreSQL connector dependencies
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
|
|
||||||
|
## LangChain & LangGraph
|
||||||
|
langchain==0.3.27
|
||||||
|
langgraph==0.6.8
|
||||||
|
langchain-core==0.3.77
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue