tf_sharepoint_integration/saas_connector_dynamodb.py
Daniel Grozdanovic bcd0f8a227
Some checks failed
CI - SharePoint Plugin with SonarQube / Test and SonarQube Analysis (push) Has been cancelled
Initial commit: SharePoint connector and ToothFairyAI integration
2026-02-22 17:58:45 +02:00

919 lines
32 KiB
Python

"""
Enterprise SaaS SharePoint Connector - DynamoDB Edition
Secure multi-tenant SharePoint connector using AWS DynamoDB for storage.
Implements OAuth 2.0 with encrypted token storage, DynamoDB persistence,
and enterprise security best practices.
"""
import os
import secrets
import hashlib
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from dataclasses import dataclass
import json
from decimal import Decimal
import requests
from msal import ConfidentialClientApplication
from cryptography.fernet import Fernet
import boto3
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError
from flask import Flask, request, redirect, session, jsonify, url_for
from functools import wraps
# ============================================================================
# DYNAMODB HELPER FUNCTIONS
# ============================================================================
def decimal_to_float(obj):
"""Convert DynamoDB Decimal types to float/int for JSON serialization."""
if isinstance(obj, list):
return [decimal_to_float(i) for i in obj]
elif isinstance(obj, dict):
return {k: decimal_to_float(v) for k, v in obj.items()}
elif isinstance(obj, Decimal):
return int(obj) if obj % 1 == 0 else float(obj)
return obj
def datetime_to_iso(dt: datetime) -> str:
"""Convert datetime to ISO 8601 string."""
return dt.isoformat() if dt else None
def iso_to_datetime(iso_string: str) -> Optional[datetime]:
"""Convert ISO 8601 string to datetime."""
try:
return datetime.fromisoformat(iso_string) if iso_string else None
except (ValueError, AttributeError):
return None
# ============================================================================
# ENCRYPTION SERVICE
# ============================================================================
class TokenEncryption:
"""
Handles encryption/decryption of OAuth tokens.
SECURITY: Never store tokens in plain text!
"""
def __init__(self, encryption_key: Optional[str] = None):
"""
Initialize encryption service.
Args:
encryption_key: Base64-encoded Fernet key. If not provided,
uses ENCRYPTION_KEY env var.
"""
if encryption_key:
self.key = encryption_key.encode()
else:
key_str = os.getenv('ENCRYPTION_KEY')
if not key_str:
raise ValueError("ENCRYPTION_KEY environment variable must be set")
self.key = key_str.encode()
self.cipher = Fernet(self.key)
@staticmethod
def generate_key() -> str:
"""Generate a new encryption key. Store this securely!"""
return Fernet.generate_key().decode()
def encrypt(self, plaintext: str) -> str:
"""Encrypt a string."""
if not plaintext:
return ""
return self.cipher.encrypt(plaintext.encode()).decode()
def decrypt(self, ciphertext: str) -> str:
"""Decrypt a string."""
if not ciphertext:
return ""
return self.cipher.decrypt(ciphertext.encode()).decode()
# ============================================================================
# DYNAMODB DATA MODELS
# ============================================================================
@dataclass
class SharePointConnectionInfo:
"""Connection information for a SharePoint connection."""
id: str
user_id: str
organization_id: Optional[str]
connection_name: Optional[str]
tenant_id: str
microsoft_user_id: str
is_active: bool
created_at: datetime
last_used_at: Optional[datetime]
# ============================================================================
# DYNAMODB CONNECTOR
# ============================================================================
class DynamoDBSharePointConnector:
"""
Enterprise-grade SharePoint connector using DynamoDB for storage.
DynamoDB Tables:
1. sharepoint_connections - Stores encrypted OAuth tokens
2. sharepoint_oauth_states - Temporary OAuth state for CSRF protection
3. sharepoint_audit_logs - Audit trail for compliance
Features:
- Multi-tenant OAuth 2.0
- Encrypted token storage
- Automatic token refresh
- Audit logging
- CSRF protection
- Serverless-friendly (no connection pooling)
"""
def __init__(
self,
client_id: str,
client_secret: str,
redirect_uri: str,
encryption_key: str,
aws_region: str = "us-east-1",
dynamodb_endpoint: Optional[str] = None, # For local development
table_prefix: str = "",
tenant_id: str = "common"
):
"""
Initialize secure DynamoDB connector.
Args:
client_id: Azure AD application ID
client_secret: Azure AD client secret
redirect_uri: OAuth callback URL (must be HTTPS in production!)
encryption_key: Fernet encryption key for token storage
aws_region: AWS region for DynamoDB
dynamodb_endpoint: Custom endpoint (e.g., http://localhost:8000 for local)
table_prefix: Prefix for table names (e.g., "prod_" or "dev_")
tenant_id: Azure AD tenant or "common" for multi-tenant
"""
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.tenant_id = tenant_id
self.table_prefix = table_prefix
# Required scopes for SharePoint access
# Note: offline_access is handled automatically by MSAL and should NOT be included
self.scopes = [
"https://graph.microsoft.com/Sites.Read.All",
"https://graph.microsoft.com/Files.Read.All",
"https://graph.microsoft.com/User.Read"
]
# Initialize encryption
self.encryption = TokenEncryption(encryption_key)
# Initialize DynamoDB
if dynamodb_endpoint:
# Local development
self.dynamodb = boto3.resource('dynamodb',
region_name=aws_region,
endpoint_url=dynamodb_endpoint)
else:
# Production
self.dynamodb = boto3.resource('dynamodb', region_name=aws_region)
# Table names
self.connections_table_name = f"{table_prefix}sharepoint_connections"
self.oauth_states_table_name = f"{table_prefix}sharepoint_oauth_states"
self.audit_logs_table_name = f"{table_prefix}sharepoint_audit_logs"
# Initialize tables
self._ensure_tables_exist()
# Initialize MSAL
self.authority = f"https://login.microsoftonline.com/{tenant_id}"
self.msal_app = ConfidentialClientApplication(
client_id=self.client_id,
client_credential=self.client_secret,
authority=self.authority
)
def _ensure_tables_exist(self):
"""Create DynamoDB tables if they don't exist."""
try:
# Connections table
self.connections_table = self.dynamodb.Table(self.connections_table_name)
self.connections_table.load()
except ClientError:
self.connections_table = self._create_connections_table()
try:
# OAuth states table
self.oauth_states_table = self.dynamodb.Table(self.oauth_states_table_name)
self.oauth_states_table.load()
except ClientError:
self.oauth_states_table = self._create_oauth_states_table()
try:
# Audit logs table
self.audit_logs_table = self.dynamodb.Table(self.audit_logs_table_name)
self.audit_logs_table.load()
except ClientError:
self.audit_logs_table = self._create_audit_logs_table()
def _create_connections_table(self):
"""Create SharePoint connections table."""
table = self.dynamodb.create_table(
TableName=self.connections_table_name,
KeySchema=[
{'AttributeName': 'id', 'KeyType': 'HASH'} # Partition key
],
AttributeDefinitions=[
{'AttributeName': 'id', 'AttributeType': 'S'},
{'AttributeName': 'user_id', 'AttributeType': 'S'},
{'AttributeName': 'organization_id', 'AttributeType': 'S'}
],
GlobalSecondaryIndexes=[
{
'IndexName': 'user_id-index',
'KeySchema': [
{'AttributeName': 'user_id', 'KeyType': 'HASH'}
],
'Projection': {'ProjectionType': 'ALL'},
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
},
{
'IndexName': 'organization_id-index',
'KeySchema': [
{'AttributeName': 'organization_id', 'KeyType': 'HASH'}
],
'Projection': {'ProjectionType': 'ALL'},
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
}
],
BillingMode='PAY_PER_REQUEST' # On-demand pricing (or use ProvisionedThroughput)
)
table.wait_until_exists()
return table
def _create_oauth_states_table(self):
"""Create OAuth states table with TTL for automatic cleanup."""
table = self.dynamodb.create_table(
TableName=self.oauth_states_table_name,
KeySchema=[
{'AttributeName': 'state', 'KeyType': 'HASH'}
],
AttributeDefinitions=[
{'AttributeName': 'state', 'AttributeType': 'S'}
],
BillingMode='PAY_PER_REQUEST'
)
table.wait_until_exists()
# Enable TTL for automatic cleanup
try:
self.dynamodb.meta.client.update_time_to_live(
TableName=self.oauth_states_table_name,
TimeToLiveSpecification={
'Enabled': True,
'AttributeName': 'ttl'
}
)
except ClientError:
pass # TTL already enabled or not supported
return table
def _create_audit_logs_table(self):
"""Create audit logs table."""
table = self.dynamodb.create_table(
TableName=self.audit_logs_table_name,
KeySchema=[
{'AttributeName': 'connection_id', 'KeyType': 'HASH'},
{'AttributeName': 'timestamp', 'KeyType': 'RANGE'} # Sort key
],
AttributeDefinitions=[
{'AttributeName': 'connection_id', 'AttributeType': 'S'},
{'AttributeName': 'timestamp', 'AttributeType': 'S'},
{'AttributeName': 'user_id', 'AttributeType': 'S'}
],
GlobalSecondaryIndexes=[
{
'IndexName': 'user_id-timestamp-index',
'KeySchema': [
{'AttributeName': 'user_id', 'KeyType': 'HASH'},
{'AttributeName': 'timestamp', 'KeyType': 'RANGE'}
],
'Projection': {'ProjectionType': 'ALL'},
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
}
],
BillingMode='PAY_PER_REQUEST'
)
table.wait_until_exists()
return table
def initiate_connection(
self,
user_id: str,
organization_id: Optional[str] = None,
return_url: Optional[str] = None
) -> str:
"""
Initiate OAuth flow for a user to connect their SharePoint.
Args:
user_id: Your SaaS user ID
organization_id: Your SaaS organization ID (if applicable)
return_url: URL to redirect to after successful connection
Returns:
Authorization URL to redirect user to
"""
# Generate secure state token
state = secrets.token_urlsafe(32)
# Store state in DynamoDB for CSRF protection
expires_at = datetime.utcnow() + timedelta(minutes=10)
ttl = int(expires_at.timestamp()) # Unix timestamp for DynamoDB TTL
self.oauth_states_table.put_item(
Item={
'state': state,
'user_id': user_id,
'organization_id': organization_id or '',
'return_url': return_url or '',
'expires_at': datetime_to_iso(expires_at),
'ttl': ttl, # DynamoDB will auto-delete after this time
'used': False
}
)
# Generate authorization URL
auth_url = self.msal_app.get_authorization_request_url(
scopes=self.scopes,
state=state,
redirect_uri=self.redirect_uri
)
return auth_url
def complete_connection(
self,
auth_code: str,
state: str,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None
) -> SharePointConnectionInfo:
"""
Complete OAuth flow and store connection.
Args:
auth_code: Authorization code from OAuth callback
state: State parameter from OAuth callback
ip_address: User's IP address (for audit log)
user_agent: User's user agent (for audit log)
Returns:
Connection information
Raises:
ValueError: If state is invalid or expired
Exception: If token acquisition fails
"""
# Validate state (CSRF protection)
try:
response = self.oauth_states_table.get_item(Key={'state': state})
oauth_state = response.get('Item')
except ClientError:
oauth_state = None
if not oauth_state:
raise ValueError("Invalid OAuth state")
# Check if state is valid
expires_at = iso_to_datetime(oauth_state.get('expires_at'))
if oauth_state.get('used') or datetime.utcnow() > expires_at:
raise ValueError("OAuth state expired or already used")
# Mark state as used
self.oauth_states_table.update_item(
Key={'state': state},
UpdateExpression='SET used = :val',
ExpressionAttributeValues={':val': True}
)
# Exchange code for tokens
token_response = self.msal_app.acquire_token_by_authorization_code(
code=auth_code,
scopes=self.scopes,
redirect_uri=self.redirect_uri
)
if "error" in token_response:
raise Exception(f"Token acquisition failed: {token_response.get('error_description', token_response['error'])}")
# Get user info from Microsoft
user_info = self._get_user_info(token_response["access_token"])
# Calculate token expiry
expires_in = token_response.get("expires_in", 3600)
expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
# Encrypt tokens
encrypted_access_token = self.encryption.encrypt(token_response["access_token"])
encrypted_refresh_token = self.encryption.encrypt(token_response.get("refresh_token", ""))
# Create connection record
user_id = oauth_state['user_id']
organization_id = oauth_state.get('organization_id') or None
connection_id = self._generate_connection_id(user_id, user_info["id"])
now = datetime.utcnow()
connection_item = {
'id': connection_id,
'user_id': user_id,
'organization_id': organization_id or '',
'tenant_id': user_info.get('tenantId', self.tenant_id),
'microsoft_user_id': user_info["id"],
'connection_name': f"{user_info.get('displayName', 'SharePoint')} - {user_info.get('userPrincipalName', '')}",
'encrypted_access_token': encrypted_access_token,
'encrypted_refresh_token': encrypted_refresh_token,
'token_expires_at': datetime_to_iso(expires_at),
'scopes': json.dumps(self.scopes),
'is_active': True,
'created_at': datetime_to_iso(now),
'updated_at': datetime_to_iso(now),
'last_used_at': datetime_to_iso(now)
}
# Store connection
self.connections_table.put_item(Item=connection_item)
# Audit log
self._log_activity(
connection_id=connection_id,
user_id=user_id,
action="connection_created",
status="success",
ip_address=ip_address,
user_agent=user_agent
)
return SharePointConnectionInfo(
id=connection_id,
user_id=user_id,
organization_id=organization_id,
connection_name=connection_item['connection_name'],
tenant_id=connection_item['tenant_id'],
microsoft_user_id=user_info["id"],
is_active=True,
created_at=now,
last_used_at=now
)
def get_valid_token(
self,
connection_id: str,
user_id: str
) -> str:
"""
Get a valid access token, refreshing if necessary.
Args:
connection_id: SharePoint connection ID
user_id: Your SaaS user ID (for authorization check)
Returns:
Valid access token
Raises:
ValueError: If connection not found or doesn't belong to user
Exception: If token refresh fails
"""
# Get connection (with authorization check!)
try:
response = self.connections_table.get_item(Key={'id': connection_id})
connection = response.get('Item')
except ClientError:
connection = None
if not connection or connection.get('user_id') != user_id or not connection.get('is_active'):
raise ValueError("Connection not found or access denied")
# Check if token needs refresh
token_expires_at = iso_to_datetime(connection['token_expires_at'])
if datetime.utcnow() >= token_expires_at - timedelta(minutes=5):
# Refresh token
refresh_token = self.encryption.decrypt(connection['encrypted_refresh_token'])
token_response = self.msal_app.acquire_token_by_refresh_token(
refresh_token=refresh_token,
scopes=self.scopes
)
if "error" in token_response:
self._log_activity(
connection_id=connection_id,
user_id=user_id,
action="token_refresh",
status="failure",
details=json.dumps({"error": token_response.get("error")})
)
raise Exception(f"Token refresh failed: {token_response.get('error_description', token_response['error'])}")
# Update stored tokens
new_access_token = self.encryption.encrypt(token_response["access_token"])
new_refresh_token = token_response.get("refresh_token")
expires_in = token_response.get("expires_in", 3600)
new_expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
update_expression = "SET encrypted_access_token = :access, token_expires_at = :expires, updated_at = :updated, last_used_at = :used"
expression_values = {
':access': new_access_token,
':expires': datetime_to_iso(new_expires_at),
':updated': datetime_to_iso(datetime.utcnow()),
':used': datetime_to_iso(datetime.utcnow())
}
if new_refresh_token:
update_expression += ", encrypted_refresh_token = :refresh"
expression_values[':refresh'] = self.encryption.encrypt(new_refresh_token)
self.connections_table.update_item(
Key={'id': connection_id},
UpdateExpression=update_expression,
ExpressionAttributeValues=expression_values
)
self._log_activity(
connection_id=connection_id,
user_id=user_id,
action="token_refresh",
status="success"
)
connection['encrypted_access_token'] = new_access_token
else:
# Update last used timestamp
self.connections_table.update_item(
Key={'id': connection_id},
UpdateExpression='SET last_used_at = :val',
ExpressionAttributeValues={':val': datetime_to_iso(datetime.utcnow())}
)
# Decrypt and return access token
return self.encryption.decrypt(connection['encrypted_access_token'])
def list_connections(
self,
user_id: str,
organization_id: Optional[str] = None
) -> List[SharePointConnectionInfo]:
"""
List all SharePoint connections for a user or organization.
Args:
user_id: Your SaaS user ID
organization_id: Your SaaS organization ID (optional)
Returns:
List of connections
"""
# Query by user_id using GSI
response = self.connections_table.query(
IndexName='user_id-index',
KeyConditionExpression=Key('user_id').eq(user_id),
FilterExpression=Attr('is_active').eq(True)
)
connections = response.get('Items', [])
# Filter by organization if specified
if organization_id:
connections = [c for c in connections if c.get('organization_id') == organization_id]
return [
SharePointConnectionInfo(
id=conn['id'],
user_id=conn['user_id'],
organization_id=conn.get('organization_id') or None,
connection_name=conn.get('connection_name'),
tenant_id=conn.get('tenant_id'),
microsoft_user_id=conn.get('microsoft_user_id'),
is_active=conn.get('is_active', False),
created_at=iso_to_datetime(conn.get('created_at')),
last_used_at=iso_to_datetime(conn.get('last_used_at'))
)
for conn in connections
]
def disconnect(
self,
connection_id: str,
user_id: str,
ip_address: Optional[str] = None
):
"""
Disconnect (deactivate) a SharePoint connection.
Args:
connection_id: SharePoint connection ID
user_id: Your SaaS user ID (for authorization)
ip_address: User's IP for audit log
"""
# Get connection to verify ownership
try:
response = self.connections_table.get_item(Key={'id': connection_id})
connection = response.get('Item')
except ClientError:
connection = None
if not connection or connection.get('user_id') != user_id:
raise ValueError("Connection not found or access denied")
# Deactivate connection
self.connections_table.update_item(
Key={'id': connection_id},
UpdateExpression='SET is_active = :val, updated_at = :updated',
ExpressionAttributeValues={
':val': False,
':updated': datetime_to_iso(datetime.utcnow())
}
)
self._log_activity(
connection_id=connection_id,
user_id=user_id,
action="connection_disconnected",
status="success",
ip_address=ip_address
)
def _get_user_info(self, access_token: str) -> Dict[str, Any]:
"""Get Microsoft user information."""
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(
"https://graph.microsoft.com/v1.0/me?$select=id,displayName,userPrincipalName,mail",
headers=headers
)
response.raise_for_status()
return response.json()
def _generate_connection_id(self, user_id: str, microsoft_user_id: str) -> str:
"""Generate deterministic connection ID."""
combined = f"{user_id}:{microsoft_user_id}"
return hashlib.sha256(combined.encode()).hexdigest()[:32]
def _log_activity(
self,
connection_id: str,
user_id: str,
action: str,
status: str,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None,
details: Optional[str] = None
):
"""Create audit log entry."""
timestamp = datetime.utcnow()
self.audit_logs_table.put_item(
Item={
'connection_id': connection_id,
'timestamp': datetime_to_iso(timestamp),
'user_id': user_id,
'action': action,
'status': status,
'ip_address': ip_address or '',
'user_agent': user_agent or '',
'details': details or ''
}
)
# ============================================================================
# SHAREPOINT API CLIENT
# ============================================================================
class SecureSharePointClient:
"""
SharePoint API client with automatic token management.
Use this to make SharePoint API calls on behalf of connected users.
"""
def __init__(self, connector: DynamoDBSharePointConnector, connection_id: str, user_id: str):
"""
Initialize client.
Args:
connector: DynamoDBSharePointConnector instance
connection_id: SharePoint connection ID
user_id: Your SaaS user ID
"""
self.connector = connector
self.connection_id = connection_id
self.user_id = user_id
def _get_headers(self) -> Dict[str, str]:
"""Get headers with valid access token."""
access_token = self.connector.get_valid_token(self.connection_id, self.user_id)
return {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json"
}
def list_sites(self) -> List[Dict[str, Any]]:
"""List SharePoint sites the user has access to."""
response = requests.get(
"https://graph.microsoft.com/v1.0/sites?search=*",
headers=self._get_headers()
)
response.raise_for_status()
return response.json().get("value", [])
def read_file(self, site_id: str, file_path: str, as_text: bool = True) -> Any:
"""Read file content."""
encoded_path = requests.utils.quote(file_path)
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root:/{encoded_path}:/content"
response = requests.get(url, headers=self._get_headers())
response.raise_for_status()
return response.text if as_text else response.content
def list_files(self, site_id: str, folder_path: str = "") -> List[Dict[str, Any]]:
"""List files in a folder."""
if folder_path:
encoded_path = requests.utils.quote(folder_path)
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root:/{encoded_path}:/children"
else:
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root/children"
response = requests.get(url, headers=self._get_headers())
response.raise_for_status()
return response.json().get("value", [])
def search_files(self, site_id: str, query: str) -> List[Dict[str, Any]]:
"""Search for files."""
url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root/search(q='{query}')"
response = requests.get(url, headers=self._get_headers())
response.raise_for_status()
return response.json().get("value", [])
# ============================================================================
# FLASK INTEGRATION EXAMPLE
# ============================================================================
def create_app(connector: DynamoDBSharePointConnector) -> Flask:
"""Create Flask app with DynamoDB SharePoint connector integration."""
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", secrets.token_urlsafe(32))
def require_auth(f):
"""Decorator to require authentication."""
@wraps(f)
def decorated_function(*args, **kwargs):
if "user_id" not in session:
return jsonify({"error": "Authentication required"}), 401
return f(*args, **kwargs)
return decorated_function
@app.route("/sharepoint/connect")
@require_auth
def connect_sharepoint():
"""Initiate SharePoint connection."""
user_id = session["user_id"]
organization_id = session.get("organization_id")
auth_url = connector.initiate_connection(
user_id=user_id,
organization_id=organization_id,
return_url=request.args.get("return_url", "/dashboard")
)
return redirect(auth_url)
@app.route("/sharepoint/callback")
def sharepoint_callback():
"""OAuth callback endpoint."""
if "error" in request.args:
return jsonify({
"error": request.args.get("error_description", request.args["error"])
}), 400
auth_code = request.args.get("code")
state = request.args.get("state")
if not auth_code or not state:
return jsonify({"error": "Invalid callback"}), 400
try:
connection_info = connector.complete_connection(
auth_code=auth_code,
state=state,
ip_address=request.remote_addr,
user_agent=request.headers.get("User-Agent")
)
session["sharepoint_connection_id"] = connection_info.id
return jsonify({
"success": True,
"connection": {
"id": connection_info.id,
"name": connection_info.connection_name
}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/sharepoint/connections")
@require_auth
def list_connections():
"""List user's SharePoint connections."""
user_id = session["user_id"]
connections = connector.list_connections(user_id)
return jsonify({
"connections": [
{
"id": conn.id,
"name": conn.connection_name,
"created_at": conn.created_at.isoformat(),
"last_used_at": conn.last_used_at.isoformat() if conn.last_used_at else None
}
for conn in connections
]
})
@app.route("/api/sharepoint/connections/<connection_id>/disconnect", methods=["POST"])
@require_auth
def disconnect_sharepoint(connection_id):
"""Disconnect SharePoint."""
user_id = session["user_id"]
try:
connector.disconnect(
connection_id=connection_id,
user_id=user_id,
ip_address=request.remote_addr
)
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route("/api/sharepoint/<connection_id>/sites")
@require_auth
def get_sites(connection_id):
"""Get SharePoint sites."""
user_id = session["user_id"]
try:
client = SecureSharePointClient(connector, connection_id, user_id)
sites = client.list_sites()
return jsonify({"sites": decimal_to_float(sites)})
except Exception as e:
return jsonify({"error": str(e)}), 500
return app
if __name__ == "__main__":
# Initialize connector
connector = DynamoDBSharePointConnector(
client_id=os.getenv("SHAREPOINT_CLIENT_ID"),
client_secret=os.getenv("SHAREPOINT_CLIENT_SECRET"),
redirect_uri=os.getenv("REDIRECT_URI"),
encryption_key=os.getenv("ENCRYPTION_KEY"),
aws_region=os.getenv("AWS_REGION", "us-east-1"),
table_prefix=os.getenv("TABLE_PREFIX", ""),
tenant_id=os.getenv("SHAREPOINT_TENANT_ID", "common")
)
# Create and run Flask app
app = create_app(connector)
app.run(debug=False, port=5000)