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")
)
# 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
handlers = []
@ -37,6 +44,7 @@ def initLogging():
if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True):
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(consoleFormatter)
consoleHandler.addFilter(ChromeDevToolsFilter())
handlers.append(consoleHandler)
# Add file handler if enabled
@ -62,6 +70,7 @@ def initLogging():
backupCount=backupCount
)
fileHandler.setFormatter(fileFormatter)
fileHandler.addFilter(ChromeDevToolsFilter())
handlers.append(fileHandler)
# 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']})")
# Add metadata
currentTime = datetime.now().isoformat()
currentTime = datetime.now()
if "_createdAt" not in record:
record["_createdAt"] = currentTime
record["_createdAt"] = currentTime.isoformat()
record["_createdBy"] = self.userId
record["_modifiedAt"] = currentTime
record["_modifiedAt"] = currentTime.isoformat()
record["_modifiedBy"] = self.userId
# Save the record file

View file

@ -59,6 +59,18 @@ class AppAccess:
else:
# Regular users only see themselves
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
elif self.privilege == UserPrivilege.SYSADMIN:
filtered_records = recordset
@ -93,6 +105,22 @@ class AppAccess:
else:
record["_hideEdit"] = record.get("id") != self.userId
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":
# Only show sessions for the current user or if admin
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
@ -145,6 +173,15 @@ class AppAccess:
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
if self.privilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId:
return True

View file

@ -3,7 +3,7 @@ Interface to the Gateway system.
Manages users and mandates for authentication.
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, UTC
import os
import logging
from typing import Dict, Any, List, Optional, Union
@ -249,44 +249,109 @@ class GatewayInterface:
def getUser(self, userId: str) -> Optional[User]:
"""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:
# 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)
if not user:
raise ValueError(f"User {userId} not found")
# 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}")
raise ValueError(f"User not found: {userId}")
# Create new connection
# Create new connection with all required fields
connection = UserConnection(
id=str(uuid.uuid4()),
userId=userId,
authority=authority,
externalId=externalId,
externalUsername=externalUsername,
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
user.connections.append(connection)
# Update user record
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
# Save to connections table
self.db.recordCreate("connections", connection.to_dict())
return connection
@ -294,19 +359,19 @@ class GatewayInterface:
logger.error(f"Error adding user connection: {str(e)}")
raise ValueError(f"Failed to add user connection: {str(e)}")
def removeUserConnection(self, userId: str, connectionId: str) -> None:
"""Remove a connection to an external service for a user"""
def removeUserConnection(self, connectionId: str) -> None:
"""Remove a connection to an external service"""
try:
# Get user
user = self.getUser(userId)
if not user:
raise ValueError(f"User {userId} not found")
# Find and remove connection
user.connections = [c for c in user.connections if c.id != connectionId]
# Get connection
connections = self.db.getRecordset("connections", recordFilter={
"id": connectionId
})
# Update user record
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
if not connections:
raise ValueError(f"Connection {connectionId} not found")
# Delete connection
self.db.recordDelete("connections", connectionId)
except Exception as 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}")
# Delete user connections
user = self.getUser(userId)
if user and user.connections:
for conn in user.connections:
self.removeUserConnection(userId, conn.id)
logger.debug(f"Deleted connection {conn.id} for user {userId}")
connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
for conn in connections:
self.db.recordDelete("connections", conn["id"])
logger.debug(f"Deleted connection {conn['id']} for user {userId}")
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):
"""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")
userId: str = Field(description="ID of the user this connection belongs to")
authority: AuthAuthority = Field(description="Authentication authority")
externalId: str = Field(description="User ID in the external system")
externalUsername: str = Field(description="Username in the external system")
@ -59,12 +60,28 @@ class UserConnection(BaseModel, ModelMixin):
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
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_model_labels(
"UserConnection",
{"en": "User Connection", "fr": "Connexion utilisateur"},
{
"id": {"en": "ID", "fr": "ID"},
"userId": {"en": "User ID", "fr": "ID utilisateur"},
"authority": {"en": "Authority", "fr": "Autorité"},
"externalId": {"en": "External ID", "fr": "ID 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")
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
# Register labels for User
register_model_labels(
@ -152,8 +168,7 @@ register_model_labels(
"enabled": {"en": "Enabled", "fr": "Activé"},
"privilege": {"en": "Privilege", "fr": "Privilège"},
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
"connections": {"en": "Connections", "fr": "Connexions"}
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}
}
)

View file

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

View file

@ -8,6 +8,7 @@ from typing import List, Dict, Any, Optional
from fastapi import status
from datetime import datetime
import logging
import json
from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus
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"""
try:
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']:
# Admins can see all connections
users = interface.getAllUsers()
connections = []
for user in users:
connections.extend(user.connections)
connections.extend(interface.getUserConnections(user.id))
return connections
else:
# Regular users can only see their own connections
return currentUser.connections
return interface.getUserConnections(currentUser.id)
except Exception as e:
logger.error(f"Error getting connections: {str(e)}")
raise HTTPException(
@ -48,6 +54,99 @@ async def get_connections(
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")
@limiter.limit("10/minute")
async def connect_service(
@ -65,7 +164,8 @@ async def connect_service(
# Admins can connect any connection
users = interface.getAllUsers()
for user in users:
for conn in user.connections:
connections = interface.getUserConnections(user.id)
for conn in connections:
if conn.id == connectionId:
connection = conn
break
@ -73,7 +173,8 @@ async def connect_service(
break
else:
# 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:
connection = conn
break
@ -87,9 +188,21 @@ async def connect_service(
# Initiate OAuth flow with state=connect
auth_url = None
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:
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:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -124,7 +237,8 @@ async def disconnect_service(
# Admins can disconnect any connection
users = interface.getAllUsers()
for user in users:
for conn in user.connections:
connections = interface.getUserConnections(user.id)
for conn in connections:
if conn.id == connectionId:
connection = conn
break
@ -132,7 +246,8 @@ async def disconnect_service(
break
else:
# 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:
connection = conn
break
@ -147,10 +262,8 @@ async def disconnect_service(
connection.status = ConnectionStatus.INACTIVE
connection.lastChecked = datetime.now()
# Update user record
interface.db.recordModify("users", connection.userId, {
"connections": [c.to_dict() for c in currentUser.connections]
})
# Update connection record
interface.db.recordModify("connections", connectionId, connection.to_dict())
return {"message": "Service disconnected successfully"}
@ -161,4 +274,56 @@ async def disconnect_service(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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
)
# 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
auth_url, _ = flow.authorization_url(
access_type="offline",
include_granted_scopes="true",
state=json.dumps({
"type": state,
"connectionId": connectionId
})
state=state_param,
prompt="select_account" # Force account selection screen
)
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_type = state_data.get("type", "login")
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
flow = Flow.from_client_config(
@ -137,12 +150,13 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
# Create token
token = Token(
userId=user.id,
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
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None,
createdAt=datetime.now()
)
# Save token
@ -171,69 +185,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
)
else:
# 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(
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
current_user = await getCurrentUser(request)
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not authenticated"
# Get user directly by ID
rootInterface = getRootInterface()
user = rootInterface.getUser(user_id)
if not user:
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
interface = getInterface(current_user)
# Get the connection from the connections table
interface = getInterface(user)
connections = interface.getUserConnections(user_id)
connection = None
for conn in current_user.connections:
for conn in connections:
if conn.id == connection_id:
connection = conn
logger.info(f"Found existing connection for user {user.username}")
break
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Connection not found"
try:
if not connection:
logger.error(f"Connection {connection_id} not found in user's connections")
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:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
return HTMLResponse(
content=f"""
<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)

View file

@ -9,11 +9,12 @@ import json
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
import msal
import httpx
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
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
# Configure logger
@ -56,14 +57,23 @@ async def login(
client_credential=CLIENT_SECRET
)
# Generate auth URL with state
auth_url = msal_app.get_authorization_request_url(
scopes=SCOPES,
redirect_uri=REDIRECT_URI,
state=json.dumps({
# 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
})
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)
@ -83,6 +93,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
state_data = json.loads(state)
state_type = state_data.get("type", "login")
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
msal_app = msal.ConfidentialClientApplication(
@ -99,49 +112,96 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
)
if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return HTMLResponse(
content="<html><body><h1>Authentication Failed</h1><p>Could not acquire token.</p></body></html>",
status_code=400
)
# Get user info from Microsoft
user_info = msal_app.acquire_token_for_client(scopes=["User.Read"])
if "error" in user_info:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user info from Microsoft"
# Get user info using the access token
headers = {
'Authorization': f"Bearer {token_response['access_token']}",
'Content-Type': 'application/json'
}
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":
# Handle login flow
rootInterface = getRootInterface()
user = rootInterface.getUserByUsername(user_info.get("preferred_username"))
user = rootInterface.getUserByUsername(user_info.get("userPrincipalName"))
if not user:
logger.info(f"Creating new user for {user_info.get('userPrincipalName')}")
# Create new user if doesn't exist
user = rootInterface.createUser(
username=user_info.get("preferred_username"),
email=user_info.get("email"),
fullName=user_info.get("name"),
username=user_info.get("userPrincipalName"),
email=user_info.get("mail"),
fullName=user_info.get("displayName"),
authenticationAuthority=AuthAuthority.MSFT,
externalId=user_info.get("id"),
externalUsername=user_info.get("preferred_username"),
externalEmail=user_info.get("email")
externalUsername=user_info.get("userPrincipalName"),
externalEmail=user_info.get("mail")
)
# Create token
token = Token(
userId=user.id,
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)
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0),
createdAt=datetime.now()
)
# Save token
appInterface = getInterface(user)
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 HTMLResponse(
@ -153,8 +213,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_auth_success',
access_token: {json.dumps(token_response["access_token"])},
token_data: {json.dumps(token.to_dict())}
token_data: {json.dumps(token_dict)}
}}, '*');
}}
setTimeout(() => window.close(), 1000);
@ -165,69 +224,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
)
else:
# 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(
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
current_user = await getCurrentUser(request)
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not authenticated"
# Get user directly by ID
rootInterface = getRootInterface()
user = rootInterface.getUser(user_id)
if not user:
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
interface = getInterface(current_user)
# Get the connection from the connections table
interface = getInterface(user)
connections = interface.getUserConnections(user_id)
connection = None
for conn in current_user.connections:
for conn in connections:
if conn.id == connection_id:
connection = conn
logger.info(f"Found existing connection for user {user.username}")
break
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Connection not found"
try:
if not connection:
logger.error(f"Connection {connection_id} not found in user's connections")
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:
logger.error(f"Error in auth callback: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Authentication failed: {str(e)}"
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
return HTMLResponse(
content=f"""
<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)

View file

@ -16,13 +16,29 @@ class ModelMixin:
"""
Convert a Pydantic model to a dictionary.
Handles both Pydantic v1 and v2.
Properly serializes datetime fields to ISO format strings.
Returns:
Dict[str, Any]: Dictionary representation of the model
"""
# Get the raw dictionary
if hasattr(self, 'model_dump'):
return self.model_dump() # Pydantic v2
return self.dict() # Pydantic v1
data = self.model_dump() # Pydantic v2
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
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':

View file

@ -1,50 +1,18 @@
....................... 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:
- Users disabled nicht sichtbar!
@workflowManager.py only to contain the workflow steps. All implementations done with the self.service objects. this workflow steps as a draft:
- 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:
- Liste der Prompts nicht aktualisiert nach Änderungen in FormGeneric
- 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
all functions from workflow.py to check stepwise and deeply analysed:
- are they already implemented in serviceChatClass.py?
- if not to move to taskManager, documentManager, or agentManager
Agents and Manager:
@ -56,6 +24,16 @@ Agents and Manager:
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
Test paths:
- Admin
- User
- MSFT-Google
- Alle Management items
- Workflow
- Connections on/off
- Mail 2 Connectors
Walkthroughs:
- register
- login local
@ -63,9 +41,7 @@ Walkthroughs:
- management pages
- workflow
Install a Test environment with same prod_env
- add CORS url names to prod_env
-
----------------------- OPEN

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB