Add Google Drive authorization processing and error handling templates
- Implemented `google_drive_callback.html` for processing Google Drive authorization, including UI for success and error states. - Added JavaScript functionality for exchanging authorization codes, saving settings, and handling folder selection. - Created `google_drive_callback_error.html` to display error messages during the authorization process.
This commit is contained in:
@@ -13,6 +13,7 @@ from app.api.onedrive import router as onedrive_router
|
||||
from app.api.dropbox import router as dropbox_router
|
||||
from app.api.openai import router as openai_router
|
||||
from app.api.azure import router as azure_router
|
||||
from app.api.google_drive import router as google_drive_router
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,3 +30,4 @@ router.include_router(onedrive_router)
|
||||
router.include_router(dropbox_router)
|
||||
router.include_router(openai_router)
|
||||
router.include_router(azure_router)
|
||||
router.include_router(google_drive_router)
|
||||
|
||||
+74
-40
@@ -5,6 +5,9 @@ from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
@@ -157,60 +160,91 @@ async def update_dropbox_settings(
|
||||
@require_login
|
||||
async def test_dropbox_token(request: Request):
|
||||
"""
|
||||
Test if the configured Dropbox refresh token is valid.
|
||||
Test if the configured Dropbox token is valid and return expiration information.
|
||||
"""
|
||||
try:
|
||||
from app.tasks.upload_to_dropbox import get_dropbox_client
|
||||
|
||||
logger.info("Testing Dropbox token validity")
|
||||
if not settings.dropbox_refresh_token:
|
||||
logger.warning("No Dropbox refresh token configured")
|
||||
|
||||
if not settings.dropbox_refresh_token or not settings.dropbox_app_key or not settings.dropbox_app_secret:
|
||||
logger.warning("Dropbox credentials not fully configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No Dropbox refresh token is configured"
|
||||
"message": "Dropbox credentials are not fully configured"
|
||||
}
|
||||
|
||||
# Check if app key and app secret are configured
|
||||
if not settings.dropbox_app_key or not settings.dropbox_app_secret:
|
||||
logger.warning("Dropbox app key or app secret is missing")
|
||||
# Check token validity by getting current account info
|
||||
headers = {"Authorization": f"Bearer {settings.dropbox_refresh_token}"}
|
||||
response = requests.post(
|
||||
"https://api.dropboxapi.com/2/users/get_current_account",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# If token is invalid, try refreshing it
|
||||
if response.status_code == 401:
|
||||
logger.info("Dropbox access token invalid or expired, trying to refresh")
|
||||
|
||||
# Get a new access token using the refresh token
|
||||
refresh_url = "https://api.dropbox.com/oauth2/token"
|
||||
refresh_data = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": settings.dropbox_refresh_token,
|
||||
"client_id": settings.dropbox_app_key,
|
||||
"client_secret": settings.dropbox_app_secret
|
||||
}
|
||||
|
||||
refresh_response = requests.post(refresh_url, data=refresh_data)
|
||||
|
||||
if refresh_response.status_code != 200:
|
||||
logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Refresh token has expired or is invalid",
|
||||
"needs_reauth": True
|
||||
}
|
||||
|
||||
token_info = refresh_response.json()
|
||||
access_token = token_info.get("access_token")
|
||||
|
||||
# Try again with the new access token
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
response = requests.post(
|
||||
"https://api.dropboxapi.com/2/users/get_current_account",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Dropbox token test failed: {response.status_code} {response.text}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Dropbox app key or app secret is missing",
|
||||
"missing_config": True
|
||||
"message": f"Token validation failed with status {response.status_code}: {response.text}"
|
||||
}
|
||||
|
||||
# Try to get a client using the configured refresh token
|
||||
try:
|
||||
dbx = get_dropbox_client()
|
||||
# Test connection by getting account info
|
||||
account = dbx.users_get_current_account()
|
||||
logger.info(f"Successfully connected to Dropbox as {account.name.display_name}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Token is valid! Connected as {account.name.display_name}",
|
||||
"account": account.name.display_name,
|
||||
"email": account.email
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Dropbox token test failed: {error_msg}")
|
||||
|
||||
# Determine if this is an authentication error
|
||||
is_auth_error = "auth" in error_msg.lower() or "invalid" in error_msg.lower()
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Token validation failed: {error_msg}",
|
||||
"is_auth_error": is_auth_error,
|
||||
"needs_reauth": is_auth_error
|
||||
}
|
||||
|
||||
# Get account info
|
||||
account_info = response.json()
|
||||
account_email = account_info.get("email", "Unknown account")
|
||||
account_name = account_info.get("name", {}).get("display_name", "Unknown user")
|
||||
|
||||
# Dropbox refresh tokens don't expire, but we should note that in our response
|
||||
token_info = {
|
||||
"expires_in_human": "Never expires (perpetual token)",
|
||||
"is_perpetual": True
|
||||
}
|
||||
|
||||
logger.info(f"Successfully connected to Dropbox as {account_email}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Dropbox connection successful",
|
||||
"account": account_email,
|
||||
"account_name": account_name,
|
||||
"token_info": token_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing Dropbox token")
|
||||
logger.exception(f"Unexpected error testing Dropbox token: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
"message": f"Connection error: {str(e)}"
|
||||
}
|
||||
|
||||
@router.post("/dropbox/save-settings")
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
Google Drive API endpoints
|
||||
"""
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/google-drive/exchange-token")
|
||||
@require_login
|
||||
async def exchange_google_drive_token(
|
||||
request: Request,
|
||||
client_id: str = Form(...),
|
||||
client_secret: str = Form(...),
|
||||
redirect_uri: str = Form(...),
|
||||
code: str = Form(...),
|
||||
folder_id: Optional[str] = Form(None)
|
||||
):
|
||||
"""
|
||||
Exchange an authorization code for refresh and access tokens from Google.
|
||||
This is done on the server to avoid exposing client secret in the browser.
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting Google Drive token exchange process")
|
||||
|
||||
# Prepare the token request
|
||||
token_url = "https://oauth2.googleapis.com/token"
|
||||
|
||||
payload = {
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'code': code,
|
||||
'redirect_uri': redirect_uri,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
|
||||
# Log request details (excluding secret)
|
||||
safe_payload = payload.copy()
|
||||
safe_payload['client_secret'] = '[REDACTED]'
|
||||
safe_payload['code'] = f"{code[:5]}...{code[-5:]}" if len(code) > 10 else '[REDACTED]'
|
||||
logger.info(f"Token exchange request payload: {safe_payload}")
|
||||
|
||||
# Make the token request
|
||||
logger.info("Sending POST request to Google for token exchange")
|
||||
response = requests.post(token_url, data=payload)
|
||||
|
||||
# Check if the request was successful
|
||||
logger.info(f"Token exchange response status: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
# Log the error response for debugging
|
||||
try:
|
||||
error_json = response.json()
|
||||
logger.error(f"Token exchange failed with status {response.status_code}: {error_json}")
|
||||
error_detail = error_json
|
||||
except Exception as json_err:
|
||||
logger.error(f"Failed to parse error response as JSON: {str(json_err)}")
|
||||
logger.error(f"Raw response content: {response.content[:500]}") # Limit log size
|
||||
error_detail = {"error": "Unknown error", "raw_content_snippet": str(response.content[:100])}
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Token exchange failed: {error_detail}"
|
||||
)
|
||||
|
||||
# Return the token response
|
||||
token_data = response.json()
|
||||
|
||||
# Validate the token response
|
||||
if "refresh_token" not in token_data:
|
||||
logger.error(f"Google returned success but no refresh token found in response: {token_data.keys()}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Google OAuth server returned success but no refresh token was included"
|
||||
)
|
||||
|
||||
# Calculate token length for logging
|
||||
refresh_token_length = len(token_data.get("refresh_token", ""))
|
||||
access_token_length = len(token_data.get("access_token", ""))
|
||||
|
||||
logger.info(f"Successfully exchanged authorization code for Google Drive tokens. "
|
||||
f"Refresh token length: {refresh_token_length}, "
|
||||
f"Access token length: {access_token_length}")
|
||||
|
||||
# Return just what's needed by the frontend
|
||||
return {
|
||||
"refresh_token": token_data["refresh_token"],
|
||||
"access_token": token_data["access_token"],
|
||||
"expires_in": token_data.get("expires_in", 3600)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions as they already have appropriate status codes
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error during Google Drive token exchange: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to exchange token: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/google-drive/update-settings")
|
||||
@require_login
|
||||
async def update_google_drive_settings(
|
||||
request: Request,
|
||||
client_id: str = Form(None),
|
||||
client_secret: str = Form(None),
|
||||
refresh_token: str = Form(...),
|
||||
folder_id: str = Form(None),
|
||||
use_oauth: str = Form("true")
|
||||
):
|
||||
"""
|
||||
Update Google Drive settings in memory
|
||||
"""
|
||||
try:
|
||||
logger.info("Updating Google Drive settings in memory")
|
||||
|
||||
# Convert use_oauth string to boolean
|
||||
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
|
||||
|
||||
# Update settings in memory
|
||||
if refresh_token:
|
||||
settings.google_drive_refresh_token = refresh_token
|
||||
logger.info("Updated GOOGLE_DRIVE_REFRESH_TOKEN in memory")
|
||||
|
||||
if client_id:
|
||||
settings.google_drive_client_id = client_id
|
||||
logger.info("Updated GOOGLE_DRIVE_CLIENT_ID in memory")
|
||||
|
||||
if client_secret:
|
||||
settings.google_drive_client_secret = client_secret
|
||||
logger.info("Updated GOOGLE_DRIVE_CLIENT_SECRET in memory")
|
||||
|
||||
if folder_id:
|
||||
settings.google_drive_folder_id = folder_id
|
||||
logger.info("Updated GOOGLE_DRIVE_FOLDER_ID in memory")
|
||||
|
||||
# Set the OAuth flag
|
||||
settings.google_drive_use_oauth = use_oauth_bool
|
||||
logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory to {use_oauth_bool}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Google Drive settings have been updated in memory"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error updating Google Drive settings: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update Google Drive settings: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/google-drive/test-token")
|
||||
@require_login
|
||||
async def test_google_drive_token(request: Request):
|
||||
"""
|
||||
Test if the configured Google Drive token is valid.
|
||||
Tests both OAuth and service account approaches based on configuration.
|
||||
"""
|
||||
try:
|
||||
from app.tasks.upload_to_google_drive import get_drive_service_oauth, get_google_drive_service
|
||||
|
||||
logger.info("Testing Google Drive token validity")
|
||||
|
||||
# Check if OAuth is enabled and configured
|
||||
if getattr(settings, 'google_drive_use_oauth', False):
|
||||
if not (settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token):
|
||||
logger.warning("Google Drive OAuth credentials not fully configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Google Drive OAuth credentials are not fully configured"
|
||||
}
|
||||
|
||||
try:
|
||||
# Test OAuth connection
|
||||
service = get_drive_service_oauth()
|
||||
|
||||
# Get credentials for checking token validity
|
||||
import google.oauth2.credentials
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(
|
||||
token=None,
|
||||
refresh_token=settings.google_drive_refresh_token,
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=settings.google_drive_client_id,
|
||||
client_secret=settings.google_drive_client_secret
|
||||
)
|
||||
|
||||
# Force a refresh to update the token expiration
|
||||
if not credentials.valid:
|
||||
credentials.refresh(Request())
|
||||
|
||||
# Get token expiration info
|
||||
expiration_info = {}
|
||||
if hasattr(credentials, 'expiry') and credentials.expiry:
|
||||
now = datetime.now()
|
||||
expiry = credentials.expiry
|
||||
time_left = expiry - now
|
||||
expiration_info = {
|
||||
"expires_at": expiry.isoformat(),
|
||||
"expires_in_seconds": max(0, int(time_left.total_seconds())),
|
||||
"expires_in_human": format_time_remaining(time_left)
|
||||
}
|
||||
|
||||
# Test basic API operation
|
||||
about = service.about().get(fields="user").execute()
|
||||
user_email = about.get("user", {}).get("emailAddress", "Unknown")
|
||||
|
||||
logger.info(f"Successfully connected to Google Drive as {user_email}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"OAuth token is valid! Connected as {user_email}",
|
||||
"account": user_email,
|
||||
"auth_type": "oauth",
|
||||
"token_info": expiration_info
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Google Drive OAuth token test failed: {error_msg}")
|
||||
|
||||
# Check if this is a token-related error
|
||||
if "invalid_grant" in error_msg.lower() or "token" in error_msg.lower():
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"OAuth token validation failed: {error_msg}",
|
||||
"needs_reauth": True
|
||||
}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Connection error: {error_msg}"
|
||||
}
|
||||
else:
|
||||
# Test service account connection
|
||||
if not settings.google_drive_credentials_json:
|
||||
logger.warning("Google Drive service account credentials not configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Google Drive service account credentials are not configured"
|
||||
}
|
||||
|
||||
try:
|
||||
service = get_google_drive_service()
|
||||
about = service.about().get(fields="user").execute()
|
||||
|
||||
# For service accounts, try to show the delegated user if available
|
||||
user_email = about.get("user", {}).get("emailAddress", "Unknown")
|
||||
delegated_user = getattr(settings, 'google_drive_delegate_to', None)
|
||||
|
||||
if delegated_user:
|
||||
user_display = f"{user_email} (delegating as {delegated_user})"
|
||||
else:
|
||||
user_display = user_email
|
||||
|
||||
logger.info(f"Successfully connected to Google Drive using service account as {user_display}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Service account is valid! Connected as {user_display}",
|
||||
"account": user_email,
|
||||
"auth_type": "service_account"
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Google Drive service account test failed: {error_msg}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Service account validation failed: {error_msg}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing Google Drive token")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
}
|
||||
|
||||
@router.get("/google-drive/get-token-info")
|
||||
@require_login
|
||||
async def get_google_drive_token_info(request: Request):
|
||||
"""
|
||||
Get information about the current Google Drive token.
|
||||
Returns the access token if one exists and is valid.
|
||||
Used by the frontend to access the Google Picker API.
|
||||
"""
|
||||
try:
|
||||
logger.info("Getting Google Drive token information")
|
||||
|
||||
# Check if OAuth is enabled and configured
|
||||
if not getattr(settings, 'google_drive_use_oauth', False):
|
||||
logger.warning("OAuth is not enabled, using service account instead")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "OAuth is not enabled. Service accounts don't support user-facing features like folder picker."
|
||||
}
|
||||
|
||||
if not (settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token):
|
||||
logger.warning("Google Drive OAuth credentials not fully configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Google Drive OAuth credentials are not fully configured"
|
||||
}
|
||||
|
||||
try:
|
||||
# Get credentials and access token
|
||||
import google.oauth2.credentials
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(
|
||||
token=None,
|
||||
refresh_token=settings.google_drive_refresh_token,
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=settings.google_drive_client_id,
|
||||
client_secret=settings.google_drive_client_secret
|
||||
)
|
||||
|
||||
# Force a refresh to get a fresh access token
|
||||
if not credentials.valid:
|
||||
credentials.refresh(Request())
|
||||
|
||||
# Get token expiration info
|
||||
expiration_info = {}
|
||||
if hasattr(credentials, 'expiry') and credentials.expiry:
|
||||
now = datetime.now()
|
||||
expiry = credentials.expiry
|
||||
time_left = expiry - now
|
||||
expiration_info = {
|
||||
"expires_at": expiry.isoformat(),
|
||||
"expires_in_seconds": max(0, int(time_left.total_seconds())),
|
||||
"expires_in_human": format_time_remaining(time_left)
|
||||
}
|
||||
|
||||
# Return the token info
|
||||
logger.info("Successfully retrieved Google Drive access token")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Access token successfully retrieved",
|
||||
"access_token": credentials.token,
|
||||
"token_info": expiration_info
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Failed to get Google Drive token: {error_msg}")
|
||||
|
||||
# Check if this is a token-related error
|
||||
if "invalid_grant" in error_msg.lower() or "token" in error_msg.lower():
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"OAuth token retrieval failed: {error_msg}",
|
||||
"needs_reauth": True
|
||||
}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Token retrieval error: {error_msg}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error getting Google Drive token info")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected 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("/google-drive/save-settings")
|
||||
@require_login
|
||||
async def save_dropbox_settings(
|
||||
request: Request,
|
||||
client_id: str = Form(None),
|
||||
client_secret: str = Form(None),
|
||||
refresh_token: str = Form(...),
|
||||
folder_id: str = Form(None),
|
||||
use_oauth: str = Form("true")
|
||||
):
|
||||
"""
|
||||
Save Google Drive 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")
|
||||
|
||||
# Convert use_oauth string to boolean
|
||||
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
|
||||
|
||||
# Define settings to update
|
||||
drive_settings = {
|
||||
"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()
|
||||
}
|
||||
|
||||
# Only update these if provided
|
||||
if use_oauth_bool:
|
||||
if refresh_token:
|
||||
drive_settings["GOOGLE_DRIVE_REFRESH_TOKEN"] = refresh_token
|
||||
if client_id:
|
||||
drive_settings["GOOGLE_DRIVE_CLIENT_ID"] = client_id
|
||||
if client_secret:
|
||||
drive_settings["GOOGLE_DRIVE_CLIENT_SECRET"] = client_secret
|
||||
|
||||
# Always include folder ID if provided
|
||||
if folder_id:
|
||||
drive_settings["GOOGLE_DRIVE_FOLDER_ID"] = folder_id
|
||||
|
||||
# Try to update the .env file, but don't fail if it doesn't exist (for Docker containers)
|
||||
if os.path.exists(env_path):
|
||||
try:
|
||||
logger.info(f"Updating Google Drive settings in {env_path}")
|
||||
|
||||
# Read the current .env file
|
||||
with open(env_path, "r") as f:
|
||||
env_lines = f.readlines()
|
||||
|
||||
# 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 drive_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 drive_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")
|
||||
|
||||
logger.info("Successfully updated Google Drive settings in .env file")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update")
|
||||
else:
|
||||
logger.warning(f".env file not found at {env_path}, skipping file update but continuing with in-memory update")
|
||||
|
||||
# Update the settings in memory (this always happens)
|
||||
if refresh_token:
|
||||
settings.google_drive_refresh_token = refresh_token
|
||||
if client_id:
|
||||
settings.google_drive_client_id = client_id
|
||||
if client_secret:
|
||||
settings.google_drive_client_secret = client_secret
|
||||
if folder_id:
|
||||
settings.google_drive_folder_id = folder_id
|
||||
|
||||
# Set OAuth flag
|
||||
settings.google_drive_use_oauth = use_oauth_bool
|
||||
|
||||
logger.info("Successfully updated Google Drive settings in memory")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Google Drive settings have been saved",
|
||||
"in_memory_only": not os.path.exists(env_path)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error saving Google Drive settings: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to save Google Drive settings: {str(e)}"
|
||||
)
|
||||
+127
-37
@@ -5,6 +5,9 @@ from fastapi import APIRouter, Request, HTTPException, status, Form
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from app.auth import require_login
|
||||
from app.config import settings
|
||||
@@ -112,60 +115,147 @@ async def exchange_onedrive_token(
|
||||
@require_login
|
||||
async def test_onedrive_token(request: Request):
|
||||
"""
|
||||
Test if the configured OneDrive refresh token is valid.
|
||||
Provides detailed error information if token is invalid.
|
||||
Test if the configured OneDrive token is valid and return expiration information.
|
||||
"""
|
||||
try:
|
||||
from app.tasks.upload_to_onedrive import get_onedrive_token
|
||||
|
||||
logger.info("Testing OneDrive token validity")
|
||||
if not settings.onedrive_refresh_token:
|
||||
logger.warning("No OneDrive refresh token configured")
|
||||
|
||||
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": "No OneDrive refresh token is configured"
|
||||
"message": "OneDrive credentials are not fully configured"
|
||||
}
|
||||
|
||||
# Check if client ID and client secret are configured
|
||||
if not settings.onedrive_client_id or not settings.onedrive_client_secret:
|
||||
logger.warning("OneDrive client ID or client secret is missing")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "OneDrive client ID or client secret is missing",
|
||||
"missing_config": True
|
||||
}
|
||||
# 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"
|
||||
|
||||
# Try to get an access token using the configured refresh token
|
||||
try:
|
||||
access_token = get_onedrive_token()
|
||||
|
||||
# If we got here, token is valid
|
||||
logger.info("OneDrive token is valid")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "OneDrive token is valid",
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"OneDrive token test failed: {error_msg}")
|
||||
|
||||
# Determine if this is an invalid_grant error (expired token)
|
||||
is_expired = "invalid_grant" in error_msg.lower()
|
||||
|
||||
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)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to refresh OneDrive token: {response.text}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Token validation failed: {error_msg}",
|
||||
"is_expired": is_expired,
|
||||
"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)
|
||||
|
||||
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": f"OneDrive connection successful",
|
||||
"account": email,
|
||||
"account_name": display_name,
|
||||
"token_info": token_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing OneDrive token")
|
||||
logger.exception(f"Unexpected error testing OneDrive token: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
"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(
|
||||
|
||||
@@ -64,6 +64,12 @@ class Settings(BaseSettings):
|
||||
google_drive_credentials_json: Optional[str] = ""
|
||||
google_drive_folder_id: Optional[str] = ""
|
||||
google_drive_delegate_to: Optional[str] = "" # Optional delegated user email
|
||||
|
||||
# Google Drive OAuth settings
|
||||
google_drive_use_oauth: bool = False # Default to service account method
|
||||
google_drive_client_id: Optional[str] = ""
|
||||
google_drive_client_secret: Optional[str] = ""
|
||||
google_drive_refresh_token: Optional[str] = ""
|
||||
|
||||
# WebDAV settings
|
||||
webdav_url: Optional[str] = None
|
||||
|
||||
+83
-10
@@ -14,6 +14,7 @@ from app.tasks.upload_to_sftp import upload_to_sftp
|
||||
from app.tasks.upload_to_email import upload_to_email
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive
|
||||
from app.tasks.upload_to_s3 import upload_to_s3
|
||||
from app.utils.config_validator import get_provider_status
|
||||
from app.celery_app import celery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,7 +34,16 @@ def _should_upload_to_paperless():
|
||||
settings.paperless_host)
|
||||
|
||||
def _should_upload_to_google_drive():
|
||||
return settings.google_drive_credentials_json
|
||||
# Check for OAuth configuration
|
||||
if getattr(settings, 'google_drive_use_oauth', False):
|
||||
return (settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token and
|
||||
settings.google_drive_folder_id)
|
||||
# Or check for service account configuration
|
||||
else:
|
||||
return (settings.google_drive_credentials_json and
|
||||
settings.google_drive_folder_id)
|
||||
|
||||
def _should_upload_to_webdav():
|
||||
return (settings.webdav_url and
|
||||
@@ -59,18 +69,51 @@ def _should_upload_to_email():
|
||||
def _should_upload_to_onedrive():
|
||||
return (settings.onedrive_client_id and
|
||||
settings.onedrive_client_secret and
|
||||
(settings.onedrive_refresh_token or
|
||||
(settings.onedrive_tenant_id and settings.onedrive_tenant_id != "common")))
|
||||
settings.onedrive_refresh_token)
|
||||
|
||||
def _should_upload_to_s3():
|
||||
return (settings.s3_bucket_name and
|
||||
settings.aws_access_key_id and
|
||||
settings.aws_secret_access_key)
|
||||
|
||||
@celery.task(base=BaseTaskWithRetry)
|
||||
def send_to_all_destinations(file_path: str):
|
||||
"""Distribute a file to all configured storage destinations."""
|
||||
def get_configured_services_from_validator():
|
||||
"""
|
||||
Use the config validator to determine which services are configured properly.
|
||||
Returns a dictionary with service names as keys and boolean values indicating
|
||||
whether they're properly configured.
|
||||
"""
|
||||
providers = get_provider_status()
|
||||
|
||||
service_map = {
|
||||
"Dropbox": "dropbox",
|
||||
"NextCloud": "nextcloud",
|
||||
"Paperless-ngx": "paperless",
|
||||
"Google Drive": "google_drive",
|
||||
"WebDAV": "webdav",
|
||||
"FTP Storage": "ftp",
|
||||
"SFTP Storage": "sftp",
|
||||
"Email": "email",
|
||||
"OneDrive": "onedrive",
|
||||
"S3 Storage": "s3"
|
||||
}
|
||||
|
||||
result = {}
|
||||
for provider_name, internal_name in service_map.items():
|
||||
if provider_name in providers:
|
||||
result[internal_name] = providers[provider_name].get('configured', False)
|
||||
|
||||
return result
|
||||
|
||||
@celery.task(base=BaseTaskWithRetry)
|
||||
def send_to_all_destinations(file_path: str, use_validator=True):
|
||||
"""
|
||||
Distribute a file to all configured storage destinations.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to distribute
|
||||
use_validator: Whether to use the config validator to determine enabled services
|
||||
(if False, falls back to individual checks)
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
@@ -131,12 +174,42 @@ def send_to_all_destinations(file_path: str):
|
||||
},
|
||||
]
|
||||
|
||||
# Optionally get configuration status from validator
|
||||
configured_services = {}
|
||||
if use_validator:
|
||||
try:
|
||||
configured_services = get_configured_services_from_validator()
|
||||
logger.info(f"Configured services according to validator: {configured_services}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get configuration from validator: {str(e)}")
|
||||
use_validator = False
|
||||
|
||||
# Process each service
|
||||
for service in services:
|
||||
if service["should_upload"]():
|
||||
logger.info(f"Queueing {file_path} for {service['name']} upload")
|
||||
task = service["upload_func"].delay(file_path)
|
||||
results[f"{service['name']}_task_id"] = task.id
|
||||
service_name = service["name"]
|
||||
|
||||
# Determine if service is configured
|
||||
is_configured = False
|
||||
if use_validator and service_name in configured_services:
|
||||
is_configured = configured_services[service_name]
|
||||
logger.debug(f"{service_name} configuration from validator: {is_configured}")
|
||||
else:
|
||||
try:
|
||||
is_configured = service["should_upload"]()
|
||||
logger.debug(f"{service_name} configuration from function: {is_configured}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking configuration for {service_name}: {str(e)}")
|
||||
is_configured = False
|
||||
|
||||
# Queue the upload task if service is configured
|
||||
if is_configured:
|
||||
logger.info(f"Queueing {file_path} for {service_name} upload")
|
||||
try:
|
||||
task = service["upload_func"].delay(file_path)
|
||||
results[f"{service_name}_task_id"] = task.id
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to queue {service_name} task: {str(e)}")
|
||||
results[f"{service_name}_error"] = str(e)
|
||||
|
||||
return {
|
||||
"status": "Queued",
|
||||
|
||||
@@ -8,6 +8,10 @@ import logging
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
from google.oauth2.service_account import Credentials
|
||||
from google.oauth2.credentials import Credentials as OAuthCredentials
|
||||
from google_auth_oauthlib.flow import Flow
|
||||
from google.auth.transport.requests import Request
|
||||
from google.auth.exceptions import RefreshError
|
||||
|
||||
from app.config import settings
|
||||
from app.tasks.retry_config import BaseTaskWithRetry
|
||||
@@ -15,12 +19,54 @@ from app.celery_app import celery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_drive_service_oauth():
|
||||
"""
|
||||
Get Google Drive service using OAuth credentials.
|
||||
Uses saved refresh token to get a new access token.
|
||||
"""
|
||||
try:
|
||||
# Check for required OAuth settings
|
||||
if not (settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token):
|
||||
logger.error("Google Drive OAuth credentials not fully configured")
|
||||
return None
|
||||
|
||||
# Create credentials object from refresh token
|
||||
credentials = OAuthCredentials(
|
||||
None, # No access token initially, will be refreshed
|
||||
refresh_token=settings.google_drive_refresh_token,
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=settings.google_drive_client_id,
|
||||
client_secret=settings.google_drive_client_secret,
|
||||
# Use only drive.file scope
|
||||
scopes=['https://www.googleapis.com/auth/drive.file']
|
||||
)
|
||||
|
||||
# Refresh the access token
|
||||
credentials.refresh(Request())
|
||||
|
||||
# Build and return the service
|
||||
service = build('drive', 'v3', credentials=credentials)
|
||||
return service
|
||||
|
||||
except RefreshError as e:
|
||||
logger.error(f"Failed to refresh Google Drive token: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to authenticate with Google Drive OAuth: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_google_drive_service():
|
||||
"""
|
||||
Authenticate with Google Drive API using service account credentials
|
||||
and return an authorized service object.
|
||||
"""
|
||||
try:
|
||||
# Check if we should use OAuth instead of service account
|
||||
if getattr(settings, 'google_drive_use_oauth', False):
|
||||
return get_drive_service_oauth()
|
||||
|
||||
# Load service account credentials from settings
|
||||
if not settings.google_drive_credentials_json:
|
||||
logger.error("Google Drive credentials not configured")
|
||||
|
||||
+18
-630
@@ -1,633 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configuration validation for the application.
|
||||
This file serves as a backward-compatible interface to the config_validator package.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import logging
|
||||
import inspect
|
||||
from app.config import settings
|
||||
# Import and re-export all functions from the new package
|
||||
from app.utils.config_validator.validators import validate_email_config, validate_storage_configs, check_all_configs
|
||||
from app.utils.config_validator.masking import mask_sensitive_value
|
||||
from app.utils.config_validator.providers import get_provider_status
|
||||
from app.utils.config_validator.settings_display import get_settings_for_display, dump_all_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def validate_email_config():
|
||||
"""Validates email configuration settings"""
|
||||
issues = []
|
||||
|
||||
# Check for required email settings
|
||||
if not getattr(settings, 'email_host', None):
|
||||
issues.append("EMAIL_HOST is not configured")
|
||||
if not getattr(settings, 'email_port', None):
|
||||
issues.append("EMAIL_PORT is not configured")
|
||||
|
||||
# Test SMTP server connectivity if host is configured
|
||||
if getattr(settings, 'email_host', None) and getattr(settings, 'email_port', None):
|
||||
try:
|
||||
# Attempt to resolve the hostname
|
||||
socket.gethostbyname(settings.email_host)
|
||||
except socket.gaierror:
|
||||
issues.append(f"Cannot resolve email host: {settings.email_host}")
|
||||
|
||||
# Check for authentication settings
|
||||
if not getattr(settings, 'email_username', None):
|
||||
issues.append("EMAIL_USERNAME is not configured")
|
||||
if not getattr(settings, 'email_password', None):
|
||||
issues.append("EMAIL_PASSWORD is not configured")
|
||||
|
||||
return issues
|
||||
|
||||
def validate_storage_configs():
|
||||
"""Validates configuration for all storage providers"""
|
||||
issues = {}
|
||||
|
||||
# Validate Dropbox config
|
||||
dropbox_issues = []
|
||||
if not (getattr(settings, 'dropbox_app_key', None) and
|
||||
getattr(settings, 'dropbox_app_secret', None) and
|
||||
getattr(settings, 'dropbox_refresh_token', None)):
|
||||
dropbox_issues.append("Dropbox credentials are not fully configured")
|
||||
issues['dropbox'] = dropbox_issues
|
||||
|
||||
# Validate Nextcloud config
|
||||
nextcloud_issues = []
|
||||
if not (getattr(settings, 'nextcloud_upload_url', None) and
|
||||
getattr(settings, 'nextcloud_username', None) and
|
||||
getattr(settings, 'nextcloud_password', None)):
|
||||
nextcloud_issues.append("Nextcloud credentials are not fully configured")
|
||||
issues['nextcloud'] = nextcloud_issues
|
||||
|
||||
# Validate SFTP config
|
||||
sftp_issues = []
|
||||
if not getattr(settings, 'sftp_host', None):
|
||||
sftp_issues.append("SFTP_HOST is not configured")
|
||||
|
||||
sftp_key_path = getattr(settings, 'sftp_private_key', None)
|
||||
if sftp_key_path and not os.path.exists(sftp_key_path):
|
||||
sftp_issues.append(f"SFTP_KEY_PATH file not found: {sftp_key_path}")
|
||||
|
||||
if not sftp_key_path and not getattr(settings, 'sftp_password', None):
|
||||
sftp_issues.append("Neither SFTP_KEY_PATH nor SFTP_PASSWORD is configured")
|
||||
|
||||
issues['sftp'] = sftp_issues
|
||||
|
||||
# Validate Email sending
|
||||
email_issues = []
|
||||
if not getattr(settings, 'email_host', None):
|
||||
email_issues.append("EMAIL_HOST is not configured")
|
||||
if not getattr(settings, 'email_default_recipient', None):
|
||||
email_issues.append("EMAIL_DEFAULT_RECIPIENT is not configured")
|
||||
issues['email'] = email_issues
|
||||
|
||||
# Validate S3
|
||||
s3_issues = []
|
||||
if not getattr(settings, 's3_bucket_name', None):
|
||||
s3_issues.append("S3_BUCKET_NAME is not configured")
|
||||
if not (getattr(settings, 'aws_access_key_id', None) and
|
||||
getattr(settings, 'aws_secret_access_key', None)):
|
||||
s3_issues.append("AWS credentials are not configured")
|
||||
issues['s3'] = s3_issues
|
||||
|
||||
# Validate FTP
|
||||
ftp_issues = []
|
||||
if not getattr(settings, 'ftp_host', None):
|
||||
ftp_issues.append("FTP_HOST is not configured")
|
||||
if not getattr(settings, 'ftp_username', None):
|
||||
ftp_issues.append("FTP_USERNAME is not configured")
|
||||
if not getattr(settings, 'ftp_password', None):
|
||||
ftp_issues.append("FTP_PASSWORD is not configured")
|
||||
issues['ftp'] = ftp_issues
|
||||
|
||||
# Validate WebDAV
|
||||
webdav_issues = []
|
||||
if not getattr(settings, 'webdav_url', None):
|
||||
webdav_issues.append("WEBDAV_URL is not configured")
|
||||
if not getattr(settings, 'webdav_username', None):
|
||||
webdav_issues.append("WEBDAV_USERNAME is not configured")
|
||||
if not getattr(settings, 'webdav_password', None):
|
||||
webdav_issues.append("WEBDAV_PASSWORD is not configured")
|
||||
issues['webdav'] = webdav_issues
|
||||
|
||||
# Validate Google Drive
|
||||
gdrive_issues = []
|
||||
if not getattr(settings, 'google_drive_credentials_json', None):
|
||||
gdrive_issues.append("GOOGLE_DRIVE_CREDENTIALS_JSON is not configured")
|
||||
if not getattr(settings, 'google_drive_folder_id', None):
|
||||
gdrive_issues.append("GOOGLE_DRIVE_FOLDER_ID is not configured")
|
||||
issues['google_drive'] = gdrive_issues
|
||||
|
||||
# Validate Paperless
|
||||
paperless_issues = []
|
||||
if not getattr(settings, 'paperless_host', None):
|
||||
paperless_issues.append("PAPERLESS_HOST is not configured")
|
||||
if not getattr(settings, 'paperless_ngx_api_token', None):
|
||||
paperless_issues.append("PAPERLESS_NGX_API_TOKEN is not configured")
|
||||
issues['paperless'] = paperless_issues
|
||||
|
||||
# Validate OneDrive
|
||||
onedrive_issues = []
|
||||
if not (getattr(settings, 'onedrive_client_id', None) and
|
||||
getattr(settings, 'onedrive_client_secret', None) and
|
||||
getattr(settings, 'onedrive_refresh_token', None)):
|
||||
onedrive_issues.append("OneDrive credentials are not fully configured")
|
||||
issues['onedrive'] = onedrive_issues
|
||||
|
||||
# Validate Uptime Kuma
|
||||
uptime_kuma_issues = []
|
||||
if not getattr(settings, 'uptime_kuma_url', None):
|
||||
uptime_kuma_issues.append("UPTIME_KUMA_URL is not configured")
|
||||
issues['uptime_kuma'] = uptime_kuma_issues
|
||||
|
||||
return issues
|
||||
|
||||
def mask_sensitive_value(value):
|
||||
"""
|
||||
Masks sensitive values like API keys in logs and output
|
||||
"""
|
||||
# Return masked value for sensitive data
|
||||
if value and isinstance(value, str) and len(value) > 8:
|
||||
return value[:4] + "*" * (len(value) - 4)
|
||||
return value
|
||||
|
||||
def get_provider_status():
|
||||
"""
|
||||
Returns status information for all configured providers
|
||||
"""
|
||||
providers = {}
|
||||
|
||||
# Add AI services first
|
||||
providers["OpenAI"] = {
|
||||
"name": "OpenAI",
|
||||
"icon": "fa-brands fa-openai",
|
||||
"configured": bool(getattr(settings, 'openai_api_key', None) and
|
||||
str(getattr(settings, 'openai_api_key', '')).startswith('sk-')),
|
||||
"enabled": True,
|
||||
"description": "AI-powered document analysis and metadata extraction",
|
||||
"details": {
|
||||
"api_key": mask_sensitive_value(getattr(settings, 'openai_api_key', None)),
|
||||
"base_url": getattr(settings, 'openai_base_url', 'https://api.openai.com/v1'),
|
||||
"model": getattr(settings, 'openai_model', 'gpt-4')
|
||||
}
|
||||
}
|
||||
|
||||
providers["Azure AI"] = {
|
||||
"name": "Azure AI",
|
||||
"icon": "fa-solid fa-robot",
|
||||
"configured": bool(getattr(settings, 'azure_ai_key', None) and
|
||||
getattr(settings, 'azure_endpoint', None)),
|
||||
"enabled": True,
|
||||
"description": "Microsoft Azure Document Intelligence",
|
||||
"details": {
|
||||
"api_key": mask_sensitive_value(getattr(settings, 'azure_ai_key', None)),
|
||||
"endpoint": getattr(settings, 'azure_endpoint', 'Not set'),
|
||||
"region": getattr(settings, 'azure_region', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add Dropbox configuration - alphabetically ordered providers
|
||||
providers["Dropbox"] = {
|
||||
"name": "Dropbox",
|
||||
"icon": "fa-brands fa-dropbox",
|
||||
"configured": bool(getattr(settings, 'dropbox_app_key', None) and
|
||||
getattr(settings, 'dropbox_app_secret', None) and
|
||||
getattr(settings, 'dropbox_refresh_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Upload files to Dropbox cloud storage",
|
||||
"details": {
|
||||
"folder": getattr(settings, 'dropbox_folder', 'Not set'),
|
||||
"app_key": getattr(settings, 'dropbox_app_key', 'Not set'),
|
||||
"app_secret": mask_sensitive_value(getattr(settings, 'dropbox_app_secret', None)),
|
||||
"refresh_token": mask_sensitive_value(getattr(settings, 'dropbox_refresh_token', None))
|
||||
}
|
||||
}
|
||||
|
||||
# Add Email configuration
|
||||
providers["Email"] = {
|
||||
"name": "Email",
|
||||
"icon": "fa-solid fa-envelope",
|
||||
"configured": bool(getattr(settings, 'email_host', None) and
|
||||
getattr(settings, 'email_default_recipient', None)),
|
||||
"enabled": True,
|
||||
"description": "Send documents via email",
|
||||
"details": {
|
||||
"host": getattr(settings, 'email_host', 'Not set'),
|
||||
"port": getattr(settings, 'email_port', 'Not set'),
|
||||
"username": getattr(settings, 'email_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'email_password', None)),
|
||||
"use_tls": getattr(settings, 'email_use_tls', 'Not set'),
|
||||
"sender": getattr(settings, 'email_sender', 'Not set'),
|
||||
"default_recipient": getattr(settings, 'email_default_recipient', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add FTP configuration to providers
|
||||
providers["FTP Storage"] = {
|
||||
"name": "FTP Storage",
|
||||
"icon": "fa-solid fa-server",
|
||||
"configured": bool(getattr(settings, 'ftp_host', None) and
|
||||
getattr(settings, 'ftp_username', None) and
|
||||
getattr(settings, 'ftp_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Upload files to FTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, 'ftp_host', 'Not set'),
|
||||
"port": getattr(settings, 'ftp_port', 'Not set'),
|
||||
"username": getattr(settings, 'ftp_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'ftp_password', None)),
|
||||
"folder": getattr(settings, 'ftp_folder', 'Not set'),
|
||||
"tls": getattr(settings, 'ftp_use_tls', True),
|
||||
"allow_plaintext": getattr(settings, 'ftp_allow_plaintext', True)
|
||||
}
|
||||
}
|
||||
|
||||
# Check Google Drive configuration
|
||||
providers["Google Drive"] = {
|
||||
"name": "Google Drive",
|
||||
"icon": "fa-brands fa-google-drive",
|
||||
"configured": bool(getattr(settings, 'google_drive_credentials_json', None) and
|
||||
getattr(settings, 'google_drive_folder_id', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in Google Drive",
|
||||
"details": {
|
||||
"credentials_json": mask_sensitive_value(getattr(settings, 'google_drive_credentials_json', None)),
|
||||
"folder_id": getattr(settings, 'google_drive_folder_id', 'Not set'),
|
||||
"delegate": getattr(settings, 'google_drive_delegate_to', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check NextCloud configuration
|
||||
nextcloud_url = getattr(settings, 'nextcloud_upload_url', 'Not set')
|
||||
# Extract base URL from WebDAV URL (remove the /remote.php part and everything after it)
|
||||
nextcloud_base_url = nextcloud_url
|
||||
if nextcloud_url != 'Not set' and nextcloud_url is not None and '/remote.php' in nextcloud_url:
|
||||
nextcloud_base_url = nextcloud_url.split('/remote.php')[0]
|
||||
|
||||
providers["NextCloud"] = {
|
||||
"name": "NextCloud",
|
||||
"icon": "fa-solid fa-cloud",
|
||||
"configured": bool(getattr(settings, 'nextcloud_upload_url', None) and
|
||||
getattr(settings, 'nextcloud_username', None) and
|
||||
getattr(settings, 'nextcloud_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in NextCloud",
|
||||
"details": {
|
||||
"url": getattr(settings, 'nextcloud_upload_url', 'Not set'),
|
||||
"base_url": nextcloud_base_url,
|
||||
"username": getattr(settings, 'nextcloud_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'nextcloud_password', None)),
|
||||
"folder": getattr(settings, 'nextcloud_folder', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check OneDrive configuration
|
||||
providers["OneDrive"] = {
|
||||
"name": "OneDrive",
|
||||
"icon": "fa-brands fa-microsoft",
|
||||
"configured": bool(getattr(settings, 'onedrive_client_id', None) and
|
||||
getattr(settings, 'onedrive_client_secret', None) and
|
||||
getattr(settings, 'onedrive_refresh_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in Microsoft OneDrive",
|
||||
"details": {
|
||||
"client_id": getattr(settings, 'onedrive_client_id', 'Not set'),
|
||||
"client_secret": mask_sensitive_value(getattr(settings, 'onedrive_client_secret', None)),
|
||||
"tenant_id": getattr(settings, 'onedrive_tenant_id', 'Not set'),
|
||||
"refresh_token": mask_sensitive_value(getattr(settings, 'onedrive_refresh_token', None)),
|
||||
"folder": getattr(settings, 'onedrive_folder_path', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check Paperless configuration
|
||||
providers["Paperless-ngx"] = {
|
||||
"name": "Paperless-ngx",
|
||||
"icon": "fa-solid fa-file-lines",
|
||||
"configured": bool(getattr(settings, 'paperless_host', None) and
|
||||
getattr(settings, 'paperless_ngx_api_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Document management system for digital archives",
|
||||
"details": {
|
||||
"host": getattr(settings, 'paperless_host', 'Not set'),
|
||||
"api_token": mask_sensitive_value(getattr(settings, 'paperless_ngx_api_token', None))
|
||||
}
|
||||
}
|
||||
|
||||
# Check S3 configuration
|
||||
providers["S3 Storage"] = {
|
||||
"name": "S3 Storage",
|
||||
"icon": "fa-brands fa-aws",
|
||||
"configured": bool(getattr(settings, 's3_bucket_name', None) and
|
||||
getattr(settings, 'aws_access_key_id', None) and
|
||||
getattr(settings, 'aws_secret_access_key', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in S3-compatible object storage",
|
||||
"details": {
|
||||
"bucket": getattr(settings, 's3_bucket_name', 'Not set'),
|
||||
"region": getattr(settings, 'aws_region', 'Not set'),
|
||||
"access_key_id": getattr(settings, 'aws_access_key_id', 'Not set'),
|
||||
"secret_access_key": mask_sensitive_value(getattr(settings, 'aws_secret_access_key', None)),
|
||||
"folder_prefix": getattr(settings, 's3_folder_prefix', 'Not set'),
|
||||
"storage_class": getattr(settings, 's3_storage_class', 'Not set'),
|
||||
"acl": getattr(settings, 's3_acl', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check SFTP configuration
|
||||
providers["SFTP Storage"] = {
|
||||
"name": "SFTP Storage",
|
||||
"icon": "fa-solid fa-lock",
|
||||
"configured": bool(getattr(settings, 'sftp_host', None) and
|
||||
getattr(settings, 'sftp_username', None) and
|
||||
(getattr(settings, 'sftp_password', None) or
|
||||
getattr(settings, 'sftp_private_key', None))),
|
||||
"enabled": True,
|
||||
"description": "Upload files to SFTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, 'sftp_host', 'Not set'),
|
||||
"port": getattr(settings, 'sftp_port', 'Not set'),
|
||||
"username": getattr(settings, 'sftp_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'sftp_password', None)),
|
||||
"private_key": getattr(settings, 'sftp_private_key', 'Not set'),
|
||||
"private_key_passphrase": mask_sensitive_value(getattr(settings, 'sftp_private_key_passphrase', None)),
|
||||
"folder": getattr(settings, 'sftp_folder', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add Uptime Kuma configuration
|
||||
providers["Uptime Kuma"] = {
|
||||
"name": "Uptime Kuma",
|
||||
"icon": "fa-solid fa-heart-pulse",
|
||||
"configured": bool(getattr(settings, 'uptime_kuma_url', None)),
|
||||
"enabled": True,
|
||||
"description": "Server monitoring and status page",
|
||||
"details": {
|
||||
"url": getattr(settings, 'uptime_kuma_url', 'Not set'),
|
||||
"ping_interval": getattr(settings, 'uptime_kuma_ping_interval', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check WebDAV configuration
|
||||
providers["WebDAV"] = {
|
||||
"name": "WebDAV",
|
||||
"icon": "fa-solid fa-globe",
|
||||
"configured": bool(getattr(settings, 'webdav_url', None) and
|
||||
getattr(settings, 'webdav_username', None) and
|
||||
getattr(settings, 'webdav_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents on WebDAV servers",
|
||||
"details": {
|
||||
"url": getattr(settings, 'webdav_url', 'Not set'),
|
||||
"username": getattr(settings, 'webdav_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'webdav_password', None)),
|
||||
"folder": getattr(settings, 'webdav_folder', 'Not set'),
|
||||
"verify_ssl": getattr(settings, 'webdav_verify_ssl', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
|
||||
def dump_all_settings():
|
||||
"""Log all settings values for diagnostic purposes"""
|
||||
logger.info("--- DUMPING ALL SETTINGS FOR DIAGNOSTIC PURPOSES ---")
|
||||
for key in dir(settings):
|
||||
if not key.startswith('_') and not callable(getattr(settings, key)):
|
||||
value = getattr(settings, key)
|
||||
# Mask sensitive values in logs
|
||||
if key.lower().find('password') >= 0 or key.lower().find('secret') >= 0 or key.lower().find('token') >= 0 or key.lower().find('key') >= 0:
|
||||
if value:
|
||||
if isinstance(value, str) and len(value) > 10:
|
||||
visible_start = max(1, len(value) // 3)
|
||||
visible_end = max(1, len(value) // 4)
|
||||
value = f"{value[:visible_start]}{'*' * (len(value) - visible_start - visible_end)}{value[-visible_end:]}"
|
||||
else:
|
||||
value = f"{value[:2]}{'*' * (len(value) - 4)}{value[-2:]}" if isinstance(value, str) and len(value) > 4 else "****"
|
||||
logger.info(f"{key}: {value}")
|
||||
logger.info("--- END OF SETTINGS DUMP ---")
|
||||
|
||||
def get_settings_for_display(show_values=False):
|
||||
"""
|
||||
Group settings into logical categories and check if they are configured.
|
||||
Returns a dictionary with categories as keys and lists of setting items as values.
|
||||
Each setting item is a dict with name, value, and is_configured.
|
||||
|
||||
If show_values is False, sensitive values are masked.
|
||||
"""
|
||||
# First include system info with version in result
|
||||
result = {
|
||||
"System Info": [
|
||||
{
|
||||
"name": "App Version",
|
||||
"value": settings.version,
|
||||
"is_configured": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Define categories and their settings
|
||||
categories = {
|
||||
"Core": [
|
||||
"debug", # Explicitly include debug setting
|
||||
"external_hostname",
|
||||
"workdir",
|
||||
"database_url",
|
||||
"redis_url",
|
||||
"gotenberg_url"
|
||||
],
|
||||
"Authentication": [
|
||||
"auth_enabled",
|
||||
"authentik_client_id",
|
||||
"authentik_client_secret",
|
||||
"authentik_config_url"
|
||||
],
|
||||
"Email": [
|
||||
"email_host",
|
||||
"email_port",
|
||||
"email_username",
|
||||
"email_password",
|
||||
"email_use_tls",
|
||||
"email_sender",
|
||||
"email_default_recipient"
|
||||
],
|
||||
"IMAP": [
|
||||
"imap1_host",
|
||||
"imap1_port",
|
||||
"imap1_username",
|
||||
"imap1_password",
|
||||
"imap1_ssl",
|
||||
"imap1_poll_interval_minutes",
|
||||
"imap1_delete_after_process",
|
||||
"imap2_host",
|
||||
"imap2_port",
|
||||
"imap2_username",
|
||||
"imap2_password",
|
||||
"imap2_ssl",
|
||||
"imap2_poll_interval_minutes",
|
||||
"imap2_delete_after_process"
|
||||
],
|
||||
"Dropbox": [
|
||||
"dropbox_app_key",
|
||||
"dropbox_app_secret",
|
||||
"dropbox_folder",
|
||||
"dropbox_refresh_token"
|
||||
],
|
||||
"NextCloud": [
|
||||
"nextcloud_upload_url",
|
||||
"nextcloud_username",
|
||||
"nextcloud_password",
|
||||
"nextcloud_folder"
|
||||
],
|
||||
"Paperless": [
|
||||
"paperless_host",
|
||||
"paperless_ngx_api_token"
|
||||
],
|
||||
"Google Drive": [
|
||||
"google_drive_credentials_json",
|
||||
"google_drive_folder_id",
|
||||
"google_drive_delegate_to"
|
||||
],
|
||||
"OneDrive": [
|
||||
"onedrive_client_id",
|
||||
"onedrive_client_secret",
|
||||
"onedrive_tenant_id",
|
||||
"onedrive_refresh_token",
|
||||
"onedrive_folder_path"
|
||||
],
|
||||
"WebDAV": [
|
||||
"webdav_url",
|
||||
"webdav_username",
|
||||
"webdav_password",
|
||||
"webdav_folder",
|
||||
"webdav_verify_ssl"
|
||||
],
|
||||
"SFTP": [
|
||||
"sftp_host",
|
||||
"sftp_port",
|
||||
"sftp_username",
|
||||
"sftp_password",
|
||||
"sftp_folder",
|
||||
"sftp_private_key",
|
||||
"sftp_private_key_passphrase"
|
||||
],
|
||||
"FTP": [
|
||||
"ftp_host",
|
||||
"ftp_port",
|
||||
"ftp_username",
|
||||
"ftp_password",
|
||||
"ftp_folder",
|
||||
"ftp_use_tls",
|
||||
"ftp_allow_plaintext"
|
||||
],
|
||||
"S3/AWS": [
|
||||
"aws_access_key_id",
|
||||
"aws_secret_access_key",
|
||||
"aws_region",
|
||||
"s3_bucket_name",
|
||||
"s3_folder_prefix",
|
||||
"s3_storage_class",
|
||||
"s3_acl"
|
||||
],
|
||||
"AI Services": [
|
||||
"openai_api_key",
|
||||
"openai_base_url",
|
||||
"openai_model",
|
||||
"azure_ai_key",
|
||||
"azure_endpoint",
|
||||
"azure_region"
|
||||
],
|
||||
"Monitoring": [
|
||||
"uptime_kuma_url",
|
||||
"uptime_kuma_ping_interval"
|
||||
]
|
||||
}
|
||||
|
||||
# Handle any settings that don't fit into the predefined categories
|
||||
all_settings = set([key for key in dir(settings)
|
||||
if not key.startswith('_') and
|
||||
not callable(getattr(settings, key)) and
|
||||
key not in ["model_computed_fields", "model_config",
|
||||
"model_extra", "model_fields",
|
||||
"model_fields_set"]])
|
||||
|
||||
# Ensure 'version' is excluded since we display it separately
|
||||
all_settings.discard("version")
|
||||
|
||||
categorized_settings = set()
|
||||
for cat_settings in categories.values():
|
||||
categorized_settings.update(cat_settings)
|
||||
|
||||
uncategorized = all_settings - categorized_settings
|
||||
if uncategorized:
|
||||
categories["Other"] = list(uncategorized)
|
||||
|
||||
# Build the result
|
||||
for category, setting_keys in categories.items():
|
||||
items = []
|
||||
for key in setting_keys:
|
||||
if hasattr(settings, key):
|
||||
value = getattr(settings, key)
|
||||
|
||||
# List of patterns that indicate sensitive values
|
||||
sensitive_patterns = [
|
||||
'password', 'secret', 'token', 'api_key', 'private_key',
|
||||
'credentials', 'access_key', 'ai_key'
|
||||
]
|
||||
|
||||
# Check if this is a sensitive value that should be masked
|
||||
is_sensitive = any(
|
||||
pattern in key.lower() for pattern in sensitive_patterns
|
||||
)
|
||||
|
||||
# Special handling for "auth" to avoid matching prefixes like "authentik"
|
||||
if not is_sensitive and "auth" in key.lower():
|
||||
# Only mark as sensitive if "auth" is a standalone word or at the end
|
||||
# This avoids matching "authentik" as sensitive
|
||||
parts = key.lower().split('_')
|
||||
is_sensitive = any(part == "auth" for part in parts) or key.lower().endswith("auth")
|
||||
|
||||
# Mask sensitive values regardless of debug mode
|
||||
# Other values are only hidden if debug mode is off AND show_values is False
|
||||
if (is_sensitive or not show_values) and value:
|
||||
if is_sensitive:
|
||||
value = mask_sensitive_value(value)
|
||||
|
||||
# Check if the setting is configured (has a non-None value)
|
||||
# For boolean settings, consider them configured even if False
|
||||
is_configured = value is not None
|
||||
if is_configured and isinstance(value, str):
|
||||
is_configured = len(value) > 0
|
||||
|
||||
items.append({
|
||||
"name": key,
|
||||
"value": value,
|
||||
"is_configured": is_configured
|
||||
})
|
||||
|
||||
if items: # Only add categories that have items
|
||||
result[category] = items
|
||||
|
||||
return result
|
||||
|
||||
def check_all_configs():
|
||||
"""Run all configuration validations and log results"""
|
||||
logger.info("Validating application configuration...")
|
||||
|
||||
# Check if debug is enabled and dump all settings if it is
|
||||
if hasattr(settings, 'debug') and settings.debug:
|
||||
dump_all_settings()
|
||||
|
||||
# Check email config
|
||||
email_issues = validate_email_config()
|
||||
if email_issues:
|
||||
logger.warning(f"Email configuration issues: {', '.join(email_issues)}")
|
||||
else:
|
||||
logger.info("Email configuration OK")
|
||||
|
||||
# Check storage configs
|
||||
storage_issues = validate_storage_configs()
|
||||
for provider, issues in storage_issues.items():
|
||||
if issues:
|
||||
logger.warning(f"{provider.capitalize()} configuration issues: {', '.join(issues)}")
|
||||
else:
|
||||
logger.info(f"{provider.capitalize()} configuration OK")
|
||||
|
||||
# Return all identified issues
|
||||
return {
|
||||
'email': email_issues,
|
||||
'storage': storage_issues
|
||||
}
|
||||
__all__ = [
|
||||
'validate_email_config',
|
||||
'validate_storage_configs',
|
||||
'mask_sensitive_value',
|
||||
'get_provider_status',
|
||||
'get_settings_for_display',
|
||||
'dump_all_settings',
|
||||
'check_all_configs'
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Configuration validation package for the application.
|
||||
"""
|
||||
|
||||
from app.utils.config_validator.validators import (
|
||||
validate_email_config,
|
||||
validate_storage_configs,
|
||||
check_all_configs
|
||||
)
|
||||
from app.utils.config_validator.masking import mask_sensitive_value
|
||||
from app.utils.config_validator.providers import get_provider_status
|
||||
from app.utils.config_validator.settings_display import (
|
||||
get_settings_for_display,
|
||||
dump_all_settings
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'validate_email_config',
|
||||
'validate_storage_configs',
|
||||
'mask_sensitive_value',
|
||||
'get_provider_status',
|
||||
'get_settings_for_display',
|
||||
'dump_all_settings',
|
||||
'check_all_configs'
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Module for masking sensitive information in configuration values
|
||||
"""
|
||||
|
||||
def mask_sensitive_value(value):
|
||||
"""
|
||||
Masks sensitive values like API keys in logs and output
|
||||
"""
|
||||
# Return masked value for sensitive data
|
||||
if value and isinstance(value, str) and len(value) > 8:
|
||||
return value[:4] + "*" * (len(value) - 4)
|
||||
return value
|
||||
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Module for handling provider status information
|
||||
"""
|
||||
|
||||
from app.config import settings
|
||||
from app.utils.config_validator.masking import mask_sensitive_value
|
||||
|
||||
def get_provider_status():
|
||||
"""
|
||||
Returns status information for all configured providers
|
||||
"""
|
||||
providers = {}
|
||||
|
||||
# Add AI services first
|
||||
providers["OpenAI"] = {
|
||||
"name": "OpenAI",
|
||||
"icon": "fa-brands fa-openai",
|
||||
"configured": bool(getattr(settings, 'openai_api_key', None) and
|
||||
str(getattr(settings, 'openai_api_key', '')).startswith('sk-')),
|
||||
"enabled": True,
|
||||
"description": "AI-powered document analysis and metadata extraction",
|
||||
"details": {
|
||||
"api_key": mask_sensitive_value(getattr(settings, 'openai_api_key', None)),
|
||||
"base_url": getattr(settings, 'openai_base_url', 'https://api.openai.com/v1'),
|
||||
"model": getattr(settings, 'openai_model', 'gpt-4')
|
||||
}
|
||||
}
|
||||
|
||||
providers["Azure AI"] = {
|
||||
"name": "Azure AI",
|
||||
"icon": "fa-solid fa-robot",
|
||||
"configured": bool(getattr(settings, 'azure_ai_key', None) and
|
||||
getattr(settings, 'azure_endpoint', None)),
|
||||
"enabled": True,
|
||||
"description": "Microsoft Azure Document Intelligence",
|
||||
"details": {
|
||||
"api_key": mask_sensitive_value(getattr(settings, 'azure_ai_key', None)),
|
||||
"endpoint": getattr(settings, 'azure_endpoint', 'Not set'),
|
||||
"region": getattr(settings, 'azure_region', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add Dropbox configuration - alphabetically ordered providers
|
||||
providers["Dropbox"] = {
|
||||
"name": "Dropbox",
|
||||
"icon": "fa-brands fa-dropbox",
|
||||
"configured": bool(getattr(settings, 'dropbox_app_key', None) and
|
||||
getattr(settings, 'dropbox_app_secret', None) and
|
||||
getattr(settings, 'dropbox_refresh_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Upload files to Dropbox cloud storage",
|
||||
"details": {
|
||||
"folder": getattr(settings, 'dropbox_folder', 'Not set'),
|
||||
"app_key": getattr(settings, 'dropbox_app_key', 'Not set'),
|
||||
"app_secret": mask_sensitive_value(getattr(settings, 'dropbox_app_secret', None)),
|
||||
"refresh_token": mask_sensitive_value(getattr(settings, 'dropbox_refresh_token', None))
|
||||
}
|
||||
}
|
||||
|
||||
# Add Email configuration
|
||||
providers["Email"] = {
|
||||
"name": "Email",
|
||||
"icon": "fa-solid fa-envelope",
|
||||
"configured": bool(getattr(settings, 'email_host', None) and
|
||||
getattr(settings, 'email_default_recipient', None)),
|
||||
"enabled": True,
|
||||
"description": "Send documents via email",
|
||||
"details": {
|
||||
"host": getattr(settings, 'email_host', 'Not set'),
|
||||
"port": getattr(settings, 'email_port', 'Not set'),
|
||||
"username": getattr(settings, 'email_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'email_password', None)),
|
||||
"use_tls": getattr(settings, 'email_use_tls', 'Not set'),
|
||||
"sender": getattr(settings, 'email_sender', 'Not set'),
|
||||
"default_recipient": getattr(settings, 'email_default_recipient', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add FTP configuration to providers
|
||||
providers["FTP Storage"] = {
|
||||
"name": "FTP Storage",
|
||||
"icon": "fa-solid fa-server",
|
||||
"configured": bool(getattr(settings, 'ftp_host', None) and
|
||||
getattr(settings, 'ftp_username', None) and
|
||||
getattr(settings, 'ftp_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Upload files to FTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, 'ftp_host', 'Not set'),
|
||||
"port": getattr(settings, 'ftp_port', 'Not set'),
|
||||
"username": getattr(settings, 'ftp_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'ftp_password', None)),
|
||||
"folder": getattr(settings, 'ftp_folder', 'Not set'),
|
||||
"tls": getattr(settings, 'ftp_use_tls', True),
|
||||
"allow_plaintext": getattr(settings, 'ftp_allow_plaintext', True)
|
||||
}
|
||||
}
|
||||
|
||||
# Check Google Drive configuration
|
||||
gdrive_oauth_configured = bool(getattr(settings, 'google_drive_client_id', None) and
|
||||
getattr(settings, 'google_drive_client_secret', None) and
|
||||
getattr(settings, 'google_drive_refresh_token', None))
|
||||
|
||||
gdrive_sa_configured = bool(getattr(settings, 'google_drive_credentials_json', None))
|
||||
|
||||
# Determine if using OAuth or service account
|
||||
use_oauth = getattr(settings, 'google_drive_use_oauth', False)
|
||||
|
||||
is_configured = (use_oauth and gdrive_oauth_configured) or (not use_oauth and gdrive_sa_configured)
|
||||
|
||||
providers["Google Drive"] = {
|
||||
"name": "Google Drive",
|
||||
"icon": "fa-brands fa-google-drive",
|
||||
"configured": is_configured and bool(getattr(settings, 'google_drive_folder_id', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in Google Drive",
|
||||
"details": {
|
||||
"auth_type": "OAuth" if use_oauth else "Service Account",
|
||||
"client_id": getattr(settings, 'google_drive_client_id', 'Not set') if use_oauth else 'N/A',
|
||||
"client_secret": mask_sensitive_value(getattr(settings, 'google_drive_client_secret', None)) if use_oauth else 'N/A',
|
||||
"refresh_token": mask_sensitive_value(getattr(settings, 'google_drive_refresh_token', None)) if use_oauth else 'N/A',
|
||||
"credentials_json": mask_sensitive_value(getattr(settings, 'google_drive_credentials_json', None)) if not use_oauth else 'N/A',
|
||||
"folder_id": getattr(settings, 'google_drive_folder_id', 'Not set'),
|
||||
"delegate": getattr(settings, 'google_drive_delegate_to', 'Not set') if not use_oauth else 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
# Check NextCloud configuration
|
||||
nextcloud_url = getattr(settings, 'nextcloud_upload_url', 'Not set')
|
||||
# Extract base URL from WebDAV URL (remove the /remote.php part and everything after it)
|
||||
nextcloud_base_url = nextcloud_url
|
||||
if nextcloud_url != 'Not set' and nextcloud_url is not None and '/remote.php' in nextcloud_url:
|
||||
nextcloud_base_url = nextcloud_url.split('/remote.php')[0]
|
||||
|
||||
providers["NextCloud"] = {
|
||||
"name": "NextCloud",
|
||||
"icon": "fa-solid fa-cloud",
|
||||
"configured": bool(getattr(settings, 'nextcloud_upload_url', None) and
|
||||
getattr(settings, 'nextcloud_username', None) and
|
||||
getattr(settings, 'nextcloud_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in NextCloud",
|
||||
"details": {
|
||||
"url": getattr(settings, 'nextcloud_upload_url', 'Not set'),
|
||||
"base_url": nextcloud_base_url,
|
||||
"username": getattr(settings, 'nextcloud_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'nextcloud_password', None)),
|
||||
"folder": getattr(settings, 'nextcloud_folder', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check OneDrive configuration
|
||||
providers["OneDrive"] = {
|
||||
"name": "OneDrive",
|
||||
"icon": "fa-brands fa-microsoft",
|
||||
"configured": bool(getattr(settings, 'onedrive_client_id', None) and
|
||||
getattr(settings, 'onedrive_client_secret', None) and
|
||||
getattr(settings, 'onedrive_refresh_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in Microsoft OneDrive",
|
||||
"details": {
|
||||
"client_id": getattr(settings, 'onedrive_client_id', 'Not set'),
|
||||
"client_secret": mask_sensitive_value(getattr(settings, 'onedrive_client_secret', None)),
|
||||
"tenant_id": getattr(settings, 'onedrive_tenant_id', 'Not set'),
|
||||
"refresh_token": mask_sensitive_value(getattr(settings, 'onedrive_refresh_token', None)),
|
||||
"folder": getattr(settings, 'onedrive_folder_path', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check Paperless configuration
|
||||
providers["Paperless-ngx"] = {
|
||||
"name": "Paperless-ngx",
|
||||
"icon": "fa-solid fa-file-lines",
|
||||
"configured": bool(getattr(settings, 'paperless_host', None) and
|
||||
getattr(settings, 'paperless_ngx_api_token', None)),
|
||||
"enabled": True,
|
||||
"description": "Document management system for digital archives",
|
||||
"details": {
|
||||
"host": getattr(settings, 'paperless_host', 'Not set'),
|
||||
"api_token": mask_sensitive_value(getattr(settings, 'paperless_ngx_api_token', None))
|
||||
}
|
||||
}
|
||||
|
||||
# Check S3 configuration
|
||||
providers["S3 Storage"] = {
|
||||
"name": "S3 Storage",
|
||||
"icon": "fa-brands fa-aws",
|
||||
"configured": bool(getattr(settings, 's3_bucket_name', None) and
|
||||
getattr(settings, 'aws_access_key_id', None) and
|
||||
getattr(settings, 'aws_secret_access_key', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents in S3-compatible object storage",
|
||||
"details": {
|
||||
"bucket": getattr(settings, 's3_bucket_name', 'Not set'),
|
||||
"region": getattr(settings, 'aws_region', 'Not set'),
|
||||
"access_key_id": getattr(settings, 'aws_access_key_id', 'Not set'),
|
||||
"secret_access_key": mask_sensitive_value(getattr(settings, 'aws_secret_access_key', None)),
|
||||
"folder_prefix": getattr(settings, 's3_folder_prefix', 'Not set'),
|
||||
"storage_class": getattr(settings, 's3_storage_class', 'Not set'),
|
||||
"acl": getattr(settings, 's3_acl', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check SFTP configuration
|
||||
providers["SFTP Storage"] = {
|
||||
"name": "SFTP Storage",
|
||||
"icon": "fa-solid fa-lock",
|
||||
"configured": bool(getattr(settings, 'sftp_host', None) and
|
||||
getattr(settings, 'sftp_username', None) and
|
||||
(getattr(settings, 'sftp_password', None) or
|
||||
getattr(settings, 'sftp_private_key', None))),
|
||||
"enabled": True,
|
||||
"description": "Upload files to SFTP server",
|
||||
"details": {
|
||||
"host": getattr(settings, 'sftp_host', 'Not set'),
|
||||
"port": getattr(settings, 'sftp_port', 'Not set'),
|
||||
"username": getattr(settings, 'sftp_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'sftp_password', None)),
|
||||
"private_key": getattr(settings, 'sftp_private_key', 'Not set'),
|
||||
"private_key_passphrase": mask_sensitive_value(getattr(settings, 'sftp_private_key_passphrase', None)),
|
||||
"folder": getattr(settings, 'sftp_folder', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add Uptime Kuma configuration
|
||||
providers["Uptime Kuma"] = {
|
||||
"name": "Uptime Kuma",
|
||||
"icon": "fa-solid fa-heart-pulse",
|
||||
"configured": bool(getattr(settings, 'uptime_kuma_url', None)),
|
||||
"enabled": True,
|
||||
"description": "Server monitoring and status page",
|
||||
"details": {
|
||||
"url": getattr(settings, 'uptime_kuma_url', 'Not set'),
|
||||
"ping_interval": getattr(settings, 'uptime_kuma_ping_interval', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Check WebDAV configuration
|
||||
providers["WebDAV"] = {
|
||||
"name": "WebDAV",
|
||||
"icon": "fa-solid fa-globe",
|
||||
"configured": bool(getattr(settings, 'webdav_url', None) and
|
||||
getattr(settings, 'webdav_username', None) and
|
||||
getattr(settings, 'webdav_password', None)),
|
||||
"enabled": True,
|
||||
"description": "Store documents on WebDAV servers",
|
||||
"details": {
|
||||
"url": getattr(settings, 'webdav_url', 'Not set'),
|
||||
"username": getattr(settings, 'webdav_username', 'Not set'),
|
||||
"password": mask_sensitive_value(getattr(settings, 'webdav_password', None)),
|
||||
"folder": getattr(settings, 'webdav_folder', 'Not set'),
|
||||
"verify_ssl": getattr(settings, 'webdav_verify_ssl', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Module for displaying and organizing settings information
|
||||
"""
|
||||
|
||||
import logging
|
||||
from app.config import settings
|
||||
from app.utils.config_validator.masking import mask_sensitive_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def dump_all_settings():
|
||||
"""Log all settings values for diagnostic purposes"""
|
||||
logger.info("--- DUMPING ALL SETTINGS FOR DIAGNOSTIC PURPOSES ---")
|
||||
for key in dir(settings):
|
||||
if not key.startswith('_') and not callable(getattr(settings, key)):
|
||||
value = getattr(settings, key)
|
||||
# Mask sensitive values in logs
|
||||
if key.lower().find('password') >= 0 or key.lower().find('secret') >= 0 or key.lower().find('token') >= 0 or key.lower().find('key') >= 0:
|
||||
if value:
|
||||
if isinstance(value, str) and len(value) > 10:
|
||||
visible_start = max(1, len(value) // 3)
|
||||
visible_end = max(1, len(value) // 4)
|
||||
value = f"{value[:visible_start]}{'*' * (len(value) - visible_start - visible_end)}{value[-visible_end:]}"
|
||||
else:
|
||||
value = f"{value[:2]}{'*' * (len(value) - 4)}{value[-2:]}" if isinstance(value, str) and len(value) > 4 else "****"
|
||||
logger.info(f"{key}: {value}")
|
||||
logger.info("--- END OF SETTINGS DUMP ---")
|
||||
|
||||
def get_settings_for_display(show_values=False):
|
||||
"""
|
||||
Group settings into logical categories and check if they are configured.
|
||||
Returns a dictionary with categories as keys and lists of setting items as values.
|
||||
Each setting item is a dict with name, value, and is_configured.
|
||||
|
||||
If show_values is False, sensitive values are masked.
|
||||
"""
|
||||
# First include system info with version in result
|
||||
result = {
|
||||
"System Info": [
|
||||
{
|
||||
"name": "App Version",
|
||||
"value": settings.version,
|
||||
"is_configured": True
|
||||
},
|
||||
{
|
||||
"name": "Build Date",
|
||||
"value": settings.build_date,
|
||||
"is_configured": True
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Define categories and their settings
|
||||
categories = {
|
||||
"Core": [
|
||||
"debug", # Explicitly include debug setting
|
||||
"external_hostname",
|
||||
"workdir",
|
||||
"database_url",
|
||||
"redis_url",
|
||||
"gotenberg_url",
|
||||
"allow_file_delete" # Added allow_file_delete to Core settings
|
||||
],
|
||||
"Authentication": [
|
||||
"auth_enabled",
|
||||
"authentik_client_id",
|
||||
"authentik_client_secret",
|
||||
"authentik_config_url"
|
||||
],
|
||||
"Email": [
|
||||
"email_host",
|
||||
"email_port",
|
||||
"email_username",
|
||||
"email_password",
|
||||
"email_use_tls",
|
||||
"email_sender",
|
||||
"email_default_recipient"
|
||||
],
|
||||
"IMAP": [
|
||||
"imap1_host",
|
||||
"imap1_port",
|
||||
"imap1_username",
|
||||
"imap1_password",
|
||||
"imap1_ssl",
|
||||
"imap1_poll_interval_minutes",
|
||||
"imap1_delete_after_process",
|
||||
"imap2_host",
|
||||
"imap2_port",
|
||||
"imap2_username",
|
||||
"imap2_password",
|
||||
"imap2_ssl",
|
||||
"imap2_poll_interval_minutes",
|
||||
"imap2_delete_after_process"
|
||||
],
|
||||
"Dropbox": [
|
||||
"dropbox_app_key",
|
||||
"dropbox_app_secret",
|
||||
"dropbox_folder",
|
||||
"dropbox_refresh_token"
|
||||
],
|
||||
"NextCloud": [
|
||||
"nextcloud_upload_url",
|
||||
"nextcloud_username",
|
||||
"nextcloud_password",
|
||||
"nextcloud_folder"
|
||||
],
|
||||
"Paperless": [
|
||||
"paperless_host",
|
||||
"paperless_ngx_api_token"
|
||||
],
|
||||
"Google Drive": [
|
||||
"google_drive_use_oauth",
|
||||
"google_drive_client_id",
|
||||
"google_drive_client_secret",
|
||||
"google_drive_refresh_token",
|
||||
"google_drive_credentials_json",
|
||||
"google_drive_folder_id",
|
||||
"google_drive_delegate_to"
|
||||
],
|
||||
"OneDrive": [
|
||||
"onedrive_client_id",
|
||||
"onedrive_client_secret",
|
||||
"onedrive_tenant_id",
|
||||
"onedrive_refresh_token",
|
||||
"onedrive_folder_path"
|
||||
],
|
||||
"WebDAV": [
|
||||
"webdav_url",
|
||||
"webdav_username",
|
||||
"webdav_password",
|
||||
"webdav_folder",
|
||||
"webdav_verify_ssl"
|
||||
],
|
||||
"SFTP": [
|
||||
"sftp_host",
|
||||
"sftp_port",
|
||||
"sftp_username",
|
||||
"sftp_password",
|
||||
"sftp_folder",
|
||||
"sftp_private_key",
|
||||
"sftp_private_key_passphrase"
|
||||
],
|
||||
"FTP": [
|
||||
"ftp_host",
|
||||
"ftp_port",
|
||||
"ftp_username",
|
||||
"ftp_password",
|
||||
"ftp_folder",
|
||||
"ftp_use_tls",
|
||||
"ftp_allow_plaintext"
|
||||
],
|
||||
"S3/AWS": [
|
||||
"aws_access_key_id",
|
||||
"aws_secret_access_key",
|
||||
"aws_region",
|
||||
"s3_bucket_name",
|
||||
"s3_folder_prefix",
|
||||
"s3_storage_class",
|
||||
"s3_acl"
|
||||
],
|
||||
"AI Services": [
|
||||
"openai_api_key",
|
||||
"openai_base_url",
|
||||
"openai_model",
|
||||
"azure_ai_key",
|
||||
"azure_endpoint",
|
||||
"azure_region"
|
||||
],
|
||||
"Monitoring": [
|
||||
"uptime_kuma_url",
|
||||
"uptime_kuma_ping_interval"
|
||||
]
|
||||
}
|
||||
|
||||
# Handle any settings that don't fit into the predefined categories
|
||||
all_settings = set([key for key in dir(settings)
|
||||
if not key.startswith('_') and
|
||||
not callable(getattr(settings, key)) and
|
||||
key not in ["model_computed_fields", "model_config",
|
||||
"model_extra", "model_fields",
|
||||
"model_fields_set"]])
|
||||
|
||||
# Ensure 'version' is excluded since we display it separately
|
||||
all_settings.discard("version")
|
||||
|
||||
categorized_settings = set()
|
||||
for cat_settings in categories.values():
|
||||
categorized_settings.update(cat_settings)
|
||||
|
||||
uncategorized = all_settings - categorized_settings
|
||||
if uncategorized:
|
||||
categories["Other"] = list(uncategorized)
|
||||
|
||||
# Build the result
|
||||
for category, setting_keys in categories.items():
|
||||
items = []
|
||||
for key in setting_keys:
|
||||
if hasattr(settings, key):
|
||||
value = getattr(settings, key)
|
||||
|
||||
# List of patterns that indicate sensitive values
|
||||
sensitive_patterns = [
|
||||
'password', 'secret', 'token', 'api_key', 'private_key',
|
||||
'credentials', 'access_key', 'ai_key'
|
||||
]
|
||||
|
||||
# Check if this is a sensitive value that should be masked
|
||||
is_sensitive = any(
|
||||
pattern in key.lower() for pattern in sensitive_patterns
|
||||
)
|
||||
|
||||
# Special handling for "auth" to avoid matching prefixes like "authentik"
|
||||
if not is_sensitive and "auth" in key.lower():
|
||||
# Only mark as sensitive if "auth" is a standalone word or at the end
|
||||
# This avoids matching "authentik" as sensitive
|
||||
parts = key.lower().split('_')
|
||||
is_sensitive = any(part == "auth" for part in parts) or key.lower().endswith("auth")
|
||||
|
||||
# Mask sensitive values regardless of debug mode
|
||||
# Other values are only hidden if debug mode is off AND show_values is False
|
||||
if (is_sensitive or not show_values) and value:
|
||||
if is_sensitive:
|
||||
value = mask_sensitive_value(value)
|
||||
|
||||
# Check if the setting is configured (has a non-None value)
|
||||
# For boolean settings, consider them configured even if False
|
||||
is_configured = value is not None
|
||||
if is_configured and isinstance(value, str):
|
||||
is_configured = len(value) > 0
|
||||
|
||||
items.append({
|
||||
"name": key,
|
||||
"value": value,
|
||||
"is_configured": is_configured
|
||||
})
|
||||
|
||||
if items: # Only add categories that have items
|
||||
result[category] = items
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import socket
|
||||
import logging
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def validate_email_config():
|
||||
"""Validates email configuration settings"""
|
||||
issues = []
|
||||
|
||||
# Check for required email settings
|
||||
if not getattr(settings, 'email_host', None):
|
||||
issues.append("EMAIL_HOST is not configured")
|
||||
if not getattr(settings, 'email_port', None):
|
||||
issues.append("EMAIL_PORT is not configured")
|
||||
|
||||
# Test SMTP server connectivity if host is configured
|
||||
if getattr(settings, 'email_host', None) and getattr(settings, 'email_port', None):
|
||||
try:
|
||||
# Attempt to resolve the hostname
|
||||
socket.gethostbyname(settings.email_host)
|
||||
except socket.gaierror:
|
||||
issues.append(f"Cannot resolve email host: {settings.email_host}")
|
||||
|
||||
# Check for authentication settings
|
||||
if not getattr(settings, 'email_username', None):
|
||||
issues.append("EMAIL_USERNAME is not configured")
|
||||
if not getattr(settings, 'email_password', None):
|
||||
issues.append("EMAIL_PASSWORD is not configured")
|
||||
|
||||
return issues
|
||||
|
||||
def validate_storage_configs():
|
||||
"""Validates configuration for all storage providers"""
|
||||
issues = {}
|
||||
|
||||
# Validate Dropbox config
|
||||
dropbox_issues = []
|
||||
if not (getattr(settings, 'dropbox_app_key', None) and
|
||||
getattr(settings, 'dropbox_app_secret', None) and
|
||||
getattr(settings, 'dropbox_refresh_token', None)):
|
||||
dropbox_issues.append("Dropbox credentials are not fully configured")
|
||||
issues['dropbox'] = dropbox_issues
|
||||
|
||||
# Validate Nextcloud config
|
||||
nextcloud_issues = []
|
||||
if not (getattr(settings, 'nextcloud_upload_url', None) and
|
||||
getattr(settings, 'nextcloud_username', None) and
|
||||
getattr(settings, 'nextcloud_password', None)):
|
||||
nextcloud_issues.append("Nextcloud credentials are not fully configured")
|
||||
issues['nextcloud'] = nextcloud_issues
|
||||
|
||||
# Validate SFTP config
|
||||
sftp_issues = []
|
||||
if not getattr(settings, 'sftp_host', None):
|
||||
sftp_issues.append("SFTP_HOST is not configured")
|
||||
|
||||
sftp_key_path = getattr(settings, 'sftp_private_key', None)
|
||||
if sftp_key_path and not os.path.exists(sftp_key_path):
|
||||
sftp_issues.append(f"SFTP_KEY_PATH file not found: {sftp_key_path}")
|
||||
|
||||
if not sftp_key_path and not getattr(settings, 'sftp_password', None):
|
||||
sftp_issues.append("Neither SFTP_KEY_PATH nor SFTP_PASSWORD is configured")
|
||||
|
||||
issues['sftp'] = sftp_issues
|
||||
|
||||
# Validate Email sending
|
||||
email_issues = []
|
||||
if not getattr(settings, 'email_host', None):
|
||||
email_issues.append("EMAIL_HOST is not configured")
|
||||
if not getattr(settings, 'email_default_recipient', None):
|
||||
email_issues.append("EMAIL_DEFAULT_RECIPIENT is not configured")
|
||||
issues['email'] = email_issues
|
||||
|
||||
# Validate S3
|
||||
s3_issues = []
|
||||
if not getattr(settings, 's3_bucket_name', None):
|
||||
s3_issues.append("S3_BUCKET_NAME is not configured")
|
||||
if not (getattr(settings, 'aws_access_key_id', None) and
|
||||
getattr(settings, 'aws_secret_access_key', None)):
|
||||
s3_issues.append("AWS credentials are not configured")
|
||||
issues['s3'] = s3_issues
|
||||
|
||||
# Validate FTP
|
||||
ftp_issues = []
|
||||
if not getattr(settings, 'ftp_host', None):
|
||||
ftp_issues.append("FTP_HOST is not configured")
|
||||
if not getattr(settings, 'ftp_username', None):
|
||||
ftp_issues.append("FTP_USERNAME is not configured")
|
||||
if not getattr(settings, 'ftp_password', None):
|
||||
ftp_issues.append("FTP_PASSWORD is not configured")
|
||||
issues['ftp'] = ftp_issues
|
||||
|
||||
# Validate WebDAV
|
||||
webdav_issues = []
|
||||
if not getattr(settings, 'webdav_url', None):
|
||||
webdav_issues.append("WEBDAV_URL is not configured")
|
||||
if not getattr(settings, 'webdav_username', None):
|
||||
webdav_issues.append("WEBDAV_USERNAME is not configured")
|
||||
if not getattr(settings, 'webdav_password', None):
|
||||
webdav_issues.append("WEBDAV_PASSWORD is not configured")
|
||||
issues['webdav'] = webdav_issues
|
||||
|
||||
# Validate Google Drive
|
||||
gdrive_issues = []
|
||||
if not getattr(settings, 'google_drive_credentials_json', None):
|
||||
gdrive_issues.append("GOOGLE_DRIVE_CREDENTIALS_JSON is not configured")
|
||||
if not getattr(settings, 'google_drive_folder_id', None):
|
||||
gdrive_issues.append("GOOGLE_DRIVE_FOLDER_ID is not configured")
|
||||
issues['google_drive'] = gdrive_issues
|
||||
|
||||
# Validate Paperless
|
||||
paperless_issues = []
|
||||
if not getattr(settings, 'paperless_host', None):
|
||||
paperless_issues.append("PAPERLESS_HOST is not configured")
|
||||
if not getattr(settings, 'paperless_ngx_api_token', None):
|
||||
paperless_issues.append("PAPERLESS_NGX_API_TOKEN is not configured")
|
||||
issues['paperless'] = paperless_issues
|
||||
|
||||
# Validate OneDrive
|
||||
onedrive_issues = []
|
||||
if not (getattr(settings, 'onedrive_client_id', None) and
|
||||
getattr(settings, 'onedrive_client_secret', None) and
|
||||
getattr(settings, 'onedrive_refresh_token', None)):
|
||||
onedrive_issues.append("OneDrive credentials are not fully configured")
|
||||
issues['onedrive'] = onedrive_issues
|
||||
|
||||
# Validate Uptime Kuma
|
||||
uptime_kuma_issues = []
|
||||
if not getattr(settings, 'uptime_kuma_url', None):
|
||||
uptime_kuma_issues.append("UPTIME_KUMA_URL is not configured")
|
||||
issues['uptime_kuma'] = uptime_kuma_issues
|
||||
|
||||
return issues
|
||||
|
||||
def check_all_configs():
|
||||
"""Run all configuration validations and log results"""
|
||||
from app.utils.config_validator.settings_display import dump_all_settings
|
||||
|
||||
logger.info("Validating application configuration...")
|
||||
|
||||
# Check if debug is enabled and dump all settings if it is
|
||||
if hasattr(settings, 'debug') and settings.debug:
|
||||
dump_all_settings()
|
||||
|
||||
# Check email config
|
||||
email_issues = validate_email_config()
|
||||
if email_issues:
|
||||
logger.warning(f"Email configuration issues: {', '.join(email_issues)}")
|
||||
else:
|
||||
logger.info("Email configuration OK")
|
||||
|
||||
# Check storage configs
|
||||
storage_issues = validate_storage_configs()
|
||||
for provider, issues in storage_issues.items():
|
||||
if issues:
|
||||
logger.warning(f"{provider.capitalize()} configuration issues: {', '.join(issues)}")
|
||||
else:
|
||||
logger.info(f"{provider.capitalize()} configuration OK")
|
||||
|
||||
# Return all identified issues
|
||||
return {
|
||||
'email': email_issues,
|
||||
'storage': storage_issues
|
||||
}
|
||||
@@ -10,6 +10,7 @@ from app.views.files import router as files_router
|
||||
from app.views.status import router as status_router
|
||||
from app.views.dropbox import router as dropbox_router
|
||||
from app.views.onedrive import router as onedrive_router
|
||||
from app.views.google_drive import router as google_drive_router
|
||||
|
||||
# Create a combined router
|
||||
router = APIRouter()
|
||||
@@ -20,3 +21,4 @@ router.include_router(files_router)
|
||||
router.include_router(status_router)
|
||||
router.include_router(dropbox_router)
|
||||
router.include_router(onedrive_router)
|
||||
router.include_router(google_drive_router)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Google Drive integration views for setup and OAuth callback.
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
import urllib.parse
|
||||
|
||||
from app.views.base import APIRouter, templates, require_login, settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/google-drive-setup")
|
||||
@require_login
|
||||
async def google_drive_setup_page(request: Request):
|
||||
"""
|
||||
Setup page for the Google Drive integration.
|
||||
Shows configuration status and setup instructions.
|
||||
"""
|
||||
# Check if using OAuth
|
||||
use_oauth = getattr(settings, 'google_drive_use_oauth', False)
|
||||
|
||||
# Check Google Drive OAuth configuration
|
||||
oauth_configured = bool(settings.google_drive_client_id and
|
||||
settings.google_drive_client_secret and
|
||||
settings.google_drive_refresh_token)
|
||||
|
||||
# Check Google Drive service account configuration
|
||||
sa_configured = bool(settings.google_drive_credentials_json)
|
||||
|
||||
# Overall configuration status
|
||||
is_configured = (use_oauth and oauth_configured) or (not use_oauth and sa_configured)
|
||||
|
||||
if settings.google_drive_folder_id:
|
||||
is_configured = is_configured and True
|
||||
else:
|
||||
is_configured = False
|
||||
|
||||
# Get configuration values to display status (hide sensitive values)
|
||||
return templates.TemplateResponse(
|
||||
"google_drive.html",
|
||||
{
|
||||
"request": request,
|
||||
"is_configured": is_configured,
|
||||
"use_oauth": use_oauth,
|
||||
"oauth_configured": oauth_configured,
|
||||
"sa_configured": sa_configured,
|
||||
"client_id": bool(settings.google_drive_client_id),
|
||||
"client_id_value": settings.google_drive_client_id or "",
|
||||
"client_secret": bool(settings.google_drive_client_secret),
|
||||
"client_secret_value": settings.google_drive_client_secret or "",
|
||||
"refresh_token": bool(settings.google_drive_refresh_token),
|
||||
"refresh_token_value": settings.google_drive_refresh_token or "",
|
||||
"folder_id": settings.google_drive_folder_id or "",
|
||||
"has_credentials_json": bool(settings.google_drive_credentials_json)
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/google-drive-callback")
|
||||
@require_login
|
||||
async def google_drive_callback(request: Request, code: str = None, error: str = None, state: str = None):
|
||||
"""
|
||||
Callback endpoint for Google Drive OAuth flow.
|
||||
Now automatically exchanges the code for a token and saves it to the configuration.
|
||||
"""
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback_error.html",
|
||||
{"request": request, "error": error}
|
||||
)
|
||||
|
||||
if not code:
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback_error.html",
|
||||
{"request": request, "error": "No authorization code received from Google"}
|
||||
)
|
||||
|
||||
# Display the processing page with automatic token exchange
|
||||
return templates.TemplateResponse(
|
||||
"google_drive_callback.html",
|
||||
{
|
||||
"request": request,
|
||||
"code": code,
|
||||
"state": state
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/google-drive-auth-start")
|
||||
@require_login
|
||||
async def google_drive_auth_start(
|
||||
request: Request,
|
||||
client_id: str,
|
||||
redirect_uri: str = None
|
||||
):
|
||||
"""
|
||||
Start the Google Drive OAuth flow by redirecting to Google's authorization page.
|
||||
"""
|
||||
if not redirect_uri:
|
||||
redirect_uri = f"{request.url.scheme}://{request.url.netloc}/google-drive-callback"
|
||||
|
||||
# Create the authorization URL with required scopes
|
||||
# Use only drive.file scope to minimize required permissions
|
||||
scopes = [
|
||||
"https://www.googleapis.com/auth/drive.file" # Access to files created or opened by the app
|
||||
]
|
||||
|
||||
scope_str = urllib.parse.quote(' '.join(scopes))
|
||||
|
||||
auth_url = (
|
||||
f"https://accounts.google.com/o/oauth2/auth"
|
||||
f"?client_id={client_id}"
|
||||
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
|
||||
f"&response_type=code"
|
||||
f"&scope={scope_str}"
|
||||
f"&access_type=offline"
|
||||
f"&prompt=consent" # Force to show consent screen to get refresh token
|
||||
)
|
||||
|
||||
return RedirectResponse(url=auth_url)
|
||||
Reference in New Issue
Block a user