prod azure 1.0.12

This commit is contained in:
ValueOn AG 2025-05-07 13:06:42 +02:00
parent e66e6772b6
commit 8fe1c75288
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
staticFolder = baseDir / "static"
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
@app.get("/", tags=["General"])

View file

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

View file

@ -7,8 +7,8 @@ import logging
import json
import base64
import os
import msal
import requests
import msal
from typing import Dict, Any, List, Optional
from modules.configuration import APP_CONFIG
@ -41,11 +41,8 @@ class AgentEmail(AgentBase):
self.authority = None
self.scopes = ["Mail.ReadWrite", "User.Read"]
# Token storage directory
self.token_dir = './token_storage'
if not os.path.exists(self.token_dir):
os.makedirs(self.token_dir)
logger.info(f"Created token storage directory: {self.token_dir}")
# API base URL for Microsoft authentication
self.api_base_url = APP_CONFIG.get("APP_API_URL", "(no-url)")
def setDependencies(self, mydom=None):
"""Set external dependencies for the agent."""
@ -377,125 +374,86 @@ class AgentEmail(AgentBase):
"""
return html
def _getCurrentUserToken(self):
def _getCurrentUserToken(self) -> tuple:
"""
Get the current user's token from the token store.
Does not attempt to initiate authentication flow.
Returns:
Tuple of (user info, access token) or (None, None) if no valid token
Get the current user's Microsoft token using the current user context.
Returns tuple of (user_info, access_token) or (None, None) if not authenticated.
"""
try:
# Check if we have any token files
if not os.path.exists(self.token_dir) or not os.listdir(self.token_dir):
logger.warning("No token files found. User needs to authenticate with Microsoft.")
if not self.mydom:
logger.error("No mydom interface available")
return None, None
# Find the most recently modified token file
token_files = [os.path.join(self.token_dir, f) for f in os.listdir(self.token_dir) if f.endswith('.json')]
if not token_files:
# Get token data from database
token_data = self.mydom.getMsftToken()
if not token_data:
logger.info("No Microsoft token found for user")
return None, None
most_recent = max(token_files, key=os.path.getmtime)
user_id = os.path.basename(most_recent).split('.')[0]
# Verify token is still valid
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:
logger.error(f"Error getting current user token: {str(e)}")
return None, None
def _loadTokenFromFile(self, user_id):
"""Load token data from a file"""
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'
}
def _verifyToken(self, token: str) -> bool:
"""Verify the access token is valid"""
try:
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers)
if 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
return response.status_code == 200
except Exception as e:
logger.error(f"Exception getting user info: {str(e)}")
return None
def _refreshToken(self, user_id):
logger.error(f"Error verifying token: {str(e)}")
return False
def _refreshToken(self, token_data: Dict[str, Any]) -> bool:
"""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:
with open(filename, 'w') as f:
json.dump(token_data, f)
logger.info(f"Token saved for user: {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 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
except Exception as e:
logger.error(f"Error saving token file: {str(e)}")
logger.error(f"Error refreshing token: {str(e)}")
return False
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):
"""
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:
access_token: Microsoft Graph access token
@ -561,56 +519,47 @@ class AgentEmail(AgentBase):
email_data['attachments'] = []
for attachment in attachments:
# Get the document object
doc = attachment.get('document', {})
file_name = attachment.get('name', 'attachment.file')
logger.info(f"Processing attachment: {file_name}")
# Directly access the data attribute from the document
if 'data' in doc:
file_content = doc['data']
is_base64 = doc.get('base64Encoded', False)
# Determine content type
content_type = "application/octet-stream"
if 'mimeType' in doc:
content_type = doc['mimeType']
elif 'contentType' in doc:
content_type = doc['contentType']
# Check if we need to encode the content
if not is_base64:
logger.info(f"Base64 encoding content for {file_name}")
if isinstance(file_content, str):
try:
# Check if already valid base64
base64.b64decode(file_content)
logger.info("Content appears to be valid base64 already")
except:
# Not valid base64, encode it
logger.info("Encoding string content to base64")
file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
elif isinstance(file_content, bytes):
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})")
attachment_data = {
'@odata.type': '#microsoft.graph.fileAttachment',
'name': file_name,
'contentType': content_type,
'contentBytes': file_content
}
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
# Get the document data
file_content = doc.get('data')
if not file_content:
logger.warning(f"No data found for attachment: {file_name}")
continue
# Get content type from document
content_type = doc.get('contentType', 'application/octet-stream')
is_base64 = doc.get('base64Encoded', False)
# Handle base64 encoding if needed
if not is_base64:
logger.info(f"Base64 encoding content for {file_name}")
if isinstance(file_content, str):
try:
# Check if already valid base64
base64.b64decode(file_content)
logger.info("Content appears to be valid base64 already")
except:
# Not valid base64, encode it
logger.info("Encoding string content to base64")
file_content = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
elif isinstance(file_content, bytes):
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})")
attachment_data = {
'@odata.type': '#microsoft.graph.fileAttachment',
'name': file_name,
'contentType': content_type,
'contentBytes': file_content
}
email_data['attachments'].append(attachment_data)
logger.info(f"Successfully added attachment: {file_name}")
# Try to create draft using drafts folder endpoint (Option 1)
try:
@ -648,7 +597,7 @@ class AgentEmail(AgentBase):
except Exception as e:
logger.error(f"Exception creating draft email: {str(e)}", exc_info=True)
return None
return None
# Factory function for the Email agent
def getAgentEmail():

View file

@ -11,6 +11,7 @@ from typing import Dict, Any, List, Optional, Union
import importlib
import hashlib
import json
from modules.mimeUtils import isTextMimeType, determineContentEncoding
@ -1286,7 +1287,65 @@ class LucyDOMInterface:
except Exception as e:
logger.error(f"Error loading workflow state: {str(e)}")
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
_lucydomInterfaces = {}

View file

@ -78,6 +78,31 @@ class FileData(BaseModel):
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
class DocumentContent(BaseModel):

View file

@ -1,12 +1,11 @@
from fastapi import APIRouter, HTTPException, Depends, Request, Response, status, Cookie
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import msal
import os
import logging
import sys
import json
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
import secrets
from modules.auth import getCurrentActiveUser, getUserContext
from modules.configuration import APP_CONFIG
@ -44,26 +43,67 @@ app_config = {
"redirect_uri": REDIRECT_URI
}
# Create a simple file-based token storage
TOKEN_DIR = './token_storage'
if not os.path.exists(TOKEN_DIR):
os.makedirs(TOKEN_DIR)
logger.info(f"Created token storage directory: {TOKEN_DIR}")
async def save_token_to_file(token_data, currentUser: Dict[str, Any]):
"""Save token data to database using LucyDOMInterface"""
try:
# Get current user context
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]):
"""Save token data to a file"""
filename = os.path.join(TOKEN_DIR, f"{user_id}.json")
with open(filename, 'w') as f:
json.dump(token_data, f)
logger.info(f"Token saved for user: {user_id}")
def load_token_from_file(user_id: str) -> Optional[Dict[str, Any]]:
"""Load token data from a file"""
filename = os.path.join(TOKEN_DIR, f"{user_id}.json")
if os.path.exists(filename):
with open(filename, 'r') as f:
return json.load(f)
return None
async def load_token_from_file(currentUser: Dict[str, Any]):
"""Load token data from database using LucyDOMInterface"""
try:
# Get current user context
mandateId, userId = await getUserContext(currentUser)
if not mandateId or not userId:
logger.error("No user context available for token retrieval")
return None
# Get LucyDOM interface for current user
mydom = getLucydomInterface(
mandateId=mandateId,
userId=userId
)
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]]:
"""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)}")
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"""
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"):
logger.warning("No refresh token available")
return False
@ -138,45 +178,13 @@ def refresh_token(user_id: str) -> bool:
if "refresh_token" in result:
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")
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")
async def login():
# Modified implementation without requiring current user
"""Initiate Microsoft login for the current user"""
try:
# Create a confidential client application
msal_app = msal.ConfidentialClientApplication(
@ -185,221 +193,358 @@ async def login():
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(
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"]
)
logger.info(f"Redirecting to Microsoft login: {auth_url[:60]}...")
logger.info(f"Redirecting to Microsoft login")
return RedirectResponse(auth_url)
except Exception as e:
logger.error(f"Error initiating Microsoft login: {str(e)}")
raise HTTPException(
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")
async def auth_callback(request: Request, code: str = None, state: str = None):
"""Handle callback from Microsoft login"""
async def auth_callback(code: str, state: str, request: Request):
"""Handle Microsoft OAuth callback"""
try:
# Log callback for debugging
logger.info("Received callback from Microsoft login")
if not code:
logger.error("No authorization code received in callback")
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"]
# Create MSAL app instance
app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=AUTHORITY
)
# Get tokens using the authorization code
result = msal_app.acquire_token_by_authorization_code(
code,
# Exchange code for token
token_response = app.acquire_token_by_authorization_code(
code=code,
scopes=SCOPES,
redirect_uri=app_config["redirect_uri"]
redirect_uri=REDIRECT_URI
)
if "error" in result:
logger.error(f"Error acquiring token: {result.get('error')}")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": f"Error acquiring token: {result.get('error_description', result.get('error'))}"}
if "error" in token_response:
logger.error(f"Token acquisition failed: {token_response['error']}")
return HTMLResponse(
content="""
<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
user_info = {}
if "id_token_claims" in result:
user_info = {
"name": result["id_token_claims"].get("name", ""),
"email": result["id_token_claims"].get("preferred_username", ""),
}
# If we have user info from the token, use that for user_id
token_user_id = result["id_token_claims"].get("oid") or result["id_token_claims"].get("sub")
if token_user_id:
user_id = token_user_id
elif not user_id and user_info.get("email"):
# Fall back to email-based ID if no other ID is available
user_id = user_info.get("email", "user").replace("@", "_").replace(".", "_")
# Save tokens to file
token_data = {
"access_token": result["access_token"],
"refresh_token": result.get("refresh_token", ""),
"user_info": user_info,
"timestamp": datetime.now().isoformat()
}
# Ensure token directory exists
if not os.path.exists(TOKEN_DIR):
os.makedirs(TOKEN_DIR)
# Save token to file
token_file = os.path.join(TOKEN_DIR, f"{user_id}.json")
with open(token_file, 'w') as f:
json.dump(token_data, f)
logger.info(f"User authenticated: {user_info.get('email', 'unknown')}")
# Create a success page
html_content = """
<!DOCTYPE html>
# Get user info from token
user_info = get_user_info_from_token(token_response["access_token"])
if not user_info:
logger.error("Failed to get user info from token")
return HTMLResponse(
content="""
<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>Could not retrieve user information.</p>
<script>
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
""",
status_code=400
)
# Add user info to token data
token_response["user_info"] = user_info
# Store tokens in session storage for the frontend to pick up
response = HTMLResponse(
content=f"""
<html>
<head>
<meta charset="UTF-8">
<title>Authentication Successful</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; text-align: center; }
.success-container { max-width: 600px; margin: 0 auto; }
h1 { color: #0078d4; }
.success-icon { font-size: 72px; color: #107c10; margin: 20px 0; }
.button { display: inline-block; background-color: #0078d4; color: white;
padding: 10px 20px; text-decoration: none; border-radius: 4px;
font-weight: bold; margin-top: 20px; }
</style>
</head>
<body>
<div class="success-container">
<h1>Authentication Successful</h1>
<div class="success-icon"></div>
<p>You have successfully authenticated with Microsoft.</p>
<p>You can now close this tab and return to the application.</p>
<p>Your email templates will now be able to create drafts in your mailbox.</p>
<a href="javascript:window.close()" class="button">Close Window</a>
</div>
<script>
// Attempt to notify the opener window that authentication is complete
if (window.opener && !window.opener.closed) {
try {
window.opener.postMessage({ type: 'msft_auth_complete', success: true }, '*');
} catch (e) {
console.error('Error notifying opener:', e);
}
}
</script>
</body>
<head>
<title>Authentication Successful</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }}
.success {{ color: green; }}
</style>
</head>
<body>
<h1 class="success">Authentication Successful</h1>
<p>Welcome, {user_info.get('name', 'User')}!</p>
<p>This window will close automatically.</p>
<script>
// Store token data in session storage
sessionStorage.setItem('msft_token_data', JSON.stringify({json.dumps(token_response)}));
// Notify parent window of success
if (window.opener) {{
window.opener.postMessage({{
type: 'msft_auth_success',
user: {json.dumps(user_info)},
token_data: {json.dumps(token_response)}
}}, '*');
}}
// Close window after 3 seconds
setTimeout(() => window.close(), 3000);
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)
)
else:
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"}
)
return response
except Exception as e:
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"message": f"Error in auth callback: {str(e)}"}
logger.error(f"Authentication failed: {str(e)}")
return HTMLResponse(
content="""
<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")
async def auth_status(
msft_user_id: Optional[str] = Cookie(None),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
async def auth_status(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Check Microsoft authentication status"""
try:
# Get user ID
if not msft_user_id:
mandateId, userId = await getUserContext(currentUser)
user_id = str(userId)
else:
user_id = msft_user_id
# Get current user context
mandateId, userId = await getUserContext(currentUser)
if not mandateId or not userId:
logger.info("No user context found")
return JSONResponse({
"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:
return JSONResponse(
content={"authenticated": False, "message": "Not authenticated with Microsoft"}
logger.info(f"No token data found for user {userId}")
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
if not verify_token(token_data.get("access_token", "")):
# Try to refresh token
if refresh_token(user_id):
token_data = load_token_from_file(user_id)
user_info = token_data.get("user_info", {})
return JSONResponse(
content={
"authenticated": True,
"message": "Token refreshed successfully",
"user": user_info
}
)
else:
return JSONResponse(
content={
"authenticated": False,
"message": "Token expired and couldn't be refreshed"
}
# Verify token is still valid
if not verify_token(token_data["access_token"]):
# Try to refresh the token
if not await refresh_token(currentUser["id"], currentUser):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired and refresh failed"
)
# 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:
logger.error(f"Error checking auth status: {str(e)}")
return JSONResponse(
logger.error(f"Error getting access token: {str(e)}")
raise HTTPException(
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