commit
e1160e86cf
13 changed files with 722 additions and 393 deletions
9
app.py
9
app.py
|
|
@ -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"])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
|
||||||
42
static/31_email_preview.html
Normal file
42
static/31_email_preview.html
Normal 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>
|
||||||
|
|
||||||
6
static/32_email_template.json
Normal file
6
static/32_email_template.json
Normal 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>"
|
||||||
|
}
|
||||||
42
static/33_email_preview.html
Normal file
42
static/33_email_preview.html
Normal 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>
|
||||||
|
|
||||||
6
static/34_email_template.json
Normal file
6
static/34_email_template.json
Normal 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>"
|
||||||
|
}
|
||||||
42
static/35_email_preview.html
Normal file
42
static/35_email_preview.html
Normal 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>
|
||||||
|
|
||||||
6
static/36_email_template.json
Normal file
6
static/36_email_template.json
Normal 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
Loading…
Reference in a new issue