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:
@@ -14,7 +14,7 @@ ADMIN_PASSWORD=your_secure_password
|
||||
|
||||
# **OpenID Connect/Authentik Settings**
|
||||
AUTHENTIK_CLIENT_ID=<yourAuthentikAppClientID>
|
||||
AUTHENTIK_CLIENT_SECRET=<yourAuthentikAppClientSecret>
|
||||
AUTHENTIK_CLIENT_SECRET=<yourAuthentikClientSecret>
|
||||
AUTHENTIK_CONFIG_URL=<ConfigUrlOfYourApp, e.g. https://authentik.example.com/application/o/docuelevate/.well-known/openid-configuration>
|
||||
|
||||
# **AI/ML Services**
|
||||
@@ -81,10 +81,17 @@ DROPBOX_REFRESH_TOKEN=<DROPBOX_REFRESH_TOKEN>
|
||||
DROPBOX_FOLDER="/Documents/Uploads"
|
||||
|
||||
# Google Drive
|
||||
# Service Account Method:
|
||||
GOOGLE_DRIVE_CREDENTIALS_JSON={"type":"service_account","project_id":"your-project","private_key_id":"key-id","private_key":"-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\n","client_email":"service-account@project.iam.gserviceaccount.com","client_id":"client-id","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/service-account%40project.iam.gserviceaccount.com"}
|
||||
GOOGLE_DRIVE_FOLDER_ID=<YOUR_FOLDER_ID>
|
||||
GOOGLE_DRIVE_DELEGATE_TO=<OPTIONAL_USER_EMAIL>
|
||||
|
||||
# OAuth Method (Alternative):
|
||||
GOOGLE_DRIVE_USE_OAUTH=false # Set to true to use OAuth instead of service account
|
||||
GOOGLE_DRIVE_CLIENT_ID=your-oauth-client-id # Required for OAuth method
|
||||
GOOGLE_DRIVE_CLIENT_SECRET=your-oauth-client-secret # Required for OAuth method
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN=your-oauth-refresh-token # Required for OAuth method
|
||||
|
||||
# OneDrive
|
||||
ONEDRIVE_CLIENT_ID=your-client-id
|
||||
ONEDRIVE_CLIENT_SECRET=your-client-secret
|
||||
|
||||
@@ -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)
|
||||
@@ -78,9 +78,15 @@ For detailed setup instructions, see the [Dropbox Setup Guide](DropboxSetup.md).
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `GOOGLE_DRIVE_CREDENTIALS_JSON` | JSON string containing service account credentials |
|
||||
| `GOOGLE_DRIVE_USE_OAUTH` | Set to `true` to use OAuth flow (recommended) |
|
||||
| `GOOGLE_DRIVE_CLIENT_ID` | OAuth Client ID (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_CLIENT_SECRET` | OAuth Client Secret (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_REFRESH_TOKEN` | OAuth Refresh Token (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_FOLDER_ID` | Google Drive folder ID for file uploads |
|
||||
| `GOOGLE_DRIVE_DELEGATE_TO` | Email address to delegate permissions (optional) |
|
||||
| `GOOGLE_DRIVE_CREDENTIALS_JSON` | JSON string containing service account credentials (alternative method) |
|
||||
| `GOOGLE_DRIVE_DELEGATE_TO` | Email address to delegate permissions (optional for service accounts) |
|
||||
|
||||
**Note:** For OAuth method with non-verified apps, refresh tokens expire after 7 days. For production use, either complete the Google verification process or use the Service Account method.
|
||||
|
||||
For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveSetup.md).
|
||||
|
||||
@@ -225,6 +231,10 @@ NEXTCLOUD_FOLDER=/Documents/Uploads
|
||||
GOOGLE_DRIVE_CREDENTIALS_JSON={"type":"service_account","project_id":"..."}
|
||||
GOOGLE_DRIVE_FOLDER_ID=1a2b3c4d5e6f7g8h9i0j
|
||||
GOOGLE_DRIVE_DELEGATE_TO=optional-user@example.com
|
||||
GOOGLE_DRIVE_USE_OAUTH=true
|
||||
GOOGLE_DRIVE_CLIENT_ID=your_client_id
|
||||
GOOGLE_DRIVE_CLIENT_SECRET=your_client_secret
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN=your_refresh_token
|
||||
|
||||
# WebDAV
|
||||
WEBDAV_URL=https://webdav.example.com/path
|
||||
|
||||
+179
-4
@@ -6,13 +6,148 @@ This guide explains how to set up the Google Drive integration for DocuElevate.
|
||||
|
||||
| **Variable** | **Description** |
|
||||
|---------------------------------|-------------------------------------------------------|
|
||||
| `GOOGLE_DRIVE_CREDENTIALS_JSON` | JSON string containing service account credentials |
|
||||
| `GOOGLE_DRIVE_USE_OAUTH` | Set to `true` to use OAuth flow (recommended) |
|
||||
| `GOOGLE_DRIVE_CLIENT_ID` | OAuth Client ID (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_CLIENT_SECRET` | OAuth Client Secret (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_REFRESH_TOKEN` | OAuth Refresh Token (required if using OAuth flow) |
|
||||
| `GOOGLE_DRIVE_FOLDER_ID` | Google Drive folder ID for file uploads |
|
||||
| `GOOGLE_DRIVE_DELEGATE_TO` | Email address to delegate permissions (optional) |
|
||||
| `GOOGLE_DRIVE_CREDENTIALS_JSON` | JSON string containing service account credentials (alternative method) |
|
||||
| `GOOGLE_DRIVE_DELEGATE_TO` | Email address to delegate permissions (optional for service accounts) |
|
||||
|
||||
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
||||
|
||||
## Step-by-Step Setup Instructions
|
||||
## Authentication Methods
|
||||
|
||||
DocuElevate supports two authentication methods for Google Drive:
|
||||
|
||||
1. **OAuth Authentication (Recommended)** - User-based authentication that provides better security and control. Recommended for most deployments.
|
||||
2. **Service Account Authentication** - Server-to-server authentication that doesn't require user interaction. Useful for specific enterprise deployments.
|
||||
|
||||
## Method 1: OAuth Authentication Setup (Recommended)
|
||||
|
||||
The OAuth method is preferred as it:
|
||||
- Provides better security with token expiration and refresh
|
||||
- Integrates with personal Google accounts more seamlessly
|
||||
- Doesn't require manual folder sharing
|
||||
- Offers a streamlined setup process with our setup wizard
|
||||
|
||||
### 1. Create a Google Cloud Project
|
||||
|
||||
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select an existing one
|
||||
3. Navigate to "APIs & Services" > "Library"
|
||||
4. Search for and enable the "Google Drive API"
|
||||
|
||||
### 2. Configure OAuth Consent Screen
|
||||
|
||||
1. In "APIs & Services", go to "OAuth consent screen"
|
||||
2. Select "External" user type (or "Internal" if this is for an organization)
|
||||
3. Fill out the required application information:
|
||||
- App name: "DocuElevate" (or your preferred name)
|
||||
- User support email: Your email
|
||||
- Application homepage link: Your application URL
|
||||
- Authorized domains: Your domain
|
||||
- Developer contact information: Your email
|
||||
4. Click "Save and Continue"
|
||||
5. On the Scopes screen, click "Add or Remove Scopes" and add:
|
||||
- `https://www.googleapis.com/auth/drive.file` (allows access to files created or opened by the app)
|
||||
6. Click "Save and Continue" through the rest of the setup
|
||||
|
||||
### 3. Create OAuth Credentials
|
||||
|
||||
1. Go to "APIs & Services" > "Credentials"
|
||||
2. Click "Create Credentials" > "OAuth client ID"
|
||||
3. Select "Web application" for Application type
|
||||
4. Add a name (e.g., "DocuElevate Web Client")
|
||||
5. Under "Authorized JavaScript origins" add your application URL (e.g., `https://your-docuelevate-instance.com`)
|
||||
6. Under "Authorized redirect URIs" add:
|
||||
- `https://your-docuelevate-instance.com/google-drive-callback`
|
||||
7. Click "Create"
|
||||
8. Note down your Client ID and Client Secret
|
||||
|
||||
### 4. Use the OAuth Setup Wizard
|
||||
|
||||
DocuElevate includes a built-in OAuth setup wizard that makes configuration simple:
|
||||
|
||||
1. Go to your DocuElevate instance and navigate to Settings > Google Drive Setup
|
||||
2. Select the OAuth method tab (selected by default)
|
||||
3. Enter your Client ID and Client Secret
|
||||
4. Click "Start Authentication Flow"
|
||||
5. Follow the prompts to authenticate and select a folder
|
||||
6. The wizard will automatically save your settings and generate the required environment variables
|
||||
|
||||
### 5. Manual OAuth Configuration
|
||||
|
||||
If you prefer to set up OAuth manually, you'll need to:
|
||||
|
||||
1. Set `GOOGLE_DRIVE_USE_OAUTH=true` in your configuration
|
||||
2. Set `GOOGLE_DRIVE_CLIENT_ID` and `GOOGLE_DRIVE_CLIENT_SECRET` from the credentials you created
|
||||
3. Obtain a refresh token using OAuth2 authorization flow (outside the scope of this document)
|
||||
4. Set `GOOGLE_DRIVE_REFRESH_TOKEN` with the obtained refresh token
|
||||
5. Set `GOOGLE_DRIVE_FOLDER_ID` to your desired folder ID
|
||||
|
||||
## OAuth Token Expiration Notice
|
||||
|
||||
When using the OAuth method with Google Drive, there are important limitations to be aware of:
|
||||
|
||||
### Non-Verified Apps
|
||||
|
||||
If your Google Cloud project is not verified by Google (displayed as "unverified" during the OAuth consent screen):
|
||||
|
||||
- **Refresh tokens expire after 7 days** for test/development applications
|
||||
- You will need to re-authenticate every 7 days
|
||||
- The application will show "This app isn't verified" warning during authentication
|
||||
|
||||
This limitation primarily affects development environments. For production use, you should:
|
||||
|
||||
1. Complete the Google OAuth verification process
|
||||
2. Submit your app for verification in the Google Cloud Console
|
||||
3. Provide the required information including privacy policy and application testing instructions
|
||||
|
||||
### Verified Apps
|
||||
|
||||
Once your app is verified by Google:
|
||||
|
||||
- Refresh tokens remain valid until explicitly revoked
|
||||
- Users won't see the "unverified app" warning
|
||||
- You can request access to more sensitive scopes
|
||||
|
||||
For internal applications within your organization, consider using the Service Account method instead, which doesn't have the 7-day token expiration limitation.
|
||||
|
||||
## Folder Selection
|
||||
|
||||
DocuElevate provides two ways to select your Google Drive destination folder:
|
||||
|
||||
### 1. Web Interface Folder Picker
|
||||
|
||||
The DocuElevate web interface includes an integrated Google Drive folder picker that allows you to:
|
||||
|
||||
- Browse your Google Drive folders visually
|
||||
- Select the destination folder through a user-friendly interface
|
||||
- Automatically save the selected folder ID to your configuration
|
||||
|
||||
To use the folder picker:
|
||||
|
||||
1. Complete the OAuth authentication flow
|
||||
2. On the Google Drive setup page, click the "Select Folder with Picker" button
|
||||
3. Browse and select your desired folder in the popup window
|
||||
4. The folder ID will be automatically filled in and saved
|
||||
|
||||
### 2. Manual Folder ID Entry
|
||||
|
||||
You can also manually specify the folder ID:
|
||||
|
||||
1. Navigate to the desired folder in Google Drive
|
||||
2. Extract the folder ID from the URL: `https://drive.google.com/drive/folders/YOUR_FOLDER_ID`
|
||||
3. Enter this ID in the "Folder ID" field in the DocuElevate setup page
|
||||
|
||||
For the root folder of your Google Drive, use `root` as the folder ID.
|
||||
|
||||
**Note:** The folder selector requires that you've already authenticated with OAuth. It's not available when using the Service Account method, as it requires user interaction.
|
||||
|
||||
## Method 2: Service Account Setup (Alternative)
|
||||
|
||||
Service accounts are useful for specific enterprise deployments where interactive login is not possible or when you need to access shared drives with domain-wide delegation.
|
||||
|
||||
### 1. Create a Google Cloud Project
|
||||
|
||||
@@ -60,8 +195,9 @@ To find your Google Drive folder ID:
|
||||
1. Open the downloaded JSON key file
|
||||
2. Set the entire JSON content as the `GOOGLE_DRIVE_CREDENTIALS_JSON` environment variable
|
||||
3. For security, ensure the JSON is properly escaped if your deployment method requires it
|
||||
4. Make sure `GOOGLE_DRIVE_USE_OAUTH` is set to `false`
|
||||
|
||||
### Domain-Wide Delegation (Optional)
|
||||
## Domain-Wide Delegation (Optional, for Service Accounts)
|
||||
|
||||
If you need the service account to access files on behalf of users in your Google Workspace:
|
||||
|
||||
@@ -73,3 +209,42 @@ If you need the service account to access files on behalf of users in your Googl
|
||||
3. Set `GOOGLE_DRIVE_DELEGATE_TO` to the email address of the user to impersonate
|
||||
|
||||
This setup is only relevant for Google Workspace environments where you need the service account to access user-specific files.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
- **Error: "The caller does not have permission"**
|
||||
- For OAuth: Make sure you're signed in with an account that has access to the folder
|
||||
- For Service Account: Ensure you've properly shared the target folder with the service account email
|
||||
- Check that you've enabled the Google Drive API in your project
|
||||
|
||||
- **Error: "Invalid credentials"**
|
||||
- For OAuth: Your tokens may have expired. Try refreshing authentication in the setup wizard
|
||||
- For Service Account: Verify that your JSON credentials are properly formatted and not corrupted
|
||||
- Check that the account has not been deleted or disabled
|
||||
|
||||
- **OAuth Errors**
|
||||
- Ensure your redirect URIs exactly match your application URL
|
||||
- Check that you've added the correct scopes
|
||||
- Make sure your OAuth consent screen is properly configured
|
||||
|
||||
### File Upload Issues
|
||||
|
||||
- **Error: "File not found"**
|
||||
- Verify your folder ID is correct
|
||||
- Make sure the folder still exists and hasn't been deleted
|
||||
|
||||
- **Error: "Insufficient permissions"**
|
||||
- For OAuth: Ensure you've granted the necessary permissions during authorization
|
||||
- For Service Account: Ensure the service account has "Editor" permissions on the folder
|
||||
|
||||
### Testing Your Setup
|
||||
|
||||
To test if your Google Drive integration is working:
|
||||
|
||||
1. Go to your DocuElevate instance and navigate to Settings > Google Drive Setup
|
||||
2. Click the "Test Connection" button
|
||||
3. If the test is successful, your integration is properly configured
|
||||
|
||||
For further assistance, see the [Troubleshooting Guide](Troubleshooting.md).
|
||||
|
||||
@@ -185,12 +185,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('error-container').classList.remove('hidden');
|
||||
document.getElementById('error-message').innerText = message;
|
||||
|
||||
// Hide the spinner when showing error
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showSuccess(refreshToken, appKey, appSecret, folderPath) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('success-container').classList.remove('hidden');
|
||||
|
||||
// Hide the spinner when showing success
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
|
||||
// Update the environment variables pre block with the new token
|
||||
const envVarsCode = document.querySelector('#env-vars code');
|
||||
if (envVarsCode) {
|
||||
|
||||
@@ -0,0 +1,846 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Google Drive Setup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2">Google Drive Integration Setup</h1>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Configure the Google Drive integration for DocuElevate using our setup wizard.
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
|
||||
<p class="font-bold">Current Status:</p>
|
||||
<p>Google Drive integration is
|
||||
{% if is_configured %}
|
||||
<span class="text-green-700 font-semibold">configured</span>.
|
||||
{% else %}
|
||||
<span class="text-red-700 font-semibold">not configured</span>.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if use_oauth %}
|
||||
<p class="mt-2"><strong>Authentication Type:</strong> OAuth Flow (User Account)</p>
|
||||
{% else %}
|
||||
<p class="mt-2"><strong>Authentication Type:</strong> Service Account (Server-to-Server)</p>
|
||||
{% endif %}
|
||||
{% if folder_id %}
|
||||
<p class="mt-2"><strong>Target folder ID:</strong> {{ folder_id }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
<strong>Note:</strong> There are two ways to authenticate with Google Drive. This wizard will guide you through OAuth setup, which is recommended for most users. For service account setup, please refer to the documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<h2 class="text-2xl font-semibold">Authentication Method</h2>
|
||||
|
||||
{% if is_configured %}
|
||||
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Configured
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
Not Configured
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-4 mb-6">
|
||||
<button id="oauth-tab-btn" class="px-4 py-2 rounded-md {{ 'text-white bg-blue-600' if use_oauth else 'text-gray-700 bg-gray-200' }}">
|
||||
OAuth User Account
|
||||
</button>
|
||||
<button id="sa-tab-btn" class="px-4 py-2 rounded-md {{ 'text-white bg-blue-600' if not use_oauth else 'text-gray-700 bg-gray-200' }}">
|
||||
Service Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- OAuth Tab Content -->
|
||||
<div id="oauth-tab" class="{{ 'block' if use_oauth else 'hidden' }}">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 1: Create a Google Cloud Project</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>Go to the <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 hover:underline">Google Cloud Console</a></li>
|
||||
<li>Create a new project or select an existing one</li>
|
||||
<li>Make note of your project ID</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 2: Enable the Google Drive API</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>In your project, go to "APIs & Services" > "Library"</li>
|
||||
<li>Search for "Google Drive API" and select it</li>
|
||||
<li>Click the "Enable" button</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 3: Configure OAuth Consent Screen</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>In "APIs & Services", go to "OAuth consent screen"</li>
|
||||
<li>Select "External" user type (or "Internal" if this is for an organization)</li>
|
||||
<li>Fill out the required application information:
|
||||
<ul class="list-disc ml-6 mt-1">
|
||||
<li>App name: "DocuElevate" (or your preferred name)</li>
|
||||
<li>User support email: Your email</li>
|
||||
<li>Application homepage link: Your application URL</li>
|
||||
<li>Authorized domains: Your domain</li>
|
||||
<li>Developer contact information: Your email</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click "Save and Continue"</li>
|
||||
<li>On the Scopes screen, click "Add or Remove Scopes" and add:
|
||||
<ul class="list-disc ml-6 mt-1">
|
||||
<li><code class="bg-gray-100 p-1">https://www.googleapis.com/auth/drive.file</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click "Save and Continue" through the rest of the setup</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 4: Create OAuth Credentials</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>Go to "APIs & Services" > "Credentials"</li>
|
||||
<li>Click "Create Credentials" > "OAuth client ID"</li>
|
||||
<li>Select "Web application" for Application type</li>
|
||||
<li>Add a name (e.g., "DocuElevate Web Client")</li>
|
||||
<li>Under "Authorized redirect URIs" add:
|
||||
<div class="bg-gray-100 p-2 my-2 rounded text-sm">
|
||||
{{ request.url.scheme }}://{{ request.url.netloc }}/google-drive-callback
|
||||
</div>
|
||||
</li>
|
||||
<li>Click "Create"</li>
|
||||
<li>Copy your Client ID and Client Secret</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 5: Complete OAuth Setup</h3>
|
||||
<p class="mb-4">Now enter your Client ID and Client Secret below, and we'll help you complete the OAuth flow. You can set the folder ID after authentication is complete.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
|
||||
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client ID" value="{{ client_id_value }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
|
||||
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client Secret" value="{{ client_secret_value }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="folder-id" class="block text-sm font-medium text-gray-700">Folder ID (Optional)</label>
|
||||
<input type="text" id="folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
|
||||
<p class="text-xs text-gray-500 mt-1">Optional: You can set this after authentication if you prefer</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="select-folder-btn" class="mb-2 inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<svg class="h-5 w-5 mr-2 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
|
||||
<path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" />
|
||||
</svg>
|
||||
Select Folder with Picker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="start-oauth-flow" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Start Authentication Flow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Account Tab Content -->
|
||||
<div id="sa-tab" class="{{ 'block' if not use_oauth else 'hidden' }}">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Service Account Configuration</h3>
|
||||
|
||||
<p class="mb-4">Service accounts allow for server-to-server authentication without user involvement. This method is useful for background tasks.</p>
|
||||
|
||||
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
Service account configuration requires creating a service account in Google Cloud Console and uploading credentials. For detailed instructions, refer to the documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="sa-folder-id" class="block text-sm font-medium text-gray-700">Folder ID</label>
|
||||
<input type="text" id="sa-folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
|
||||
<p class="text-xs text-gray-500 mt-1">This is the ID from the Google Drive folder URL where files will be saved</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="sa-credentials" class="block text-sm font-medium text-gray-700">Service Account Credentials</label>
|
||||
<div class="mt-1">
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
{% if has_credentials_json %}
|
||||
Service account credentials are configured in your .env file.
|
||||
{% else %}
|
||||
No service account credentials found. You need to manually add them to your .env file.
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="/docs/GoogleDriveServiceAccount.md" target="_blank" class="text-blue-600 hover:underline text-sm">
|
||||
View Service Account Setup Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="save-sa-settings" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save Service Account Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token validation and status -->
|
||||
<div id="token-status" class="mt-6 bg-white shadow-md rounded-lg p-6 {{ 'hidden' if not is_configured else '' }}">
|
||||
<h2 class="text-xl font-semibold mb-4">Connection Status</h2>
|
||||
|
||||
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
{% if is_configured %}
|
||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium {{ 'text-green-800' if is_configured else 'text-yellow-800' }}">
|
||||
{% if is_configured %}
|
||||
Google Drive integration is properly configured! Your credentials are valid.
|
||||
{% else %}
|
||||
Google Drive integration is not completely configured or credentials may be invalid.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex space-x-3">
|
||||
<button id="test-connection" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Test Connection
|
||||
</button>
|
||||
<button id="refresh-token-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Refresh Token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Configuration for Worker Nodes section -->
|
||||
<div id="oauth-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if not use_oauth else '' }}">
|
||||
<h3 class="font-medium text-lg mb-2">OAuth Configuration for Worker Nodes</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Copy these environment variables to configure all worker nodes:
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<pre id="oauth-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=true
|
||||
GOOGLE_DRIVE_CLIENT_ID={{ client_id_value }}
|
||||
GOOGLE_DRIVE_CLIENT_SECRET={{ client_secret_value|default('YOUR_CLIENT_SECRET', true) }}
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
|
||||
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}</code></pre>
|
||||
|
||||
<button id="copy-oauth-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Add these variables to your .env file or environment configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="sa-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if use_oauth else '' }}">
|
||||
<h3 class="font-medium text-lg mb-2">Service Account Configuration for Worker Nodes</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Copy these environment variables to configure all worker nodes:
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<pre id="sa-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=false
|
||||
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
|
||||
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file</code></pre>
|
||||
|
||||
<button id="copy-sa-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Add these variables to your .env file along with your service account credentials JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-6 mt-8 mb-8">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-lg font-medium">Need More Information?</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
For detailed instructions and troubleshooting, refer to the
|
||||
<a href="/docs/GoogleDriveSetup.md" target="_blank" class="text-blue-600 hover:underline">Google Drive Setup Documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Back to Status
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Result Modal -->
|
||||
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3 text-center">
|
||||
<div id="modalIcon" class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4">
|
||||
<!-- Icon will be injected by JS -->
|
||||
</div>
|
||||
<h3 id="modalTitle" class="text-lg leading-6 font-medium text-gray-900">Success</h3>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<p id="modalMessage" class="text-sm text-gray-500">
|
||||
Operation completed successfully.
|
||||
</p>
|
||||
</div>
|
||||
<div class="items-center px-4 py-3">
|
||||
<button id="modalClose" class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-300">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Tab elements
|
||||
const oauthTabBtn = document.getElementById('oauth-tab-btn');
|
||||
const saTabBtn = document.getElementById('sa-tab-btn');
|
||||
const oauthTab = document.getElementById('oauth-tab');
|
||||
const saTab = document.getElementById('sa-tab');
|
||||
|
||||
// Form elements
|
||||
const clientIdInput = document.getElementById('client-id');
|
||||
const clientSecretInput = document.getElementById('client-secret');
|
||||
const folderIdInput = document.getElementById('folder-id');
|
||||
const saFolderIdInput = document.getElementById('sa-folder-id');
|
||||
const startOauthFlowBtn = document.getElementById('start-oauth-flow');
|
||||
const saveSaSettingsBtn = document.getElementById('save-sa-settings');
|
||||
const selectFolderBtn = document.getElementById('select-folder-btn');
|
||||
|
||||
// Status and test elements
|
||||
const tokenStatus = document.getElementById('token-status');
|
||||
const testConnectionBtn = document.getElementById('test-connection');
|
||||
const refreshTokenBtn = document.getElementById('refresh-token-btn');
|
||||
|
||||
// Environment sections
|
||||
const oauthEnvSection = document.getElementById('oauth-env-section');
|
||||
const saEnvSection = document.getElementById('sa-env-section');
|
||||
|
||||
// Modal elements
|
||||
const resultModal = document.getElementById('resultModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalMessage = document.getElementById('modalMessage');
|
||||
const modalIcon = document.getElementById('modalIcon');
|
||||
const modalClose = document.getElementById('modalClose');
|
||||
|
||||
// Google Picker variables
|
||||
let pickerApiLoaded = false;
|
||||
let pickerOAuthToken = null;
|
||||
|
||||
// Google Picker API functions
|
||||
function loadPickerApi() {
|
||||
gapi.load('picker', {
|
||||
'callback': function() {
|
||||
pickerApiLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load the Google API Loader script if select folder button exists
|
||||
if (selectFolderBtn) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://apis.google.com/js/api.js';
|
||||
script.onload = function() {
|
||||
loadPickerApi();
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Load the GSI Client for OAuth
|
||||
const gsiScript = document.createElement('script');
|
||||
gsiScript.src = 'https://accounts.google.com/gsi/client';
|
||||
document.body.appendChild(gsiScript);
|
||||
}
|
||||
|
||||
// Function to create and show the Google Picker
|
||||
function createPicker() {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
if (!clientId) {
|
||||
showModal('error', 'Missing Client ID', 'Please enter your Google OAuth Client ID first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we already have an active access token
|
||||
fetch('/api/google-drive/get-token-info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.access_token) {
|
||||
// If we have a valid access token, use it directly
|
||||
showPicker(clientId, data.access_token);
|
||||
} else {
|
||||
// If no valid token exists, inform the user they need to authenticate first
|
||||
showModal('error', 'Authentication Required',
|
||||
'You need to complete OAuth authentication before selecting a folder. ' +
|
||||
'Please click "Start Authentication Flow" first and then select a folder after authentication.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking token:', error);
|
||||
showModal('error', 'Authentication Required',
|
||||
'Unable to verify authentication status. Please complete the OAuth flow first.');
|
||||
});
|
||||
}
|
||||
|
||||
function showPicker(clientId, oauthToken) {
|
||||
if (!oauthToken) {
|
||||
showModal('error', 'Authentication Required', 'No access token available. Please authenticate first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the folders view specifically
|
||||
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
|
||||
.setIncludeFolders(true)
|
||||
.setSelectFolderEnabled(true)
|
||||
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode which doesn't require broader permissions
|
||||
|
||||
const picker = new google.picker.PickerBuilder()
|
||||
.addView(folderView)
|
||||
.setOAuthToken(oauthToken)
|
||||
.setOrigin(window.location.protocol + '//' + window.location.host)
|
||||
.setTitle('Select a folder for DocuElevate')
|
||||
.enableFeature(google.picker.Feature.NAV_HIDDEN) // Hide navigation
|
||||
.disableFeature(google.picker.Feature.MULTISELECT_ENABLED) // Disable multi-select
|
||||
.setSelectableMimeTypes('application/vnd.google-apps.folder') // Allow only folder selection
|
||||
.setCallback(pickerCallback)
|
||||
.build();
|
||||
|
||||
picker.setVisible(true);
|
||||
}
|
||||
|
||||
function pickerCallback(data) {
|
||||
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
|
||||
const folder = data[google.picker.Response.DOCUMENTS][0];
|
||||
const folderId = folder[google.picker.Document.ID];
|
||||
const folderName = folder[google.picker.Document.NAME];
|
||||
|
||||
// Update the folder ID input
|
||||
folderIdInput.value = folderId;
|
||||
if (saFolderIdInput) {
|
||||
saFolderIdInput.value = folderId;
|
||||
}
|
||||
|
||||
// Automatically save the folder ID to ensure it's stored
|
||||
saveFolderId(folderId);
|
||||
|
||||
// Show success message
|
||||
showModal('success', 'Folder Selected', `You selected folder: "${folderName}" (ID: ${folderId}) and saved it to your configuration.`);
|
||||
}
|
||||
}
|
||||
|
||||
// New function to save folder ID after picker selection
|
||||
function saveFolderId(folderId) {
|
||||
// Check which tab is active to determine if we're using OAuth or Service Account
|
||||
const isOauthActive = !oauthTab.classList.contains('hidden');
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('folder_id', folderId);
|
||||
formData.append('use_oauth', isOauthActive ? 'true' : 'false');
|
||||
|
||||
// If using OAuth, also include client credentials if available
|
||||
if (isOauthActive) {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
const clientSecret = clientSecretInput.value.trim();
|
||||
|
||||
if (clientId) formData.append('client_id', clientId);
|
||||
if (clientSecret) formData.append('client_secret', clientSecret);
|
||||
}
|
||||
|
||||
// Send the folder ID to be saved server-side
|
||||
fetch('/api/google-drive/save-settings', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.detail || 'Failed to save folder ID');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status !== 'success') {
|
||||
console.error('Error saving folder ID:', data.message);
|
||||
} else {
|
||||
console.log('Folder ID saved successfully');
|
||||
// Make the token status visible if it was hidden
|
||||
tokenStatus.classList.remove('hidden');
|
||||
|
||||
// Update environment variables display if they exist
|
||||
updateEnvVarsDisplay();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving folder ID:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update environment variables display
|
||||
function updateEnvVarsDisplay() {
|
||||
const folderId = folderIdInput.value || saFolderIdInput.value || 'YOUR_FOLDER_ID';
|
||||
|
||||
// Update OAuth env vars if the element exists
|
||||
const oauthEnvVarsCode = document.querySelector('#oauth-env-vars code');
|
||||
if (oauthEnvVarsCode) {
|
||||
const clientId = clientIdInput.value || 'YOUR_CLIENT_ID';
|
||||
const clientSecret = clientSecretInput.value ? 'YOUR_CLIENT_SECRET' : 'YOUR_CLIENT_SECRET';
|
||||
|
||||
oauthEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=true
|
||||
GOOGLE_DRIVE_CLIENT_ID=${clientId}
|
||||
GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN=YOUR_REFRESH_TOKEN
|
||||
GOOGLE_DRIVE_FOLDER_ID=${folderId}`;
|
||||
}
|
||||
|
||||
// Update Service Account env vars if the element exists
|
||||
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
|
||||
if (saEnvVarsCode) {
|
||||
saEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=false
|
||||
GOOGLE_DRIVE_FOLDER_ID=${folderId}
|
||||
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file`;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function showModal(status, title, message) {
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.textContent = message;
|
||||
|
||||
// Set the appropriate icon
|
||||
if (status === 'success') {
|
||||
modalIcon.innerHTML = `
|
||||
<svg class="h-6 w-6 text-green-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
`;
|
||||
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
|
||||
} else {
|
||||
modalIcon.innerHTML = `
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
`;
|
||||
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
|
||||
}
|
||||
|
||||
resultModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
resultModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Close modal when clicking the close button or outside
|
||||
modalClose.addEventListener('click', hideModal);
|
||||
resultModal.addEventListener('click', function(e) {
|
||||
if (e.target === resultModal) {
|
||||
hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Attach Google Picker button event listener
|
||||
if (selectFolderBtn) {
|
||||
selectFolderBtn.addEventListener('click', createPicker);
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
oauthTabBtn.addEventListener('click', function() {
|
||||
oauthTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
|
||||
saTabBtn.className = 'px-4 py-2 rounded-md text-gray-700 bg-gray-200';
|
||||
oauthTab.classList.remove('hidden');
|
||||
saTab.classList.add('hidden');
|
||||
oauthEnvSection.classList.remove('hidden');
|
||||
saEnvSection.classList.add('hidden');
|
||||
});
|
||||
|
||||
saTabBtn.addEventListener('click', function() {
|
||||
saTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
|
||||
oauthTabBtn.className = 'px-4 py-2 rounded-md text-gray-700 bg-gray-200';
|
||||
saTab.classList.remove('hidden');
|
||||
oauthTab.classList.add('hidden');
|
||||
saEnvSection.classList.remove('hidden');
|
||||
oauthEnvSection.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Sync folder IDs between tabs
|
||||
folderIdInput.addEventListener('input', function() {
|
||||
if (saFolderIdInput) {
|
||||
saFolderIdInput.value = folderIdInput.value;
|
||||
}
|
||||
});
|
||||
|
||||
if (saFolderIdInput) {
|
||||
saFolderIdInput.addEventListener('input', function() {
|
||||
folderIdInput.value = saFolderIdInput.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Start OAuth flow button
|
||||
if (startOauthFlowBtn) {
|
||||
startOauthFlowBtn.addEventListener('click', function() {
|
||||
const clientId = clientIdInput.value.trim();
|
||||
const clientSecret = clientSecretInput.value.trim();
|
||||
const folderId = folderIdInput.value.trim();
|
||||
|
||||
if (!clientId) {
|
||||
showModal('error', 'Validation Error', 'Please enter your Client ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientSecret) {
|
||||
showModal('error', 'Validation Error', 'Please enter your Client Secret');
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't require folder ID, make it optional
|
||||
|
||||
// Save values to session storage for use after redirect
|
||||
sessionStorage.setItem('google_drive_client_id', clientId);
|
||||
sessionStorage.setItem('google_drive_client_secret', clientSecret);
|
||||
if (folderId) {
|
||||
sessionStorage.setItem('google_drive_folder_id', folderId);
|
||||
}
|
||||
sessionStorage.setItem('google_drive_use_oauth', 'true');
|
||||
|
||||
// Create redirect URI
|
||||
const redirectUri = `${window.location.origin}/google-drive-callback`;
|
||||
|
||||
// Redirect to auth start endpoint
|
||||
window.location.href = `/google-drive-auth-start?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Save service account settings button
|
||||
if (saveSaSettingsBtn) {
|
||||
saveSaSettingsBtn.addEventListener('click', function() {
|
||||
const folderId = saFolderIdInput.value.trim();
|
||||
|
||||
if (!folderId) {
|
||||
showModal('error', 'Validation Error', 'Please enter your Google Drive Folder ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare form data
|
||||
const formData = new FormData();
|
||||
formData.append('folder_id', folderId);
|
||||
formData.append('use_oauth', 'false');
|
||||
|
||||
// Send update request
|
||||
const originalText = saveSaSettingsBtn.textContent;
|
||||
saveSaSettingsBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
|
||||
saveSaSettingsBtn.disabled = true;
|
||||
|
||||
fetch('/api/google-drive/save-settings', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.detail || 'Failed to save settings');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showModal('success', 'Settings Saved', 'Google Drive service account settings have been saved');
|
||||
tokenStatus.classList.remove('hidden');
|
||||
|
||||
// Update environment variables display
|
||||
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
|
||||
if (saEnvVarsCode) {
|
||||
saEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=false
|
||||
GOOGLE_DRIVE_FOLDER_ID=${folderId}
|
||||
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file`;
|
||||
}
|
||||
} else {
|
||||
showModal('error', 'Error', data.message || 'Failed to save settings');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showModal('error', 'Error', error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
saveSaSettingsBtn.textContent = originalText;
|
||||
saveSaSettingsBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Test connection button
|
||||
if (testConnectionBtn) {
|
||||
testConnectionBtn.addEventListener('click', function() {
|
||||
const originalText = testConnectionBtn.textContent;
|
||||
testConnectionBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
|
||||
testConnectionBtn.disabled = true;
|
||||
|
||||
fetch('/api/google-drive/test-token')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showModal('success', 'Connection Test Successful', data.message);
|
||||
} else {
|
||||
if (data.needs_reauth) {
|
||||
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure the connection.');
|
||||
} else {
|
||||
showModal('error', 'Connection Test Failed', data.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showModal('error', 'Connection Error', 'Error testing connection: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
testConnectionBtn.textContent = originalText;
|
||||
testConnectionBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh token button
|
||||
if (refreshTokenBtn) {
|
||||
refreshTokenBtn.addEventListener('click', function() {
|
||||
showModal('info', 'Confirm', 'This will start a new OAuth flow to obtain a fresh token. Continue?');
|
||||
modalClose.textContent = "Cancel";
|
||||
|
||||
// Add a confirm button
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300';
|
||||
confirmBtn.textContent = 'Continue';
|
||||
confirmBtn.addEventListener('click', function() {
|
||||
hideModal();
|
||||
|
||||
// Check if we have necessary info before starting flow
|
||||
if (!clientIdInput.value.trim() || !clientSecretInput.value.trim()) {
|
||||
showModal('error', 'Missing Information', 'Please enter your Client ID and Client Secret first');
|
||||
return;
|
||||
}
|
||||
|
||||
startOauthFlowBtn.click();
|
||||
});
|
||||
|
||||
// Add to modal
|
||||
modalClose.parentNode.appendChild(confirmBtn);
|
||||
|
||||
// Clean up modal when closed
|
||||
const onModalClose = function() {
|
||||
if (confirmBtn.parentNode) {
|
||||
confirmBtn.parentNode.removeChild(confirmBtn);
|
||||
}
|
||||
modalClose.textContent = "Close";
|
||||
resultModal.removeEventListener('hidden', onModalClose);
|
||||
};
|
||||
|
||||
resultModal.addEventListener('hidden', onModalClose);
|
||||
});
|
||||
}
|
||||
|
||||
// Copy environment variables buttons
|
||||
const copyOAuthEnvVarsBtn = document.getElementById('copy-oauth-env-vars');
|
||||
const copySAEnvVarsBtn = document.getElementById('copy-sa-env-vars');
|
||||
|
||||
if (copyOAuthEnvVarsBtn) {
|
||||
copyOAuthEnvVarsBtn.addEventListener('click', function() {
|
||||
const envVarsText = document.getElementById('oauth-env-vars').textContent;
|
||||
navigator.clipboard.writeText(envVarsText)
|
||||
.then(() => {
|
||||
copyOAuthEnvVarsBtn.textContent = 'Copied!';
|
||||
copyOAuthEnvVarsBtn.classList.add('bg-green-700');
|
||||
setTimeout(() => {
|
||||
copyOAuthEnvVarsBtn.textContent = 'Copy';
|
||||
copyOAuthEnvVarsBtn.classList.remove('bg-green-700');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (copySAEnvVarsBtn) {
|
||||
copySAEnvVarsBtn.addEventListener('click', function() {
|
||||
const envVarsText = document.getElementById('sa-env-vars').textContent;
|
||||
navigator.clipboard.writeText(envVarsText)
|
||||
.then(() => {
|
||||
copySAEnvVarsBtn.textContent = 'Copied!';
|
||||
copySAEnvVarsBtn.classList.add('bg-green-700');
|
||||
setTimeout(() => {
|
||||
copySAEnvVarsBtn.textContent = 'Copy';
|
||||
copySAEnvVarsBtn.classList.remove('bg-green-700');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,476 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Google Drive Authorization Processing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="bg-white shadow-md rounded-lg p-6">
|
||||
<div class="text-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
||||
<p class="text-gray-600 mt-2">Please wait while we complete the Google Drive authorization process...</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center my-6">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<div id="processing-message" class="text-center text-gray-700">
|
||||
<p>Exchanging authorization code for refresh token...</p>
|
||||
</div>
|
||||
|
||||
<div id="error-container" class="hidden mt-6">
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
|
||||
<div class="mt-2 text-sm text-red-700" id="error-message">
|
||||
<!-- Error message will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Return to Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="folder-selection-container" class="hidden mt-6">
|
||||
<div class="rounded-md bg-blue-50 p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-blue-800">
|
||||
Authentication successful! Please select or enter the Google Drive folder where you want to save files.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mb-6">
|
||||
<div>
|
||||
<label for="folder-id-input" class="block text-sm font-medium text-gray-700">Google Drive Folder ID</label>
|
||||
<div class="mt-1 flex rounded-md shadow-sm">
|
||||
<input type="text" id="folder-id-input" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" placeholder="Enter Google Drive folder ID">
|
||||
<button id="folder-select-picker-btn" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<svg class="h-4 w-4 mr-1 text-white-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Select Folder
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
You can paste a folder ID directly or use the selector to pick a folder. Root folder is "root".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="save-folder-btn" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="success-container" class="hidden mt-6">
|
||||
<div class="rounded-md bg-green-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800">
|
||||
Google Drive authorization successful! Your refresh token has been saved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-100 rounded-md">
|
||||
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Copy these environment variables to configure all worker nodes:
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
|
||||
|
||||
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Add these variables to your .env file or environment configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Go to Status Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const code = "{{ code }}";
|
||||
|
||||
// Get credentials from session storage
|
||||
const clientId = sessionStorage.getItem('google_drive_client_id');
|
||||
const clientSecret = sessionStorage.getItem('google_drive_client_secret');
|
||||
const folderId = sessionStorage.getItem('google_drive_folder_id');
|
||||
|
||||
const redirectUri = window.location.origin + "/google-drive-callback";
|
||||
|
||||
let accessToken = null;
|
||||
let refreshToken = null;
|
||||
|
||||
// Define folderIdInput at the top level so it's accessible throughout the script
|
||||
const folderIdInput = document.getElementById('folder-id-input');
|
||||
const folderSelectBtn = document.getElementById('folder-select-picker-btn');
|
||||
const saveFolderBtn = document.getElementById('save-folder-btn');
|
||||
|
||||
// Automatically exchange the code for a refresh token
|
||||
if (code) {
|
||||
if (!clientId || !clientSecret) {
|
||||
showError("Missing Client ID or Client Secret. Please go back to the setup page and try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
exchangeCode(code, clientId, clientSecret, redirectUri, folderId);
|
||||
} else {
|
||||
showError("No authorization code was found in the URL");
|
||||
}
|
||||
|
||||
function exchangeCode(code, clientId, clientSecret, redirectUri, folderId) {
|
||||
const formData = new FormData();
|
||||
formData.append('client_id', clientId);
|
||||
formData.append('client_secret', clientSecret);
|
||||
formData.append('redirect_uri', redirectUri);
|
||||
formData.append('code', code);
|
||||
if (folderId) {
|
||||
formData.append('folder_id', folderId);
|
||||
}
|
||||
|
||||
document.getElementById('processing-message').innerHTML =
|
||||
'<p>Exchanging authorization code for refresh token...</p>';
|
||||
|
||||
fetch('/api/google-drive/exchange-token', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.detail || 'Failed to exchange token');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.refresh_token) {
|
||||
// Store tokens
|
||||
refreshToken = data.refresh_token;
|
||||
accessToken = data.access_token;
|
||||
|
||||
// Update settings in memory first
|
||||
const updateFormData = new FormData();
|
||||
updateFormData.append('refresh_token', data.refresh_token);
|
||||
|
||||
// Use the client ID and client secret from session storage
|
||||
updateFormData.append('client_id', clientId);
|
||||
updateFormData.append('client_secret', clientSecret);
|
||||
if (folderId) {
|
||||
updateFormData.append('folder_id', folderId);
|
||||
}
|
||||
updateFormData.append('use_oauth', 'true');
|
||||
|
||||
document.getElementById('processing-message').innerHTML =
|
||||
'<p>Updating system settings with new token...</p>';
|
||||
|
||||
return fetch('/api/google-drive/update-settings', {
|
||||
method: 'POST',
|
||||
body: updateFormData
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error('Token received but failed to update settings: ' + (err.detail || 'Unknown error'));
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
}).then(() => {
|
||||
// If we have a folder ID, save settings directly
|
||||
if (folderId) {
|
||||
return saveSettings(data.refresh_token, clientId, clientSecret, folderId);
|
||||
} else {
|
||||
// Show folder selection UI
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('folder-selection-container').classList.remove('hidden');
|
||||
|
||||
// Initialize Google Picker for folder selection
|
||||
loadGooglePicker(accessToken, clientId);
|
||||
|
||||
return null; // Return null to avoid further .then() processing
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error('No refresh token was received from the server');
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
// Only proceed if we saved settings directly
|
||||
if (result) {
|
||||
showSuccess(result.refresh_token, result.client_id, result.client_secret, result.folderId);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('folder-selection-container').classList.add('hidden');
|
||||
document.getElementById('error-container').classList.remove('hidden');
|
||||
document.getElementById('error-message').innerText = message;
|
||||
|
||||
// Hide the spinner when showing error
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
}
|
||||
|
||||
function saveSettings(refreshToken, clientId, clientSecret, folderId) {
|
||||
const saveFormData = new FormData();
|
||||
saveFormData.append('refresh_token', refreshToken);
|
||||
saveFormData.append('client_id', clientId);
|
||||
saveFormData.append('client_secret', clientSecret);
|
||||
if (folderId) {
|
||||
saveFormData.append('folder_id', folderId);
|
||||
}
|
||||
saveFormData.append('use_oauth', 'true');
|
||||
|
||||
document.getElementById('processing-message').innerHTML =
|
||||
'<p>Saving settings to configuration...</p>';
|
||||
|
||||
return fetch('/api/google-drive/save-settings', {
|
||||
method: 'POST',
|
||||
body: saveFormData
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
// Check if the error is about .env file not found
|
||||
if (err.detail && err.detail.includes('.env file not found')) {
|
||||
// Continue despite .env file issue
|
||||
return {
|
||||
status: 'warning',
|
||||
message: 'Settings saved in memory only. The .env file could not be updated.',
|
||||
in_memory_only: true
|
||||
};
|
||||
}
|
||||
throw new Error('Failed to save settings to file: ' + (err.detail || 'Unknown error'));
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
}).then((result) => {
|
||||
// Return the values for showing success screen
|
||||
return {
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
folderId: folderId,
|
||||
in_memory_only: result.in_memory_only || false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(refreshToken, clientId, clientSecret, folderId, inMemoryOnly) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('folder-selection-container').classList.add('hidden');
|
||||
document.getElementById('success-container').classList.remove('hidden');
|
||||
|
||||
// Hide the spinner when showing success
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
|
||||
// Update the environment variables pre block with the new token
|
||||
const envVarsCode = document.querySelector('#env-vars code');
|
||||
if (envVarsCode) {
|
||||
envVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=true
|
||||
GOOGLE_DRIVE_CLIENT_ID=${clientId}
|
||||
GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
|
||||
GOOGLE_DRIVE_REFRESH_TOKEN=${refreshToken}
|
||||
GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
|
||||
}
|
||||
|
||||
// If settings were only saved in memory, add a warning
|
||||
if (inMemoryOnly) {
|
||||
const successContainer = document.getElementById('success-container');
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.className = 'rounded-md bg-yellow-50 p-4 mt-4';
|
||||
warningDiv.innerHTML = `
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-yellow-800">
|
||||
Note: Settings were saved in memory only. The .env file could not be updated.
|
||||
Make sure to add these environment variables to your configuration files manually.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert warning after the success message but before the environment vars section
|
||||
const envVarsSection = document.querySelector('#success-container .mt-6');
|
||||
successContainer.insertBefore(warningDiv, envVarsSection);
|
||||
}
|
||||
|
||||
// Add copy functionality
|
||||
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
|
||||
if (copyEnvVarsBtn) {
|
||||
copyEnvVarsBtn.addEventListener('click', function() {
|
||||
const envVarsText = document.getElementById('env-vars').textContent;
|
||||
navigator.clipboard.writeText(envVarsText)
|
||||
.then(() => {
|
||||
const originalText = copyEnvVarsBtn.textContent;
|
||||
copyEnvVarsBtn.textContent = 'Copied!';
|
||||
copyEnvVarsBtn.classList.add('bg-green-700');
|
||||
setTimeout(() => {
|
||||
copyEnvVarsBtn.textContent = originalText;
|
||||
copyEnvVarsBtn.classList.remove('bg-green-700');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy text to clipboard');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Clear session storage
|
||||
sessionStorage.removeItem('google_drive_client_id');
|
||||
sessionStorage.removeItem('google_drive_client_secret');
|
||||
sessionStorage.removeItem('google_drive_folder_id');
|
||||
|
||||
// In 10 seconds, redirect to status page
|
||||
setTimeout(() => {
|
||||
window.location.href = '/status';
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Handle folder selection UI
|
||||
if (saveFolderBtn) {
|
||||
saveFolderBtn.addEventListener('click', function() {
|
||||
if (!folderIdInput) {
|
||||
alert('Error: Folder input element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const folderId = folderIdInput.value.trim() || 'root';
|
||||
saveFolderBtn.disabled = true;
|
||||
saveFolderBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
|
||||
|
||||
saveSettings(refreshToken, clientId, clientSecret, folderId)
|
||||
.then(result => {
|
||||
showSuccess(result.refresh_token, result.client_id, result.client_secret, result.folderId);
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Failed to save settings: ' + error.message);
|
||||
saveFolderBtn.disabled = false;
|
||||
saveFolderBtn.textContent = 'Save Settings';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to load and initialize the Google Picker
|
||||
function loadGooglePicker(accessToken, clientId) {
|
||||
// Load the Google API Loader script
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://apis.google.com/js/api.js';
|
||||
script.onload = function() {
|
||||
gapi.load('picker', function() {
|
||||
initGooglePicker(accessToken, clientId);
|
||||
});
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
// Initialize and setup the Google Picker
|
||||
function initGooglePicker(accessToken, clientId) {
|
||||
if (!accessToken || !clientId || !folderSelectBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the click handler for the folder select button
|
||||
folderSelectBtn.addEventListener('click', function() {
|
||||
// Create the folder picker view
|
||||
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
|
||||
.setIncludeFolders(true)
|
||||
.setSelectFolderEnabled(true)
|
||||
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode to work with the drive.file scope
|
||||
|
||||
// Create and render the picker
|
||||
const picker = new google.picker.PickerBuilder()
|
||||
.addView(folderView)
|
||||
.setOAuthToken(accessToken)
|
||||
.setOrigin(window.location.protocol + '//' + window.location.host)
|
||||
.setTitle('Select a folder for DocuElevate')
|
||||
.setCallback(pickerCallback)
|
||||
.build();
|
||||
|
||||
picker.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Callback function for picker
|
||||
function pickerCallback(data) {
|
||||
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
|
||||
const folder = data[google.picker.Response.DOCUMENTS][0];
|
||||
const folderId = folder[google.picker.Document.ID];
|
||||
const folderName = folder[google.picker.Document.NAME];
|
||||
|
||||
// Update the folder ID input
|
||||
if (folderIdInput) {
|
||||
folderIdInput.value = folderId;
|
||||
|
||||
// Add visual confirmation instead of alert
|
||||
const confirmationMsg = document.createElement('div');
|
||||
confirmationMsg.className = 'mt-2 text-sm text-green-600';
|
||||
confirmationMsg.innerHTML = `<svg class="inline-block h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg> Selected folder: "${folderName}"`;
|
||||
|
||||
// Remove previous confirmation if it exists
|
||||
const existingConfirmation = folderIdInput.parentNode.querySelector('.text-green-600');
|
||||
if (existingConfirmation) {
|
||||
existingConfirmation.remove();
|
||||
}
|
||||
|
||||
// Insert the confirmation message after the input field
|
||||
folderIdInput.parentNode.appendChild(confirmationMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Google Drive Authorization Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="bg-white shadow-md rounded-lg p-6">
|
||||
<div class="text-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
|
||||
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the Google Drive authorization.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Return to Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -194,12 +194,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('error-container').classList.remove('hidden');
|
||||
document.getElementById('error-message').innerText = message;
|
||||
|
||||
// Hide the spinner when showing error
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showSuccess(refreshToken, clientId, clientSecret, tenantId, folderPath) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('success-container').classList.remove('hidden');
|
||||
|
||||
// Hide the spinner when showing success
|
||||
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
|
||||
|
||||
// Update the environment variables pre block with the new token
|
||||
const envVarsCode = document.querySelector('#env-vars code');
|
||||
if (envVarsCode) {
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
<p class="text-gray-700">
|
||||
<strong>No Additional Sharing:</strong> We do not share, sell, or otherwise disclose your personal data to third parties for advertising or marketing purposes.
|
||||
</p>
|
||||
<p class="text-gray-700 mt-3">
|
||||
<strong>Cloud Storage Integration:</strong> We integrate with Google Drive, Dropbox, OneDrive and other storage providers to enable you to save and access your documents across services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-6">
|
||||
|
||||
@@ -136,6 +136,22 @@
|
||||
Configure Now
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif name == "Google Drive" %}
|
||||
{% if provider.configured %}
|
||||
<button
|
||||
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
data-provider="google_drive">
|
||||
Test Connection
|
||||
</button>
|
||||
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
<i class="fa-solid fa-gear h-3 w-3 mr-1"></i>
|
||||
Manage
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/google-drive-setup" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Configure Now
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif name == "OpenAI" %}
|
||||
{% if provider.configured %}
|
||||
<button
|
||||
@@ -396,6 +412,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
endpoint = '/api/dropbox/test-token';
|
||||
} else if (provider === 'onedrive') {
|
||||
endpoint = '/api/onedrive/test-token';
|
||||
} else if (provider === 'google_drive') {
|
||||
endpoint = '/api/google-drive/test-token';
|
||||
} else if (provider === 'openai') {
|
||||
endpoint = '/api/openai/test';
|
||||
} else if (provider === 'azure') {
|
||||
@@ -406,14 +424,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showModal('success', 'Connection Test Successful', `${data.message} ${data.account ? 'as ' + data.account : ''}`);
|
||||
// Create successful message
|
||||
let message = data.message || 'Connection successful';
|
||||
|
||||
// Add token expiration info if available (especially for Google Drive)
|
||||
if (data.token_info && data.token_info.expires_in_human) {
|
||||
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
|
||||
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
|
||||
</div>`;
|
||||
|
||||
// Show the message with HTML
|
||||
modalTitle.textContent = 'Connection Test Successful';
|
||||
modalMessage.innerHTML = message;
|
||||
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
|
||||
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4';
|
||||
resultModal.classList.remove('hidden');
|
||||
} else {
|
||||
// Use the regular modal without HTML
|
||||
showModal('success', 'Connection Test Successful', `${data.message} ${data.account ? 'as ' + data.account : ''}`);
|
||||
}
|
||||
} else {
|
||||
if (data.needs_reauth) {
|
||||
showModal('error', 'Authentication Required', 'Your token has expired or is invalid. Please reconfigure this connection.');
|
||||
// Add option to go to config page
|
||||
modalClose.textContent = "Configure Now";
|
||||
modalClose.addEventListener('click', function redirectToConfig() {
|
||||
window.location.href = `/${provider}-setup`;
|
||||
window.location.href = `/${provider.replace('_', '-')}-setup`;
|
||||
modalClose.removeEventListener('click', redirectToConfig);
|
||||
}, { once: true });
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,6 @@ theme:
|
||||
- search.suggest
|
||||
- content.code.copy
|
||||
nav:
|
||||
- Home: /
|
||||
- User Guide: UserGuide
|
||||
- API: API
|
||||
- Deployment: DeploymentGuide
|
||||
|
||||
Reference in New Issue
Block a user