Merge pull request #1 from valueonag/dev-patrick

prod azure 1.0.12
This commit is contained in:
ValueOn AG 2025-05-07 13:07:54 +02:00 committed by GitHub
commit e1160e86cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 722 additions and 393 deletions

9
app.py
View file

@ -114,7 +114,14 @@ app.add_middleware(
baseDir = pathlib.Path(__file__).parent baseDir = pathlib.Path(__file__).parent
staticFolder = baseDir / "static" staticFolder = baseDir / "static"
os.makedirs(staticFolder, exist_ok=True) os.makedirs(staticFolder, exist_ok=True)
app.mount("/static", StaticFiles(directory=str(staticFolder)), name="static")
# Mount static files with proper configuration
app.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
# Add favicon route
@app.get("/favicon.ico")
async def favicon():
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
# General Elements # General Elements
@app.get("/", tags=["General"]) @app.get("/", tags=["General"])

View file

@ -3,7 +3,7 @@
# System Configuration # System Configuration
APP_ENV_TYPE = dev APP_ENV_TYPE = dev
APP_ENV_LABEL = Development Instance Patrick APP_ENV_LABEL = Development Instance Patrick
APP_API_URL = http://localhost:8080 APP_API_URL = http://localhost:8000
# Database Configuration System # Database Configuration System
DB_SYSTEM_HOST=D:/Temp/_powerondb DB_SYSTEM_HOST=D:/Temp/_powerondb

View file

@ -7,8 +7,8 @@ import logging
import json import json
import base64 import base64
import os import os
import msal
import requests import requests
import msal
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from modules.configuration import APP_CONFIG from modules.configuration import APP_CONFIG
@ -41,11 +41,8 @@ class AgentEmail(AgentBase):
self.authority = None self.authority = None
self.scopes = ["Mail.ReadWrite", "User.Read"] self.scopes = ["Mail.ReadWrite", "User.Read"]
# Token storage directory # API base URL for Microsoft authentication
self.token_dir = './token_storage' self.api_base_url = APP_CONFIG.get("APP_API_URL", "(no-url)")
if not os.path.exists(self.token_dir):
os.makedirs(self.token_dir)
logger.info(f"Created token storage directory: {self.token_dir}")
def setDependencies(self, mydom=None): def setDependencies(self, mydom=None):
"""Set external dependencies for the agent.""" """Set external dependencies for the agent."""
@ -377,125 +374,86 @@ class AgentEmail(AgentBase):
""" """
return html return html
def _getCurrentUserToken(self): def _getCurrentUserToken(self) -> tuple:
""" """
Get the current user's token from the token store. Get the current user's Microsoft token using the current user context.
Does not attempt to initiate authentication flow. Returns tuple of (user_info, access_token) or (None, None) if not authenticated.
Returns:
Tuple of (user info, access token) or (None, None) if no valid token
""" """
try: try:
# Check if we have any token files if not self.mydom:
if not os.path.exists(self.token_dir) or not os.listdir(self.token_dir): logger.error("No mydom interface available")
logger.warning("No token files found. User needs to authenticate with Microsoft.")
return None, None return None, None
# Find the most recently modified token file # Get token data from database
token_files = [os.path.join(self.token_dir, f) for f in os.listdir(self.token_dir) if f.endswith('.json')] token_data = self.mydom.getMsftToken()
if not token_files: if not token_data:
logger.info("No Microsoft token found for user")
return None, None return None, None
most_recent = max(token_files, key=os.path.getmtime) # Verify token is still valid
user_id = os.path.basename(most_recent).split('.')[0] if not self._verifyToken(token_data.get("access_token")):
logger.info("Token invalid, attempting refresh")
if not self._refreshToken(token_data):
logger.info("Token refresh failed")
return None, None
# Get updated token data after refresh
token_data = self.mydom.getMsftToken()
return token_data.get("user_info"), token_data.get("access_token")
# Load the token
token_data = self._loadTokenFromFile(user_id)
if not token_data or not token_data.get("access_token"):
logger.warning(f"No valid token data for user {user_id}")
return None, None
# Get user info from token
user_info = self._getUserInfoFromToken(token_data["access_token"])
if not user_info:
# Try to refresh the token
if self._refreshToken(user_id):
# Load the refreshed token
token_data = self._loadTokenFromFile(user_id)
if token_data and token_data.get("access_token"):
user_info = self._getUserInfoFromToken(token_data["access_token"])
if user_info:
return user_info, token_data["access_token"]
logger.warning(f"Could not get user info for user {user_id}")
return None, None
return user_info, token_data["access_token"]
except Exception as e: except Exception as e:
logger.error(f"Error getting current user token: {str(e)}") logger.error(f"Error getting current user token: {str(e)}")
return None, None return None, None
def _loadTokenFromFile(self, user_id): def _verifyToken(self, token: str) -> bool:
"""Load token data from a file""" """Verify the access token is valid"""
filename = os.path.join(self.token_dir, f"{user_id}.json")
if os.path.exists(filename):
try:
with open(filename, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Error loading token file: {str(e)}")
return None
return None
def _getUserInfoFromToken(self, access_token):
"""Get user information using the access token"""
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}
try: try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers) response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if response.status_code == 200: return response.status_code == 200
user_data = response.json()
return {
"name": user_data.get("displayName", ""),
"email": user_data.get("userPrincipalName", ""),
"id": user_data.get("id", "")
}
else:
logger.error(f"Error getting user info: {response.status_code} - {response.text}")
return None
except Exception as e: except Exception as e:
logger.error(f"Exception getting user info: {str(e)}") logger.error(f"Error verifying token: {str(e)}")
return None return False
def _refreshToken(self, user_id): def _refreshToken(self, token_data: Dict[str, Any]) -> bool:
"""Refresh the access token using the stored refresh token""" """Refresh the access token using the stored refresh token"""
token_data = self._loadTokenFromFile(user_id)
if not token_data or not token_data.get("refresh_token"):
logger.warning("No refresh token available")
return False
msal_app = msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
result = msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=self.scopes
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
# Update tokens in storage
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
# Save the updated token
filename = os.path.join(self.token_dir, f"{user_id}.json")
try: try:
with open(filename, 'w') as f: if not token_data or not token_data.get("refresh_token"):
json.dump(token_data, f) logger.warning("No refresh token available")
logger.info(f"Token saved for user: {user_id}") return False
msal_app = msal.ConfidentialClientApplication(
self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
result = msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=self.scopes
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
# Update token data
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
# Save updated token
self.mydom.saveMsftToken(token_data)
logger.info("Access token refreshed successfully")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error saving token file: {str(e)}") logger.error(f"Error refreshing token: {str(e)}")
return False return False
def _createDraftEmail(self, recipient, subject, body, attachments=None): def _createDraftEmail(self, recipient, subject, body, attachments=None):
@ -523,7 +481,7 @@ class AgentEmail(AgentBase):
def _createGraphDraftEmail(self, access_token, recipient, subject, body, attachments=None): def _createGraphDraftEmail(self, access_token, recipient, subject, body, attachments=None):
""" """
Create a draft email using Microsoft Graph API with fixed attachment handling. Create a draft email using Microsoft Graph API with fixed attachment handling.
Directly uses the document's data attribute for attachments. Uses the complete document data for attachments.
Args: Args:
access_token: Microsoft Graph access token access_token: Microsoft Graph access token
@ -561,56 +519,47 @@ class AgentEmail(AgentBase):
email_data['attachments'] = [] email_data['attachments'] = []
for attachment in attachments: for attachment in attachments:
# Get the document object
doc = attachment.get('document', {}) doc = attachment.get('document', {})
file_name = attachment.get('name', 'attachment.file') file_name = attachment.get('name', 'attachment.file')
logger.info(f"Processing attachment: {file_name}") logger.info(f"Processing attachment: {file_name}")
# Directly access the data attribute from the document # Get the document data
if 'data' in doc: file_content = doc.get('data')
file_content = doc['data'] if not file_content:
is_base64 = doc.get('base64Encoded', False) logger.warning(f"No data found for attachment: {file_name}")
continue
# Determine content type
content_type = "application/octet-stream" # Get content type from document
if 'mimeType' in doc: content_type = doc.get('contentType', 'application/octet-stream')
content_type = doc['mimeType'] is_base64 = doc.get('base64Encoded', False)
elif 'contentType' in doc:
content_type = doc['contentType'] # Handle base64 encoding if needed
if not is_base64:
# Check if we need to encode the content logger.info(f"Base64 encoding content for {file_name}")
if not is_base64: if isinstance(file_content, str):
logger.info(f"Base64 encoding content for {file_name}") try:
if isinstance(file_content, str): # Check if already valid base64
try: base64.b64decode(file_content)
# Check if already valid base64 logger.info("Content appears to be valid base64 already")
base64.b64decode(file_content) except:
logger.info("Content appears to be valid base64 already") # Not valid base64, encode it
except: logger.info("Encoding string content to base64")
# Not valid base64, encode it file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
logger.info("Encoding string content to base64") elif isinstance(file_content, bytes):
file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8') logger.info("Encoding bytes content to base64")
elif isinstance(file_content, bytes): file_content = base64.b64encode(file_content).decode('utf-8')
logger.info("Encoding bytes content to base64")
file_content = base64.b64encode(file_content).decode('utf-8') # Add attachment to email data
logger.info(f"Adding attachment: {file_name} ({content_type})")
# Add attachment to email data attachment_data = {
logger.info(f"Adding attachment: {file_name} ({content_type})") '@odata.type': '#microsoft.graph.fileAttachment',
attachment_data = { 'name': file_name,
'@odata.type': '#microsoft.graph.fileAttachment', 'contentType': content_type,
'name': file_name, 'contentBytes': file_content
'contentType': content_type, }
'contentBytes': file_content email_data['attachments'].append(attachment_data)
} logger.info(f"Successfully added attachment: {file_name}")
email_data['attachments'].append(attachment_data)
logger.info(f"Successfully added attachment: {file_name}")
else:
logger.warning(f"Document does not contain 'data' attribute: {file_name}")
# Try to find data in the fileId
if 'fileId' in doc:
logger.info(f"Found fileId: {doc['fileId']} - could implement fileId-based attachment lookup here")
# Future enhancement: implement file lookup by fileId
# Try to create draft using drafts folder endpoint (Option 1) # Try to create draft using drafts folder endpoint (Option 1)
try: try:
@ -648,7 +597,7 @@ class AgentEmail(AgentBase):
except Exception as e: except Exception as e:
logger.error(f"Exception creating draft email: {str(e)}", exc_info=True) logger.error(f"Exception creating draft email: {str(e)}", exc_info=True)
return None return None
# Factory function for the Email agent # Factory function for the Email agent
def getAgentEmail(): def getAgentEmail():

View file

@ -11,6 +11,7 @@ from typing import Dict, Any, List, Optional, Union
import importlib import importlib
import hashlib import hashlib
import json
from modules.mimeUtils import isTextMimeType, determineContentEncoding from modules.mimeUtils import isTextMimeType, determineContentEncoding
@ -1286,7 +1287,65 @@ class LucyDOMInterface:
except Exception as e: except Exception as e:
logger.error(f"Error loading workflow state: {str(e)}") logger.error(f"Error loading workflow state: {str(e)}")
return None return None
# Microsoft Login
def getMsftToken(self) -> Optional[Dict[str, Any]]:
"""Get Microsoft token data for the current user from database"""
try:
# Get token from database using current user's mandateId and userId
tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
})
if tokens and len(tokens) > 0:
token_data = json.loads(tokens[0]["token_data"])
logger.info(f"Retrieved Microsoft token for user {self.userId}")
return token_data
else:
logger.info(f"No Microsoft token found for user {self.userId}")
return None
except Exception as e:
logger.error(f"Error retrieving Microsoft token: {str(e)}")
return None
def saveMsftToken(self, token_data: Dict[str, Any]) -> bool:
"""Save Microsoft token data for the current user to database"""
try:
# Check if token already exists
tokens = self.db.getRecordset("msftTokens", recordFilter={
"mandateId": self.mandateId,
"userId": self.userId
})
if tokens and len(tokens) > 0:
# Update existing token
token_id = tokens[0]["id"]
updated_data = {
"token_data": json.dumps(token_data),
"updated_at": datetime.now().isoformat()
}
self.db.recordModify("msftTokens", token_id, updated_data)
logger.info(f"Updated Microsoft token for user {self.userId}")
else:
# Create new token
new_token = {
"mandateId": self.mandateId,
"userId": self.userId,
"token_data": json.dumps(token_data),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.db.recordCreate("msftTokens", new_token)
logger.info(f"Saved new Microsoft token for user {self.userId}")
return True
except Exception as e:
logger.error(f"Error saving Microsoft token: {str(e)}")
return False
# Singleton factory for LucyDOMInterface instances per context # Singleton factory for LucyDOMInterface instances per context
_lucydomInterfaces = {} _lucydomInterfaces = {}

View file

@ -78,6 +78,31 @@ class FileData(BaseModel):
base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded") base64Encoded: bool = Field(description="Flag indicating whether the data is base64 encoded")
class MsftToken(BaseModel):
"""Data model for Microsoft authentication tokens"""
id: int = Field(description="Unique ID of the token")
mandateId: int = Field(description="ID of the associated mandate")
userId: int = Field(description="ID of the user")
token_data: str = Field(description="JSON string containing the token data")
created_at: str = Field(description="Timestamp when the token was created")
updated_at: str = Field(description="Timestamp when the token was last updated")
label: Label = Field(
default=Label(default="Microsoft Token", translations={"en": "Microsoft Token", "fr": "Jeton Microsoft"}),
description="Label for the class"
)
# Labels for attributes
fieldLabels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandateId": Label(default="Mandate ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"userId": Label(default="User ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"token_data": Label(default="Token Data", translations={"en": "Token Data", "fr": "Données du jeton"}),
"created_at": Label(default="Created At", translations={"en": "Created At", "fr": "Créé le"}),
"updated_at": Label(default="Updated At", translations={"en": "Updated At", "fr": "Mis à jour le"})
}
# Workflow model classes # Workflow model classes
class DocumentContent(BaseModel): class DocumentContent(BaseModel):

View file

@ -1,12 +1,11 @@
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import msal import msal
import os
import logging import logging
import sys
import json import json
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
import secrets
from modules.auth import getCurrentActiveUser, getUserContext from modules.auth import getCurrentActiveUser, getUserContext
from modules.configuration import APP_CONFIG from modules.configuration import APP_CONFIG
@ -44,26 +43,67 @@ app_config = {
"redirect_uri": REDIRECT_URI "redirect_uri": REDIRECT_URI
} }
# Create a simple file-based token storage async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
TOKEN_DIR = './token_storage' """Save token data to database using LucyDOMInterface"""
if not os.path.exists(TOKEN_DIR): try:
os.makedirs(TOKEN_DIR) # Get current user context
logger.info(f"Created token storage directory: {TOKEN_DIR}") mandateId, userId = await getUserContext(currentUser)
if not mandateId or not userId:
logger.error("No user context available for token storage")
return False
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
mandateId=mandateId,
userId=userId
)
if not mydom:
logger.error("No LucyDOM interface available for token storage")
return False
# Save token to database
success = mydom.saveMsftToken(token_data)
if success:
logger.info("Token saved successfully to database")
return True
else:
logger.error("Failed to save token to database")
return False
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
return False
def save_token_to_file(user_id: str, token_data: Dict[str, Any]): async def load_token_from_file(currentUser: Dict[str, Any]):
"""Save token data to a file""" """Load token data from database using LucyDOMInterface"""
filename = os.path.join(TOKEN_DIR, f"{user_id}.json") try:
with open(filename, 'w') as f: # Get current user context
json.dump(token_data, f) mandateId, userId = await getUserContext(currentUser)
logger.info(f"Token saved for user: {user_id}") if not mandateId or not userId:
logger.error("No user context available for token retrieval")
def load_token_from_file(user_id: str) -> Optional[Dict[str, Any]]: return None
"""Load token data from a file"""
filename = os.path.join(TOKEN_DIR, f"{user_id}.json") # Get LucyDOM interface for current user
if os.path.exists(filename): mydom = getLucydomInterface(
with open(filename, 'r') as f: mandateId=mandateId,
return json.load(f) userId=userId
return None )
if not mydom:
logger.error("No LucyDOM interface available for token retrieval")
return None
# Get token from database
token_data = mydom.getMsftToken()
if token_data:
logger.info("Token loaded successfully from database")
return token_data
else:
logger.info("No token found in database")
return None
except Exception as e:
logger.error(f"Error loading token: {str(e)}")
return None
def get_user_info_from_token(access_token: str) -> Optional[Dict[str, Any]]: def get_user_info_from_token(access_token: str) -> Optional[Dict[str, Any]]:
"""Get user information using the access token""" """Get user information using the access token"""
@ -111,9 +151,9 @@ def verify_token(token: str) -> bool:
logger.error(f"Exception verifying token: {str(e)}") logger.error(f"Exception verifying token: {str(e)}")
return False return False
def refresh_token(user_id: str) -> bool: async def refresh_token(user_id: str, currentUser: Dict[str, Any]) -> bool:
"""Refresh the access token using the stored refresh token""" """Refresh the access token using the stored refresh token"""
token_data = load_token_from_file(user_id) token_data = await load_token_from_file(currentUser)
if not token_data or not token_data.get("refresh_token"): if not token_data or not token_data.get("refresh_token"):
logger.warning("No refresh token available") logger.warning("No refresh token available")
return False return False
@ -138,45 +178,13 @@ def refresh_token(user_id: str) -> bool:
if "refresh_token" in result: if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"] token_data["refresh_token"] = result["refresh_token"]
save_token_to_file(user_id, token_data) await save_token_to_file(token_data, currentUser)
logger.info("Access token refreshed successfully") logger.info("Access token refreshed successfully")
return True return True
def silent_login(user_id: str) -> bool:
"""Try to silently log in a user using their refresh token"""
token_data = load_token_from_file(user_id)
if not token_data or not token_data.get("refresh_token"):
logger.info(f"No refresh token found for user: {user_id}")
return False
# Try to refresh the token
msal_app = msal.ConfidentialClientApplication(
app_config["client_id"],
authority=app_config["authority"],
client_credential=app_config["client_credential"]
)
result = msal_app.acquire_token_by_refresh_token(
token_data["refresh_token"],
scopes=SCOPES
)
if "error" in result:
logger.error(f"Error refreshing token: {result.get('error')}")
return False
# Update tokens in storage
token_data["access_token"] = result["access_token"]
if "refresh_token" in result:
token_data["refresh_token"] = result["refresh_token"]
save_token_to_file(user_id, token_data)
return True
@router.get("/login") @router.get("/login")
async def login(): async def login():
# Modified implementation without requiring current user """Initiate Microsoft login for the current user"""
try: try:
# Create a confidential client application # Create a confidential client application
msal_app = msal.ConfidentialClientApplication( msal_app = msal.ConfidentialClientApplication(
@ -185,221 +193,358 @@ async def login():
client_credential=app_config["client_credential"] client_credential=app_config["client_credential"]
) )
# Build the auth URL # Build the auth URL with a random state
state = secrets.token_urlsafe(32)
auth_url = msal_app.get_authorization_request_url( auth_url = msal_app.get_authorization_request_url(
SCOPES, SCOPES,
state="anonymous-user", # Use a general state since we don't have user context state=state, # Use random state
redirect_uri=app_config["redirect_uri"] redirect_uri=app_config["redirect_uri"]
) )
logger.info(f"Redirecting to Microsoft login: {auth_url[:60]}...") logger.info(f"Redirecting to Microsoft login")
return RedirectResponse(auth_url) return RedirectResponse(auth_url)
except Exception as e: except Exception as e:
logger.error(f"Error initiating Microsoft login: {str(e)}") logger.error(f"Error initiating Microsoft login: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error initiating Microsoft login: {str(e)}" detail=f"Failed to initiate Microsoft login: {str(e)}"
) )
@router.get("/auth/callback") @router.get("/auth/callback")
async def auth_callback(request: Request, code: str = None, state: str = None): async def auth_callback(code: str, state: str, request: Request):
"""Handle callback from Microsoft login""" """Handle Microsoft OAuth callback"""
try: try:
# Log callback for debugging # Create MSAL app instance
logger.info("Received callback from Microsoft login") app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
if not code: client_credential=CLIENT_SECRET,
logger.error("No authorization code received in callback") authority=AUTHORITY
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": "No authorization code received"}
)
# Extract user and mandate info from state if available
user_id = None
mandate_id = None
if state and state != "anonymous-user":
try:
mandate_id, user_id = state.split(":")
logger.info(f"State contains mandate_id: {mandate_id}, user_id: {user_id}")
except ValueError:
logger.warning(f"Invalid state format: {state}")
# Generate a generic user ID if state is invalid
user_id = f"user_{datetime.now().strftime('%Y%m%d%H%M%S')}"
else:
# For anonymous authentication, create a generic user ID
logger.info("Anonymous authentication (no user context)")
user_id = f"user_{datetime.now().strftime('%Y%m%d%H%M%S')}"
# Create a confidential client application
msal_app = msal.ConfidentialClientApplication(
app_config["client_id"],
authority=app_config["authority"],
client_credential=app_config["client_credential"]
) )
# Get tokens using the authorization code # Exchange code for token
result = msal_app.acquire_token_by_authorization_code( token_response = app.acquire_token_by_authorization_code(
code, code=code,
scopes=SCOPES, scopes=SCOPES,
redirect_uri=app_config["redirect_uri"] redirect_uri=REDIRECT_URI
) )
if "error" in result: if "error" in token_response:
logger.error(f"Error acquiring token: {result.get('error')}") logger.error(f"Token acquisition failed: {token_response['error']}")
return JSONResponse( return HTMLResponse(
status_code=status.HTTP_400_BAD_REQUEST, content="""
content={"message": f"Error acquiring token: {result.get('error_description', result.get('error'))}"} <html>
<head>
<title>Authentication Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Authentication Failed</h1>
<p>Please try again.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
) )
# Store user information # Get user info from token
user_info = {} user_info = get_user_info_from_token(token_response["access_token"])
if "id_token_claims" in result: if not user_info:
user_info = { logger.error("Failed to get user info from token")
"name": result["id_token_claims"].get("name", ""), return HTMLResponse(
"email": result["id_token_claims"].get("preferred_username", ""), content="""
} <html>
<head>
# If we have user info from the token, use that for user_id <title>Authentication Failed</title>
token_user_id = result["id_token_claims"].get("oid") or result["id_token_claims"].get("sub") <style>
if token_user_id: body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
user_id = token_user_id .error { color: red; }
elif not user_id and user_info.get("email"): </style>
# Fall back to email-based ID if no other ID is available </head>
user_id = user_info.get("email", "user").replace("@", "_").replace(".", "_") <body>
<h1 class="error">Authentication Failed</h1>
# Save tokens to file <p>Could not retrieve user information.</p>
token_data = { <script>
"access_token": result["access_token"], setTimeout(() => window.close(), 3000);
"refresh_token": result.get("refresh_token", ""), </script>
"user_info": user_info, </body>
"timestamp": datetime.now().isoformat() </html>
} """,
status_code=400
# Ensure token directory exists )
if not os.path.exists(TOKEN_DIR):
os.makedirs(TOKEN_DIR) # Add user info to token data
token_response["user_info"] = user_info
# Save token to file
token_file = os.path.join(TOKEN_DIR, f"{user_id}.json") # Store tokens in session storage for the frontend to pick up
with open(token_file, 'w') as f: response = HTMLResponse(
json.dump(token_data, f) content=f"""
logger.info(f"User authenticated: {user_info.get('email', 'unknown')}")
# Create a success page
html_content = """
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <title>Authentication Successful</title>
<title>Authentication Successful</title> <style>
<style> body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }}
body { font-family: Arial, sans-serif; margin: 40px; text-align: center; } .success {{ color: green; }}
.success-container { max-width: 600px; margin: 0 auto; } </style>
h1 { color: #0078d4; } </head>
.success-icon { font-size: 72px; color: #107c10; margin: 20px 0; } <body>
.button { display: inline-block; background-color: #0078d4; color: white; <h1 class="success">Authentication Successful</h1>
padding: 10px 20px; text-decoration: none; border-radius: 4px; <p>Welcome, {user_info.get('name', 'User')}!</p>
font-weight: bold; margin-top: 20px; } <p>This window will close automatically.</p>
</style> <script>
</head> // Store token data in session storage
<body> sessionStorage.setItem('msft_token_data', JSON.stringify({json.dumps(token_response)}));
<div class="success-container">
<h1>Authentication Successful</h1> // Notify parent window of success
<div class="success-icon"></div> if (window.opener) {{
<p>You have successfully authenticated with Microsoft.</p> window.opener.postMessage({{
<p>You can now close this tab and return to the application.</p> type: 'msft_auth_success',
<p>Your email templates will now be able to create drafts in your mailbox.</p> user: {json.dumps(user_info)},
<a href="javascript:window.close()" class="button">Close Window</a> token_data: {json.dumps(token_response)}
</div> }}, '*');
<script> }}
// Attempt to notify the opener window that authentication is complete // Close window after 3 seconds
if (window.opener && !window.opener.closed) { setTimeout(() => window.close(), 3000);
try { </script>
window.opener.postMessage({ type: 'msft_auth_complete', success: true }, '*'); </body>
} catch (e) {
console.error('Error notifying opener:', e);
}
}
</script>
</body>
</html> </html>
""" """
)
return HTMLResponse(content=html_content)
else: return response
logger.warning("No id_token_claims found in result")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": "Failed to retrieve user information"}
)
except Exception as e: except Exception as e:
logger.error(f"Error in auth callback: {str(e)}", exc_info=True) logger.error(f"Authentication failed: {str(e)}")
return JSONResponse( return HTMLResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content="""
content={"message": f"Error in auth callback: {str(e)}"} <html>
<head>
<title>Authentication Failed</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.error { color: red; }
</style>
</head>
<body>
<h1 class="error">Authentication Failed</h1>
<p>An error occurred during authentication.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=500
) )
@router.get("/status") @router.get("/status")
async def auth_status( async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
msft_user_id: Optional[str] = Cookie(None),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Check Microsoft authentication status""" """Check Microsoft authentication status"""
try: try:
# Get user ID # Get current user context
if not msft_user_id: mandateId, userId = await getUserContext(currentUser)
mandateId, userId = await getUserContext(currentUser) if not mandateId or not userId:
user_id = str(userId) logger.info("No user context found")
else: return JSONResponse({
user_id = msft_user_id "authenticated": False,
"message": "Not authenticated with Microsoft"
})
# Check if we have a token for the current user
token_data = await load_token_from_file(currentUser)
# Check if user has a token
token_data = load_token_from_file(user_id)
if not token_data: if not token_data:
return JSONResponse( logger.info(f"No token data found for user {userId}")
content={"authenticated": False, "message": "Not authenticated with Microsoft"} return JSONResponse({
"authenticated": False,
"message": "Not authenticated with Microsoft"
})
# Verify token is still valid
if not verify_token(token_data["access_token"]):
logger.info("Token invalid, attempting refresh")
# Try to refresh the token
if not await refresh_token(userId, currentUser):
logger.info("Token refresh failed")
return JSONResponse({
"authenticated": False,
"message": "Token expired and refresh failed"
})
# Reload token data after refresh
token_data = await load_token_from_file(currentUser)
# Get user info from token data
user_info = token_data.get("user_info")
if not user_info:
logger.info("No user info found in token data")
return JSONResponse({
"authenticated": False,
"message": "No user information available"
})
logger.info(f"User {user_info.get('name')} is authenticated")
return JSONResponse({
"authenticated": True,
"user": user_info
})
except Exception as e:
logger.error(f"Error checking authentication status: {str(e)}")
return JSONResponse({
"authenticated": False,
"message": f"Error checking authentication status: {str(e)}"
})
@router.post("/logout")
async def logout(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Logout from Microsoft"""
try:
# Get current user context
mandateId, userId = await getUserContext(currentUser)
if not mandateId or not userId:
return JSONResponse({
"message": "Not authenticated with Microsoft"
})
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
mandateId=mandateId,
userId=userId
)
if not mydom:
return JSONResponse({
"message": "Not authenticated with Microsoft"
})
# Remove token from database
tokens = mydom.db.getRecordset("msftTokens", recordFilter={
"mandateId": mandateId,
"userId": userId
})
if tokens and len(tokens) > 0:
mydom.db.recordDelete("msftTokens", tokens[0]["id"])
logger.info(f"Removed Microsoft token for user {userId}")
return JSONResponse({
"message": "Successfully logged out from Microsoft"
})
except Exception as e:
logger.error(f"Error during logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Logout failed: {str(e)}"
)
@router.get("/token")
async def get_access_token(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get the current user's access token for Microsoft Graph API"""
try:
# Check if we have a token for the current user
token_data = await load_token_from_file(currentUser)
if not token_data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated with Microsoft"
) )
# Check if token is valid # Verify token is still valid
if not verify_token(token_data.get("access_token", "")): if not verify_token(token_data["access_token"]):
# Try to refresh token # Try to refresh the token
if refresh_token(user_id): if not await refresh_token(currentUser["id"], currentUser):
token_data = load_token_from_file(user_id) raise HTTPException(
user_info = token_data.get("user_info", {}) status_code=status.HTTP_401_UNAUTHORIZED,
return JSONResponse( detail="Token expired and refresh failed"
content={
"authenticated": True,
"message": "Token refreshed successfully",
"user": user_info
}
)
else:
return JSONResponse(
content={
"authenticated": False,
"message": "Token expired and couldn't be refreshed"
}
) )
# Reload token data after refresh
token_data = await load_token_from_file(currentUser)
return JSONResponse({
"access_token": token_data["access_token"]
})
# Token is valid, return user info
user_info = token_data.get("user_info", {})
return JSONResponse(
content={
"authenticated": True,
"message": "Authenticated with Microsoft",
"user": user_info
}
)
except Exception as e: except Exception as e:
logger.error(f"Error checking auth status: {str(e)}") logger.error(f"Error getting access token: {str(e)}")
return JSONResponse( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"message": f"Error checking auth status: {str(e)}"} detail=f"Error getting access token: {str(e)}"
) )
@router.post("/save-token")
async def save_token(token_data: Dict[str, Any], currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Save Microsoft token data from frontend"""
try:
# Save token to database
success = await save_token_to_file(token_data, currentUser)
if success:
return JSONResponse({
"success": True,
"message": "Token saved successfully"
})
else:
return JSONResponse({
"success": False,
"message": "Failed to save token"
})
except Exception as e:
logger.error(f"Error saving token: {str(e)}")
return JSONResponse({
"success": False,
"message": f"Error saving token: {str(e)}"
})
async def generateFinalMessage(self, objUserResponse: str, objFinalDocuments: List[str], objResults: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate the final message for the workflow"""
try:
# Get list of delivered documents
matchingDocuments = []
for result in objResults:
if "documents" in result:
for doc in result["documents"]:
if doc.get("label") in objFinalDocuments:
matchingDocuments.append(doc.get("label"))
# Use the mydom for language-aware AI calls
finalPrompt = await self.mydom.callAi([
{"role": "system", "content": "You are a project manager, who delivers results to a user."},
{"role": "user", "content": f"""
Give a brief summary of what has been accomplished, referencing the initial request (objUserResponse). List only the files that have been successfully delivered (filesDelivered). Keep the message concise and professional.
Here the data:
objUserResponse = {self.parseJson2text(objUserResponse)}
filesDelivered = {self.parseJson2text(matchingDocuments)}
"""
}
], produceUserAnswer=True)
# Create basic message structure with proper fields
logger.debug(f"FINAL PROMPT = {self.parseJson2text(finalPrompt)}.")
finalMessage = {
"role": "assistant",
"agentName": "Project Manager",
"content": finalPrompt,
"documents": [] # DO NOT include the results documents, already with agents
}
logger.debug(f"FINAL MESSAGE = {self.parseJson2text(finalMessage)}.")
return finalMessage
except Exception as e:
logger.error(f"Error generating final message: {str(e)}")
return {
"role": "assistant",
"agentName": "Project Manager",
"content": "I apologize, but there was an error generating the final message. Please check the logs for more details.",
"documents": []
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Preview: Verschiebung des Meetings auf Freitag</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
.email-content { padding: 20px; }
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
.field { margin-bottom: 10px; }
.field-label { font-weight: bold; color: #555; }
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h2>Email Template Preview</h2>
</div>
<div class="email-content">
<div class="field">
<div class="field-label">To:</div>
<div>peter.muster@domain.com</div>
</div>
<div class="field">
<div class="field-label">Subject:</div>
<div>Verschiebung des Meetings auf Freitag</div>
</div>
<div class="email-body">
<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.</p><p>Vielen Dank für Ihr Verständnis.</p><p>Mit freundlichen Grüßen,<br>[Ihr Name]</p>
</div>
</div>
<div class="email-footer">
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"recipient": "peter.muster@domain.com",
"subject": "Verschiebung des Meetings auf Freitag",
"plainBody": "Sehr geehrter Herr Muster,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
"htmlBody": "<p>Sehr geehrter Herr Muster,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.</p><p>Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>[Ihr Name]</p>"
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Preview: Erneuter Versand: Verschiebung des Meetings auf Freitag</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
.email-content { padding: 20px; }
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
.field { margin-bottom: 10px; }
.field-label { font-weight: bold; color: #555; }
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h2>Email Template Preview</h2>
</div>
<div class="email-content">
<div class="field">
<div class="field-label">To:</div>
<div>patrick@motsch.ch</div>
</div>
<div class="field">
<div class="field-label">Subject:</div>
<div>Erneuter Versand: Verschiebung des Meetings auf Freitag</div>
</div>
<div class="email-body">
<p>Sehr geehrter Herr Motsch,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.</p><p>Vielen Dank für Ihr Verständnis.</p><p>Mit freundlichen Grüßen,<br>[Ihr Name]</p>
</div>
</div>
<div class="email-footer">
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"recipient": "patrick@motsch.ch",
"subject": "Erneuter Versand: Verschiebung des Meetings auf Freitag",
"plainBody": "Sehr geehrter Herr Motsch,\n\nich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.\n\nVielen Dank f\u00fcr Ihr Verst\u00e4ndnis.\n\nMit freundlichen Gr\u00fc\u00dfen,\n\n[Ihr Name]",
"htmlBody": "<p>Sehr geehrter Herr Motsch,</p><p>ich hoffe, es geht Ihnen gut. Ich schreibe Ihnen, um unser geplantes Meeting von 10 Uhr auf Freitag zu verschieben. Bitte lassen Sie mich wissen, ob Ihnen dieser neue Termin passt.</p><p>Vielen Dank f\u00fcr Ihr Verst\u00e4ndnis.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>[Ihr Name]</p>"
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Preview: Python Scripts Attached for Your Review</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; }
.email-container { max-width: 600px; margin: 20px auto; background-color: white; border: 1px solid #ddd; border-radius: 5px; overflow: hidden; }
.email-header { background-color: #f0f0f0; padding: 15px; border-bottom: 1px solid #ddd; }
.email-content { padding: 20px; }
.email-footer { background-color: #f0f0f0; padding: 15px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
.field { margin-bottom: 10px; }
.field-label { font-weight: bold; color: #555; }
.email-body { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h2>Email Template Preview</h2>
</div>
<div class="email-content">
<div class="field">
<div class="field-label">To:</div>
<div>example@domain.com</div>
</div>
<div class="field">
<div class="field-label">Subject:</div>
<div>Python Scripts Attached for Your Review</div>
</div>
<div class="email-body">
<html><body><p>Sehr geehrter Empfänger,</p><p>im Anhang finden Sie die beiden Python-Skripte, die Sie angefordert haben. Bitte zögern Sie nicht, sich bei Fragen oder weiteren Anliegen an mich zu wenden.</p><p>Mit freundlichen Grüßen,<br>Ihr Name</p></body></html>
</div>
</div>
<div class="email-footer">
<p>This is a preview of the email template. The actual email may appear differently in various email clients.</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"recipient": "example@domain.com",
"subject": "Python Scripts Attached for Your Review",
"plainBody": "Sehr geehrter Empf\u00e4nger,\n\nim Anhang finden Sie die beiden Python-Skripte, die Sie angefordert haben. Bitte z\u00f6gern Sie nicht, sich bei Fragen oder weiteren Anliegen an mich zu wenden.\n\nMit freundlichen Gr\u00fc\u00dfen,\nIhr Name",
"htmlBody": "<html><body><p>Sehr geehrter Empf\u00e4nger,</p><p>im Anhang finden Sie die beiden Python-Skripte, die Sie angefordert haben. Bitte z\u00f6gern Sie nicht, sich bei Fragen oder weiteren Anliegen an mich zu wenden.</p><p>Mit freundlichen Gr\u00fc\u00dfen,<br>Ihr Name</p></body></html>"
}

File diff suppressed because one or more lines are too long