Files
gh-christianlouis-docuelevate/app/api/onedrive.py
T
copilot-swe-agent[bot] d08040ac4a style: fix all flake8 linter errors across app/ directory
- Run Black formatter and isort on all app/ files
- Remove unused imports (F401) across multiple files
- Add # noqa: F401 for intentional re-exports in celery_worker.py,
  tasks/__init__.py, utils.py, frontend.py, views/base.py
- Fix f-strings without placeholders (F541) in azure.py, notification.py,
  check_credentials.py, upload_to_onedrive.py, settings.py
- Fix bare except (E722) in upload_to_sftp.py
- Fix block comment format (E265) in models.py
- Move imports to top of file to fix E402 in celery_app.py, celery_worker.py
- Fix line-too-long (E501) by wrapping strings in multiple files
- Remove unused variable (F841) in upload_to_nextcloud.py

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-02-08 17:42:33 +00:00

378 lines
14 KiB
Python

"""
OneDrive API endpoints
"""
import logging
import os
from datetime import datetime, timedelta
import requests
from fastapi import APIRouter, Form, HTTPException, Request, status
from app.auth import require_login
from app.config import settings
from app.utils.oauth_helper import exchange_oauth_token
# Set up logging
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/onedrive/exchange-token")
@require_login
async def exchange_onedrive_token(
request: Request,
client_id: str = Form(...),
client_secret: str = Form(...),
redirect_uri: str = Form(...),
code: str = Form(...),
tenant_id: str = Form(...),
):
"""
Exchange an authorization code for a refresh token.
This is done on the server to avoid exposing client secret in the browser.
"""
# Prepare the token request
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
payload = {
"client_id": client_id,
"scope": "https://graph.microsoft.com/.default offline_access",
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"client_secret": client_secret,
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(provider_name="OneDrive", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {"refresh_token": token_data["refresh_token"], "expires_in": token_data.get("expires_in", 3600)}
@router.get("/onedrive/test-token")
@require_login
async def test_onedrive_token(request: Request):
"""
Test if the configured OneDrive token is valid and return expiration information.
"""
try:
logger.info("Testing OneDrive token validity")
if (
not settings.onedrive_refresh_token
or not settings.onedrive_client_id
or not settings.onedrive_client_secret
):
logger.warning("OneDrive credentials not fully configured")
return {"status": "error", "message": "OneDrive credentials are not fully configured"}
# Refresh token to get a new access token and expiration info
tenant_id = settings.onedrive_tenant_id or "common"
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
refresh_data = {
"client_id": settings.onedrive_client_id,
"client_secret": settings.onedrive_client_secret,
"refresh_token": settings.onedrive_refresh_token,
"grant_type": "refresh_token",
"scope": "offline_access Files.ReadWrite",
}
response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout)
if response.status_code != 200:
logger.error(f"Failed to refresh OneDrive token: {response.text}")
return {"status": "error", "message": "Refresh token has expired or is invalid", "needs_reauth": True}
token_data = response.json()
access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in", 3600) # Default to 1 hour if not specified
# Check if we got a new refresh token (Microsoft sometimes issues a new one)
new_refresh_token = token_data.get("refresh_token")
if new_refresh_token and new_refresh_token != settings.onedrive_refresh_token:
logger.info("Received new refresh token from Microsoft - will update configuration")
# Update refresh token in memory
settings.onedrive_refresh_token = new_refresh_token
# Also try to update .env file if it exists
try:
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path, "r") as f:
env_lines = f.readlines()
updated_lines = []
updated = False
for line in env_lines:
if line.startswith("ONEDRIVE_REFRESH_TOKEN="):
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
updated = True
else:
updated_lines.append(line)
if not updated:
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
with open(env_path, "w") as f:
f.writelines(updated_lines)
logger.info("Updated refresh token in .env file")
except Exception as e:
logger.warning(f"Failed to update refresh token in .env file: {e}")
# Test the access token by getting user information
user_info_url = "https://graph.microsoft.com/v1.0/me"
headers = {"Authorization": f"Bearer {access_token}"}
user_response = requests.get(user_info_url, headers=headers, timeout=settings.http_request_timeout)
if user_response.status_code != 200:
logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}")
return {
"status": "error",
"message": f"Token validation failed with status {user_response.status_code}: {user_response.text}",
}
# Get user info
user_info = user_response.json()
display_name = user_info.get("displayName", "Unknown user")
email = user_info.get("userPrincipalName", "Unknown email")
# Calculate expiration time
now = datetime.now()
expiry_time = now + timedelta(seconds=expires_in)
# Format expiration info
time_left = expiry_time - now
token_info = {
"expires_at": expiry_time.isoformat(),
"expires_in_seconds": expires_in,
"expires_in_human": format_time_remaining(time_left),
"refresh_token_validity": "Refresh token is valid for 90 days of inactivity",
}
logger.info(f"Successfully connected to OneDrive as {email}")
return {
"status": "success",
"message": "OneDrive connection successful",
"account": email,
"account_name": display_name,
"token_info": token_info,
}
except Exception as e:
logger.exception(f"Unexpected error testing OneDrive token: {str(e)}")
return {"status": "error", "message": f"Connection error: {str(e)}"}
def format_time_remaining(time_delta):
"""Format a timedelta into a human-readable string."""
if time_delta.total_seconds() <= 0:
return "Expired"
days = time_delta.days
hours, remainder = divmod(time_delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days > 0:
parts.append(f"{days} day{'s' if days != 1 else ''}")
if hours > 0:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0 and days == 0: # Only show minutes if less than a day
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return ", ".join(parts)
@router.post("/onedrive/save-settings")
@require_login
async def save_onedrive_settings(
request: Request,
client_id: str = Form(None),
client_secret: str = Form(None),
refresh_token: str = Form(...),
tenant_id: str = Form("common"),
folder_path: str = Form(None),
):
"""
Save OneDrive settings to the .env file
"""
try:
# Get the path to the .env file
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if not os.path.exists(env_path):
logger.error(f".env file not found at {env_path}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not find .env file to update"
)
logger.info(f"Updating OneDrive settings in {env_path}")
# Read the current .env file
with open(env_path, "r") as f:
env_lines = f.readlines()
# Define settings to update
onedrive_settings = {
"ONEDRIVE_REFRESH_TOKEN": refresh_token,
}
# Only update these if provided
if client_id:
onedrive_settings["ONEDRIVE_CLIENT_ID"] = client_id
if client_secret:
onedrive_settings["ONEDRIVE_CLIENT_SECRET"] = client_secret
if tenant_id:
onedrive_settings["ONEDRIVE_TENANT_ID"] = tenant_id
if folder_path:
onedrive_settings["ONEDRIVE_FOLDER_PATH"] = folder_path
# Process each line and update or add settings
updated = set()
new_env_lines = []
for line in env_lines:
line = line.rstrip()
is_updated = False
for key, value in onedrive_settings.items():
if line.startswith(f"{key}=") or line.startswith(f"# {key}="):
if line.startswith("# "): # Uncomment if commented out
line = line[2:]
new_env_lines.append(f"{key}={value}")
updated.add(key)
is_updated = True
break
if not is_updated:
new_env_lines.append(line)
# Add any settings that weren't updated (they weren't in the file)
for key, value in onedrive_settings.items():
if key not in updated:
new_env_lines.append(f"{key}={value}")
# Write the updated .env file
with open(env_path, "w") as f:
f.write("\n".join(new_env_lines) + "\n")
# Update the settings in memory
if refresh_token:
settings.onedrive_refresh_token = refresh_token
if client_id:
settings.onedrive_client_id = client_id
if client_secret:
settings.onedrive_client_secret = client_secret
if tenant_id:
settings.onedrive_tenant_id = tenant_id
if folder_path:
settings.onedrive_folder_path = folder_path
logger.info("Successfully updated OneDrive settings")
return {"status": "success", "message": "OneDrive settings have been saved"}
except HTTPException:
raise
except Exception as e:
logger.exception(f"Unexpected error saving OneDrive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save OneDrive settings: {str(e)}"
)
@router.post("/onedrive/update-settings")
@require_login
async def update_onedrive_settings(
request: Request,
client_id: str = Form(None),
client_secret: str = Form(None),
refresh_token: str = Form(...),
tenant_id: str = Form("common"),
folder_path: str = Form(None),
):
"""
Update OneDrive settings in memory (without modifying .env file)
"""
try:
logger.info("Updating OneDrive settings in memory")
# Update settings in memory
if refresh_token:
settings.onedrive_refresh_token = refresh_token
logger.info("Updated ONEDRIVE_REFRESH_TOKEN in memory")
if client_id:
settings.onedrive_client_id = client_id
logger.info("Updated ONEDRIVE_CLIENT_ID in memory")
if client_secret:
settings.onedrive_client_secret = client_secret
logger.info("Updated ONEDRIVE_CLIENT_SECRET in memory")
if tenant_id:
settings.onedrive_tenant_id = tenant_id
logger.info("Updated ONEDRIVE_TENANT_ID in memory")
if folder_path:
settings.onedrive_folder_path = folder_path
logger.info("Updated ONEDRIVE_FOLDER_PATH in memory")
# Test the token to make sure it works
try:
from app.tasks.upload_to_onedrive import get_onedrive_token
get_onedrive_token() # Test that token can be retrieved
logger.info("Successfully tested OneDrive token")
except Exception as e:
logger.error(f"Token test failed after updating settings: {str(e)}")
return {"status": "warning", "message": "Settings updated but token test failed: " + str(e)}
return {"status": "success", "message": "OneDrive settings have been updated in memory"}
except Exception as e:
logger.exception(f"Unexpected error updating OneDrive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update OneDrive settings: {str(e)}"
)
@router.get("/onedrive/get-full-config")
@require_login
async def get_onedrive_full_config(request: Request):
"""
Get the full OneDrive configuration for sharing with worker nodes
"""
try:
# Create a configuration object with all OneDrive settings
config = {
"client_id": settings.onedrive_client_id or "",
"client_secret": settings.onedrive_client_secret or "",
"tenant_id": settings.onedrive_tenant_id or "common",
"refresh_token": settings.onedrive_refresh_token or "",
"folder_path": settings.onedrive_folder_path or "Documents/Uploads",
}
# Generate environment variable format
env_format = "\n".join(
[
f"ONEDRIVE_CLIENT_ID={config['client_id']}",
f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}",
f"ONEDRIVE_TENANT_ID={config['tenant_id']}",
f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}",
f"ONEDRIVE_FOLDER_PATH={config['folder_path']}",
]
)
return {"status": "success", "config": config, "env_format": env_format}
except Exception as e:
logger.exception("Error getting OneDrive configuration")
return {"status": "error", "message": str(e)}