data management tested
This commit is contained in:
parent
096e4052f0
commit
d8954f95af
12 changed files with 835 additions and 235 deletions
9
app.py
9
app.py
|
|
@ -30,6 +30,13 @@ def initLogging():
|
|||
datefmt=APP_CONFIG.get("APP_LOGGING_DATE_FORMAT", "%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
# Add filter to exclude Chrome DevTools requests
|
||||
class ChromeDevToolsFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
return not (isinstance(record.msg, str) and
|
||||
('.well-known/appspecific/com.chrome.devtools.json' in record.msg or
|
||||
'Request: /index.html' in record.msg))
|
||||
|
||||
# Configure handlers based on config
|
||||
handlers = []
|
||||
|
||||
|
|
@ -37,6 +44,7 @@ def initLogging():
|
|||
if APP_CONFIG.get("APP_LOGGING_CONSOLE_ENABLED", True):
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(consoleFormatter)
|
||||
consoleHandler.addFilter(ChromeDevToolsFilter())
|
||||
handlers.append(consoleHandler)
|
||||
|
||||
# Add file handler if enabled
|
||||
|
|
@ -62,6 +70,7 @@ def initLogging():
|
|||
backupCount=backupCount
|
||||
)
|
||||
fileHandler.setFormatter(fileFormatter)
|
||||
fileHandler.addFilter(ChromeDevToolsFilter())
|
||||
handlers.append(fileHandler)
|
||||
|
||||
# Configure the root logger
|
||||
|
|
|
|||
|
|
@ -162,11 +162,11 @@ class DatabaseConnector:
|
|||
raise ValueError(f"Record ID mismatch: file name ID ({recordId}) does not match record ID ({record['id']})")
|
||||
|
||||
# Add metadata
|
||||
currentTime = datetime.now().isoformat()
|
||||
currentTime = datetime.now()
|
||||
if "_createdAt" not in record:
|
||||
record["_createdAt"] = currentTime
|
||||
record["_createdAt"] = currentTime.isoformat()
|
||||
record["_createdBy"] = self.userId
|
||||
record["_modifiedAt"] = currentTime
|
||||
record["_modifiedAt"] = currentTime.isoformat()
|
||||
record["_modifiedBy"] = self.userId
|
||||
|
||||
# Save the record file
|
||||
|
|
|
|||
|
|
@ -59,6 +59,18 @@ class AppAccess:
|
|||
else:
|
||||
# Regular users only see themselves
|
||||
filtered_records = [r for r in recordset if r.get("id") == self.userId]
|
||||
# Special handling for connections table
|
||||
elif table == "connections":
|
||||
if self.privilege == UserPrivilege.SYSADMIN:
|
||||
# SysAdmin sees all connections
|
||||
filtered_records = recordset
|
||||
elif self.privilege == UserPrivilege.ADMIN:
|
||||
# Admin sees connections for users in their mandate
|
||||
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
|
||||
filtered_records = [r for r in recordset if r.get("userId") in user_ids]
|
||||
else:
|
||||
# Regular users only see their own connections
|
||||
filtered_records = [r for r in recordset if r.get("userId") == self.userId]
|
||||
# System admins see all other records
|
||||
elif self.privilege == UserPrivilege.SYSADMIN:
|
||||
filtered_records = recordset
|
||||
|
|
@ -93,6 +105,22 @@ class AppAccess:
|
|||
else:
|
||||
record["_hideEdit"] = record.get("id") != self.userId
|
||||
record["_hideDelete"] = True # Regular users cannot delete users
|
||||
elif table == "connections":
|
||||
# Everyone can view connections they have access to
|
||||
record["_hideView"] = False
|
||||
# SysAdmin can edit/delete any connection
|
||||
if self.privilege == UserPrivilege.SYSADMIN:
|
||||
record["_hideEdit"] = False
|
||||
record["_hideDelete"] = False
|
||||
# Admin can edit/delete connections for users in their mandate
|
||||
elif self.privilege == UserPrivilege.ADMIN:
|
||||
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
|
||||
record["_hideEdit"] = record.get("userId") not in user_ids
|
||||
record["_hideDelete"] = record.get("userId") not in user_ids
|
||||
# Regular users can only edit/delete their own connections
|
||||
else:
|
||||
record["_hideEdit"] = record.get("userId") != self.userId
|
||||
record["_hideDelete"] = record.get("userId") != self.userId
|
||||
elif table == "sessions":
|
||||
# Only show sessions for the current user or if admin
|
||||
if self.privilege in [UserPrivilege.SYSADMIN, UserPrivilege.ADMIN]:
|
||||
|
|
@ -145,6 +173,15 @@ class AppAccess:
|
|||
|
||||
record = records[0]
|
||||
|
||||
# Special handling for connections
|
||||
if table == "connections":
|
||||
# Admin can modify connections for users in their mandate
|
||||
if self.privilege == UserPrivilege.ADMIN:
|
||||
user_ids = [u["id"] for u in self.db.getRecordset("users", recordFilter={"mandateId": self.mandateId})]
|
||||
return record.get("userId") in user_ids
|
||||
# Users can only modify their own connections
|
||||
return record.get("userId") == self.userId
|
||||
|
||||
# Admins can modify anything in their mandate
|
||||
if self.privilege == UserPrivilege.ADMIN and record.get("mandateId","-") == self.mandateId:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Interface to the Gateway system.
|
|||
Manages users and mandates for authentication.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, UTC
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
|
@ -249,44 +249,109 @@ class GatewayInterface:
|
|||
|
||||
def getUser(self, userId: str) -> Optional[User]:
|
||||
"""Returns a user by ID if user has access."""
|
||||
users = self.db.getRecordset("users", recordFilter={"id": userId})
|
||||
if not users:
|
||||
return None
|
||||
|
||||
filteredUsers = self._uam("users", users)
|
||||
if not filteredUsers:
|
||||
return None
|
||||
|
||||
return User.from_dict(filteredUsers[0])
|
||||
|
||||
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
|
||||
externalUsername: str, externalEmail: Optional[str] = None) -> UserConnection:
|
||||
"""Add a new connection to an external service for a user"""
|
||||
try:
|
||||
# Get user
|
||||
# Get all users
|
||||
users = self.db.getRecordset("users")
|
||||
if not users:
|
||||
return None
|
||||
|
||||
# Find user by ID
|
||||
for user_dict in users:
|
||||
if user_dict.get("id") == userId:
|
||||
# Apply access control
|
||||
filteredUsers = self._uam("users", [user_dict])
|
||||
if filteredUsers:
|
||||
return User.from_dict(filteredUsers[0])
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user by ID: {str(e)}")
|
||||
return None
|
||||
|
||||
def getUserConnections(self, userId: str) -> List[UserConnection]:
|
||||
"""Returns all connections for a user."""
|
||||
try:
|
||||
# Get connections for this user
|
||||
connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
|
||||
|
||||
# Convert to UserConnection objects
|
||||
result = []
|
||||
for conn_dict in connections:
|
||||
try:
|
||||
# Convert string dates to datetime objects
|
||||
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
|
||||
if field in conn_dict and conn_dict[field]:
|
||||
try:
|
||||
if isinstance(conn_dict[field], str):
|
||||
conn_dict[field] = datetime.fromisoformat(conn_dict[field].replace('Z', '+00:00'))
|
||||
except (ValueError, TypeError):
|
||||
conn_dict[field] = None
|
||||
|
||||
# Create UserConnection object
|
||||
connection = UserConnection(
|
||||
id=conn_dict["id"],
|
||||
userId=conn_dict["userId"],
|
||||
authority=conn_dict.get("authority"),
|
||||
externalId=conn_dict.get("externalId", ""),
|
||||
externalUsername=conn_dict.get("externalUsername", ""),
|
||||
externalEmail=conn_dict.get("externalEmail"),
|
||||
status=conn_dict.get("status", "pending"),
|
||||
connectedAt=conn_dict.get("connectedAt"),
|
||||
lastChecked=conn_dict.get("lastChecked"),
|
||||
expiresAt=conn_dict.get("expiresAt")
|
||||
)
|
||||
result.append(connection)
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting connection dict to object: {str(e)}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user connections: {str(e)}")
|
||||
return []
|
||||
|
||||
def addUserConnection(self, userId: str, authority: AuthAuthority, externalId: str,
|
||||
externalUsername: str, externalEmail: Optional[str] = None,
|
||||
status: ConnectionStatus = ConnectionStatus.PENDING) -> UserConnection:
|
||||
"""
|
||||
Adds a new connection for a user.
|
||||
|
||||
Args:
|
||||
userId: The ID of the user
|
||||
authority: The authentication authority (e.g., MSFT, GOOGLE)
|
||||
externalId: The external ID from the authority
|
||||
externalUsername: The username from the authority
|
||||
externalEmail: Optional email from the authority
|
||||
status: The connection status (defaults to PENDING)
|
||||
|
||||
Returns:
|
||||
The created UserConnection object
|
||||
"""
|
||||
try:
|
||||
# Get the user
|
||||
user = self.getUser(userId)
|
||||
if not user:
|
||||
raise ValueError(f"User {userId} not found")
|
||||
|
||||
# Check if connection already exists
|
||||
for conn in user.connections:
|
||||
if conn.authority == authority and conn.externalId == externalId:
|
||||
raise ValueError(f"Connection to {authority} already exists for user {userId}")
|
||||
raise ValueError(f"User not found: {userId}")
|
||||
|
||||
# Create new connection
|
||||
# Create new connection with all required fields
|
||||
connection = UserConnection(
|
||||
id=str(uuid.uuid4()),
|
||||
userId=userId,
|
||||
authority=authority,
|
||||
externalId=externalId,
|
||||
externalUsername=externalUsername,
|
||||
externalEmail=externalEmail,
|
||||
status=ConnectionStatus.ACTIVE
|
||||
status=status,
|
||||
connectedAt=datetime.now(UTC),
|
||||
lastChecked=datetime.now(UTC),
|
||||
expiresAt=None # Optional field, set to None by default
|
||||
)
|
||||
|
||||
# Add connection to user
|
||||
user.connections.append(connection)
|
||||
|
||||
# Update user record
|
||||
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
|
||||
# Save to connections table
|
||||
self.db.recordCreate("connections", connection.to_dict())
|
||||
|
||||
return connection
|
||||
|
||||
|
|
@ -294,19 +359,19 @@ class GatewayInterface:
|
|||
logger.error(f"Error adding user connection: {str(e)}")
|
||||
raise ValueError(f"Failed to add user connection: {str(e)}")
|
||||
|
||||
def removeUserConnection(self, userId: str, connectionId: str) -> None:
|
||||
"""Remove a connection to an external service for a user"""
|
||||
def removeUserConnection(self, connectionId: str) -> None:
|
||||
"""Remove a connection to an external service"""
|
||||
try:
|
||||
# Get user
|
||||
user = self.getUser(userId)
|
||||
if not user:
|
||||
raise ValueError(f"User {userId} not found")
|
||||
|
||||
# Find and remove connection
|
||||
user.connections = [c for c in user.connections if c.id != connectionId]
|
||||
# Get connection
|
||||
connections = self.db.getRecordset("connections", recordFilter={
|
||||
"id": connectionId
|
||||
})
|
||||
|
||||
# Update user record
|
||||
self.db.recordModify("users", userId, {"connections": [c.to_dict() for c in user.connections]})
|
||||
if not connections:
|
||||
raise ValueError(f"Connection {connectionId} not found")
|
||||
|
||||
# Delete connection
|
||||
self.db.recordDelete("connections", connectionId)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing user connection: {str(e)}")
|
||||
|
|
@ -468,11 +533,10 @@ class GatewayInterface:
|
|||
logger.debug(f"Deleted token {token['id']} for user {userId}")
|
||||
|
||||
# Delete user connections
|
||||
user = self.getUser(userId)
|
||||
if user and user.connections:
|
||||
for conn in user.connections:
|
||||
self.removeUserConnection(userId, conn.id)
|
||||
logger.debug(f"Deleted connection {conn.id} for user {userId}")
|
||||
connections = self.db.getRecordset("connections", recordFilter={"userId": userId})
|
||||
for conn in connections:
|
||||
self.db.recordDelete("connections", conn["id"])
|
||||
logger.debug(f"Deleted connection {conn['id']} for user {userId}")
|
||||
|
||||
logger.info(f"All referenced data for user {userId} has been deleted")
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ register_model_labels(
|
|||
class UserConnection(BaseModel, ModelMixin):
|
||||
"""Data model for a user's connection to an external service"""
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection")
|
||||
userId: str = Field(description="ID of the user this connection belongs to")
|
||||
authority: AuthAuthority = Field(description="Authentication authority")
|
||||
externalId: str = Field(description="User ID in the external system")
|
||||
externalUsername: str = Field(description="Username in the external system")
|
||||
|
|
@ -59,12 +60,28 @@ class UserConnection(BaseModel, ModelMixin):
|
|||
lastChecked: datetime = Field(default_factory=datetime.now, description="When the connection was last verified")
|
||||
expiresAt: Optional[datetime] = Field(None, description="When the connection expires")
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert the model to a dictionary with proper datetime serialization"""
|
||||
data = super().to_dict()
|
||||
# Convert datetime fields to ISO format strings
|
||||
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
|
||||
if field in data and data[field] is not None:
|
||||
if isinstance(data[field], datetime):
|
||||
data[field] = data[field].isoformat()
|
||||
elif isinstance(data[field], (int, float)):
|
||||
try:
|
||||
data[field] = datetime.fromtimestamp(data[field]).isoformat()
|
||||
except (ValueError, TypeError):
|
||||
data[field] = None
|
||||
return data
|
||||
|
||||
# Register labels for UserConnection
|
||||
register_model_labels(
|
||||
"UserConnection",
|
||||
{"en": "User Connection", "fr": "Connexion utilisateur"},
|
||||
{
|
||||
"id": {"en": "ID", "fr": "ID"},
|
||||
"userId": {"en": "User ID", "fr": "ID utilisateur"},
|
||||
"authority": {"en": "Authority", "fr": "Autorité"},
|
||||
"externalId": {"en": "External ID", "fr": "ID externe"},
|
||||
"externalUsername": {"en": "External Username", "fr": "Nom d'utilisateur externe"},
|
||||
|
|
@ -137,7 +154,6 @@ class User(BaseModel, ModelMixin):
|
|||
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level")
|
||||
authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL, description="Primary authentication authority")
|
||||
mandateId: Optional[str] = Field(None, description="ID of the mandate this user belongs to")
|
||||
connections: List[UserConnection] = Field(default_factory=list, description="List of external service connections")
|
||||
|
||||
# Register labels for User
|
||||
register_model_labels(
|
||||
|
|
@ -152,8 +168,7 @@ register_model_labels(
|
|||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||
"privilege": {"en": "Privilege", "fr": "Privilège"},
|
||||
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
||||
"connections": {"en": "Connections", "fr": "Connexions"}
|
||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ class ManagementAccess:
|
|||
self.privilege = currentUser.privilege
|
||||
self.db = db
|
||||
|
||||
def getInitialUserid(self):
|
||||
return "----"
|
||||
# return self.db.getInitialUserId() --> to get from AdminDB !
|
||||
|
||||
def canModifyAttribute(self, table: str, attribute: str) -> bool:
|
||||
"""
|
||||
Checks if the current user can modify a specific attribute in a table.
|
||||
|
|
@ -59,6 +63,8 @@ class ManagementAccess:
|
|||
|
||||
filtered_records = []
|
||||
|
||||
initialid = self.getInitialUserid()
|
||||
|
||||
# Apply filtering based on privilege
|
||||
if userPrivilege == "sysadmin":
|
||||
filtered_records = recordset # System admins see all records
|
||||
|
|
@ -69,10 +75,15 @@ class ManagementAccess:
|
|||
# For prompts, users can see all prompts from their mandate
|
||||
if table == "prompts":
|
||||
filtered_records = [r for r in recordset if r.get("mandateId") == self.mandateId]
|
||||
elif table == "users":
|
||||
# For users table, users can only see their own record
|
||||
filtered_records = [r for r in recordset if r.get("id") == self.userId]
|
||||
else:
|
||||
# Users see only their records for other tables
|
||||
filtered_records = [r for r in recordset
|
||||
if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId]
|
||||
filtered_records = [
|
||||
r for r in recordset
|
||||
if r.get("mandateId") == self.mandateId and r.get("_createdBy") == self.userId
|
||||
]
|
||||
|
||||
# Add access control attributes to each record
|
||||
for record in filtered_records:
|
||||
|
|
@ -104,6 +115,16 @@ class ManagementAccess:
|
|||
record["_hideView"] = False # Everyone can view
|
||||
record["_hideEdit"] = not self.canModify("workflows", record.get("workflowId"))
|
||||
record["_hideDelete"] = not self.canModify("workflows", record.get("workflowId"))
|
||||
elif table == "users":
|
||||
# For users table, users can only modify their own connections
|
||||
record["_hideView"] = False
|
||||
record["_hideEdit"] = record_id != self.userId
|
||||
record["_hideDelete"] = record_id != self.userId
|
||||
# Add connection-specific permissions
|
||||
if "connections" in record:
|
||||
for conn in record["connections"]:
|
||||
conn["_hideEdit"] = record_id != self.userId
|
||||
conn["_hideDelete"] = record_id != self.userId
|
||||
else:
|
||||
# Default access control for other tables
|
||||
record["_hideView"] = False
|
||||
|
|
@ -138,6 +159,12 @@ class ManagementAccess:
|
|||
|
||||
record = records[0]
|
||||
|
||||
# Special case for users table - users can modify their own connections
|
||||
if table == "users":
|
||||
if record.get("id") == self.userId:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Admins can modify anything in their mandate, if mandate is specified for a record
|
||||
if userPrivilege == "admin" and record.get("mandateId","-") == self.mandateId:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import List, Dict, Any, Optional
|
|||
from fastapi import status
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import json
|
||||
|
||||
from modules.interfaces.serviceAppModel import User, UserConnection, AuthAuthority, ConnectionStatus
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
|
|
@ -31,16 +32,21 @@ async def get_connections(
|
|||
"""Get all connections for the current user or all connections if admin"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
|
||||
# Clear connections cache to ensure fresh data
|
||||
if "connections" in interface.db._tablesCache:
|
||||
del interface.db._tablesCache["connections"]
|
||||
|
||||
if currentUser.privilege in ['admin', 'sysadmin']:
|
||||
# Admins can see all connections
|
||||
users = interface.getAllUsers()
|
||||
connections = []
|
||||
for user in users:
|
||||
connections.extend(user.connections)
|
||||
connections.extend(interface.getUserConnections(user.id))
|
||||
return connections
|
||||
else:
|
||||
# Regular users can only see their own connections
|
||||
return currentUser.connections
|
||||
return interface.getUserConnections(currentUser.id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting connections: {str(e)}")
|
||||
raise HTTPException(
|
||||
|
|
@ -48,6 +54,99 @@ async def get_connections(
|
|||
detail=f"Failed to get connections: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/", response_model=UserConnection)
|
||||
@limiter.limit("10/minute")
|
||||
async def create_connection(
|
||||
request: Request,
|
||||
connection_data: Dict[str, Any] = Body(...),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> UserConnection:
|
||||
"""Create a new connection for the current user or update existing one"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
|
||||
# Map type to authority
|
||||
authority_map = {
|
||||
'msft': AuthAuthority.MSFT,
|
||||
'google': AuthAuthority.GOOGLE
|
||||
}
|
||||
|
||||
authority = authority_map.get(connection_data.get('type'))
|
||||
if not authority:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unsupported connection type: {connection_data.get('type')}"
|
||||
)
|
||||
|
||||
# Get fresh copy of user from database
|
||||
user = interface.getUser(currentUser.id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Check for existing connection of the same authority
|
||||
existing_connection = None
|
||||
connections = interface.getUserConnections(currentUser.id)
|
||||
for conn in connections:
|
||||
if conn.authority == authority:
|
||||
existing_connection = conn
|
||||
break
|
||||
|
||||
if existing_connection:
|
||||
# Update existing connection
|
||||
existing_connection.status = ConnectionStatus.PENDING
|
||||
existing_connection.lastChecked = datetime.now()
|
||||
existing_connection.externalId = "" # Reset for new OAuth flow
|
||||
existing_connection.externalUsername = "" # Reset for new OAuth flow
|
||||
|
||||
# Convert connection to dict and ensure datetime fields are serialized
|
||||
connection_dict = existing_connection.to_dict()
|
||||
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
|
||||
if field in connection_dict and connection_dict[field] is not None:
|
||||
if isinstance(connection_dict[field], datetime):
|
||||
connection_dict[field] = connection_dict[field].isoformat()
|
||||
elif isinstance(connection_dict[field], (int, float)):
|
||||
connection_dict[field] = datetime.fromtimestamp(connection_dict[field]).isoformat()
|
||||
|
||||
# Update connection record directly
|
||||
interface.db.recordModify("connections", existing_connection.id, connection_dict)
|
||||
|
||||
return existing_connection
|
||||
else:
|
||||
# Create new connection with PENDING status
|
||||
connection = interface.addUserConnection(
|
||||
userId=currentUser.id,
|
||||
authority=authority,
|
||||
externalId="", # Will be set after OAuth
|
||||
externalUsername="", # Will be set after OAuth
|
||||
status=ConnectionStatus.PENDING # Start with PENDING status
|
||||
)
|
||||
|
||||
# Convert connection to dict and ensure datetime fields are serialized
|
||||
connection_dict = connection.to_dict()
|
||||
for field in ['connectedAt', 'lastChecked', 'expiresAt']:
|
||||
if field in connection_dict and connection_dict[field] is not None:
|
||||
if isinstance(connection_dict[field], datetime):
|
||||
connection_dict[field] = connection_dict[field].isoformat()
|
||||
elif isinstance(connection_dict[field], (int, float)):
|
||||
connection_dict[field] = datetime.fromtimestamp(connection_dict[field]).isoformat()
|
||||
|
||||
# Save connection record
|
||||
interface.db.recordModify("connections", connection.id, connection_dict)
|
||||
|
||||
return connection
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating connection: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create connection: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/{connectionId}/connect")
|
||||
@limiter.limit("10/minute")
|
||||
async def connect_service(
|
||||
|
|
@ -65,7 +164,8 @@ async def connect_service(
|
|||
# Admins can connect any connection
|
||||
users = interface.getAllUsers()
|
||||
for user in users:
|
||||
for conn in user.connections:
|
||||
connections = interface.getUserConnections(user.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
|
@ -73,7 +173,8 @@ async def connect_service(
|
|||
break
|
||||
else:
|
||||
# Regular users can only connect their own connections
|
||||
for conn in currentUser.connections:
|
||||
connections = interface.getUserConnections(currentUser.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
|
@ -87,9 +188,21 @@ async def connect_service(
|
|||
# Initiate OAuth flow with state=connect
|
||||
auth_url = None
|
||||
if connection.authority == AuthAuthority.MSFT:
|
||||
auth_url = f"/api/msft/login?state=connect&connectionId={connectionId}"
|
||||
# Use the same login endpoint with state=connect to ensure account selection
|
||||
# Include current user ID in state
|
||||
state_data = {
|
||||
"type": "connect",
|
||||
"connectionId": connectionId,
|
||||
"userId": currentUser.id # Add current user ID
|
||||
}
|
||||
auth_url = f"/api/msft/login?state={json.dumps(state_data)}"
|
||||
elif connection.authority == AuthAuthority.GOOGLE:
|
||||
auth_url = f"/api/google/login?state=connect&connectionId={connectionId}"
|
||||
state_data = {
|
||||
"type": "connect",
|
||||
"connectionId": connectionId,
|
||||
"userId": currentUser.id # Add current user ID
|
||||
}
|
||||
auth_url = f"/api/google/login?state={json.dumps(state_data)}"
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -124,7 +237,8 @@ async def disconnect_service(
|
|||
# Admins can disconnect any connection
|
||||
users = interface.getAllUsers()
|
||||
for user in users:
|
||||
for conn in user.connections:
|
||||
connections = interface.getUserConnections(user.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
|
@ -132,7 +246,8 @@ async def disconnect_service(
|
|||
break
|
||||
else:
|
||||
# Regular users can only disconnect their own connections
|
||||
for conn in currentUser.connections:
|
||||
connections = interface.getUserConnections(currentUser.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
|
@ -147,10 +262,8 @@ async def disconnect_service(
|
|||
connection.status = ConnectionStatus.INACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", connection.userId, {
|
||||
"connections": [c.to_dict() for c in currentUser.connections]
|
||||
})
|
||||
# Update connection record
|
||||
interface.db.recordModify("connections", connectionId, connection.to_dict())
|
||||
|
||||
return {"message": "Service disconnected successfully"}
|
||||
|
||||
|
|
@ -161,4 +274,56 @@ async def disconnect_service(
|
|||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to disconnect service: {str(e)}"
|
||||
)
|
||||
|
||||
@router.delete("/{connectionId}")
|
||||
@limiter.limit("10/minute")
|
||||
async def delete_connection(
|
||||
request: Request,
|
||||
connectionId: str = Path(..., description="The ID of the connection to delete"),
|
||||
currentUser: User = Depends(getCurrentUser)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a connection"""
|
||||
try:
|
||||
interface = getInterface(currentUser)
|
||||
|
||||
# Find the connection
|
||||
connection = None
|
||||
if currentUser.privilege in ['admin', 'sysadmin']:
|
||||
# Admins can delete any connection
|
||||
users = interface.getAllUsers()
|
||||
for user in users:
|
||||
connections = interface.getUserConnections(user.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
if connection:
|
||||
break
|
||||
else:
|
||||
# Regular users can only delete their own connections
|
||||
connections = interface.getUserConnections(currentUser.id)
|
||||
for conn in connections:
|
||||
if conn.id == connectionId:
|
||||
connection = conn
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
)
|
||||
|
||||
# Remove the connection - only need connectionId since permissions are verified
|
||||
interface.removeUserConnection(connectionId)
|
||||
|
||||
return {"message": "Connection deleted successfully"}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting connection: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete connection: {str(e)}"
|
||||
)
|
||||
|
|
@ -68,14 +68,24 @@ async def login(
|
|||
scopes=SCOPES
|
||||
)
|
||||
|
||||
# Generate auth URL with state - use state as is if it's already JSON, otherwise create new state
|
||||
try:
|
||||
# Try to parse state as JSON to check if it's already encoded
|
||||
json.loads(state)
|
||||
state_param = state # Use state as is if it's valid JSON
|
||||
except json.JSONDecodeError:
|
||||
# If not JSON, create new state object
|
||||
state_param = json.dumps({
|
||||
"type": state,
|
||||
"connectionId": connectionId
|
||||
})
|
||||
|
||||
# Generate auth URL with state
|
||||
auth_url, _ = flow.authorization_url(
|
||||
access_type="offline",
|
||||
include_granted_scopes="true",
|
||||
state=json.dumps({
|
||||
"type": state,
|
||||
"connectionId": connectionId
|
||||
})
|
||||
state=state_param,
|
||||
prompt="select_account" # Force account selection screen
|
||||
)
|
||||
|
||||
return RedirectResponse(auth_url)
|
||||
|
|
@ -95,6 +105,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
state_data = json.loads(state)
|
||||
state_type = state_data.get("type", "login")
|
||||
connection_id = state_data.get("connectionId")
|
||||
user_id = state_data.get("userId") # Get user ID from state
|
||||
|
||||
logger.info(f"Processing Google auth callback: state_type={state_type}, connection_id={connection_id}, user_id={user_id}")
|
||||
|
||||
# Create OAuth flow
|
||||
flow = Flow.from_client_config(
|
||||
|
|
@ -137,12 +150,13 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
|
||||
# Create token
|
||||
token = Token(
|
||||
userId=user.id,
|
||||
userId=user.id, # Use local user's ID
|
||||
authority=AuthAuthority.GOOGLE,
|
||||
tokenAccess=credentials.token,
|
||||
tokenRefresh=credentials.refresh_token,
|
||||
tokenType=credentials.token_type,
|
||||
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None
|
||||
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None,
|
||||
createdAt=datetime.now()
|
||||
)
|
||||
|
||||
# Save token
|
||||
|
|
@ -171,69 +185,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
)
|
||||
else:
|
||||
# Handle connection flow
|
||||
if not connection_id:
|
||||
if not connection_id or not user_id:
|
||||
logger.error("Connection ID or User ID is missing in connection flow")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Connection ID is required for connection flow"
|
||||
detail="Connection ID and User ID are required for connection flow"
|
||||
)
|
||||
|
||||
# Get current user from session
|
||||
current_user = await getCurrentUser(request)
|
||||
if not current_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not authenticated"
|
||||
# Get user directly by ID
|
||||
rootInterface = getRootInterface()
|
||||
user = rootInterface.getUser(user_id)
|
||||
|
||||
if not user:
|
||||
logger.error(f"User {user_id} not found in database")
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_error',
|
||||
error: 'User not found in database'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
# Find and update connection
|
||||
interface = getInterface(current_user)
|
||||
# Get the connection from the connections table
|
||||
interface = getInterface(user)
|
||||
connections = interface.getUserConnections(user_id)
|
||||
connection = None
|
||||
for conn in current_user.connections:
|
||||
for conn in connections:
|
||||
if conn.id == connection_id:
|
||||
connection = conn
|
||||
logger.info(f"Found existing connection for user {user.username}")
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
try:
|
||||
if not connection:
|
||||
logger.error(f"Connection {connection_id} not found in user's connections")
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_error',
|
||||
error: 'Connection not found in user\'s connections'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
logger.info(f"Updating connection {connection_id} for user {user.username}")
|
||||
# Update connection with external service details
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = credentials.expiry if credentials.expiry else None
|
||||
connection.externalId = user_info.get("id")
|
||||
connection.externalUsername = user_info.get("email")
|
||||
connection.externalEmail = user_info.get("email")
|
||||
|
||||
# Update connection record directly
|
||||
rootInterface.db.recordModify("connections", connection_id, connection.to_dict())
|
||||
|
||||
# Save token
|
||||
token = Token(
|
||||
userId=user.id, # Use local user's ID
|
||||
authority=AuthAuthority.GOOGLE,
|
||||
tokenAccess=credentials.token,
|
||||
tokenRefresh=credentials.refresh_token,
|
||||
tokenType=credentials.token_type,
|
||||
expiresAt=credentials.expiry.timestamp() if credentials.expiry else None,
|
||||
createdAt=datetime.now()
|
||||
)
|
||||
interface.saveToken(token)
|
||||
|
||||
# Return success page with connection data
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_success',
|
||||
connection: {{
|
||||
id: '{connection.id}',
|
||||
status: 'connected',
|
||||
type: 'google',
|
||||
lastChecked: '{datetime.now().isoformat()}',
|
||||
expiresAt: '{credentials.expiry.isoformat() if credentials.expiry else None}'
|
||||
}}
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating connection: {str(e)}", exc_info=True)
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_error',
|
||||
error: 'Failed to update connection: {str(e)}'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
# Update connection
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = credentials.expiry if credentials.expiry else None
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", current_user.id, {
|
||||
"connections": [c.to_dict() for c in current_user.connections]
|
||||
})
|
||||
|
||||
# Return success page
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_success',
|
||||
connectionId: {json.dumps(connection_id)}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auth callback: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Authentication failed: {str(e)}"
|
||||
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'google_connection_error',
|
||||
error: 'Authentication failed: {str(e)}'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@router.get("/me", response_model=User)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ import json
|
|||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import msal
|
||||
import httpx
|
||||
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.interfaces.serviceAppClass import getInterface, getRootInterface
|
||||
from modules.interfaces.serviceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection
|
||||
from modules.security.auth import getCurrentUser, limiter
|
||||
from modules.security.auth import getCurrentUser, limiter, createAccessToken
|
||||
from modules.shared.attributeUtils import ModelMixin
|
||||
|
||||
# Configure logger
|
||||
|
|
@ -56,14 +57,23 @@ async def login(
|
|||
client_credential=CLIENT_SECRET
|
||||
)
|
||||
|
||||
# Generate auth URL with state
|
||||
auth_url = msal_app.get_authorization_request_url(
|
||||
scopes=SCOPES,
|
||||
redirect_uri=REDIRECT_URI,
|
||||
state=json.dumps({
|
||||
# Generate auth URL with state - use state as is if it's already JSON, otherwise create new state
|
||||
try:
|
||||
# Try to parse state as JSON to check if it's already encoded
|
||||
json.loads(state)
|
||||
state_param = state # Use state as is if it's valid JSON
|
||||
except json.JSONDecodeError:
|
||||
# If not JSON, create new state object
|
||||
state_param = json.dumps({
|
||||
"type": state,
|
||||
"connectionId": connectionId
|
||||
})
|
||||
|
||||
auth_url = msal_app.get_authorization_request_url(
|
||||
scopes=SCOPES,
|
||||
redirect_uri=REDIRECT_URI,
|
||||
state=state_param,
|
||||
prompt="select_account" # Force account selection screen
|
||||
)
|
||||
|
||||
return RedirectResponse(auth_url)
|
||||
|
|
@ -83,6 +93,9 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
state_data = json.loads(state)
|
||||
state_type = state_data.get("type", "login")
|
||||
connection_id = state_data.get("connectionId")
|
||||
user_id = state_data.get("userId") # Get user ID from state
|
||||
|
||||
logger.info(f"Processing Microsoft auth callback: state_type={state_type}, connection_id={connection_id}, user_id={user_id}")
|
||||
|
||||
# Create MSAL app
|
||||
msal_app = msal.ConfidentialClientApplication(
|
||||
|
|
@ -99,49 +112,96 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
)
|
||||
|
||||
if "error" in token_response:
|
||||
logger.error(f"Token acquisition failed: {token_response['error']}")
|
||||
return HTMLResponse(
|
||||
content="<html><body><h1>Authentication Failed</h1><p>Could not acquire token.</p></body></html>",
|
||||
status_code=400
|
||||
)
|
||||
|
||||
# Get user info from Microsoft
|
||||
user_info = msal_app.acquire_token_for_client(scopes=["User.Read"])
|
||||
if "error" in user_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user info from Microsoft"
|
||||
# Get user info using the access token
|
||||
headers = {
|
||||
'Authorization': f"Bearer {token_response['access_token']}",
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
user_info_response = await client.get(
|
||||
"https://graph.microsoft.com/v1.0/me",
|
||||
headers=headers
|
||||
)
|
||||
if user_info_response.status_code != 200:
|
||||
logger.error(f"Failed to get user info: {user_info_response.text}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user info from Microsoft"
|
||||
)
|
||||
user_info = user_info_response.json()
|
||||
logger.info(f"Got user info from Microsoft: {user_info.get('userPrincipalName')}")
|
||||
|
||||
if state_type == "login":
|
||||
# Handle login flow
|
||||
rootInterface = getRootInterface()
|
||||
user = rootInterface.getUserByUsername(user_info.get("preferred_username"))
|
||||
user = rootInterface.getUserByUsername(user_info.get("userPrincipalName"))
|
||||
|
||||
if not user:
|
||||
logger.info(f"Creating new user for {user_info.get('userPrincipalName')}")
|
||||
# Create new user if doesn't exist
|
||||
user = rootInterface.createUser(
|
||||
username=user_info.get("preferred_username"),
|
||||
email=user_info.get("email"),
|
||||
fullName=user_info.get("name"),
|
||||
username=user_info.get("userPrincipalName"),
|
||||
email=user_info.get("mail"),
|
||||
fullName=user_info.get("displayName"),
|
||||
authenticationAuthority=AuthAuthority.MSFT,
|
||||
externalId=user_info.get("id"),
|
||||
externalUsername=user_info.get("preferred_username"),
|
||||
externalEmail=user_info.get("email")
|
||||
externalUsername=user_info.get("userPrincipalName"),
|
||||
externalEmail=user_info.get("mail")
|
||||
)
|
||||
|
||||
# Create token
|
||||
token = Token(
|
||||
userId=user.id,
|
||||
userId=user.id, # Use local user's ID
|
||||
authority=AuthAuthority.MSFT,
|
||||
tokenAccess=token_response["access_token"],
|
||||
tokenRefresh=token_response.get("refresh_token", ""),
|
||||
tokenType=token_response.get("token_type", "bearer"),
|
||||
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0)
|
||||
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0),
|
||||
createdAt=datetime.now()
|
||||
)
|
||||
|
||||
# Save token
|
||||
appInterface = getInterface(user)
|
||||
appInterface.saveToken(token)
|
||||
|
||||
# Create JWT token data
|
||||
jwt_token_data = {
|
||||
"sub": user.username,
|
||||
"mandateId": str(user.mandateId),
|
||||
"userId": str(user.id),
|
||||
"authenticationAuthority": AuthAuthority.MSFT
|
||||
}
|
||||
|
||||
# Create JWT access token
|
||||
jwt_token, jwt_expires_at = createAccessToken(jwt_token_data)
|
||||
|
||||
# Create JWT token
|
||||
jwt_token_obj = Token(
|
||||
userId=user.id,
|
||||
authority=AuthAuthority.MSFT,
|
||||
tokenAccess=jwt_token,
|
||||
tokenType="bearer",
|
||||
expiresAt=jwt_expires_at.timestamp(),
|
||||
createdAt=datetime.now()
|
||||
)
|
||||
|
||||
# Save JWT token
|
||||
appInterface.saveToken(jwt_token_obj)
|
||||
|
||||
# Convert token to dict and ensure all datetime fields are serialized
|
||||
token_dict = jwt_token_obj.to_dict()
|
||||
if isinstance(token_dict.get('createdAt'), datetime):
|
||||
token_dict['createdAt'] = token_dict['createdAt'].isoformat()
|
||||
if isinstance(token_dict.get('expiresAt'), datetime):
|
||||
token_dict['expiresAt'] = token_dict['expiresAt'].isoformat()
|
||||
elif isinstance(token_dict.get('expiresAt'), float):
|
||||
token_dict['expiresAt'] = int(token_dict['expiresAt'])
|
||||
|
||||
# Return success page with token data
|
||||
return HTMLResponse(
|
||||
|
|
@ -153,8 +213,7 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_auth_success',
|
||||
access_token: {json.dumps(token_response["access_token"])},
|
||||
token_data: {json.dumps(token.to_dict())}
|
||||
token_data: {json.dumps(token_dict)}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
|
|
@ -165,69 +224,178 @@ async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse
|
|||
)
|
||||
else:
|
||||
# Handle connection flow
|
||||
if not connection_id:
|
||||
if not connection_id or not user_id:
|
||||
logger.error("Connection ID or User ID is missing in connection flow")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Connection ID is required for connection flow"
|
||||
detail="Connection ID and User ID are required for connection flow"
|
||||
)
|
||||
|
||||
# Get current user from session
|
||||
current_user = await getCurrentUser(request)
|
||||
if not current_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not authenticated"
|
||||
# Get user directly by ID
|
||||
rootInterface = getRootInterface()
|
||||
user = rootInterface.getUser(user_id)
|
||||
|
||||
if not user:
|
||||
logger.error(f"User {user_id} not found in database")
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_error',
|
||||
error: 'User not found in database'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
# Find and update connection
|
||||
interface = getInterface(current_user)
|
||||
# Get the connection from the connections table
|
||||
interface = getInterface(user)
|
||||
connections = interface.getUserConnections(user_id)
|
||||
connection = None
|
||||
for conn in current_user.connections:
|
||||
for conn in connections:
|
||||
if conn.id == connection_id:
|
||||
connection = conn
|
||||
logger.info(f"Found existing connection for user {user.username}")
|
||||
break
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Connection not found"
|
||||
try:
|
||||
if not connection:
|
||||
logger.error(f"Connection {connection_id} not found in user's connections")
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_error',
|
||||
error: 'Connection not found in user\'s connections'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
logger.info(f"Updating connection {connection_id} for user {user.username}")
|
||||
# Update connection with external service details
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
|
||||
connection.externalId = user_info.get("id")
|
||||
connection.externalUsername = user_info.get("userPrincipalName")
|
||||
connection.externalEmail = user_info.get("mail")
|
||||
|
||||
# Update connection record directly
|
||||
rootInterface.db.recordModify("connections", connection_id, connection.to_dict())
|
||||
|
||||
# Save token
|
||||
token = Token(
|
||||
userId=user.id, # Use local user's ID
|
||||
authority=AuthAuthority.MSFT,
|
||||
tokenAccess=token_response["access_token"],
|
||||
tokenRefresh=token_response.get("refresh_token", ""),
|
||||
tokenType=token_response.get("token_type", "bearer"),
|
||||
expiresAt=datetime.now().timestamp() + token_response.get("expires_in", 0),
|
||||
createdAt=datetime.now()
|
||||
)
|
||||
interface.saveToken(token)
|
||||
|
||||
# Return success page with connection data
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_success',
|
||||
connection: {{
|
||||
id: '{connection.id}',
|
||||
status: 'connected',
|
||||
type: 'msft',
|
||||
lastChecked: '{datetime.now().isoformat()}',
|
||||
expiresAt: '{(datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))).isoformat()}'
|
||||
}}
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating connection: {str(e)}", exc_info=True)
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_error',
|
||||
error: 'Failed to update connection: {str(e)}'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
# Update connection
|
||||
connection.status = ConnectionStatus.ACTIVE
|
||||
connection.lastChecked = datetime.now()
|
||||
connection.expiresAt = datetime.now() + timedelta(seconds=token_response.get("expires_in", 0))
|
||||
|
||||
# Update user record
|
||||
interface.db.recordModify("users", current_user.id, {
|
||||
"connections": [c.to_dict() for c in current_user.connections]
|
||||
})
|
||||
|
||||
# Return success page
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Connection Successful</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_success',
|
||||
connectionId: {json.dumps(connection_id)}
|
||||
}}, '*');
|
||||
}}
|
||||
setTimeout(() => window.close(), 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auth callback: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Authentication failed: {str(e)}"
|
||||
logger.error(f"Error in auth callback: {str(e)}", exc_info=True)
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<html>
|
||||
<head><title>Authentication Failed</title></head>
|
||||
<body>
|
||||
<script>
|
||||
if (window.opener) {{
|
||||
window.opener.postMessage({{
|
||||
type: 'msft_connection_error',
|
||||
error: 'Authentication failed: {str(e)}'
|
||||
}}, '*');
|
||||
// Wait for message to be sent before closing
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}} else {{
|
||||
window.close();
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
@router.get("/me", response_model=User)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,29 @@ class ModelMixin:
|
|||
"""
|
||||
Convert a Pydantic model to a dictionary.
|
||||
Handles both Pydantic v1 and v2.
|
||||
Properly serializes datetime fields to ISO format strings.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary representation of the model
|
||||
"""
|
||||
# Get the raw dictionary
|
||||
if hasattr(self, 'model_dump'):
|
||||
return self.model_dump() # Pydantic v2
|
||||
return self.dict() # Pydantic v1
|
||||
data = self.model_dump() # Pydantic v2
|
||||
else:
|
||||
data = self.dict() # Pydantic v1
|
||||
|
||||
# Convert datetime fields to ISO format strings
|
||||
for key, value in data.items():
|
||||
if isinstance(value, datetime):
|
||||
data[key] = value.isoformat()
|
||||
elif isinstance(value, (int, float)) and key.lower().endswith(('at', 'date')):
|
||||
# Handle timestamp fields
|
||||
try:
|
||||
data[key] = datetime.fromtimestamp(value).isoformat()
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ModelMixin':
|
||||
|
|
|
|||
|
|
@ -1,50 +1,18 @@
|
|||
....................... TASKS
|
||||
|
||||
FIXES:
|
||||
|
||||
now to adapt users.js with following specifics:
|
||||
- MandateId not editable, to use user's mandateId
|
||||
- authenticationAuthority to be "local" authority, not editable
|
||||
- Actions: Edit, Delete, Toggle "enabled"
|
||||
- Connections: not to render
|
||||
- language to select
|
||||
- privilege to select
|
||||
clean the module and remove legacy code and renderings, as now everything done in formGeneric
|
||||
|
||||
|
||||
now to adapt mandates.js with following specifics:
|
||||
- Actions: Edit, Delete, Toggle "enabled"
|
||||
- language to select
|
||||
clean the module and remove legacy code and renderings, as now everything done in formGeneric
|
||||
|
||||
|
||||
now to adapt connections.js with following specifics:
|
||||
- Actions: Delete, toggle Connect/Disconnect (based on status attribute's value)
|
||||
- Adding a new connection has two custom buttons, no form: One to connect to a "google" account and one to connect to a "msft" account. api connectors are already available. New record is produced.
|
||||
clean the module and remove legacy code and renderings, as now everything done in formGeneric
|
||||
|
||||
|
||||
|
||||
We need to adapt the workflow part.
|
||||
|
||||
UAC:
|
||||
- Users disabled nicht sichtbar!
|
||||
@workflowManager.py only to contain the workflow steps. All implementations done with the self.service objects. this workflow steps as a draft:
|
||||
- init or load workflow (workflow id) --> chat workflow object
|
||||
- get user input into new workflow message --> chat message
|
||||
- generate Task (chat message, workflow object)
|
||||
|
||||
Workflow:
|
||||
- Liste der Prompts nicht aktualisiert nach Änderungen in FormGeneric
|
||||
|
||||
|
||||
|
||||
- diese seite gesucht: "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 ?
|
||||
|
||||
|
||||
Test paths:
|
||||
- Admin
|
||||
- User
|
||||
- MSFT-Google
|
||||
- Alle Management items
|
||||
- Workflow
|
||||
- Connections on/off
|
||||
- Mail 2 Connectors
|
||||
all functions from workflow.py to check stepwise and deeply analysed:
|
||||
- are they already implemented in serviceChatClass.py?
|
||||
- if not to move to taskManager, documentManager, or agentManager
|
||||
|
||||
|
||||
Agents and Manager:
|
||||
|
|
@ -56,6 +24,16 @@ Agents and Manager:
|
|||
4. task manager to add a task based on agents result and feedback
|
||||
- document extraction to have error handling for big documents. if document too large, then to get content in pieces - depending on document type
|
||||
|
||||
|
||||
Test paths:
|
||||
- Admin
|
||||
- User
|
||||
- MSFT-Google
|
||||
- Alle Management items
|
||||
- Workflow
|
||||
- Connections on/off
|
||||
- Mail 2 Connectors
|
||||
|
||||
Walkthroughs:
|
||||
- register
|
||||
- login local
|
||||
|
|
@ -63,9 +41,7 @@ Walkthroughs:
|
|||
- management pages
|
||||
- workflow
|
||||
|
||||
Install a Test environment with same prod_env
|
||||
- add CORS url names to prod_env
|
||||
-
|
||||
|
||||
|
||||
----------------------- OPEN
|
||||
|
||||
|
|
|
|||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Loading…
Reference in a new issue