Merge pull request #1 from machinepainting/runpod-oauth

OAuth implementation for DropSendNode
This commit is contained in:
MachinePainting
2025-08-20 23:01:06 -07:00
committed by GitHub
9 changed files with 1604 additions and 89 deletions
+20
View File
@@ -11,3 +11,23 @@ NODE_CLASS_MAPPINGS.update(SETUP_CLASS)
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(RUN_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(SETUP_DISPLAY)
# Define web directory for JavaScript extensions
WEB_DIRECTORY = "./web"
# Register OAuth callback route with ComfyUI server
def register_oauth_routes():
"""Register OAuth callback routes with ComfyUI's PromptServer"""
try:
from server import PromptServer
from .oauth_handler import handle_oauth_callback
# Add the OAuth callback route
PromptServer.instance.app.router.add_get('/oauth/dropbox/callback', handle_oauth_callback)
print("[DropSendNode] Registered OAuth callback route: /oauth/dropbox/callback")
except Exception as e:
print(f"[DropSendNode] Warning: Could not register OAuth routes: {e}")
# Register routes when the node is loaded
register_oauth_routes()
+248
View File
@@ -0,0 +1,248 @@
# dropbox_auth_manager.py
import os
import requests
import json
class DropboxAuthManager:
def __init__(self, app_key=None, app_secret=None):
# Always store provided credentials
self.app_key = app_key
self.app_secret = app_secret
self.refresh_token = None
# Try to load from file storage if no credentials provided
if app_key is None or app_secret is None:
self._try_load_from_file()
def _get_credentials_file_path(self):
"""Get path to credentials file"""
plugin_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(plugin_dir, ".dropbox_credentials.json")
def _try_load_from_file(self):
"""Try to load credentials from JSON file"""
try:
credentials_path = self._get_credentials_file_path()
if os.path.exists(credentials_path):
with open(credentials_path, 'r') as f:
creds = json.load(f)
if self.app_key is None:
self.app_key = creds.get("app_key")
if self.app_secret is None:
self.app_secret = creds.get("app_secret")
if self.refresh_token is None:
self.refresh_token = creds.get("refresh_token")
print(f"[DropboxAuthManager] Loaded credentials from file: {credentials_path}")
except Exception as e:
print(f"[DropboxAuthManager] Could not load from credentials file: {e}")
def is_connected(self):
# Load from file if we don't have credentials yet
if not (self.app_key and self.app_secret and self.refresh_token):
self._try_load_from_file()
return bool(self.app_key and self.app_secret and self.refresh_token)
def store_tokens(self, app_key, app_secret, refresh_token):
"""Store tokens in JSON file"""
try:
credentials_path = self._get_credentials_file_path()
credentials = {
"app_key": app_key,
"app_secret": app_secret,
"refresh_token": refresh_token
}
print(f"[DropboxAuthManager] Storing tokens in file: {credentials_path}")
with open(credentials_path, 'w') as f:
json.dump(credentials, f, indent=2)
# Set appropriate file permissions (readable only by owner)
os.chmod(credentials_path, 0o600)
# Update instance variables after successful storage
self.app_key = app_key
self.app_secret = app_secret
self.refresh_token = refresh_token
print(f"[DropboxAuthManager] Tokens stored successfully in credentials file")
except Exception as e:
error_msg = f"Failed to store Dropbox tokens: {e}"
print(f"[DropboxAuthManager] {error_msg}")
raise RuntimeError(error_msg)
def exchange_auth_code(self, auth_code, redirect_uri=None):
if not (self.app_key and self.app_secret):
raise ValueError("App key and secret must be set before exchanging auth code.")
data = {
"code": auth_code,
"grant_type": "authorization_code",
"client_id": self.app_key,
"client_secret": self.app_secret
}
# Include redirect_uri if it was used in the authorization request
if redirect_uri:
data["redirect_uri"] = redirect_uri
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data)
response.raise_for_status()
creds = response.json()
refresh_token = creds.get("refresh_token")
if not refresh_token:
raise RuntimeError("Failed to obtain refresh token from Dropbox.")
self.store_tokens(self.app_key, self.app_secret, refresh_token)
self.refresh_token = refresh_token
return True
def exchange_auth_code_raw(self, auth_code, redirect_uri=None):
"""Exchange auth code for tokens without storing them"""
if not (self.app_key and self.app_secret):
raise ValueError("App key and secret must be set before exchanging auth code.")
data = {
"code": auth_code,
"grant_type": "authorization_code",
"client_id": self.app_key,
"client_secret": self.app_secret
}
# Include redirect_uri if it was used in the authorization request
if redirect_uri:
data["redirect_uri"] = redirect_uri
headers = {"Content-Type": "application/x-www-form-urlencoded"}
print(f"[DropboxAuthManager] Token exchange request data: {data}")
print(f"[DropboxAuthManager] Making request to: https://api.dropbox.com/oauth2/token")
response = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data)
print(f"[DropboxAuthManager] Response status: {response.status_code}")
print(f"[DropboxAuthManager] Response text: {response.text}")
# If we get a 400 error and we're using a callback URL, try without it
# This handles cases where the auth code was obtained without a callback URL
if response.status_code == 400 and redirect_uri:
print(f"[DropboxAuthManager] 400 error with callback URL, trying without callback URL...")
data_without_redirect = {
"code": auth_code,
"grant_type": "authorization_code",
"client_id": self.app_key,
"client_secret": self.app_secret
}
print(f"[DropboxAuthManager] Retry request data (no callback URL): {data_without_redirect}")
response = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data_without_redirect)
print(f"[DropboxAuthManager] Retry response status: {response.status_code}")
print(f"[DropboxAuthManager] Retry response text: {response.text}")
response.raise_for_status()
return response.json()
def get_access_token(self):
if not self.is_connected():
raise RuntimeError("Dropbox not connected. Please run authentication first.")
data = {
"refresh_token": self.refresh_token,
"grant_type": "refresh_token",
"client_id": self.app_key,
"client_secret": self.app_secret
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data)
response.raise_for_status()
access_token = response.json().get("access_token")
if not access_token:
raise RuntimeError("Failed to retrieve access token.")
return access_token
def reset(self, revoke_token=True):
"""Clear stored tokens and optionally revoke authorization with Dropbox"""
if revoke_token:
# Load credentials from file first if we don't have them in memory
if not (self.refresh_token and self.app_key and self.app_secret):
print("[DropboxAuthManager] Loading credentials for token revocation...")
self._try_load_from_file()
# First, revoke the token with Dropbox if requested and possible
if self.refresh_token and self.app_key and self.app_secret:
try:
print("[DropboxAuthManager] Revoking token with Dropbox...")
# Get current access token to revoke
access_token = self.get_access_token()
# Revoke the token with Dropbox
revoke_data = {
"token": access_token
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(
"https://api.dropboxapi.com/2/auth/token/revoke",
headers=headers,
json=revoke_data
)
if response.status_code == 200:
print("[DropboxAuthManager] Token successfully revoked with Dropbox")
else:
print(f"[DropboxAuthManager] Token revocation failed: {response.status_code} - {response.text}")
except Exception as e:
print(f"[DropboxAuthManager] Could not revoke token with Dropbox: {e}")
# Continue with local cleanup even if revocation fails
else:
print("[DropboxAuthManager] Skipping token revocation (revoke_token=False)")
# Clear credentials file
try:
credentials_path = self._get_credentials_file_path()
if os.path.exists(credentials_path):
os.remove(credentials_path)
print(f"[DropboxAuthManager] Removed credentials file: {credentials_path}")
except Exception as e:
print(f"[DropboxAuthManager] Could not remove credentials file: {e}")
# Clear instance variables
self.app_key = None
self.app_secret = None
self.refresh_token = None
def get_oauth_url(self, redirect_uri=None, state=None, force_reapprove=False):
"""Generate OAuth URL for initial authorization"""
if not self.app_key:
raise ValueError("App key must be set to generate OAuth URL.")
url = f"https://www.dropbox.com/oauth2/authorize?response_type=code&client_id={self.app_key}&token_access_type=offline"
if redirect_uri:
url += f"&redirect_uri={redirect_uri}"
if state:
url += f"&state={state}"
# Force reapproval to ensure user sees authorization screen
if force_reapprove:
url += "&force_reapprove=true"
return url
-10
View File
@@ -1,10 +0,0 @@
# ComfyUI_DropSendNode/generate_base64.py
import base64
app_key = input("Enter your App Key: ")
app_secret = input("Enter your App Secret: ")
credentials = f"{app_key}:{app_secret}"
encoded = base64.b64encode(credentials.encode()).decode()
print(f"\nBasic {encoded}")
-34
View File
@@ -1,34 +0,0 @@
# ComfyUI_DropSendNode/get_refresh_token.py
import base64
import requests
from .runpod_utils import get_redirect_uri, get_dropbox_authorization_url
APP_KEY = input("Enter your APP KEY: ")
APP_SECRET = input("Enter your APP SECRET: ")
redirect_uri = get_redirect_uri()
print(f"Using redirect_uri: {redirect_uri}")
print(f"Authorization URL: {get_dropbox_authorization_url(APP_KEY)}")
print()
AUTH_CODE = input("Paste the Dropbox authorization code: ")
auth_header = base64.b64encode(f"{APP_KEY}:{APP_SECRET}".encode()).decode()
res = requests.post(
"https://api.dropbox.com/oauth2/token",
data={
"code": AUTH_CODE,
"grant_type": "authorization_code",
"redirect_uri": redirect_uri
},
headers={
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/x-www-form-urlencoded"
}
)
if res.ok:
print("\nYour refresh token is:\n", res.json().get("refresh_token"))
else:
print("Error:", res.text)
+766
View File
@@ -0,0 +1,766 @@
# oauth_handler.py - OAuth callback handling for DropSendNode
import asyncio
from aiohttp import web
from .dropbox_auth_manager import DropboxAuthManager
# Global state to track pending OAuth flows
pending_oauth_sessions = {}
class OAuthCallbackHandler:
def __init__(self):
self.session_id = None
self.app_key = None
self.app_secret = None
self.completion_callback = None
def start_oauth_session(self, session_id, app_key, app_secret, completion_callback=None, storage_method="keyring", dropbox_folder="/Apps/ComfyUI_Output_Files", original_redirect_uri=None):
"""Start a new OAuth session and store the credentials for callback processing"""
print(f"[OAuthHandler] Starting OAuth session: {session_id}")
self.session_id = session_id
self.app_key = app_key
self.app_secret = app_secret
self.completion_callback = completion_callback
self.storage_method = storage_method
self.dropbox_folder = dropbox_folder
# Store in global pending sessions
pending_oauth_sessions[session_id] = {
'app_key': app_key,
'app_secret': app_secret,
'completion_callback': completion_callback,
'storage_method': storage_method,
'dropbox_folder': dropbox_folder,
'original_redirect_uri': original_redirect_uri, # Store the original redirect URI
'handler': self
}
print(f"[OAuthHandler] OAuth session stored for: {session_id} with storage method: {storage_method}")
async def process_callback(self, auth_code, redirect_uri=None):
"""Process the OAuth callback with the received auth code"""
try:
print(f"[OAuthHandler] Processing callback for session: {self.session_id}")
print(f"[OAuthHandler] Auth code received: {auth_code[:10]}...")
print(f"[OAuthHandler] Processing callback for auth code exchange")
# Use DropboxAuthManager to exchange the code
print(f"[OAuthHandler] Creating DropboxAuthManager for token exchange...")
auth_manager = DropboxAuthManager(self.app_key, self.app_secret)
print(f"[OAuthHandler] Exchanging auth code for tokens...")
result = auth_manager.exchange_auth_code_raw(auth_code, redirect_uri=redirect_uri)
refresh_token = result.get("refresh_token")
print(f"[OAuthHandler] Auth code exchange successful - got refresh token: {bool(refresh_token)}")
print(f"[OAuthHandler] Using storage method: {self.storage_method}")
# Handle different storage methods
success_message = None
if self.storage_method == "env_file":
# Store in .env file
import os
node_dir = os.path.dirname(__file__)
env_path = os.path.join(node_dir, ".env")
with open(env_path, "w") as f:
f.write(f"DROPBOX_APP_KEY={self.app_key}\n")
f.write(f"DROPBOX_APP_SECRET={self.app_secret}\n")
f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n")
f.write(f"DROPBOX_FOLDER={self.dropbox_folder}\n")
success_message = "Dropbox connected successfully! Credentials saved to .env file."
elif self.storage_method == "display_only":
# Display credentials for manual copying
import os
# Print credentials to console for easy copy/paste
print("\n" + "=" * 80)
print("DROPBOX CREDENTIALS READY FOR PRODUCTION - COPY FROM CONSOLE")
print("=" * 80)
print(f"DROPBOX_APP_KEY={self.app_key}")
print(f"DROPBOX_APP_SECRET={self.app_secret}")
print(f"DROPBOX_REFRESH_TOKEN={refresh_token}")
print(f"DROPBOX_FOLDER={self.dropbox_folder}")
print("=" * 80)
print("Copy the lines above to your environment variables!")
print("Perfect for RunPod, Docker, and production environments!")
print("=" * 80 + "\n")
success_message = f"""Dropbox Connected Successfully!
=====================================================================
ENVIRONMENT VARIABLES - Copy & Paste Ready
=====================================================================
DROPBOX_APP_KEY={self.app_key}
DROPBOX_APP_SECRET={self.app_secret}
DROPBOX_REFRESH_TOKEN={refresh_token}
DROPBOX_FOLDER={self.dropbox_folder}
=====================================================================
Perfect for RunPod, Docker, and production environments!
These credentials are ready to use immediately.
====================================================================="""
else:
# Fallback to env_file
import os
node_dir = os.path.dirname(__file__)
env_path = os.path.join(node_dir, ".env")
with open(env_path, "w") as f:
f.write(f"DROPBOX_APP_KEY={self.app_key}\n")
f.write(f"DROPBOX_APP_SECRET={self.app_secret}\n")
f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n")
f.write(f"DROPBOX_FOLDER={self.dropbox_folder}\n")
success_message = "Dropbox connected successfully! Credentials saved to .env file."
# Send WebSocket message to trigger ComfyUI refresh (except for display_only)
if self.storage_method != "display_only":
try:
from server import PromptServer
message_data = {
"type": "dropbox_oauth_complete",
"session_id": self.session_id,
"success": True,
"message": success_message
}
PromptServer.instance.send_sync("dropbox_oauth_complete", message_data)
print(f"[OAuthHandler] Sent WebSocket notification for OAuth completion")
except Exception as e:
print(f"[OAuthHandler] Warning: Could not send WebSocket notification: {e}")
else:
print(f"[OAuthHandler] Skipping WebSocket notification for display_only method")
# Call completion callback if provided
if self.completion_callback:
await self.completion_callback(True, success_message)
return True, success_message
except Exception as e:
error_msg = f"Error: OAuth callback failed: {e}"
print(f"[OAuthHandler] ERROR: {error_msg}")
if self.completion_callback:
await self.completion_callback(False, error_msg)
return False, error_msg
def create_display_only_success_page(session_id, app_key, app_secret, refresh_token, dropbox_folder):
"""Create a success page with textarea fields for display_only storage method"""
from aiohttp import web
return web.Response(text=f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropbox Authorization Successful</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}}
.container {{
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 90%;
color: #333;
}}
.success-icon {{
font-size: 4rem;
margin-bottom: 20px;
color: #22c55e;
}}
h2 {{
margin: 0 0 10px 0;
color: #1f2937;
font-size: 1.5rem;
}}
h3 {{
margin: 20px 0 15px 0;
color: #374151;
font-size: 1.1rem;
text-align: left;
}}
.subtitle {{
margin: 0 0 30px 0;
color: #6b7280;
line-height: 1.5;
font-size: 0.95rem;
}}
.credential-group {{
margin-bottom: 20px;
text-align: left;
}}
.credential-label {{
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
font-size: 0.9rem;
}}
.credential-input {{
width: 100%;
padding: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.85rem;
background: #f9fafb;
color: #1f2937;
resize: none;
box-sizing: border-box;
transition: border-color 0.2s;
}}
.credential-input:focus {{
outline: none;
border-color: #3b82f6;
background: white;
}}
.copy-button {{
margin-top: 8px;
padding: 6px 12px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
transition: background 0.2s;
}}
.copy-button:hover {{
background: #2563eb;
}}
.copy-button:active {{
background: #1d4ed8;
}}
.instructions {{
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
padding: 20px;
margin-top: 25px;
text-align: left;
}}
.instructions h4 {{
margin: 0 0 12px 0;
color: #1e40af;
font-size: 1rem;
}}
.instructions ol {{
margin: 0;
padding-left: 18px;
color: #1e40af;
}}
.instructions li {{
margin-bottom: 6px;
line-height: 1.4;
}}
.auto-close-notice {{
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
padding: 12px;
margin-top: 20px;
color: #166534;
font-size: 0.85rem;
}}
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✅</div>
<h2>Dropbox Connected Successfully!</h2>
<p class="subtitle">Your credentials are ready for production use. Copy them to your environment variables.</p>
<h3>🔐 Environment Variables</h3>
<div class="credential-group">
<label class="credential-label">DROPBOX_APP_KEY</label>
<textarea class="credential-input" readonly onclick="this.select()" rows="1">{app_key}</textarea>
<button class="copy-button" onclick="copyToClipboard(this.previousElementSibling)">📋 Copy</button>
</div>
<div class="credential-group">
<label class="credential-label">DROPBOX_APP_SECRET</label>
<textarea class="credential-input" readonly onclick="this.select()" rows="1">{app_secret}</textarea>
<button class="copy-button" onclick="copyToClipboard(this.previousElementSibling)">📋 Copy</button>
</div>
<div class="credential-group">
<label class="credential-label">DROPBOX_REFRESH_TOKEN</label>
<textarea class="credential-input" readonly onclick="this.select()" rows="2">{refresh_token}</textarea>
<button class="copy-button" onclick="copyToClipboard(this.previousElementSibling)">📋 Copy</button>
</div>
<div class="credential-group">
<label class="credential-label">DROPBOX_FOLDER</label>
<textarea class="credential-input" readonly onclick="this.select()" rows="1">{dropbox_folder}</textarea>
<button class="copy-button" onclick="copyToClipboard(this.previousElementSibling)">📋 Copy</button>
</div>
<div class="instructions">
<h4>📋 How to Use These Credentials</h4>
<ol>
<li>Copy each environment variable above</li>
<li>Add them to your production environment (RunPod, Docker, etc.)</li>
<li>The ComfyUI DropSend node will detect and use them automatically</li>
<li>No additional setup required!</li>
</ol>
</div>
<div class="auto-close-notice">
Copy the credentials above, then close this window when done.
</div>
</div>
<script>
function copyToClipboard(textarea) {{
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
try {{
document.execCommand('copy');
// Show success feedback
const button = textarea.nextElementSibling;
const originalText = button.textContent;
button.textContent = '✅ Copied!';
button.style.background = '#22c55e';
setTimeout(() => {{
button.textContent = originalText;
button.style.background = '#3b82f6';
}}, 2000);
}} catch (err) {{
console.error('Could not copy text: ', err);
alert('Copy failed. Please select the text manually.');
}}
}}
// Auto-select text when clicking on textarea
document.querySelectorAll('.credential-input').forEach(textarea => {{
textarea.addEventListener('click', function() {{
this.select();
}});
}});
// For display_only, let user copy credentials first
console.log('Display-only OAuth completed - credentials ready for copying');
// User can close this window when done copying credentials
</script>
</body>
</html>
""", content_type='text/html')
def get_server_base_url(request=None):
"""Dynamically detect the server base URL for OAuth callbacks"""
import os
import socket
try:
if request:
# Try to get from the incoming request
scheme = 'https' if request.secure else 'http'
host = request.host
base_url = f"{scheme}://{host}"
print(f"[OAuthHandler] Detected server URL from request: {base_url}")
return base_url
else:
# Fallback: try to detect from ComfyUI server instance
from server import PromptServer
if hasattr(PromptServer, 'instance') and PromptServer.instance:
# Get the port from ComfyUI server
port = getattr(PromptServer.instance, 'port', 8188)
# Try to detect if we're on RunPod or similar cloud service
hostname = socket.gethostname()
print(f"[OAuthHandler] Detected hostname: {hostname}")
# Check for RunPod-specific hostname patterns
if 'runpod' in hostname.lower() or hostname.endswith('.runpod.net'):
# For RunPod, try to get the public URL
runpod_public_ip = os.getenv('RUNPOD_PUBLIC_IP')
if runpod_public_ip:
base_url = f"https://{runpod_public_ip}:{port}"
print(f"[OAuthHandler] Detected RunPod URL: {base_url}")
return base_url
# Check for common cloud service environment variables
public_url = os.getenv('COMFYUI_PUBLIC_URL') or os.getenv('PUBLIC_URL')
if public_url:
base_url = public_url.rstrip('/')
print(f"[OAuthHandler] Using configured PUBLIC_URL: {base_url}")
return base_url
# Default fallback to localhost
base_url = f"http://localhost:{port}"
print(f"[OAuthHandler] Fallback to localhost: {base_url}")
return base_url
except Exception as e:
print(f"[OAuthHandler] Error detecting server URL: {e}")
import traceback
traceback.print_exc()
# Final fallback
base_url = "http://localhost:8188"
print(f"[OAuthHandler] Using final fallback: {base_url}")
return base_url
# OAuth callback route handler
async def handle_oauth_callback(request):
"""Handle incoming OAuth callback from Dropbox"""
try:
# Extract parameters from callback
auth_code = request.query.get('code')
error = request.query.get('error')
session_id = request.query.get('state') # We'll use state parameter to track sessions
print(f"[OAuthCallback] Received callback - code: {bool(auth_code)}, error: {error}, state: {session_id}")
if error:
error_msg = f"OAuth authorization failed: {error}"
print(f"[OAuthCallback] ERROR: {error_msg}")
return web.Response(text=f"""
<html><body>
<h2>❌ Authorization Failed</h2>
<p>{error_msg}</p>
<p>You can close this window and try again in ComfyUI.</p>
</body></html>
""", content_type='text/html', status=400)
if not auth_code:
print(f"[OAuthCallback] ERROR: No authorization code received")
return web.Response(text="""
<html><body>
<h2>❌ No Authorization Code</h2>
<p>No authorization code was received. Please try again.</p>
<p>You can close this window and try again in ComfyUI.</p>
</body></html>
""", content_type='text/html', status=400)
# Find the pending OAuth session
if session_id and session_id in pending_oauth_sessions:
session_data = pending_oauth_sessions[session_id]
handler = session_data['handler']
# Use the original redirect URI that was used for authorization (not the detected one)
original_callback_url = session_data.get('original_redirect_uri')
if original_callback_url:
print(f"[OAuthCallback] Using stored redirect URI: {original_callback_url}")
callback_url = original_callback_url
else:
# Fallback to dynamic detection
base_url = get_server_base_url(request)
callback_url = f"{base_url}/oauth/dropbox/callback"
print(f"[OAuthCallback] Using detected redirect URI: {callback_url}")
success, message = await handler.process_callback(auth_code, redirect_uri=callback_url)
# Clean up the session
del pending_oauth_sessions[session_id]
if success:
# Check if this is a display_only flow and show appropriate success page
storage_method = session_data.get('storage_method', 'keyring')
if storage_method == 'display_only':
# Extract refresh token from the success message (token exchange already happened in process_callback)
if 'DROPBOX_REFRESH_TOKEN=' in message:
import re
refresh_token_match = re.search(r'DROPBOX_REFRESH_TOKEN=([^\s\n]+)', message)
if refresh_token_match:
refresh_token = refresh_token_match.group(1)
app_key = session_data['app_key']
app_secret = session_data['app_secret']
dropbox_folder = session_data['dropbox_folder']
# Special success page with textarea fields for credentials (stays open for copying)
return create_display_only_success_page(session_id, app_key, app_secret, refresh_token, dropbox_folder)
# If we couldn't extract the refresh token, fall back to error handling
return web.Response(text=f"""
<html><body>
<h2>❌ Display Setup Error</h2>
<p>Could not extract refresh token from OAuth response.</p>
<p>Please close this window and try again in ComfyUI.</p>
<p>Debug info: {message[:200]}...</p>
</body></html>
""", content_type='text/html', status=500)
else:
# Standard success page for env_file storage method (auto-closes quickly)
return web.Response(text=f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropbox Authorization Successful</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}}
.container {{
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 400px;
color: #333;
}}
.success-icon {{
font-size: 4rem;
margin-bottom: 20px;
color: #22c55e;
}}
h2 {{
margin: 0 0 20px 0;
color: #1f2937;
font-size: 1.5rem;
}}
p {{
margin: 10px 0;
color: #6b7280;
line-height: 1.5;
}}
.close-button {{
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
margin-top: 20px;
transition: background 0.2s;
}}
.close-button:hover {{
background: #5a67d8;
}}
.loading {{
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}}
@keyframes spin {{
0% {{ transform: rotate(0deg); }}
100% {{ transform: rotate(360deg); }}
}}
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✓</div>
<h2>Authorization Successful!</h2>
<p>{message}</p>
<button class="close-button" onclick="window.close()">Close Window</button>
</div>
<script>
// Notify parent window (ComfyUI) about successful OAuth
if (window.opener) {{
try {{
window.opener.postMessage({{
type: 'dropbox_oauth_complete',
success: true,
message: '{message}',
session_id: '{session_id}'
}}, '*');
console.log('Sent OAuth completion message to parent window');
}} catch (e) {{
console.error('Could not notify parent window:', e);
}}
}}
// User can now manually close the window with the button
console.log('Success page loaded for env_file - user can close manually');
</script>
</body></html>
""", content_type='text/html')
else:
return web.Response(text=f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropbox Authorization Failed</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
margin: 0;
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}}
.container {{
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 400px;
color: #333;
}}
.error-icon {{
font-size: 4rem;
margin-bottom: 20px;
color: #ef4444;
}}
h2 {{
margin: 0 0 20px 0;
color: #1f2937;
font-size: 1.5rem;
}}
p {{
margin: 10px 0;
color: #6b7280;
line-height: 1.5;
}}
.retry-hint {{
background: #f3f4f6;
padding: 15px;
border-radius: 10px;
margin-top: 20px;
font-size: 0.9rem;
}}
</style>
</head>
<body>
<div class="container">
<div class="error-icon">❌</div>
<h2>Setup Failed</h2>
<p>{message}</p>
<div class="retry-hint">
<p>You can close this window and try again in ComfyUI.</p>
</div>
</div>
</body></html>
""", content_type='text/html', status=500)
else:
# Fallback for sessions without state tracking
print("[OAuthCallback] No session found, attempting generic processing")
return web.Response(text=f"""
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dropbox Authorization Code</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
margin: 0;
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}}
.container {{
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 500px;
color: #333;
}}
.code-icon {{
font-size: 4rem;
margin-bottom: 20px;
color: #3b82f6;
}}
h2 {{
margin: 0 0 20px 0;
color: #1f2937;
font-size: 1.5rem;
}}
p {{
margin: 10px 0;
color: #6b7280;
line-height: 1.5;
}}
.auth-code {{
background: #f3f4f6;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 1.1rem;
word-break: break-all;
color: #1f2937;
border: 2px solid #3b82f6;
position: relative;
}}
.copy-hint {{
background: #eff6ff;
padding: 15px;
border-radius: 10px;
margin-top: 20px;
font-size: 0.9rem;
color: #1e40af;
}}
</style>
</head>
<body>
<div class="container">
<div class="code-icon">🔄</div>
<h2>Authorization Code Received</h2>
<p>Please return to ComfyUI and manually enter this code:</p>
<div class="auth-code">{auth_code}</div>
<div class="copy-hint">
<p>📋 Select the code above and copy it to ComfyUI</p>
</div>
</div>
</body></html>
""", content_type='text/html')
except Exception as e:
print(f"[OAuthCallback] Unexpected error: {e}")
return web.Response(text=f"""
<html><body>
<h2>❌ Callback Error</h2>
<p>An unexpected error occurred: {e}</p>
<p>You can close this window and try again in ComfyUI.</p>
</body></html>
""", content_type='text/html', status=500)
+1
View File
@@ -1,3 +1,4 @@
requests
python-dotenv
watchdog
dropbox
+218 -45
View File
@@ -2,18 +2,35 @@
import os
import requests
import webbrowser
import uuid
import json
from dotenv import load_dotenv, dotenv_values
from .runpod_utils import get_redirect_uri
import urllib.parse
from .dropbox_auth_manager import DropboxAuthManager
from .oauth_handler import OAuthCallbackHandler, get_server_base_url
class DropboxSetupNode:
@classmethod
def INPUT_TYPES(cls):
# Always show all fields for simplicity - no dynamic field hiding
return {
"required": {
"app_key": ("STRING", {"default": ""}),
"app_secret": ("STRING", {"default": ""}),
"auth_code": ("STRING", {"default": ""}),
"app_key": ("STRING", {"default": "", "multiline": False}),
"app_secret": ("STRING", {"default": "", "multiline": False}),
"auth_code": ("STRING", {"default": "", "multiline": False}),
"dropbox_dest_folder": ("STRING", {"default": "/Apps/ComfyUI_Output_Files"}),
},
"optional": {
"reconnect": ("BOOLEAN", {
"label": "Reset stored credentials",
"default": False
}),
"storage_method": (["env_file", "display_only"], {
"label": "Credential Storage Method",
"default": "env_file"
})
}
}
@@ -21,54 +38,210 @@ class DropboxSetupNode:
OUTPUT_NODE = True
FUNCTION = "setup"
def setup(self, app_key, app_secret, auth_code, dropbox_dest_folder):
node_dir = os.path.dirname(__file__)
env_path = os.path.join(node_dir, ".env")
def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False, storage_method="env_file"):
try:
print(f"[DropboxSetup] Called with:")
print(f" app_key: '{app_key}' (type: {type(app_key)}, bool: {bool(app_key)})")
print(f" app_secret: '{app_secret}' (type: {type(app_secret)}, bool: {bool(app_secret)})")
print(f" auth_code: '{auth_code}' (type: {type(auth_code)}, bool: {bool(auth_code)})")
print(f" reconnect: {reconnect}")
print(f" storage_method: {storage_method}")
# Initialize auth manager
auth_manager = DropboxAuthManager()
print(f"[DropboxSetup] Auth manager initialized")
# Handle reconnect/reset request
if reconnect:
print("[DropboxSetup] Reconnect requested - clearing all credentials")
# Clear all credentials (skip token revocation to avoid delays)
print("[DropboxSetup] Clearing all stored credentials")
auth_manager.reset(revoke_token=False)
# Manually clear all credential files
node_dir = os.path.dirname(__file__)
# Clear .env file if it exists
env_path = os.path.join(node_dir, ".env")
if os.path.exists(env_path):
print(f"[DropboxSetup] Removing .env file: {env_path}")
os.remove(env_path)
# Send WebSocket message to trigger ComfyUI refresh after clearing credentials
try:
from server import PromptServer
message_data = {
"type": "dropbox_reconnect_complete",
"success": True,
"message": "Credentials cleared - ComfyUI will refresh to show auth fields"
}
PromptServer.instance.send_sync("dropbox_reconnect_complete", message_data)
print(f"[DropboxSetup] Sent WebSocket notification for reconnect completion")
except Exception as e:
print(f"[DropboxSetup] Warning: Could not send WebSocket notification: {e}")
message = "Dropbox credentials cleared. ComfyUI will refresh to show auth fields..."
print(f"[DropboxSetup] {message}")
return {
"ui": {"text": [message]},
"result": (message,)
}
# Check for environment variables (from any source)
env_vars_set = all([
os.getenv("DROPBOX_APP_KEY"),
os.getenv("DROPBOX_APP_SECRET"),
os.getenv("DROPBOX_REFRESH_TOKEN")
])
if env_vars_set:
message = "Dropbox credentials found in system environment variables. Ready to upload files."
print(f"[DropboxSetup] {message}")
return {
"ui": {"text": [message]},
"result": (message,)
}
# 1. Skip setup if generic environment variables already exist
general_env_set = all([
os.getenv("DROPBOX_APP_KEY"),
os.getenv("DROPBOX_APP_SECRET"),
os.getenv("DROPBOX_REFRESH_TOKEN")
])
if general_env_set:
return ("⚠️ Dropbox credentials already set in system environment variables. Skipping .env creation.",)
# Check for RunPod secrets
runpod_env_set = all([
os.getenv("RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN"),
os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN")
])
if runpod_env_set:
return ("Warning: Detected RunPod secrets. Using those instead.",)
# 2. Skip setup if RunPod secrets are defined
runpod_env_set = all([
os.getenv("RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN"),
os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN")
])
if runpod_env_set:
return ("⚠️ Detected RunPod secrets (RUNPOD_SECRET_DROPBOX_ACCESS_TOKEN, RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN). Skipping .env creation.",)
# New setup flow using DropboxAuthManager
print(f"[DropboxSetup] Starting new setup flow")
# Clean up the inputs first
app_key_clean = app_key.strip() if app_key else ""
app_secret_clean = app_secret.strip() if app_secret else ""
auth_code_clean = auth_code.strip() if auth_code else ""
print(f"[DropboxSetup] Cleaned inputs:")
print(f" app_key_clean: '{app_key_clean}' (len: {len(app_key_clean)})")
print(f" app_secret_clean: '{app_secret_clean}' (len: {len(app_secret_clean)})")
print(f" auth_code_clean: '{auth_code_clean}' (len: {len(auth_code_clean)})")
# Check if we have app credentials
if not app_key_clean or not app_secret_clean:
message = "Error: Missing App Key or App Secret. Please provide both."
print(f"[DropboxSetup] {message}")
return (message,)
# If no auth code, generate OAuth URL for manual code flow
if not auth_code_clean:
print(f"[DropboxSetup] No auth code provided - generating OAuth URL for manual flow")
auth_temp = DropboxAuthManager(app_key=app_key_clean)
# Manual OAuth flow without redirect_uri (Dropbox will display the code)
oauth_url = auth_temp.get_oauth_url(force_reapprove=True)
try:
print(f"[DropboxSetup] Setting up manual OAuth popup...")
# JavaScript will automatically open a popup window
message = f"Dropbox OAuth Ready!\n\nClick the link below to authorize with Dropbox:\n\n{oauth_url}\n\nA popup window will open. After authorization, Dropbox will show your auth code.\nCopy the code and paste it into the 'auth_code' field above, then run this node again."
print(f"[DropboxSetup] OAuth URL ready for popup: {oauth_url}")
except Exception as e:
print(f"[DropboxSetup] Error setting up OAuth: {e}")
message = f"Dropbox Authorization:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, Dropbox will display your auth code. Copy it and paste into the 'auth_code' field above."
print(f"[DropboxSetup] OAuth URL: {oauth_url}")
# Use ComfyUI's dynamic return format for better UI integration
return {
"ui": {"text": [message]},
"result": (message,)
}
# 3. Run auth_code flow if no env config is present
if not all([app_key, app_secret, auth_code]):
return ("❌ Missing App Key, Secret, or Authorization Code.",)
# Exchange auth code for refresh token using DropboxAuthManager
print(f"[DropboxSetup] Attempting to exchange auth code")
auth_manager_setup = DropboxAuthManager(app_key_clean, app_secret_clean)
# Get the tokens without storing them yet
# Use manual OAuth flow (auth codes from manual copy/paste)
print(f"[DropboxSetup] Using manual OAuth flow")
result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean)
refresh_token = result.get("refresh_token")
print(f"[DropboxSetup] Auth code exchange successful")
print(f"[DropboxSetup] Using storage method: {storage_method}")
# Handle different storage methods
if storage_method == "env_file":
# Store in .env file
node_dir = os.path.dirname(__file__)
env_path = os.path.join(node_dir, ".env")
with open(env_path, "w") as f:
f.write(f"DROPBOX_APP_KEY={app_key_clean}\n")
f.write(f"DROPBOX_APP_SECRET={app_secret_clean}\n")
f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n")
f.write(f"DROPBOX_FOLDER={dropbox_dest_folder}\n")
message = "Dropbox connected successfully! Credentials saved to .env file."
elif storage_method == "display_only":
# Display credentials for manual copying
message = f"""Dropbox Connected Successfully!
data = {
"code": auth_code,
"grant_type": "authorization_code",
"client_id": app_key,
"client_secret": app_secret,
"redirect_uri": get_redirect_uri()
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
=====================================================================
ENVIRONMENT VARIABLES - Copy & Paste Ready
=====================================================================
resp = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data)
resp.raise_for_status()
refresh_token = resp.json().get("refresh_token")
DROPBOX_APP_KEY={app_key_clean}
# Write .env with fallback credentials
with open(env_path, "w") as f:
f.write(f"DROPBOX_APP_KEY={app_key}\n")
f.write(f"DROPBOX_APP_SECRET={app_secret}\n")
f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n")
f.write(f"DROPBOX_FOLDER={dropbox_dest_folder}\n")
DROPBOX_APP_SECRET={app_secret_clean}
load_dotenv(env_path, override=True)
return ("✅ .env file created with Dropbox credentials.",)
DROPBOX_REFRESH_TOKEN={refresh_token}
DROPBOX_FOLDER={dropbox_dest_folder}
=====================================================================
Perfect for RunPod, Docker, and production environments!
These credentials are ready to use immediately.
====================================================================="""
else:
# Fallback to env_file storage
node_dir = os.path.dirname(__file__)
env_path = os.path.join(node_dir, ".env")
with open(env_path, "w") as f:
f.write(f"DROPBOX_APP_KEY={app_key_clean}\n")
f.write(f"DROPBOX_APP_SECRET={app_secret_clean}\n")
f.write(f"DROPBOX_REFRESH_TOKEN={refresh_token}\n")
f.write(f"DROPBOX_FOLDER={dropbox_dest_folder}\n")
message = "Dropbox connected successfully! Credentials saved to .env file."
print(f"[DropboxSetup] {message}")
# For display_only, ensure credentials are prominently shown in console
if storage_method == "display_only":
print("\n" + "="*80)
print("DROPBOX CREDENTIALS READY FOR PRODUCTION - COPY FROM CONSOLE")
print("="*80)
print(f"DROPBOX_APP_KEY={app_key_clean}")
print(f"DROPBOX_APP_SECRET={app_secret_clean}")
print(f"DROPBOX_REFRESH_TOKEN={refresh_token}")
print(f"DROPBOX_FOLDER={dropbox_dest_folder}")
print("="*80)
print("Copy the lines above to your environment variables!")
print("Perfect for RunPod, Docker, and production environments!")
print("="*80 + "\n")
# Use ComfyUI's dynamic return format for better UI integration
return {
"ui": {"text": [message]},
"result": (message,)
}
except Exception as e:
message = f"Error: Setup failed: {e}"
print(f"[DropboxSetup] ERROR: {message}")
return {
"ui": {"text": [message]},
"result": (message,)
}
# Required mappings for ComfyUI
NODE_CLASS_MAPPINGS = {"DropboxSetupNode": DropboxSetupNode}
NODE_DISPLAY_NAME_MAPPINGS = {"DropboxSetupNode": "📦⚙️ Dropbox AutoUploader Setup"}
NODE_DISPLAY_NAME_MAPPINGS = {"DropboxSetupNode": "Dropbox AutoUploader Setup"}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
Simple non-interactive test script for DropboxAuthManager
Usage:
python test_dropbox_auth_simple.py
This script tests the DropboxAuthManager without requiring user input.
"""
import sys
import os
# Add parent directory to path so we can import from the main module
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dropbox_auth_manager import DropboxAuthManager
def test_basic_functionality():
"""Test basic DropboxAuthManager functionality"""
print("=" * 50)
print("Testing DropboxAuthManager Basic Functionality")
print("=" * 50)
try:
# Test initialization without credentials
print("\n1. Testing initialization without credentials...")
auth = DropboxAuthManager()
print(f"PASS: DropboxAuthManager initialized")
print(f" is_connected(): {auth.is_connected()}")
# Test initialization with test credentials
print("\n2. Testing initialization with test credentials...")
test_auth = DropboxAuthManager(app_key="test_key_123", app_secret="test_secret_456")
print(f"PASS: DropboxAuthManager initialized with test credentials")
print(f" app_key: {test_auth.app_key}")
print(f" is_connected(): {test_auth.is_connected()}")
# Test OAuth URL generation
print("\n3. Testing OAuth URL generation...")
oauth_url = test_auth.get_oauth_url()
print(f"PASS: Basic OAuth URL: {oauth_url[:80]}...")
# Test OAuth URL with manual flow (no redirect_uri needed)
oauth_url_manual = test_auth.get_oauth_url(state="test-session-123", force_reapprove=True)
print(f"PASS: Manual OAuth URL: {oauth_url_manual[:100]}...")
# Test that manual OAuth URL is properly formatted
if "response_type=code" in oauth_url_manual and "token_access_type=offline" in oauth_url_manual:
print("PASS: OAuth URL properly formatted for manual flow")
else:
print("FAIL: OAuth URL missing required parameters")
# Test that state is included when provided
if "state=test-session-123" in oauth_url_manual:
print("PASS: state parameter properly included in OAuth URL")
else:
print("FAIL: state parameter not found in OAuth URL")
print("\n" + "=" * 50)
print("PASS: All basic functionality tests passed!")
print("=" * 50)
except Exception as e:
print(f"FAIL: Error during basic functionality test: {e}")
raise
def test_error_handling():
"""Test error handling scenarios"""
print("\n" + "=" * 50)
print("Testing Error Handling")
print("=" * 50)
try:
# Test OAuth URL generation without app_key
print("\n1. Testing OAuth URL without app_key...")
auth_no_key = DropboxAuthManager()
try:
auth_no_key.get_oauth_url()
print("FAIL: Should have raised ValueError")
except ValueError as e:
print(f"PASS: Correctly raised ValueError: {e}")
# Test auth code exchange without credentials
print("\n2. Testing auth code exchange without credentials...")
try:
auth_no_key.exchange_auth_code_raw("test_code")
print("FAIL: Should have raised ValueError")
except ValueError as e:
print(f"PASS: Correctly raised ValueError: {e}")
print("\n" + "=" * 50)
print("PASS: All error handling tests passed!")
print("=" * 50)
except Exception as e:
print(f"FAIL: Unexpected error during error handling test: {e}")
raise
def test_token_exchange_retry_logic():
"""Test the token exchange retry logic (without actual API calls)"""
print("\n" + "=" * 50)
print("Testing Token Exchange Retry Logic")
print("=" * 50)
# This test just verifies the method exists and can be called with parameters
# It won't make actual API calls since we don't have real credentials
try:
test_auth = DropboxAuthManager(app_key="test_key", app_secret="test_secret")
print("\n1. Testing exchange_auth_code_raw method signature...")
# Test method exists and accepts parameters (will fail with API call, but that's expected)
try:
test_auth.exchange_auth_code_raw("fake_code")
except Exception as e:
if "400" in str(e) or "requests" in str(e) or "HTTPSConnectionPool" in str(e):
print(f"PASS: Method executed correctly (expected API failure): {type(e).__name__}")
else:
print(f"FAIL: Unexpected error type: {e}")
# Test method with redirect_uri (for backward compatibility)
try:
test_auth.exchange_auth_code_raw("fake_code", redirect_uri="http://localhost:8188/oauth/dropbox/callback")
except Exception as e:
if "400" in str(e) or "requests" in str(e) or "HTTPSConnectionPool" in str(e):
print(f"PASS: Method with redirect_uri executed correctly (expected API failure): {type(e).__name__}")
else:
print(f"FAIL: Unexpected error type: {e}")
print("\n" + "=" * 50)
print("PASS: Token exchange retry logic tests completed!")
print("=" * 50)
except Exception as e:
print(f"FAIL: Error during retry logic test: {e}")
raise
def main():
"""Run all tests"""
print("Starting DropboxAuthManager Tests")
print("This script tests the auth manager without making real API calls")
try:
test_basic_functionality()
test_error_handling()
test_token_exchange_retry_logic()
print("\nAll tests completed successfully!")
print("To test with real Dropbox credentials, use the interactive flow in ComfyUI")
except Exception as e:
print(f"\nTest failed: {e}")
return 1
return 0
if __name__ == "__main__":
exit(main())
+194
View File
@@ -0,0 +1,194 @@
// dropbox_oauth.js - ComfyUI extension for seamless Dropbox OAuth flow
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// Track OAuth popup windows
let oauthPopup = null;
app.registerExtension({
name: "DropSendNode.OAuthHandler",
async setup() {
console.log("[DropSendNode] OAuth extension loaded");
// Listen for WebSocket messages about OAuth completion
api.addEventListener("dropbox_oauth_complete", (event) => {
console.log("[DropSendNode] Received OAuth completion notification via WebSocket", event.detail);
const data = event.detail;
if (data.success) {
console.log("[DropSendNode] OAuth successful");
this.handleOAuthSuccess();
} else {
console.error("[DropSendNode] OAuth failed:", data.message);
}
});
// Listen for WebSocket messages about reconnect completion
api.addEventListener("dropbox_reconnect_complete", (event) => {
console.log("[DropSendNode] Received reconnect completion notification via WebSocket", event.detail);
const data = event.detail;
if (data.success) {
console.log("[DropSendNode] Reconnect successful");
this.handleReconnectSuccess();
}
});
// Listen for postMessage from OAuth popup window
window.addEventListener("message", (event) => {
if (event.data && event.data.type === 'dropbox_oauth_complete') {
console.log("[DropSendNode] Received OAuth completion notification via postMessage", event.data);
if (event.data.success) {
console.log("[DropSendNode] OAuth successful");
this.handleOAuthSuccess();
} else {
console.error("[DropSendNode] OAuth failed:", event.data.message);
}
}
});
},
handleOAuthSuccess() {
// Don't auto-close popup - let user close manually after seeing success message
console.log("[DropSendNode] OAuth success detected - popup will stay open for user to close manually");
// Reset any reconnect checkboxes
this.resetReconnectFields();
// Note: Manual refresh required if user wants to update UI
console.log("[DropSendNode] OAuth success handled - manual refresh available");
},
resetReconnectFields() {
try {
// Find all DropboxSetupNode instances and reset their reconnect fields
const nodes = app.graph.nodes.filter(node => node.type === "DropboxSetupNode");
nodes.forEach(node => {
if (node.widgets) {
const reconnectWidget = node.widgets.find(w => w.name === "reconnect");
if (reconnectWidget && reconnectWidget.value === true) {
console.log("[DropSendNode] Resetting reconnect field to false");
reconnectWidget.value = false;
if (node.onWidgetChanged) {
node.onWidgetChanged("reconnect", false);
}
}
}
});
} catch (e) {
console.log("[DropSendNode] Could not reset reconnect fields:", e);
}
},
handleReconnectSuccess() {
// For reconnect, we DO want auto-refresh to show the auth fields again
console.log("[DropSendNode] Reconnect success - refreshing interface to show auth fields");
// Reset any cached state that might prevent popup detection
this.resetReconnectFields();
// Refresh ComfyUI interface to show auth fields after reconnect
setTimeout(() => {
console.log("[DropSendNode] Refreshing ComfyUI interface after reconnect");
window.location.reload();
}, 1000); // Short delay for reconnect
},
// Override node execution to detect and handle OAuth URLs
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "DropboxSetupNode") {
console.log("[DropSendNode] Registered DropboxSetupNode enhancement");
// Add custom OAuth popup handling
const originalExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function(message) {
console.log("[DropSendNode] Node executed with message:", message);
// Check if this execution includes OAuth URL generation
if (message && typeof message === 'object' && message.text && message.text[0]) {
const text = message.text[0];
console.log("[DropSendNode] Message text content:", text.substring(0, 100) + "...");
if (text.includes('Dropbox OAuth Ready!')) {
console.log("[DropSendNode] Detected Dropbox OAuth ready message");
// Extract OAuth URL from the message
const urlMatch = text.match(/(https:\/\/www\.dropbox\.com\/oauth2\/authorize[^\s]+)/);
if (urlMatch) {
const oauthUrl = urlMatch[1];
console.log("[DropSendNode] Extracted OAuth URL:", oauthUrl);
// Auto-open the popup after a brief delay
setTimeout(() => {
console.log("[DropSendNode] Auto-opening OAuth popup...");
window.openDropboxOAuth(oauthUrl);
}, 500);
} else {
console.log("[DropSendNode] No OAuth URL found in message");
}
} else {
console.log("[DropSendNode] Message does not contain 'Dropbox OAuth Ready!' trigger");
}
} else {
console.log("[DropSendNode] Message structure not recognized for OAuth detection");
}
if (originalExecuted) {
return originalExecuted.apply(this, arguments);
}
};
}
}
});
// Utility function to open OAuth in a small popup window
window.openDropboxOAuth = function(url) {
console.log("[DropSendNode] Opening OAuth popup:", url);
// Close existing popup if open
if (oauthPopup && !oauthPopup.closed) {
oauthPopup.close();
}
// Open small popup window
const popup = window.open(
url,
'dropbox_oauth',
'width=500,height=700,scrollbars=yes,resizable=yes,status=no,location=no,toolbar=no,menubar=no'
);
if (popup) {
oauthPopup = popup;
// Focus the popup
popup.focus();
console.log("[DropSendNode] OAuth popup opened successfully");
// Monitor popup closure with more detailed logging
const checkClosed = setInterval(() => {
if (popup.closed) {
console.log("[DropSendNode] OAuth popup closed by user or completed");
clearInterval(checkClosed);
oauthPopup = null;
}
}, 500); // Check more frequently
// Also monitor popup URL changes (if possible)
try {
popup.addEventListener('beforeunload', () => {
console.log("[DropSendNode] OAuth popup is navigating");
});
} catch (e) {
console.log("[DropSendNode] Cannot monitor popup navigation (cross-origin)");
}
return popup;
} else {
console.error("[DropSendNode] Could not open OAuth popup - popup blocked?");
return null;
}
};