data management tested

This commit is contained in:
ValueOn AG 2025-06-06 07:42:56 +02:00
parent 096e4052f0
commit d8954f95af
12 changed files with 835 additions and 235 deletions

9
app.py
View file

@ -30,6 +30,13 @@ def initLogging():
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S") datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
) )
# Add filter to exclude Chrome DevTools requests
class ChromeDevToolsFilter(logging.Filter):
def filter(self, record):
return not (isinstance(record.msg, str) and
('.well-known/appspecific/com.chrome.devtools.json' in record.msg or
'Request: /index.html' in record.msg))
# Configure handlers based on config # Configure handlers based on config
handlers = [] handlers = []
@ -37,6 +44,7 @@ def initLogging():
if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True): if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True):
consoleHandler = logging.StreamHandler() consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(consoleFormatter) consoleHandler.setFormatter(consoleFormatter)
consoleHandler.addFilter(ChromeDevToolsFilter())
handlers.append(consoleHandler) handlers.append(consoleHandler)
# Add file handler if enabled # Add file handler if enabled
@ -62,6 +70,7 @@ def initLogging():
backupCount=backupCount backupCount=backupCount
) )
fileHandler.setFormatter(fileFormatter) fileHandler.setFormatter(fileFormatter)
fileHandler.addFilter(ChromeDevToolsFilter())
handlers.append(fileHandler) handlers.append(fileHandler)
# Configure the root logger # Configure the root logger

View file

@ -162,11 +162,11 @@ class DatabaseConnector:
raise ValueError(f"Record ID mismatch: file name ID ({recordId}) does not match record ID ({record['id']})") raise ValueError(f"Record ID mismatch: file name ID ({recordId}) does not match record ID ({record['id']})")
# Add metadata # Add metadata
currentTime = datetime.now().isoformat() currentTime = datetime.now()
if "_createdAt" not in record: if "_createdAt" not in record:
record["_createdAt"] = currentTime record["_createdAt"] = currentTime.isoformat()
record["_createdBy"] = self.userId record["_createdBy"] = self.userId
record["_modifiedAt"] = currentTime record["_modifiedAt"] = currentTime.isoformat()
record["_modifiedBy"] = self.userId record["_modifiedBy"] = self.userId
# Save the record file # Save the record file

View file

@ -59,6 +59,18 @@ class AppAccess:
else: else:
# Regular users only see themselves # Regular users only see themselves
filtered_records = [r for r in recordset if r.get("id") == self.userId] filtered_records = [r for r in recordset if r.get("id") == self.userId]
# Special handling for connections table
elif table == "connections":
if self.privilege == UserPrivilege.SYSADMIN:
# SysAdmin sees all connections
filtered_records = recordset
elif self.privilege == UserPrivilege.ADMIN:
# Admin sees connections for users in their mandate
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
filtered_records = [r for r in recordset if r.get("userId") in user_ids]
else:
# Regular users only see their own connections
filtered_records = [r for r in recordset if r.get("userId") == self.userId]
# System admins see all other records # System admins see all other records
elif self.privilege == UserPrivilege.SYSADMIN: elif self.privilege == UserPrivilege.SYSADMIN:
filtered_records = recordset filtered_records = recordset
@ -93,6 +105,22 @@ class AppAccess:
else: else:
record["_hideEdit"] = record.get("id") != self.userId record["_hideEdit"] = record.get("id") != self.userId
record["_hideDelete"] = True # Regular users cannot delete users record["_hideDelete"] = True # Regular users cannot delete users
elif table == "connections":
# Everyone can view connections they have access to
record["_hideView"] = False
# SysAdmin can edit/delete any connection
if self.privilege == UserPrivilege.SYSADMIN:
record["_hideEdit"] = False
record["_hideDelete"] = False
# Admin can edit/delete connections for users in their mandate
elif self.privilege == UserPrivilege.ADMIN:
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
record["_hideEdit"] = record.get("userId") not in user_ids
record["_hideDelete"] = record.get("userId") not in user_ids
# Regular users can only edit/delete their own connections
else:
record["_hideEdit"] = record.get("userId") != self.userId
record["_hideDelete"] = record.get("userId") != self.userId
elif table == "sessions": elif table == "sessions":
# Only show sessions for the current user or if admin # Only show sessions for the current user or if admin
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]: if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
@ -145,6 +173,15 @@ class AppAccess:
record = records[0] record = records[0]
# Special handling for connections
if table == "connections":
# Admin can modify connections for users in their mandate
if self.privilege == UserPrivilege.ADMIN:
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
return record.get("userId") in user_ids
# Users can only modify their own connections
return record.get("userId") == self.userId
# Admins can modify anything in their mandate # Admins can modify anything in their mandate
if self.privilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId: if self.privilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId:
return True return True

View file

@ -3,7 +3,7 @@ Interface to the Gateway system.
Manages users and mandates for authentication. Manages users and mandates for authentication.
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta, UTC
import os import os
import logging import logging
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
@ -249,44 +249,109 @@ class GatewayInterface:
def getUser(self, userId: str) -> Optional[User]: def getUser(self, userId: str) -> Optional[User]:
"""Returns a user by ID if user has access.""" """Returns a user by ID if user has access."""
users = self.db.getRecordset("users", recordFilter={"id": userId})
if not users:
return None
filteredUsers = self._uam("users", users)
if not filteredUsers:
return None
return User.from_dict(filteredUsers[0])
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
externalUsername: str, externalEmail: Optional[str] = None) -> UserConnection:
"""Add a new connection to an external service for a user"""
try: try:
# Get user # Get all users
users = self.db.getRecordset("users")
if not users:
return None
# Find user by ID
for user_dict in users:
if user_dict.get("id") == userId:
# Apply access control
filteredUsers = self._uam("users", [user_dict])
if filteredUsers:
return User.from_dict(filteredUsers[0])
return None
return None
except Exception as e:
logger.error(f"Error getting user by ID: {str(e)}")
return None
def getUserConnections(self, userId: str) -> List[UserConnection]:
"""Returns all connections for a user."""
try:
# Get connections for this user
connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
# Convert to UserConnection objects
result = []
for conn_dict in connections:
try:
# Convert string dates to datetime objects
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
if field in conn_dict and conn_dict[field]:
try:
if isinstance(conn_dict[field], str):
conn_dict[field] = datetime.fromisoformat(conn_dict[field].replace('Z', '+00:00'))
except (ValueError, TypeError):
conn_dict[field] = None
# Create UserConnection object
connection = UserConnection(
id=conn_dict["id"],
userId=conn_dict["userId"],
authority=conn_dict.get("authority"),
externalId=conn_dict.get("externalId", ""),
externalUsername=conn_dict.get("externalUsername", ""),
externalEmail=conn_dict.get("externalEmail"),
status=conn_dict.get("status", "pending"),
connectedAt=conn_dict.get("connectedAt"),
lastChecked=conn_dict.get("lastChecked"),
expiresAt=conn_dict.get("expiresAt")
)
result.append(connection)
except Exception as e:
logger.error(f"Error converting connection dict to object: {str(e)}")
continue
return result
except Exception as e:
logger.error(f"Error getting user connections: {str(e)}")
return []
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
externalUsername: str, externalEmail: Optional[str] = None,
status: ConnectionStatus = ConnectionStatus.PENDING) -> UserConnection:
"""
Adds a new connection for a user.
Args:
userId: The ID of the user
authority: The authentication authority (e.g., MSFT, GOOGLE)
externalId: The external ID from the authority
externalUsername: The username from the authority
externalEmail: Optional email from the authority
status: The connection status (defaults to PENDING)
Returns:
The created UserConnection object
"""
try:
# Get the user
user = self.getUser(userId) user = self.getUser(userId)
if not user: if not user:
raise ValueError(f"User {userId} not found") raise ValueError(f"User not found: {userId}")
# Check if connection already exists
for conn in user.connections:
if conn.authority == authority and conn.externalId == externalId:
raise ValueError(f"Connection to {authority} already exists for user {userId}")
# Create new connection # Create new connection with all required fields
connection = UserConnection( connection = UserConnection(
id=str(uuid.uuid4()),
userId=userId,
authority=authority, authority=authority,
externalId=externalId, externalId=externalId,
externalUsername=externalUsername, externalUsername=externalUsername,
externalEmail=externalEmail, externalEmail=externalEmail,
status=ConnectionStatus.ACTIVE status=status,
connectedAt=datetime.now(UTC),
lastChecked=datetime.now(UTC),
expiresAt=None # Optional field, set to None by default
) )
# Add connection to user # Save to connections table
user.connections.append(connection) self.db.recordCreate("connections", connection.to_dict())
# Update user record
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
return connection return connection
@ -294,19 +359,19 @@ class GatewayInterface:
logger.error(f"Error adding user connection: {str(e)}") logger.error(f"Error adding user connection: {str(e)}")
raise ValueError(f"Failed to add user connection: {str(e)}") raise ValueError(f"Failed to add user connection: {str(e)}")
def removeUserConnection(self, userId: str, connectionId: str) -> None: def removeUserConnection(self, connectionId: str) -> None:
"""Remove a connection to an external service for a user""" """Remove a connection to an external service"""
try: try:
# Get user # Get connection
user = self.getUser(userId) connections = self.db.getRecordset("connections", recordFilter={
if not user: "id": connectionId
raise ValueError(f"User {userId} not found") })
# Find and remove connection
user.connections = [c for c in user.connections if c.id != connectionId]
# Update user record if not connections:
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]}) raise ValueError(f"Connection {connectionId} not found")
# Delete connection
self.db.recordDelete("connections", 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)}")
@ -468,11 +533,10 @@ class GatewayInterface:
logger.debug(f"Deleted token {token['id']} for user {userId}") logger.debug(f"Deleted token {token['id']} for user {userId}")
# Delete user connections # Delete user connections
user = self.getUser(userId) connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
if user and user.connections: for conn in connections:
for conn in user.connections: self.db.recordDelete("connections", conn["id"])
self.removeUserConnection(userId, conn.id) logger.debug(f"Deleted connection {conn['id']} for user {userId}")
logger.debug(f"Deleted connection {conn.id} for user {userId}")
logger.info(f"All referenced data for user {userId} has been deleted") logger.info(f"All referenced data for user {userId} has been deleted")

View file

@ -50,6 +50,7 @@ register_model_labels(
class UserConnection(BaseModel, ModelMixin): class UserConnection(BaseModel, ModelMixin):
"""Data model for a user's connection to an external service""" """Data model for a user's connection to an external service"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection") id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
userId: str = Field(description="ID of the user this connection belongs to")
authority: AuthAuthority = Field(description="Authentication authority") authority: AuthAuthority = Field(description="Authentication authority")
externalId: str = Field(description="User ID in the external system") externalId: str = Field(description="User ID in the external system")
externalUsername: str = Field(description="Username in the external system") externalUsername: str = Field(description="Username in the external system")
@ -59,12 +60,28 @@ class UserConnection(BaseModel, ModelMixin):
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified") lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
expiresAt: Optional[datetime] = Field(None, description="When the connection expires") expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
def to_dict(self) -> Dict[str, Any]:
"""Convert the model to a dictionary with proper datetime serialization"""
data = super().to_dict()
# Convert datetime fields to ISO format strings
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
if field in data and data[field] is not None:
if isinstance(data[field], datetime):
data[field] = data[field].isoformat()
elif isinstance(data[field], (int, float)):
try:
data[field] = datetime.fromtimestamp(data[field]).isoformat()
except (ValueError, TypeError):
data[field] = None
return data
# Register labels for UserConnection # Register labels for UserConnection
register_model_labels( register_model_labels(
"UserConnection", "UserConnection",
{"en": "User Connection", "fr": "Connexion utilisateur"}, {"en": "User Connection", "fr": "Connexion utilisateur"},
{ {
"id": {"en": "ID", "fr": "ID"}, "id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"}, "authority": {"en": "Authority", "fr": "Autorité"},
"externalId": {"en": "External ID", "fr": "ID externe"}, "externalId": {"en": "External ID", "fr": "ID externe"},
"externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"}, "externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
@ -137,7 +154,6 @@ class User(BaseModel, ModelMixin):
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level") privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority") authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to") mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
# Register labels for User # Register labels for User
register_model_labels( register_model_labels(
@ -152,8 +168,7 @@ register_model_labels(
"enabled": {"en": "Enabled", "fr": "Activé"}, "enabled": {"en": "Enabled", "fr": "Activé"},
"privilege": {"en": "Privilege", "fr": "Privilège"}, "privilege": {"en": "Privilege", "fr": "Privilège"},
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"}, "authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}, "mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}
"connections": {"en": "Connections", "fr": "Connexions"}
} }
) )

View file

@ -24,6 +24,10 @@ class ManagementAccess:
self.privilege = currentUser.privilege self.privilege = currentUser.privilege
self.db = db self.db = db
def getInitialUserid(self):
return "----"
# return self.db.getInitialUserId() --> to get from AdminDB !
def canModifyAttribute(self, table: str, attribute: str) -> bool: def canModifyAttribute(self, table: str, attribute: str) -> bool:
""" """
Checks if the current user can modify a specific attribute in a table. Checks if the current user can modify a specific attribute in a table.
@ -59,6 +63,8 @@ class ManagementAccess:
filtered_records = [] filtered_records = []
initialid = self.getInitialUserid()
# Apply filtering based on privilege # Apply filtering based on privilege
if userPrivilege == "sysadmin": if userPrivilege == "sysadmin":
filtered_records = recordset # System admins see all records filtered_records = recordset # System admins see all records
@ -69,10 +75,15 @@ class ManagementAccess:
# For prompts, users can see all prompts from their mandate # For prompts, users can see all prompts from their mandate
if table == "prompts": if table == "prompts":
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]
elif table == "users":
# For users table, users can only see their own record
filtered_records = [r for r in recordset if r.get("id") == self.userId]
else: else:
# Users see only their records for other tables # Users see only their records for other tables
filtered_records = [r for r in recordset filtered_records = [
if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId] r for r in recordset
if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId
]
# Add access control attributes to each record # Add access control attributes to each record
for record in filtered_records: for record in filtered_records:
@ -104,6 +115,16 @@ class ManagementAccess:
record["_hideView"] = False # Everyone can view record["_hideView"] = False # Everyone can view
record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId")) record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId")) record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
elif table == "users":
# For users table, users can only modify their own connections
record["_hideView"] = False
record["_hideEdit"] = record_id != self.userId
record["_hideDelete"] = record_id != self.userId
# Add connection-specific permissions
if "connections" in record:
for conn in record["connections"]:
conn["_hideEdit"] = record_id != self.userId
conn["_hideDelete"] = record_id != self.userId
else: else:
# Default access control for other tables # Default access control for other tables
record["_hideView"] = False record["_hideView"] = False
@ -138,6 +159,12 @@ class ManagementAccess:
record = records[0] record = records[0]
# Special case for users table - users can modify their own connections
if table == "users":
if record.get("id") == self.userId:
return True
return False
# Admins can modify anything in their mandate, if mandate is specified for a record # Admins can modify anything in their mandate, if mandate is specified for a record
if userPrivilege == "admin" and record.get("mandateId","-") == self.mandateId: if userPrivilege == "admin" and record.get("mandateId","-") == self.mandateId:
return True return True

View file

@ -8,6 +8,7 @@ from typing import List, Dict, Any, Optional
from fastapi import status from fastapi import status
from datetime import datetime from datetime import datetime
import logging import logging
import json
from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter
@ -31,16 +32,21 @@ async def get_connections(
"""Get all connections for the current user or all connections if admin""" """Get all connections for the current user or all connections if admin"""
try: try:
interface = getInterface(currentUser) interface = getInterface(currentUser)
# Clear connections cache to ensure fresh data
if "connections" in interface.db._tablesCache:
del interface.db._tablesCache["connections"]
if currentUser.privilege in ['admin', 'sysadmin']: if currentUser.privilege in ['admin', 'sysadmin']:
# Admins can see all connections # Admins can see all connections
users = interface.getAllUsers() users = interface.getAllUsers()
connections = [] connections = []
for user in users: for user in users:
connections.extend(user.connections) connections.extend(interface.getUserConnections(user.id))
return connections return connections
else: else:
# Regular users can only see their own connections # Regular users can only see their own connections
return currentUser.connections return interface.getUserConnections(currentUser.id)
except Exception as e: except Exception as e:
logger.error(f"Error getting connections: {str(e)}") logger.error(f"Error getting connections: {str(e)}")
raise HTTPException( raise HTTPException(
@ -48,6 +54,99 @@ async def get_connections(
detail=f"Failed to get connections: {str(e)}" detail=f"Failed to get connections: {str(e)}"
) )
@router.post("/", response_model=UserConnection)
@limiter.limit("10/minute")
async def create_connection(
request: Request,
connection_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> UserConnection:
"""Create a new connection for the current user or update existing one"""
try:
interface = getInterface(currentUser)
# Map type to authority
authority_map = {
'msft': AuthAuthority.MSFT,
'google': AuthAuthority.GOOGLE
}
authority = authority_map.get(connection_data.get('type'))
if not authority:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported connection type: {connection_data.get('type')}"
)
# Get fresh copy of user from database
user = interface.getUser(currentUser.id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Check for existing connection of the same authority
existing_connection = None
connections = interface.getUserConnections(currentUser.id)
for conn in connections:
if conn.authority == authority:
existing_connection = conn
break
if existing_connection:
# Update existing connection
existing_connection.status = ConnectionStatus.PENDING
existing_connection.lastChecked = datetime.now()
existing_connection.externalId = "" # Reset for new OAuth flow
existing_connection.externalUsername = "" # Reset for new OAuth flow
# Convert connection to dict and ensure datetime fields are serialized
connection_dict = existing_connection.to_dict()
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
if field in connection_dict and connection_dict[field] is not None:
if isinstance(connection_dict[field], datetime):
connection_dict[field] = connection_dict[field].isoformat()
elif isinstance(connection_dict[field], (int, float)):
connection_dict[field] = datetime.fromtimestamp(connection_dict[field]).isoformat()
# Update connection record directly
interface.db.recordModify("connections", existing_connection.id, connection_dict)
return existing_connection
else:
# Create new connection with PENDING status
connection = interface.addUserConnection(
userId=currentUser.id,
authority=authority,
externalId="", # Will be set after OAuth
externalUsername="", # Will be set after OAuth
status=ConnectionStatus.PENDING # Start with PENDING status
)
# Convert connection to dict and ensure datetime fields are serialized
connection_dict = connection.to_dict()
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
if field in connection_dict and connection_dict[field] is not None:
if isinstance(connection_dict[field], datetime):
connection_dict[field] = connection_dict[field].isoformat()
elif isinstance(connection_dict[field], (int, float)):
connection_dict[field] = datetime.fromtimestamp(connection_dict[field]).isoformat()
# Save connection record
interface.db.recordModify("connections", connection.id, connection_dict)
return connection
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating connection: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create connection: {str(e)}"
)
@router.post("/{connectionId}/connect") @router.post("/{connectionId}/connect")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def connect_service( async def connect_service(
@ -65,7 +164,8 @@ async def connect_service(
# Admins can connect any connection # Admins can connect any connection
users = interface.getAllUsers() users = interface.getAllUsers()
for user in users: for user in users:
for conn in user.connections: connections = interface.getUserConnections(user.id)
for conn in connections:
if conn.id == connectionId: if conn.id == connectionId:
connection = conn connection = conn
break break
@ -73,7 +173,8 @@ async def connect_service(
break break
else: else:
# Regular users can only connect their own connections # Regular users can only connect their own connections
for conn in currentUser.connections: connections = interface.getUserConnections(currentUser.id)
for conn in connections:
if conn.id == connectionId: if conn.id == connectionId:
connection = conn connection = conn
break break
@ -87,9 +188,21 @@ async def connect_service(
# Initiate OAuth flow with state=connect # Initiate OAuth flow with state=connect
auth_url = None auth_url = None
if connection.authority == AuthAuthority.MSFT: if connection.authority == AuthAuthority.MSFT:
auth_url = f"/api/msft/login?state=connect&connectionId={connectionId}" # Use the same login endpoint with state=connect to ensure account selection
# Include current user ID in state
state_data = {
"type": "connect",
"connectionId": connectionId,
"userId": currentUser.id # Add current user ID
}
auth_url = f"/api/msft/login?state={json.dumps(state_data)}"
elif connection.authority == AuthAuthority.GOOGLE: elif connection.authority == AuthAuthority.GOOGLE:
auth_url = f"/api/google/login?state=connect&connectionId={connectionId}" state_data = {
"type": "connect",
"connectionId": connectionId,
"userId": currentUser.id # Add current user ID
}
auth_url = f"/api/google/login?state={json.dumps(state_data)}"
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -124,7 +237,8 @@ async def disconnect_service(
# Admins can disconnect any connection # Admins can disconnect any connection
users = interface.getAllUsers() users = interface.getAllUsers()
for user in users: for user in users:
for conn in user.connections: connections = interface.getUserConnections(user.id)
for conn in connections:
if conn.id == connectionId: if conn.id == connectionId:
connection = conn connection = conn
break break
@ -132,7 +246,8 @@ async def disconnect_service(
break break
else: else:
# Regular users can only disconnect their own connections # Regular users can only disconnect their own connections
for conn in currentUser.connections: connections = interface.getUserConnections(currentUser.id)
for conn in connections:
if conn.id == connectionId: if conn.id == connectionId:
connection = conn connection = conn
break break
@ -147,10 +262,8 @@ async def disconnect_service(
connection.status = ConnectionStatus.INACTIVE connection.status = ConnectionStatus.INACTIVE
connection.lastChecked = datetime.now() connection.lastChecked = datetime.now()
# Update user record # Update connection record
interface.db.recordModify("users", connection.userId, { interface.db.recordModify("connections", connectionId, connection.to_dict())
"connections": [c.to_dict() for c in currentUser.connections]
})
return {"message": "Service disconnected successfully"} return {"message": "Service disconnected successfully"}
@ -161,4 +274,56 @@ async def disconnect_service(
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to disconnect service: {str(e)}" detail=f"Failed to disconnect service: {str(e)}"
)
@router.delete("/{connectionId}")
@limiter.limit("10/minute")
async def delete_connection(
request: Request,
connectionId: str = Path(..., description="The ID of the connection to delete"),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a connection"""
try:
interface = getInterface(currentUser)
# Find the connection
connection = None
if currentUser.privilege in ['admin', 'sysadmin']:
# Admins can delete any connection
users = interface.getAllUsers()
for user in users:
connections = interface.getUserConnections(user.id)
for conn in connections:
if conn.id == connectionId:
connection = conn
break
if connection:
break
else:
# Regular users can only delete their own connections
connections = interface.getUserConnections(currentUser.id)
for conn in connections:
if conn.id == connectionId:
connection = conn
break
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Connection not found"
)
# Remove the connection - only need connectionId since permissions are verified
interface.removeUserConnection(connectionId)
return {"message": "Connection deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting connection: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete connection: {str(e)}"
) )

View file

@ -68,14 +68,24 @@ async def login(
scopes=SCOPES scopes=SCOPES
) )
# Generate auth URL with state - use state as is if it's already JSON, otherwise create new state
try:
# Try to parse state as JSON to check if it's already encoded
json.loads(state)
state_param = state # Use state as is if it's valid JSON
except json.JSONDecodeError:
# If not JSON, create new state object
state_param = json.dumps({
"type": state,
"connectionId": connectionId
})
# Generate auth URL with state # Generate auth URL with state
auth_url, _ = flow.authorization_url( auth_url, _ = flow.authorization_url(
access_type="offline", access_type="offline",
include_granted_scopes="true", include_granted_scopes="true",
state=json.dumps({ state=state_param,
"type": state, prompt="select_account" # Force account selection screen
"connectionId": connectionId
})
) )
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
@ -95,6 +105,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
state_data = json.loads(state) state_data = json.loads(state)
state_type = state_data.get("type", "login") state_type = state_data.get("type", "login")
connection_id = state_data.get("connectionId") connection_id = state_data.get("connectionId")
user_id = state_data.get("userId") # Get user ID from state
logger.info(f"Processing Google auth callback: state_type={state_type}, connection_id={connection_id}, user_id={user_id}")
# Create OAuth flow # Create OAuth flow
flow = Flow.from_client_config( flow = Flow.from_client_config(
@ -137,12 +150,13 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
# Create token # Create token
token = Token( token = Token(
userId=user.id, userId=user.id, # Use local user's ID
authority=AuthAuthority.GOOGLE, authority=AuthAuthority.GOOGLE,
tokenAccess=credentials.token, tokenAccess=credentials.token,
tokenRefresh=credentials.refresh_token, tokenRefresh=credentials.refresh_token,
tokenType=credentials.token_type, tokenType=credentials.token_type,
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None expiresAt=credentials.expiry.timestamp() if credentials.expiry else None,
createdAt=datetime.now()
) )
# Save token # Save token
@ -171,69 +185,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
) )
else: else:
# Handle connection flow # Handle connection flow
if not connection_id: if not connection_id or not user_id:
logger.error("Connection ID or User ID is missing in connection flow")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Connection ID is required for connection flow" detail="Connection ID and User ID are required for connection flow"
) )
# Get current user from session # Get user directly by ID
current_user = await getCurrentUser(request) rootInterface = getRootInterface()
if not current_user: user = rootInterface.getUser(user_id)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, if not user:
detail="User not authenticated" logger.error(f"User {user_id} not found in database")
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_error',
error: 'User not found in database'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=404
) )
# Find and update connection # Get the connection from the connections table
interface = getInterface(current_user) interface = getInterface(user)
connections = interface.getUserConnections(user_id)
connection = None connection = None
for conn in current_user.connections: for conn in connections:
if conn.id == connection_id: if conn.id == connection_id:
connection = conn connection = conn
logger.info(f"Found existing connection for user {user.username}")
break break
if not connection: try:
raise HTTPException( if not connection:
status_code=status.HTTP_404_NOT_FOUND, logger.error(f"Connection {connection_id} not found in user's connections")
detail="Connection not found" return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_error',
error: 'Connection not found in user\'s connections'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=404
)
logger.info(f"Updating connection {connection_id} for user {user.username}")
# Update connection with external service details
connection.status = ConnectionStatus.ACTIVE
connection.lastChecked = datetime.now()
connection.expiresAt = credentials.expiry if credentials.expiry else None
connection.externalId = user_info.get("id")
connection.externalUsername = user_info.get("email")
connection.externalEmail = user_info.get("email")
# Update connection record directly
rootInterface.db.recordModify("connections", connection_id, connection.to_dict())
# Save token
token = Token(
userId=user.id, # Use local user's ID
authority=AuthAuthority.GOOGLE,
tokenAccess=credentials.token,
tokenRefresh=credentials.refresh_token,
tokenType=credentials.token_type,
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None,
createdAt=datetime.now()
)
interface.saveToken(token)
# Return success page with connection data
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_success',
connection: {{
id: '{connection.id}',
status: 'connected',
type: 'google',
lastChecked: '{datetime.now().isoformat()}',
expiresAt: '{credentials.expiry.isoformat() if credentials.expiry else None}'
}}
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
"""
)
except Exception as e:
logger.error(f"Error updating connection: {str(e)}", exc_info=True)
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_error',
error: 'Failed to update connection: {str(e)}'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=500
) )
# Update connection
connection.status = ConnectionStatus.ACTIVE
connection.lastChecked = datetime.now()
connection.expiresAt = credentials.expiry if credentials.expiry else None
# Update user record
interface.db.recordModify("users", current_user.id, {
"connections": [c.to_dict() for c in current_user.connections]
})
# Return success page
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_success',
connectionId: {json.dumps(connection_id)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
except Exception as e: except Exception as e:
logger.error(f"Error in auth callback: {str(e)}") logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
raise HTTPException( return HTMLResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=f"""
detail=f"Authentication failed: {str(e)}" <html>
<head><title>Authentication Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'google_connection_error',
error: 'Authentication failed: {str(e)}'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=500
) )
@router.get("/me", response_model=User) @router.get("/me", response_model=User)

View file

@ -9,11 +9,12 @@ import json
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
import msal import msal
import httpx
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface from modules.interfaces.serviceAppClass import getInterface, getRootInterface
from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
from modules.security.auth import getCurrentUser, limiter from modules.security.auth import getCurrentUser, limiter, createAccessToken
from modules.shared.attributeUtils import ModelMixin from modules.shared.attributeUtils import ModelMixin
# Configure logger # Configure logger
@ -56,14 +57,23 @@ async def login(
client_credential=CLIENT_SECRET client_credential=CLIENT_SECRET
) )
# Generate auth URL with state # Generate auth URL with state - use state as is if it's already JSON, otherwise create new state
auth_url = msal_app.get_authorization_request_url( try:
scopes=SCOPES, # Try to parse state as JSON to check if it's already encoded
redirect_uri=REDIRECT_URI, json.loads(state)
state=json.dumps({ state_param = state # Use state as is if it's valid JSON
except json.JSONDecodeError:
# If not JSON, create new state object
state_param = json.dumps({
"type": state, "type": state,
"connectionId": connectionId "connectionId": connectionId
}) })
auth_url = msal_app.get_authorization_request_url(
scopes=SCOPES,
redirect_uri=REDIRECT_URI,
state=state_param,
prompt="select_account" # Force account selection screen
) )
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
@ -83,6 +93,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
state_data = json.loads(state) state_data = json.loads(state)
state_type = state_data.get("type", "login") state_type = state_data.get("type", "login")
connection_id = state_data.get("connectionId") connection_id = state_data.get("connectionId")
user_id = state_data.get("userId") # Get user ID from state
logger.info(f"Processing Microsoft auth callback: state_type={state_type}, connection_id={connection_id}, user_id={user_id}")
# Create MSAL app # Create MSAL app
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
@ -99,49 +112,96 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
) )
if "error" in token_response: if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return HTMLResponse( return HTMLResponse(
content="<html><body><h1>Authentication Failed</h1><p>Could not acquire token.</p></body></html>", content="<html><body><h1>Authentication Failed</h1><p>Could not acquire token.</p></body></html>",
status_code=400 status_code=400
) )
# Get user info from Microsoft # Get user info using the access token
user_info = msal_app.acquire_token_for_client(scopes=["User.Read"]) headers = {
if "error" in user_info: 'Authorization': f"Bearer {token_response['access_token']}",
raise HTTPException( 'Content-Type': 'application/json'
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, }
detail="Failed to get user info from Microsoft" async with httpx.AsyncClient() as client:
user_info_response = await client.get(
"https://graph.microsoft.com/v1.0/me",
headers=headers
) )
if user_info_response.status_code != 200:
logger.error(f"Failed to get user info: {user_info_response.text}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user info from Microsoft"
)
user_info = user_info_response.json()
logger.info(f"Got user info from Microsoft: {user_info.get('userPrincipalName')}")
if state_type == "login": if state_type == "login":
# Handle login flow # Handle login flow
rootInterface = getRootInterface() rootInterface = getRootInterface()
user = rootInterface.getUserByUsername(user_info.get("preferred_username")) user = rootInterface.getUserByUsername(user_info.get("userPrincipalName"))
if not user: if not user:
logger.info(f"Creating new user for {user_info.get('userPrincipalName')}")
# Create new user if doesn't exist # Create new user if doesn't exist
user = rootInterface.createUser( user = rootInterface.createUser(
username=user_info.get("preferred_username"), username=user_info.get("userPrincipalName"),
email=user_info.get("email"), email=user_info.get("mail"),
fullName=user_info.get("name"), fullName=user_info.get("displayName"),
authenticationAuthority=AuthAuthority.MSFT, authenticationAuthority=AuthAuthority.MSFT,
externalId=user_info.get("id"), externalId=user_info.get("id"),
externalUsername=user_info.get("preferred_username"), externalUsername=user_info.get("userPrincipalName"),
externalEmail=user_info.get("email") externalEmail=user_info.get("mail")
) )
# Create token # Create token
token = Token( token = Token(
userId=user.id, userId=user.id, # Use local user's ID
authority=AuthAuthority.MSFT, authority=AuthAuthority.MSFT,
tokenAccess=token_response["access_token"], tokenAccess=token_response["access_token"],
tokenRefresh=token_response.get("refresh_token", ""), tokenRefresh=token_response.get("refresh_token", ""),
tokenType=token_response.get("token_type", "bearer"), tokenType=token_response.get("token_type", "bearer"),
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0) expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0),
createdAt=datetime.now()
) )
# Save token # Save token
appInterface = getInterface(user) appInterface = getInterface(user)
appInterface.saveToken(token) appInterface.saveToken(token)
# Create JWT token data
jwt_token_data = {
"sub": user.username,
"mandateId": str(user.mandateId),
"userId": str(user.id),
"authenticationAuthority": AuthAuthority.MSFT
}
# Create JWT access token
jwt_token, jwt_expires_at = createAccessToken(jwt_token_data)
# Create JWT token
jwt_token_obj = Token(
userId=user.id,
authority=AuthAuthority.MSFT,
tokenAccess=jwt_token,
tokenType="bearer",
expiresAt=jwt_expires_at.timestamp(),
createdAt=datetime.now()
)
# Save JWT token
appInterface.saveToken(jwt_token_obj)
# Convert token to dict and ensure all datetime fields are serialized
token_dict = jwt_token_obj.to_dict()
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()
elif isinstance(token_dict.get('expiresAt'), float):
token_dict['expiresAt'] = int(token_dict['expiresAt'])
# Return success page with token data # Return success page with token data
return HTMLResponse( return HTMLResponse(
@ -153,8 +213,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
if (window.opener) {{ if (window.opener) {{
window.opener.postMessage({{ window.opener.postMessage({{
type: 'msft_auth_success', type: 'msft_auth_success',
access_token: {json.dumps(token_response["access_token"])}, token_data: {json.dumps(token_dict)}
token_data: {json.dumps(token.to_dict())}
}}, '*'); }}, '*');
}} }}
setTimeout(() => window.close(), 1000); setTimeout(() => window.close(), 1000);
@ -165,69 +224,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
) )
else: else:
# Handle connection flow # Handle connection flow
if not connection_id: if not connection_id or not user_id:
logger.error("Connection ID or User ID is missing in connection flow")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Connection ID is required for connection flow" detail="Connection ID and User ID are required for connection flow"
) )
# Get current user from session # Get user directly by ID
current_user = await getCurrentUser(request) rootInterface = getRootInterface()
if not current_user: user = rootInterface.getUser(user_id)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, if not user:
detail="User not authenticated" logger.error(f"User {user_id} not found in database")
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_error',
error: 'User not found in database'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=404
) )
# Find and update connection # Get the connection from the connections table
interface = getInterface(current_user) interface = getInterface(user)
connections = interface.getUserConnections(user_id)
connection = None connection = None
for conn in current_user.connections: for conn in connections:
if conn.id == connection_id: if conn.id == connection_id:
connection = conn connection = conn
logger.info(f"Found existing connection for user {user.username}")
break break
if not connection: try:
raise HTTPException( if not connection:
status_code=status.HTTP_404_NOT_FOUND, logger.error(f"Connection {connection_id} not found in user's connections")
detail="Connection not found" return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_error',
error: 'Connection not found in user\'s connections'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=404
)
logger.info(f"Updating connection {connection_id} for user {user.username}")
# Update connection with external service details
connection.status = ConnectionStatus.ACTIVE
connection.lastChecked = datetime.now()
connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
connection.externalId = user_info.get("id")
connection.externalUsername = user_info.get("userPrincipalName")
connection.externalEmail = user_info.get("mail")
# Update connection record directly
rootInterface.db.recordModify("connections", connection_id, connection.to_dict())
# Save token
token = Token(
userId=user.id, # Use local user's ID
authority=AuthAuthority.MSFT,
tokenAccess=token_response["access_token"],
tokenRefresh=token_response.get("refresh_token", ""),
tokenType=token_response.get("token_type", "bearer"),
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0),
createdAt=datetime.now()
)
interface.saveToken(token)
# Return success page with connection data
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_success',
connection: {{
id: '{connection.id}',
status: 'connected',
type: 'msft',
lastChecked: '{datetime.now().isoformat()}',
expiresAt: '{(datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))).isoformat()}'
}}
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
"""
)
except Exception as e:
logger.error(f"Error updating connection: {str(e)}", exc_info=True)
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_error',
error: 'Failed to update connection: {str(e)}'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=500
) )
# Update connection
connection.status = ConnectionStatus.ACTIVE
connection.lastChecked = datetime.now()
connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
# Update user record
interface.db.recordModify("users", current_user.id, {
"connections": [c.to_dict() for c in current_user.connections]
})
# Return success page
return HTMLResponse(
content=f"""
<html>
<head><title>Connection Successful</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_success',
connectionId: {json.dumps(connection_id)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
</script>
</body>
</html>
"""
)
except Exception as e: except Exception as e:
logger.error(f"Error in auth callback: {str(e)}") logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
raise HTTPException( return HTMLResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=f"""
detail=f"Authentication failed: {str(e)}" <html>
<head><title>Authentication Failed</title></head>
<body>
<script>
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_connection_error',
error: 'Authentication failed: {str(e)}'
}}, '*');
// Wait for message to be sent before closing
setTimeout(() => window.close(), 1000);
}} else {{
window.close();
}}
</script>
</body>
</html>
""",
status_code=500
) )
@router.get("/me", response_model=User) @router.get("/me", response_model=User)

View file

@ -16,13 +16,29 @@ class ModelMixin:
""" """
Convert a Pydantic model to a dictionary. Convert a Pydantic model to a dictionary.
Handles both Pydantic v1 and v2. Handles both Pydantic v1 and v2.
Properly serializes datetime fields to ISO format strings.
Returns: Returns:
Dict[str, Any]: Dictionary representation of the model Dict[str, Any]: Dictionary representation of the model
""" """
# Get the raw dictionary
if hasattr(self, 'model_dump'): if hasattr(self, 'model_dump'):
return self.model_dump() # Pydantic v2 data = self.model_dump() # Pydantic v2
return self.dict() # Pydantic v1 else:
data = self.dict() # Pydantic v1
# Convert datetime fields to ISO format strings
for key, value in data.items():
if isinstance(value, datetime):
data[key] = value.isoformat()
elif isinstance(value, (int, float)) and key.lower().endswith(('at', 'date')):
# Handle timestamp fields
try:
data[key] = datetime.fromtimestamp(value).isoformat()
except (ValueError, TypeError):
pass
return data
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin': def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':

View file

@ -1,50 +1,18 @@
....................... TASKS ....................... TASKS
FIXES:
now to adapt users.js with following specifics:
- MandateId not editable, to use user's mandateId
- authenticationAuthority to be "local" authority, not editable
- Actions: Edit, Delete, Toggle "enabled"
- Connections: not to render
- language to select
- privilege to select
clean the module and remove legacy code and renderings, as now everything done in formGeneric
now to adapt mandates.js with following specifics:
- Actions: Edit, Delete, Toggle "enabled"
- language to select
clean the module and remove legacy code and renderings, as now everything done in formGeneric
now to adapt connections.js with following specifics:
- Actions: Delete, toggle Connect/Disconnect (based on status attribute's value)
- Adding a new connection has two custom buttons, no form: One to connect to a "google" account and one to connect to a "msft" account. api connectors are already available. New record is produced.
clean the module and remove legacy code and renderings, as now everything done in formGeneric
We need to adapt the workflow part.
UAC: @workflowManager.py only to contain the workflow steps. All implementations done with the self.service objects. this workflow steps as a draft:
- Users disabled nicht sichtbar! - init or load workflow (workflow id) --> chat workflow object
- get user input into new workflow message --> chat message
- generate Task (chat message, workflow object)
Workflow: all functions from workflow.py to check stepwise and deeply analysed:
- Liste der Prompts nicht aktualisiert nach Änderungen in FormGeneric - are they already implemented in serviceChatClass.py?
- if not to move to taskManager, documentManager, or agentManager
- diese seite gesucht: "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 ?
Test paths:
- Admin
- User
- MSFT-Google
- Alle Management items
- Workflow
- Connections on/off
- Mail 2 Connectors
Agents and Manager: Agents and Manager:
@ -56,6 +24,16 @@ Agents and Manager:
4. task manager to add a task based on agents result and feedback 4. task manager to add a task based on agents result and feedback
- document extraction to have error handling for big documents. if document too large, then to get content in pieces - depending on document type - document extraction to have error handling for big documents. if document too large, then to get content in pieces - depending on document type
Test paths:
- Admin
- User
- MSFT-Google
- Alle Management items
- Workflow
- Connections on/off
- Mail 2 Connectors
Walkthroughs: Walkthroughs:
- register - register
- login local - login local
@ -63,9 +41,7 @@ Walkthroughs:
- management pages - management pages
- workflow - workflow
Install a Test environment with same prod_env
- add CORS url names to prod_env
-
----------------------- OPEN ----------------------- OPEN

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB