From 17677a6d7c1ce88164e160c7b7a7c21637b43b07 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 12:04:54 -0700 Subject: [PATCH 01/45] adding dropbox to deps --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 24e2bf4..a41eb3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests python-dotenv watchdog +dropbox From ece745c19c3425653827146dde2f1c28e08a4741 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 13:22:39 -0700 Subject: [PATCH 02/45] adding dropbox auth manager class, test script --- dropbox_auth_manager.py | 93 ++++++++++++++++++++++++++++++ requirements.txt | 1 + test_dropbox_auth.py | 121 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 dropbox_auth_manager.py create mode 100644 test_dropbox_auth.py diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py new file mode 100644 index 0000000..be76449 --- /dev/null +++ b/dropbox_auth_manager.py @@ -0,0 +1,93 @@ +# dropbox_auth_manager.py +import os +import requests +import keyring +from keyring.errors import KeyringError + +DROPBOX_KEYRING_SERVICE = "comfyui_dropbox" + +class DropboxAuthManager: + def __init__(self, app_key=None, app_secret=None): + self.app_key = app_key or keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") + self.app_secret = app_secret or keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") + self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + + def is_connected(self): + return bool(self.app_key and self.app_secret and self.refresh_token) + + def store_tokens(self, app_key, app_secret, refresh_token): + try: + keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_key", app_key) + keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_secret", app_secret) + keyring.set_password(DROPBOX_KEYRING_SERVICE, "refresh_token", refresh_token) + + # Update instance variables after successful storage + self.app_key = app_key + self.app_secret = app_secret + self.refresh_token = refresh_token + except KeyringError as e: + raise RuntimeError(f"Failed to store Dropbox tokens securely: {e}") + + def exchange_auth_code(self, auth_code): + 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 + } + 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 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): + """Clear stored tokens""" + try: + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + + # Clear instance variables + self.app_key = None + self.app_secret = None + self.refresh_token = None + except KeyringError: + # If passwords don't exist, that's fine + pass + + def get_oauth_url(self): + """Generate OAuth URL for initial authorization""" + if not self.app_key: + raise ValueError("App key must be set to generate OAuth URL.") + return f"https://www.dropbox.com/oauth2/authorize?response_type=code&client_id={self.app_key}&token_access_type=offline" + diff --git a/requirements.txt b/requirements.txt index a41eb3f..edeadcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ requests python-dotenv watchdog dropbox +keyring diff --git a/test_dropbox_auth.py b/test_dropbox_auth.py new file mode 100644 index 0000000..c468118 --- /dev/null +++ b/test_dropbox_auth.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# test_dropbox_auth.py - Manual testing script for DropboxAuthManager + +from dropbox_auth_manager import DropboxAuthManager + +def test_first_time_setup(): + """Test the initial auth code exchange flow""" + print("๐Ÿงช STEP 1: First-time setup test") + print("-" * 40) + + auth_code = input("Paste your Dropbox auth code: ").strip() + app_key = input("App Key: ").strip() + app_secret = input("App Secret: ").strip() + + auth = DropboxAuthManager(app_key, app_secret) + + try: + auth.exchange_auth_code(auth_code) + print("โœ… Refresh token stored securely in system keyring.") + return auth + except Exception as e: + print(f"โŒ Error exchanging auth code: {e}") + return None + +def test_stored_credentials(): + """Test retrieving and using stored credentials""" + print("\n๐Ÿงช STEP 2: Stored credentials test") + print("-" * 40) + + try: + # Test loading from keyring (no app_key/secret needed) + auth = DropboxAuthManager() + + if not auth.is_connected(): + print("โŒ No stored credentials found. Run first-time setup first.") + return False + + print("โœ… Stored credentials found.") + + # Test getting access token + access_token = auth.get_access_token() + print(f"โœ… Got access token: {access_token[:10]}...") + return True + + except Exception as e: + print(f"โŒ Error getting access token: {e}") + return False + +def test_oauth_url_generation(): + """Test OAuth URL generation""" + print("\n๐Ÿงช STEP 3: OAuth URL generation test") + print("-" * 40) + + app_key = input("App Key (for URL generation): ").strip() + auth = DropboxAuthManager(app_key=app_key) + + try: + oauth_url = auth.get_oauth_url() + print(f"โœ… Generated OAuth URL:") + print(f" {oauth_url}") + print(f" Copy this URL to authorize your app") + return True + except Exception as e: + print(f"โŒ Error generating OAuth URL: {e}") + return False + +def test_reset(): + """Test credential reset functionality""" + print("\n๐Ÿงช STEP 4: Reset credentials test") + print("-" * 40) + + confirm = input("Are you sure you want to clear stored credentials? (y/N): ").strip().lower() + if confirm != 'y': + print("โš ๏ธ Skipping reset test.") + return + + try: + auth = DropboxAuthManager() + auth.reset() + print("โœ… Credentials cleared from keyring.") + + # Verify they're actually gone + auth_check = DropboxAuthManager() + if not auth_check.is_connected(): + print("โœ… Verified: No stored credentials found after reset.") + else: + print("โŒ Warning: Credentials still found after reset.") + + except Exception as e: + print(f"โŒ Error during reset: {e}") + +def main(): + print("๐Ÿš€ Dropbox Auth Manager Test Suite") + print("=" * 50) + + while True: + print("\nSelect a test:") + print("1. First-time setup (exchange auth code)") + print("2. Test stored credentials") + print("3. Generate OAuth URL") + print("4. Reset stored credentials") + print("5. Exit") + + choice = input("\nEnter choice (1-5): ").strip() + + if choice == "1": + test_first_time_setup() + elif choice == "2": + test_stored_credentials() + elif choice == "3": + test_oauth_url_generation() + elif choice == "4": + test_reset() + elif choice == "5": + print("๐Ÿ‘‹ Exiting test suite.") + break + else: + print("โŒ Invalid choice. Please select 1-5.") + +if __name__ == "__main__": + main() \ No newline at end of file From 377c9b794ffff3a8ba012ac36cee79a299221a05 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:07:43 -0700 Subject: [PATCH 03/45] added stripping of whitespace from app key, secret values before sending. added params to get refresh_token. verified working --- setup_dropbox_node.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index cca15ca..e0fe357 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -3,6 +3,8 @@ import os import requests from dotenv import load_dotenv, dotenv_values +import urllib.parse + class DropboxSetupNode: @classmethod @@ -45,14 +47,23 @@ class DropboxSetupNode: if not all([app_key, app_secret, auth_code]): return ("โŒ Missing App Key, Secret, or Authorization Code.",) + app_key = app_key.strip() + app_secret = app_secret.strip() + auth_code = auth_code.strip() + + data = { "code": auth_code, "grant_type": "authorization_code", "client_id": app_key, - "client_secret": app_secret + "client_secret": app_secret, + "token_access_type": "offline" } headers = {"Content-Type": "application/x-www-form-urlencoded"} + + encoded_data = urllib.parse.urlencode(data) + resp = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data) resp.raise_for_status() refresh_token = resp.json().get("refresh_token") From c88a471b58e119d49a09e465d1bc0e12519053c5 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:21:42 -0700 Subject: [PATCH 04/45] modifications to test script and fallbacks in dropbox auth mgr --- dropbox_auth_manager.py | 4 ++-- test_dropbox_auth.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index be76449..34af144 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -8,8 +8,8 @@ DROPBOX_KEYRING_SERVICE = "comfyui_dropbox" class DropboxAuthManager: def __init__(self, app_key=None, app_secret=None): - self.app_key = app_key or keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") - self.app_secret = app_secret or keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") + self.app_key = app_key if app_key is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") + self.app_secret = app_secret if app_secret is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") def is_connected(self): diff --git a/test_dropbox_auth.py b/test_dropbox_auth.py index c468118..641611a 100644 --- a/test_dropbox_auth.py +++ b/test_dropbox_auth.py @@ -12,6 +12,10 @@ def test_first_time_setup(): app_key = input("App Key: ").strip() app_secret = input("App Secret: ").strip() + print(f"Debug - app_key: '{app_key}' (len={len(app_key)})") + print(f"Debug - app_secret: '{app_secret}' (len={len(app_secret)})") + print(f"Debug - auth_code: '{auth_code}' (len={len(auth_code)})") + auth = DropboxAuthManager(app_key, app_secret) try: From 03dcfc69d81f3732b8effad04c36f646626db4f4 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:27:09 -0700 Subject: [PATCH 05/45] updates to setup_dropbox_node.py to integrate dropbox auth manager --- setup_dropbox_node.py | 116 +++++++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index e0fe357..bba7c69 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -4,6 +4,7 @@ import os import requests from dotenv import load_dotenv, dotenv_values import urllib.parse +from .dropbox_auth_manager import DropboxAuthManager class DropboxSetupNode: @@ -15,6 +16,12 @@ class DropboxSetupNode: "app_secret": ("STRING", {"default": ""}), "auth_code": ("STRING", {"default": ""}), "dropbox_dest_folder": ("STRING", {"default": "/Apps/ComfyUI_Output_Files"}), + }, + "optional": { + "reconnect": ("BOOLEAN", { + "label": "Reconnect Dropbox (clear saved credentials)", + "default": False + }), } } @@ -22,61 +29,66 @@ 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, app_key, app_secret, auth_code, dropbox_dest_folder, reconnect=False): + try: + # Initialize auth manager + auth_manager = DropboxAuthManager() + + # Handle reconnect/reset request + if reconnect: + auth_manager.reset() + return ("๐Ÿ”„ Dropbox credentials cleared. Please provide new auth code to reconnect.",) + + # Check if already connected (keyring has credentials) + if auth_manager.is_connected(): + # Test the stored credentials by getting an access token + try: + access_token = auth_manager.get_access_token() + return ("โœ… Dropbox already connected using stored credentials. Ready to upload files.",) + except Exception as e: + return (f"โš ๏ธ Stored credentials found but invalid: {e}. Use 'reconnect' to reset.",) + + # Check for legacy environment variables (keep existing fallback logic) + 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 found in system environment variables. Using those instead of keyring.",) - # 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 ("โš ๏ธ Detected RunPod secrets. Using those instead of keyring.",) - # 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 + if not all([app_key.strip(), app_secret.strip(), auth_code.strip()]): + # Generate OAuth URL if no auth code provided + if app_key.strip(): + auth_temp = DropboxAuthManager(app_key=app_key.strip()) + oauth_url = auth_temp.get_oauth_url() + return (f"โŒ Missing credentials. Visit this URL to get auth code:\n{oauth_url}",) + else: + return ("โŒ Missing App Key, Secret, or Authorization Code.",) - # 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.",) - - app_key = app_key.strip() - app_secret = app_secret.strip() - auth_code = auth_code.strip() - - - data = { - "code": auth_code, - "grant_type": "authorization_code", - "client_id": app_key, - "client_secret": app_secret, - "token_access_type": "offline" - } - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - - encoded_data = urllib.parse.urlencode(data) - - resp = requests.post("https://api.dropbox.com/oauth2/token", headers=headers, data=data) - resp.raise_for_status() - refresh_token = resp.json().get("refresh_token") - - # 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") - - load_dotenv(env_path, override=True) - return ("โœ… .env file created with Dropbox credentials.",) + # Exchange auth code for refresh token using DropboxAuthManager + auth_manager_setup = DropboxAuthManager(app_key.strip(), app_secret.strip()) + auth_manager_setup.exchange_auth_code(auth_code.strip()) + + # Store destination folder in .env as fallback for other nodes + 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_FOLDER={dropbox_dest_folder}\n") + + return ("โœ… Dropbox connected successfully! Credentials stored securely in system keyring.",) + + except Exception as e: + return (f"โŒ Setup failed: {e}",) # Required mappings for ComfyUI NODE_CLASS_MAPPINGS = {"DropboxSetupNode": DropboxSetupNode} From edc518ded0075c81da644688868ef62a74efda41 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:28:59 -0700 Subject: [PATCH 06/45] simplified test script --- test_dropbox_auth.py | 203 +++++++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 94 deletions(-) diff --git a/test_dropbox_auth.py b/test_dropbox_auth.py index 641611a..7f8a9a1 100644 --- a/test_dropbox_auth.py +++ b/test_dropbox_auth.py @@ -1,125 +1,140 @@ #!/usr/bin/env python3 -# test_dropbox_auth.py - Manual testing script for DropboxAuthManager +# test_dropbox_auth_simple.py - Non-interactive testing script for DropboxAuthManager from dropbox_auth_manager import DropboxAuthManager -def test_first_time_setup(): - """Test the initial auth code exchange flow""" - print("๐Ÿงช STEP 1: First-time setup test") - print("-" * 40) - - auth_code = input("Paste your Dropbox auth code: ").strip() - app_key = input("App Key: ").strip() - app_secret = input("App Secret: ").strip() - - print(f"Debug - app_key: '{app_key}' (len={len(app_key)})") - print(f"Debug - app_secret: '{app_secret}' (len={len(app_secret)})") - print(f"Debug - auth_code: '{auth_code}' (len={len(auth_code)})") - - auth = DropboxAuthManager(app_key, app_secret) - - try: - auth.exchange_auth_code(auth_code) - print("โœ… Refresh token stored securely in system keyring.") - return auth - except Exception as e: - print(f"โŒ Error exchanging auth code: {e}") - return None - -def test_stored_credentials(): - """Test retrieving and using stored credentials""" - print("\n๐Ÿงช STEP 2: Stored credentials test") - print("-" * 40) - - try: - # Test loading from keyring (no app_key/secret needed) - auth = DropboxAuthManager() - - if not auth.is_connected(): - print("โŒ No stored credentials found. Run first-time setup first.") - return False - - print("โœ… Stored credentials found.") - - # Test getting access token - access_token = auth.get_access_token() - print(f"โœ… Got access token: {access_token[:10]}...") - return True - - except Exception as e: - print(f"โŒ Error getting access token: {e}") - return False - def test_oauth_url_generation(): """Test OAuth URL generation""" - print("\n๐Ÿงช STEP 3: OAuth URL generation test") + print("๐Ÿงช TEST 1: OAuth URL generation") print("-" * 40) - app_key = input("App Key (for URL generation): ").strip() - auth = DropboxAuthManager(app_key=app_key) + # Use a dummy app key for testing + test_app_key = "test_app_key_12345" try: + auth = DropboxAuthManager(app_key=test_app_key) oauth_url = auth.get_oauth_url() print(f"โœ… Generated OAuth URL:") print(f" {oauth_url}") - print(f" Copy this URL to authorize your app") - return True + expected_url = f"https://www.dropbox.com/oauth2/authorize?response_type=code&client_id={test_app_key}&token_access_type=offline" + if oauth_url == expected_url: + print("โœ… URL format is correct") + return True + else: + print("โŒ URL format mismatch") + return False except Exception as e: print(f"โŒ Error generating OAuth URL: {e}") return False -def test_reset(): - """Test credential reset functionality""" - print("\n๐Ÿงช STEP 4: Reset credentials test") +def test_stored_credentials(): + """Test retrieving stored credentials""" + print("\n๐Ÿงช TEST 2: Check for stored credentials") print("-" * 40) - confirm = input("Are you sure you want to clear stored credentials? (y/N): ").strip().lower() - if confirm != 'y': - print("โš ๏ธ Skipping reset test.") - return - try: auth = DropboxAuthManager() - auth.reset() - print("โœ… Credentials cleared from keyring.") - # Verify they're actually gone - auth_check = DropboxAuthManager() - if not auth_check.is_connected(): - print("โœ… Verified: No stored credentials found after reset.") + print(f"App key stored: {bool(auth.app_key)}") + print(f"App secret stored: {bool(auth.app_secret)}") + print(f"Refresh token stored: {bool(auth.refresh_token)}") + print(f"Is connected: {auth.is_connected()}") + + if auth.is_connected(): + print("โœ… Stored credentials found - testing access token...") + try: + access_token = auth.get_access_token() + print(f"โœ… Got access token: {access_token[:10]}...") + return True + except Exception as e: + print(f"โŒ Error getting access token: {e}") + return False else: - print("โŒ Warning: Credentials still found after reset.") + print("โ„น๏ธ No stored credentials found (this is expected for first run)") + return True except Exception as e: - print(f"โŒ Error during reset: {e}") + print(f"โŒ Error checking stored credentials: {e}") + return False + +def test_auth_code_exchange(): + """Test auth code exchange with placeholder values""" + print("\n๐Ÿงช TEST 3: Auth code exchange test (with dummy values)") + print("-" * 40) + + # These are dummy values - replace with real ones to test + test_app_key = "your_app_key_here" + test_app_secret = "your_app_secret_here" + test_auth_code = "your_auth_code_here" + + if test_app_key == "your_app_key_here": + print("โ„น๏ธ Skipping auth code exchange test - placeholder values detected") + print("โ„น๏ธ To test this, replace the dummy values in this function with real credentials") + return True + + try: + auth = DropboxAuthManager(test_app_key, test_app_secret) + print(f"Created DropboxAuthManager with app_key: {bool(auth.app_key)}, app_secret: {bool(auth.app_secret)}") + + auth.exchange_auth_code(test_auth_code) + print("โœ… Auth code exchange successful") + print("โœ… Credentials stored in keyring") + return True + + except Exception as e: + print(f"โŒ Error during auth code exchange: {e}") + return False + +def test_manager_initialization(): + """Test DropboxAuthManager initialization with different parameters""" + print("\n๐Ÿงช TEST 4: Manager initialization tests") + print("-" * 40) + + try: + # Test 1: Empty initialization + auth1 = DropboxAuthManager() + print(f"โœ… Empty init - app_key: {auth1.app_key}, app_secret: {auth1.app_secret}") + + # Test 2: With parameters + auth2 = DropboxAuthManager("test_key", "test_secret") + print(f"โœ… With params - app_key: {auth2.app_key}, app_secret: {auth2.app_secret}") + + # Test 3: With empty strings (this was causing the bug) + auth3 = DropboxAuthManager("", "") + print(f"โœ… Empty strings - app_key: '{auth3.app_key}', app_secret: '{auth3.app_secret}'") + + return True + except Exception as e: + print(f"โŒ Error during initialization tests: {e}") + return False def main(): - print("๐Ÿš€ Dropbox Auth Manager Test Suite") - print("=" * 50) + print("๐Ÿš€ Dropbox Auth Manager Test Suite (Non-Interactive)") + print("=" * 60) - while True: - print("\nSelect a test:") - print("1. First-time setup (exchange auth code)") - print("2. Test stored credentials") - print("3. Generate OAuth URL") - print("4. Reset stored credentials") - print("5. Exit") - - choice = input("\nEnter choice (1-5): ").strip() - - if choice == "1": - test_first_time_setup() - elif choice == "2": - test_stored_credentials() - elif choice == "3": - test_oauth_url_generation() - elif choice == "4": - test_reset() - elif choice == "5": - print("๐Ÿ‘‹ Exiting test suite.") - break - else: - print("โŒ Invalid choice. Please select 1-5.") + # Run all tests + tests = [ + test_oauth_url_generation, + test_stored_credentials, + test_manager_initialization, + test_auth_code_exchange + ] + + results = [] + for test in tests: + result = test() + results.append(result) + + print("\n" + "=" * 60) + print("๐Ÿ“Š TEST SUMMARY:") + passed = sum(results) + total = len(results) + print(f"โœ… Passed: {passed}/{total}") + + if passed == total: + print("๐ŸŽ‰ All tests passed!") + else: + print("โš ๏ธ Some tests failed - check output above") if __name__ == "__main__": main() \ No newline at end of file From 82347f877e9a3c64f652aca330923e6a3bdc3a01 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:33:54 -0700 Subject: [PATCH 07/45] modified defaults to account for credentials already stored and refreshing --- setup_dropbox_node.py | 45 ++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index bba7c69..d10657c 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -10,26 +10,46 @@ from .dropbox_auth_manager import DropboxAuthManager class DropboxSetupNode: @classmethod def INPUT_TYPES(cls): - return { + # Check if credentials are already stored + try: + auth_manager = DropboxAuthManager() + is_connected = auth_manager.is_connected() + except: + is_connected = False + + # Base inputs that are always shown + inputs = { "required": { - "app_key": ("STRING", {"default": ""}), - "app_secret": ("STRING", {"default": ""}), - "auth_code": ("STRING", {"default": ""}), "dropbox_dest_folder": ("STRING", {"default": "/Apps/ComfyUI_Output_Files"}), }, - "optional": { - "reconnect": ("BOOLEAN", { - "label": "Reconnect Dropbox (clear saved credentials)", - "default": False - }), - } + "optional": {} } + + if is_connected: + # If connected, only show reconnect option + inputs["optional"]["reconnect"] = ("BOOLEAN", { + "label": "Reconnect Dropbox (clear saved credentials)", + "default": False + }) + else: + # If not connected, show setup fields + inputs["required"].update({ + "app_key": ("STRING", {"default": ""}), + "app_secret": ("STRING", {"default": ""}), + "auth_code": ("STRING", {"default": ""}), + }) + inputs["optional"]["reconnect"] = ("BOOLEAN", { + "label": "Reset stored credentials", + "default": False + }) + + return inputs RETURN_TYPES = ("STRING",) OUTPUT_NODE = True FUNCTION = "setup" - def setup(self, app_key, app_secret, auth_code, dropbox_dest_folder, reconnect=False): + def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False): try: # Initialize auth manager auth_manager = DropboxAuthManager() @@ -66,6 +86,9 @@ class DropboxSetupNode: return ("โš ๏ธ Detected RunPod secrets. Using those instead of keyring.",) # New setup flow using DropboxAuthManager + if not app_key or not app_secret or not auth_code: + return ("โŒ Missing App Key, Secret, or Authorization Code. Please provide all credentials.",) + if not all([app_key.strip(), app_secret.strip(), auth_code.strip()]): # Generate OAuth URL if no auth code provided if app_key.strip(): From 1b1a655e1136d060fe1cc039d5b4b295614653c5 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 16:57:59 -0700 Subject: [PATCH 08/45] udpates with refinements to auth flow - core functionality working --- setup_dropbox_node.py | 81 ++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index d10657c..975335e 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -14,7 +14,9 @@ class DropboxSetupNode: try: auth_manager = DropboxAuthManager() is_connected = auth_manager.is_connected() - except: + print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected}") + except Exception as e: + print(f"[DropboxSetup] INPUT_TYPES error: {e}") is_connected = False # Base inputs that are always shown @@ -34,9 +36,9 @@ class DropboxSetupNode: else: # If not connected, show setup fields inputs["required"].update({ - "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}), }) inputs["optional"]["reconnect"] = ("BOOLEAN", { "label": "Reset stored credentials", @@ -51,22 +53,37 @@ class DropboxSetupNode: def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False): 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}") + # Initialize auth manager auth_manager = DropboxAuthManager() + print(f"[DropboxSetup] Auth manager initialized. Is connected: {auth_manager.is_connected()}") # Handle reconnect/reset request if reconnect: + print("[DropboxSetup] Reconnect requested - clearing credentials") auth_manager.reset() - return ("๐Ÿ”„ Dropbox credentials cleared. Please provide new auth code to reconnect.",) + message = "๐Ÿ”„ Dropbox credentials cleared. Please provide new auth code to reconnect." + print(f"[DropboxSetup] {message}") + return (message,) # Check if already connected (keyring has credentials) if auth_manager.is_connected(): + print("[DropboxSetup] Already connected - testing stored credentials") # Test the stored credentials by getting an access token try: access_token = auth_manager.get_access_token() - return ("โœ… Dropbox already connected using stored credentials. Ready to upload files.",) + message = "โœ… Dropbox already connected using stored credentials. Ready to upload files." + print(f"[DropboxSetup] {message}") + return (message,) except Exception as e: - return (f"โš ๏ธ Stored credentials found but invalid: {e}. Use 'reconnect' to reset.",) + message = f"โš ๏ธ Stored credentials found but invalid: {e}. Use 'reconnect' to reset." + print(f"[DropboxSetup] {message}") + return (message,) # Check for legacy environment variables (keep existing fallback logic) general_env_set = all([ @@ -86,32 +103,54 @@ class DropboxSetupNode: return ("โš ๏ธ Detected RunPod secrets. Using those instead of keyring.",) # New setup flow using DropboxAuthManager - if not app_key or not app_secret or not auth_code: - return ("โŒ Missing App Key, Secret, or Authorization Code. Please provide all credentials.",) + print(f"[DropboxSetup] Starting new setup flow") - if not all([app_key.strip(), app_secret.strip(), auth_code.strip()]): - # Generate OAuth URL if no auth code provided - if app_key.strip(): - auth_temp = DropboxAuthManager(app_key=app_key.strip()) - oauth_url = auth_temp.get_oauth_url() - return (f"โŒ Missing credentials. Visit this URL to get auth code:\n{oauth_url}",) - else: - return ("โŒ Missing App Key, Secret, or Authorization Code.",) + # 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 = "โŒ Missing App Key or App Secret. Please provide both." + print(f"[DropboxSetup] {message}") + return (message,) + + # If no auth code, generate OAuth URL + if not auth_code_clean: + print(f"[DropboxSetup] No auth code provided - generating OAuth URL") + auth_temp = DropboxAuthManager(app_key=app_key_clean) + oauth_url = auth_temp.get_oauth_url() + message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" + print(f"[DropboxSetup] OAuth URL: {oauth_url}") + return (message,) # Exchange auth code for refresh token using DropboxAuthManager - auth_manager_setup = DropboxAuthManager(app_key.strip(), app_secret.strip()) - auth_manager_setup.exchange_auth_code(auth_code.strip()) + print(f"[DropboxSetup] Attempting to exchange auth code") + auth_manager_setup = DropboxAuthManager(app_key_clean, app_secret_clean) + auth_manager_setup.exchange_auth_code(auth_code_clean) + print(f"[DropboxSetup] Auth code exchange successful") # Store destination folder in .env as fallback for other nodes 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_FOLDER={dropbox_dest_folder}\n") + print(f"[DropboxSetup] Stored destination folder: {dropbox_dest_folder}") - return ("โœ… Dropbox connected successfully! Credentials stored securely in system keyring.",) + message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + print(f"[DropboxSetup] {message}") + return (message,) except Exception as e: - return (f"โŒ Setup failed: {e}",) + message = f"โŒ Setup failed: {e}" + print(f"[DropboxSetup] ERROR: {message}") + return (message,) # Required mappings for ComfyUI NODE_CLASS_MAPPINGS = {"DropboxSetupNode": DropboxSetupNode} From c79e7e5d5e3e7826cfc7b0119012963ef5d11b0e Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 17:20:13 -0700 Subject: [PATCH 09/45] added automatic auth window opening to do oauth flow --- setup_dropbox_node.py | 46 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 975335e..7f4db42 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -2,6 +2,7 @@ import os import requests +import webbrowser from dotenv import load_dotenv, dotenv_values import urllib.parse from .dropbox_auth_manager import DropboxAuthManager @@ -69,7 +70,10 @@ class DropboxSetupNode: auth_manager.reset() message = "๐Ÿ”„ Dropbox credentials cleared. Please provide new auth code to reconnect." print(f"[DropboxSetup] {message}") - return (message,) + return { + "ui": {"text": [message]}, + "result": (message,) + } # Check if already connected (keyring has credentials) if auth_manager.is_connected(): @@ -79,11 +83,17 @@ class DropboxSetupNode: access_token = auth_manager.get_access_token() message = "โœ… Dropbox already connected using stored credentials. Ready to upload files." print(f"[DropboxSetup] {message}") - return (message,) + return { + "ui": {"text": [message]}, + "result": (message,) + } except Exception as e: message = f"โš ๏ธ Stored credentials found but invalid: {e}. Use 'reconnect' to reset." print(f"[DropboxSetup] {message}") - return (message,) + return { + "ui": {"text": [message]}, + "result": (message,) + } # Check for legacy environment variables (keep existing fallback logic) general_env_set = all([ @@ -126,9 +136,23 @@ class DropboxSetupNode: print(f"[DropboxSetup] No auth code provided - generating OAuth URL") auth_temp = DropboxAuthManager(app_key=app_key_clean) oauth_url = auth_temp.get_oauth_url() - message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" + + # Automatically open browser for better UX + try: + print(f"[DropboxSetup] Opening browser automatically...") + webbrowser.open(oauth_url) + message = f"๐ŸŒ Browser opened for Dropbox authorization!\n\n๐Ÿ“‹ After authorizing, paste the code in the auth_code field:\n{oauth_url}" + except Exception as e: + print(f"[DropboxSetup] Could not auto-open browser: {e}") + message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" + print(f"[DropboxSetup] OAuth URL: {oauth_url}") - return (message,) + + # Use ComfyUI's dynamic return format for better UI integration + return { + "ui": {"text": [message]}, + "result": (message,) + } # Exchange auth code for refresh token using DropboxAuthManager print(f"[DropboxSetup] Attempting to exchange auth code") @@ -145,12 +169,20 @@ class DropboxSetupNode: message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." print(f"[DropboxSetup] {message}") - return (message,) + + # Use ComfyUI's dynamic return format for better UI integration + return { + "ui": {"text": [message]}, + "result": (message,) + } except Exception as e: message = f"โŒ Setup failed: {e}" print(f"[DropboxSetup] ERROR: {message}") - return (message,) + return { + "ui": {"text": [message]}, + "result": (message,) + } # Required mappings for ComfyUI NODE_CLASS_MAPPINGS = {"DropboxSetupNode": DropboxSetupNode} From 71c9369b593e9162e4e6129da170f2d637befe7f Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 17:37:30 -0700 Subject: [PATCH 10/45] implement seamless automatic OAuth flow with callback handling --- __init__.py | 17 +++++ dropbox_auth_manager.py | 20 +++++- oauth_handler.py | 148 ++++++++++++++++++++++++++++++++++++++++ setup_dropbox_node.py | 47 ++++++++++--- 4 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 oauth_handler.py diff --git a/__init__.py b/__init__.py index c440aa5..7f364db 100644 --- a/__init__.py +++ b/__init__.py @@ -11,3 +11,20 @@ NODE_CLASS_MAPPINGS.update(SETUP_CLASS) NODE_DISPLAY_NAME_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS.update(RUN_DISPLAY) NODE_DISPLAY_NAME_MAPPINGS.update(SETUP_DISPLAY) + +# 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() diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 34af144..0f49279 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -28,7 +28,7 @@ class DropboxAuthManager: except KeyringError as e: raise RuntimeError(f"Failed to store Dropbox tokens securely: {e}") - def exchange_auth_code(self, auth_code): + 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.") @@ -38,6 +38,11 @@ class DropboxAuthManager: "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() @@ -85,9 +90,18 @@ class DropboxAuthManager: # If passwords don't exist, that's fine pass - def get_oauth_url(self): + def get_oauth_url(self, redirect_uri=None, state=None): """Generate OAuth URL for initial authorization""" if not self.app_key: raise ValueError("App key must be set to generate OAuth URL.") - return f"https://www.dropbox.com/oauth2/authorize?response_type=code&client_id={self.app_key}&token_access_type=offline" + + 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}" + + return url diff --git a/oauth_handler.py b/oauth_handler.py new file mode 100644 index 0000000..4f46512 --- /dev/null +++ b/oauth_handler.py @@ -0,0 +1,148 @@ +# 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): + """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 + + # Store in global pending sessions + pending_oauth_sessions[session_id] = { + 'app_key': app_key, + 'app_secret': app_secret, + 'completion_callback': completion_callback, + 'handler': self + } + + print(f"[OAuthHandler] OAuth session stored for: {session_id}") + + 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] Redirect URI: {redirect_uri}") + + # Use DropboxAuthManager to exchange the code + auth_manager = DropboxAuthManager(self.app_key, self.app_secret) + auth_manager.exchange_auth_code(auth_code, redirect_uri=redirect_uri) + + print(f"[OAuthHandler] Auth code exchange successful") + + # Call completion callback if provided + if self.completion_callback: + await self.completion_callback(True, "โœ… Dropbox connected successfully!") + + return True, "โœ… Dropbox connected successfully! Credentials stored securely." + + except Exception as e: + error_msg = f"โŒ 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 + +# 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""" + +

โŒ Authorization Failed

+

{error_msg}

+

You can close this window and try again in ComfyUI.

+ + """, content_type='text/html', status=400) + + if not auth_code: + print(f"[OAuthCallback] ERROR: No authorization code received") + return web.Response(text=""" + +

โŒ No Authorization Code

+

No authorization code was received. Please try again.

+

You can close this window and try again in ComfyUI.

+ + """, 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'] + + # Process the callback with the redirect URI + callback_url = "http://localhost:8188/oauth/dropbox/callback" + success, message = await handler.process_callback(auth_code, redirect_uri=callback_url) + + # Clean up the session + del pending_oauth_sessions[session_id] + + if success: + return web.Response(text=f""" + +

โœ… Authorization Successful!

+

{message}

+

You can now close this window and return to ComfyUI.

+ + + """, content_type='text/html') + else: + return web.Response(text=f""" + +

โŒ Setup Failed

+

{message}

+

You can close this window and try again in ComfyUI.

+ + """, 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""" + +

๐Ÿ”„ Authorization Code Received

+

Please return to ComfyUI and manually enter this code:

+

{auth_code}

+

You can close this window after copying the code.

+ + """, content_type='text/html') + + except Exception as e: + print(f"[OAuthCallback] Unexpected error: {e}") + return web.Response(text=f""" + +

โŒ Callback Error

+

An unexpected error occurred: {e}

+

You can close this window and try again in ComfyUI.

+ + """, content_type='text/html', status=500) \ No newline at end of file diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 7f4db42..0162abf 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -3,9 +3,11 @@ import os import requests import webbrowser +import uuid from dotenv import load_dotenv, dotenv_values import urllib.parse from .dropbox_auth_manager import DropboxAuthManager +from .oauth_handler import OAuthCallbackHandler class DropboxSetupNode: @@ -45,6 +47,10 @@ class DropboxSetupNode: "label": "Reset stored credentials", "default": False }) + inputs["optional"]["auto_oauth"] = ("BOOLEAN", { + "label": "Automatic OAuth (no manual auth code needed)", + "default": True + }) return inputs @@ -52,7 +58,7 @@ class DropboxSetupNode: OUTPUT_NODE = True FUNCTION = "setup" - def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False): + def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False, auto_oauth=True): try: print(f"[DropboxSetup] Called with:") print(f" app_key: '{app_key}' (type: {type(app_key)}, bool: {bool(app_key)})") @@ -135,16 +141,37 @@ class DropboxSetupNode: if not auth_code_clean: print(f"[DropboxSetup] No auth code provided - generating OAuth URL") auth_temp = DropboxAuthManager(app_key=app_key_clean) - oauth_url = auth_temp.get_oauth_url() - # Automatically open browser for better UX - try: - print(f"[DropboxSetup] Opening browser automatically...") - webbrowser.open(oauth_url) - message = f"๐ŸŒ Browser opened for Dropbox authorization!\n\n๐Ÿ“‹ After authorizing, paste the code in the auth_code field:\n{oauth_url}" - except Exception as e: - print(f"[DropboxSetup] Could not auto-open browser: {e}") - message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" + if auto_oauth: + # Automatic OAuth flow with callback + session_id = str(uuid.uuid4()) + callback_url = "http://localhost:8188/oauth/dropbox/callback" + oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id) + + # Set up OAuth session for callback handling + oauth_handler = OAuthCallbackHandler() + oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean) + + try: + print(f"[DropboxSetup] Opening browser for automatic OAuth flow...") + webbrowser.open(oauth_url) + message = f"๐Ÿš€ Automatic OAuth Started!\n\n๐ŸŒ Browser opened for authorization. After you authorize in Dropbox, this will complete automatically.\n\nโณ Waiting for authorization..." + print(f"[DropboxSetup] Session ID: {session_id}") + print(f"[DropboxSetup] Callback URL: {callback_url}") + except Exception as e: + print(f"[DropboxSetup] Could not auto-open browser: {e}") + message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, setup will complete automatically!" + else: + # Manual OAuth flow (original behavior) + oauth_url = auth_temp.get_oauth_url() + + try: + print(f"[DropboxSetup] Opening browser for manual OAuth flow...") + webbrowser.open(oauth_url) + message = f"๐ŸŒ Browser opened for Dropbox authorization!\n\n๐Ÿ“‹ After authorizing, paste the code in the auth_code field:\n{oauth_url}" + except Exception as e: + print(f"[DropboxSetup] Could not auto-open browser: {e}") + message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" print(f"[DropboxSetup] OAuth URL: {oauth_url}") From 212578b42ece58aa32f81a9271f0ce5778a4309b Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 17:44:38 -0700 Subject: [PATCH 11/45] added javascript extensions dir and auto-reload after oauth flow --- __init__.py | 3 ++ oauth_handler.py | 36 ++++++++++++- setup_dropbox_node.py | 4 +- web/dropbox_oauth.js | 117 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 web/dropbox_oauth.js diff --git a/__init__.py b/__init__.py index 7f364db..7e4364d 100644 --- a/__init__.py +++ b/__init__.py @@ -12,6 +12,9 @@ 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""" diff --git a/oauth_handler.py b/oauth_handler.py index 4f46512..031adc9 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -46,6 +46,20 @@ class OAuthCallbackHandler: print(f"[OAuthHandler] Auth code exchange successful") + # Send WebSocket message to trigger ComfyUI refresh + try: + from server import PromptServer + message_data = { + "type": "dropbox_oauth_complete", + "session_id": self.session_id, + "success": True, + "message": "โœ… Dropbox connected successfully!" + } + 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}") + # Call completion callback if provided if self.completion_callback: await self.completion_callback(True, "โœ… Dropbox connected successfully!") @@ -110,9 +124,27 @@ async def handle_oauth_callback(request):

โœ… Authorization Successful!

{message}

-

You can now close this window and return to ComfyUI.

+

This window will close automatically and ComfyUI will refresh...

""", content_type='text/html') diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 0162abf..8e8f907 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -155,12 +155,12 @@ class DropboxSetupNode: try: print(f"[DropboxSetup] Opening browser for automatic OAuth flow...") webbrowser.open(oauth_url) - message = f"๐Ÿš€ Automatic OAuth Started!\n\n๐ŸŒ Browser opened for authorization. After you authorize in Dropbox, this will complete automatically.\n\nโณ Waiting for authorization..." + message = f"๐Ÿš€ Automatic OAuth Started!\n\n๐ŸŒ Browser opened for authorization. After you authorize in Dropbox, ComfyUI will refresh automatically.\n\nโณ Waiting for authorization...\n\n๐Ÿ”— OAuth URL: {oauth_url}" print(f"[DropboxSetup] Session ID: {session_id}") print(f"[DropboxSetup] Callback URL: {callback_url}") except Exception as e: print(f"[DropboxSetup] Could not auto-open browser: {e}") - message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, setup will complete automatically!" + message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, ComfyUI will refresh automatically!" else: # Manual OAuth flow (original behavior) oauth_url = auth_temp.get_oauth_url() diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js new file mode 100644 index 0000000..93bea74 --- /dev/null +++ b/web/dropbox_oauth.js @@ -0,0 +1,117 @@ +// 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 - refreshing interface"); + this.handleOAuthSuccess(); + } else { + console.error("[DropSendNode] OAuth failed:", data.message); + } + }); + + // 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 - refreshing interface"); + this.handleOAuthSuccess(); + } else { + console.error("[DropSendNode] OAuth failed:", event.data.message); + } + } + }); + }, + + handleOAuthSuccess() { + // Close OAuth popup if it's open + if (oauthPopup && !oauthPopup.closed) { + oauthPopup.close(); + console.log("[DropSendNode] Closed OAuth popup window"); + } + + // Refresh ComfyUI interface to update node UI + setTimeout(() => { + console.log("[DropSendNode] Refreshing ComfyUI interface"); + window.location.reload(); + }, 1500); // Small delay to ensure OAuth processing is complete + }, + + // Override the webbrowser.open functionality for better popup control + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "DropboxSetupNode") { + console.log("[DropSendNode] Registered DropboxSetupNode enhancement"); + + // Add custom OAuth popup handling + const originalExecute = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function(message) { + // Check if this execution includes OAuth URL generation + if (message && typeof message === 'string' && message.includes('๐Ÿš€ Automatic OAuth Started!')) { + console.log("[DropSendNode] Detected automatic OAuth start"); + + // Extract OAuth URL from the message (it should be in the callback setup) + // We'll enhance this to open a proper popup instead of a full tab + } + + if (originalExecute) { + return originalExecute.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=600,scrollbars=yes,resizable=yes,status=no,location=no,toolbar=no,menubar=no' + ); + + if (popup) { + oauthPopup = popup; + + // Focus the popup + popup.focus(); + + // Monitor popup closure (optional - for fallback UI updates) + const checkClosed = setInterval(() => { + if (popup.closed) { + console.log("[DropSendNode] OAuth popup closed by user"); + clearInterval(checkClosed); + oauthPopup = null; + } + }, 1000); + + return popup; + } else { + console.error("[DropSendNode] Could not open OAuth popup - popup blocked?"); + return null; + } +}; \ No newline at end of file From 7f30df6c2b578c4798113cc0f90bd1fd51d997fa Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 17:51:22 -0700 Subject: [PATCH 12/45] oauth window now opening in a small, discreet window --- setup_dropbox_node.py | 9 +++++---- web/dropbox_oauth.js | 33 ++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 8e8f907..47b9e93 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -153,13 +153,14 @@ class DropboxSetupNode: oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean) try: - print(f"[DropboxSetup] Opening browser for automatic OAuth flow...") - webbrowser.open(oauth_url) - message = f"๐Ÿš€ Automatic OAuth Started!\n\n๐ŸŒ Browser opened for authorization. After you authorize in Dropbox, ComfyUI will refresh automatically.\n\nโณ Waiting for authorization...\n\n๐Ÿ”— OAuth URL: {oauth_url}" + print(f"[DropboxSetup] Setting up automatic OAuth popup...") + # We'll use JavaScript to open a proper popup window + message = f"๐Ÿš€ Automatic OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to open a small popup window for authorization:\n\n๐Ÿ”— {oauth_url}\n\nโœจ After authorization, the popup will close and ComfyUI will refresh automatically!" print(f"[DropboxSetup] Session ID: {session_id}") print(f"[DropboxSetup] Callback URL: {callback_url}") + print(f"[DropboxSetup] OAuth URL ready for popup: {oauth_url}") except Exception as e: - print(f"[DropboxSetup] Could not auto-open browser: {e}") + print(f"[DropboxSetup] Error setting up OAuth: {e}") message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, ComfyUI will refresh automatically!" else: # Manual OAuth flow (original behavior) diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index 93bea74..cfadadf 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -54,24 +54,39 @@ app.registerExtension({ }, 1500); // Small delay to ensure OAuth processing is complete }, - // Override the webbrowser.open functionality for better popup control + // 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 originalExecute = nodeType.prototype.onExecuted; + 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 === 'string' && message.includes('๐Ÿš€ Automatic OAuth Started!')) { - console.log("[DropSendNode] Detected automatic OAuth start"); - - // Extract OAuth URL from the message (it should be in the callback setup) - // We'll enhance this to open a proper popup instead of a full tab + if (message && typeof message === 'object' && message.text && message.text[0]) { + const text = message.text[0]; + if (text.includes('๐Ÿš€ Automatic OAuth Ready!')) { + console.log("[DropSendNode] Detected automatic 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); + } + } } - if (originalExecute) { - return originalExecute.apply(this, arguments); + if (originalExecuted) { + return originalExecuted.apply(this, arguments); } }; } From 78ace94c8eb160bb3101add46dd17b5860976b2f Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 17:53:29 -0700 Subject: [PATCH 13/45] auto-refreshing when user chose "reconnect dropbox" --- setup_dropbox_node.py | 18 ++++++++++++++++-- web/dropbox_oauth.js | 21 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 47b9e93..9778071 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -33,7 +33,7 @@ class DropboxSetupNode: if is_connected: # If connected, only show reconnect option inputs["optional"]["reconnect"] = ("BOOLEAN", { - "label": "Reconnect Dropbox (clear saved credentials)", + "label": "๐Ÿ”„ Reconnect Dropbox (clears credentials & refreshes UI)", "default": False }) else: @@ -74,7 +74,21 @@ class DropboxSetupNode: if reconnect: print("[DropboxSetup] Reconnect requested - clearing credentials") auth_manager.reset() - message = "๐Ÿ”„ Dropbox credentials cleared. Please provide new auth code to reconnect." + + # 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]}, diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index cfadadf..2d8c2ca 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -25,6 +25,17 @@ app.registerExtension({ } }); + // 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 - refreshing interface"); + this.handleReconnectSuccess(); + } + }); + // Listen for postMessage from OAuth popup window window.addEventListener("message", (event) => { if (event.data && event.data.type === 'dropbox_oauth_complete') { @@ -49,11 +60,19 @@ app.registerExtension({ // Refresh ComfyUI interface to update node UI setTimeout(() => { - console.log("[DropSendNode] Refreshing ComfyUI interface"); + console.log("[DropSendNode] Refreshing ComfyUI interface after OAuth success"); window.location.reload(); }, 1500); // Small delay to ensure OAuth processing is complete }, + handleReconnectSuccess() { + // Refresh ComfyUI interface to show auth fields again + setTimeout(() => { + console.log("[DropSendNode] Refreshing ComfyUI interface after reconnect"); + window.location.reload(); + }, 1000); // Shorter delay for reconnect + }, + // Override node execution to detect and handle OAuth URLs async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "DropboxSetupNode") { From ece75981f33a0369c8a1e3b451d7d3aa6947ec2b Mon Sep 17 00:00:00 2001 From: adamhaley Date: Fri, 15 Aug 2025 18:01:20 -0700 Subject: [PATCH 14/45] some polish with clean styling on oauth success and oauth failure pages. auto-set reconnect field to false to prevent users from accidentally clearing their auth --- oauth_handler.py | 133 ++++++++++++++++++++++++++++++++++++++++--- web/dropbox_oauth.js | 26 ++++++++- 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 031adc9..9370593 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -121,10 +121,71 @@ async def handle_oauth_callback(request): if success: return web.Response(text=f""" - -

โœ… Authorization Successful!

-

{message}

-

This window will close automatically and ComfyUI will refresh...

+ + + + + Dropbox Authorization Successful + + + +
+
โœ…
+

Authorization Successful!

+

{message}

+

ComfyUI will refresh automatically

+
+ + + """, content_type='text/html') + # OAuth callback route handler async def handle_oauth_callback(request): """Handle incoming OAuth callback from Dropbox""" @@ -120,95 +411,124 @@ async def handle_oauth_callback(request): del pending_oauth_sessions[session_id] if success: - return web.Response(text=f""" - - - - - Dropbox Authorization Successful - - - -
-
โœ…
-

Authorization Successful!

-

{message}

-

ComfyUI will refresh automatically

-
- - - """, content_type='text/html') + .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; + }} + .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); }} + }} + + + +
+
โœ…
+

Authorization Successful!

+

{message}

+

ComfyUI will refresh automatically

+
+ + + """, content_type='text/html') else: return web.Response(text=f""" @@ -278,11 +598,80 @@ async def handle_oauth_callback(request): # Fallback for sessions without state tracking print("[OAuthCallback] No session found, attempting generic processing") return web.Response(text=f""" - -

๐Ÿ”„ Authorization Code Received

-

Please return to ComfyUI and manually enter this code:

-

{auth_code}

-

You can close this window after copying the code.

+ + + + + Dropbox Authorization Code + + + +
+
๐Ÿ”„
+

Authorization Code Received

+

Please return to ComfyUI and manually enter this code:

+
{auth_code}
+
+

๐Ÿ“‹ Select the code above and copy it to ComfyUI

+
+
""", content_type='text/html') diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 9778071..ac23f2c 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -13,10 +13,39 @@ from .oauth_handler import OAuthCallbackHandler class DropboxSetupNode: @classmethod def INPUT_TYPES(cls): - # Check if credentials are already stored + # Check if credentials are already stored (keyring or .env file) try: auth_manager = DropboxAuthManager() is_connected = auth_manager.is_connected() + + # Also check for .env file credentials + if not is_connected: + node_dir = os.path.dirname(__file__) + env_path = os.path.join(node_dir, ".env") + if os.path.exists(env_path): + from dotenv import dotenv_values + env_vars = dotenv_values(env_path) + if all([env_vars.get("DROPBOX_APP_KEY"), env_vars.get("DROPBOX_APP_SECRET"), env_vars.get("DROPBOX_REFRESH_TOKEN")]): + is_connected = True + print(f"[DropboxSetup] Found credentials in .env file") + + # Also check for display_only completion marker - but only if environment variables actually exist + if not is_connected: + node_dir = os.path.dirname(__file__) + display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") + if os.path.exists(display_marker_path): + # For display_only, verify that the user actually set up environment variables + display_only_env_set = all([ + os.getenv("DROPBOX_APP_KEY"), + os.getenv("DROPBOX_APP_SECRET"), + os.getenv("DROPBOX_REFRESH_TOKEN") + ]) + if display_only_env_set: + is_connected = True + print(f"[DropboxSetup] Found display_only completion marker with valid environment variables") + else: + print(f"[DropboxSetup] Found display_only completion marker but environment variables not set yet") + print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected}") except Exception as e: print(f"[DropboxSetup] INPUT_TYPES error: {e}") @@ -51,6 +80,10 @@ class DropboxSetupNode: "label": "Automatic OAuth (no manual auth code needed)", "default": True }) + inputs["optional"]["storage_method"] = (["keyring", "env_file", "display_only"], { + "label": "Credential Storage Method", + "default": "keyring" + }) return inputs @@ -58,13 +91,15 @@ class DropboxSetupNode: OUTPUT_NODE = True FUNCTION = "setup" - def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False, auto_oauth=True): + def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False, auto_oauth=True, storage_method="keyring"): 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" auto_oauth: {auto_oauth}") + print(f" storage_method: {storage_method}") # Initialize auth manager auth_manager = DropboxAuthManager() @@ -73,22 +108,37 @@ class DropboxSetupNode: # Handle reconnect/reset request if reconnect: print("[DropboxSetup] Reconnect requested - clearing credentials") + + # Clear keyring credentials auth_manager.reset() + # Also clear .env file if it exists + node_dir = os.path.dirname(__file__) + 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) + + # Also clear display_only marker if it exists + display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") + if os.path.exists(display_marker_path): + print(f"[DropboxSetup] Removing display_only marker: {display_marker_path}") + os.remove(display_marker_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" + "message": "๐Ÿ”„ All 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..." + message = "๐Ÿ”„ Dropbox credentials cleared from all storage locations. ComfyUI will refresh to show auth fields..." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -101,7 +151,7 @@ class DropboxSetupNode: # Test the stored credentials by getting an access token try: access_token = auth_manager.get_access_token() - message = "โœ… Dropbox already connected using stored credentials. Ready to upload files." + message = "โœ… Dropbox already connected using stored keyring credentials. Ready to upload files." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -115,14 +165,31 @@ class DropboxSetupNode: "result": (message,) } - # Check for legacy environment variables (keep existing fallback logic) - general_env_set = all([ + # Check for display_only environment variables (user set them up after OAuth flow) + display_only_env_set = all([ os.getenv("DROPBOX_APP_KEY"), - os.getenv("DROPBOX_APP_SECRET"), + os.getenv("DROPBOX_APP_SECRET"), os.getenv("DROPBOX_REFRESH_TOKEN") ]) - if general_env_set: - return ("โš ๏ธ Dropbox credentials found in system environment variables. Using those instead of keyring.",) + if display_only_env_set: + # Check if this came from display_only flow + node_dir = os.path.dirname(__file__) + display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") + if os.path.exists(display_marker_path): + message = "โœ… Dropbox connected using environment variables (display_only setup). Ready to upload files." + print(f"[DropboxSetup] {message}") + return { + "ui": {"text": [message]}, + "result": (message,) + } + else: + # Legacy environment variables (not from display_only) + message = "โœ… Dropbox credentials found in system environment variables. Ready to upload files." + print(f"[DropboxSetup] {message}") + return { + "ui": {"text": [message]}, + "result": (message,) + } # Check for RunPod secrets runpod_env_set = all([ @@ -164,7 +231,7 @@ class DropboxSetupNode: # Set up OAuth session for callback handling oauth_handler = OAuthCallbackHandler() - oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean) + oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder) try: print(f"[DropboxSetup] Setting up automatic OAuth popup...") @@ -199,19 +266,84 @@ class DropboxSetupNode: # 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) - auth_manager_setup.exchange_auth_code(auth_code_clean) + + # Get the tokens without storing them yet + # Only pass redirect_uri if this was an automatic OAuth flow + if auto_oauth: + callback_url = "http://localhost:8188/oauth/dropbox/callback" + print(f"[DropboxSetup] Using automatic OAuth with redirect_uri: {callback_url}") + result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean, redirect_uri=callback_url) + else: + print(f"[DropboxSetup] Using manual OAuth (no redirect_uri)") + 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}") - # Store destination folder in .env as fallback for other nodes - 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_FOLDER={dropbox_dest_folder}\n") - print(f"[DropboxSetup] Stored destination folder: {dropbox_dest_folder}") + # Handle different storage methods + if storage_method == "keyring": + # Store in system keyring (original behavior) + auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) + message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + + elif 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 and create completion marker + node_dir = os.path.dirname(__file__) + display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") + with open(display_marker_path, "w") as f: + f.write("display_only_setup_completed") + + message = f"""โœ… Dropbox Connected Successfully! + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +๐Ÿ“‹ ENVIRONMENT VARIABLES - Copy & Paste Ready +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” + +DROPBOX_APP_KEY={app_key_clean} + +DROPBOX_APP_SECRET={app_secret_clean} + +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 keyring + auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) + message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." - message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." print(f"[DropboxSetup] {message}") + # For display_only, ensure credentials are prominently shown in console + if storage_method == "display_only": + print("\n" + "="*60) + print("๐Ÿ”ฅ DROPBOX CREDENTIALS READY FOR PRODUCTION ๐Ÿ”ฅ") + print("="*60) + 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("="*60) + print("๐Ÿ“‹ Copy these to your environment variables!") + print("="*60 + "\n") + # Use ComfyUI's dynamic return format for better UI integration return { "ui": {"text": [message]}, diff --git a/test_dropbox_auth_simple.py b/test_dropbox_auth_simple.py new file mode 100644 index 0000000..df32107 --- /dev/null +++ b/test_dropbox_auth_simple.py @@ -0,0 +1,155 @@ +#!/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. +""" + +from dropbox_auth_manager import DropboxAuthManager +import os + +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"โœ… 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"โœ… 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"โœ… Basic OAuth URL: {oauth_url[:80]}...") + + # Test OAuth URL with redirect_uri + callback_url = "http://localhost:8188/oauth/dropbox/callback" + oauth_url_with_redirect = test_auth.get_oauth_url(redirect_uri=callback_url, state="test-session-123") + print(f"โœ… OAuth URL with redirect: {oauth_url_with_redirect[:100]}...") + + # Test that redirect_uri is included + if "redirect_uri=" in oauth_url_with_redirect and callback_url in oauth_url_with_redirect: + print("โœ… redirect_uri properly included in OAuth URL") + else: + print("โŒ redirect_uri not found in OAuth URL") + + # Test that state is included + if "state=test-session-123" in oauth_url_with_redirect: + print("โœ… state parameter properly included in OAuth URL") + else: + print("โŒ state parameter not found in OAuth URL") + + print("\n" + "=" * 50) + print("โœ… All basic functionality tests passed!") + print("=" * 50) + + except Exception as e: + print(f"โŒ 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("โŒ Should have raised ValueError") + except ValueError as e: + print(f"โœ… 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("โŒ Should have raised ValueError") + except ValueError as e: + print(f"โœ… Correctly raised ValueError: {e}") + + print("\n" + "=" * 50) + print("โœ… All error handling tests passed!") + print("=" * 50) + + except Exception as e: + print(f"โŒ 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", 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"โœ… Method executed correctly (expected API failure): {type(e).__name__}") + else: + print(f"โŒ Unexpected error type: {e}") + + # Test method without redirect_uri + 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"โœ… Method executed correctly without redirect_uri (expected API failure): {type(e).__name__}") + else: + print(f"โŒ Unexpected error type: {e}") + + print("\n" + "=" * 50) + print("โœ… Token exchange retry logic tests completed!") + print("=" * 50) + + except Exception as e: + print(f"โŒ 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("\n๐ŸŽ‰ All tests completed successfully!") + print("๐Ÿ’ก To test with real Dropbox credentials, use the interactive flow in ComfyUI") + + except Exception as e: + print(f"\n๐Ÿ’ฅ Test failed: {e}") + return 1 + + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file From 53f8ded658e6a475ded71455b09693b3143bffbd Mon Sep 17 00:00:00 2001 From: adamhaley Date: Mon, 18 Aug 2025 17:07:06 -0700 Subject: [PATCH 16/45] updates for keyring.alt and fallbacks on RUnPod or environments where keyring is not available --- dropbox_auth_manager.py | 124 +++++++++++++++++++++++++++++++++++----- setup_dropbox_node.py | 26 +++++++-- 2 files changed, 131 insertions(+), 19 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index b18c7b7..76abe82 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -6,16 +6,109 @@ from keyring.errors import KeyringError DROPBOX_KEYRING_SERVICE = "comfyui_dropbox" +def setup_keyring_backend(): + """Setup the best available keyring backend for the environment""" + try: + # Try to use the default backend first + keyring.get_keyring() + test_service = "test_keyring_availability" + keyring.set_password(test_service, "test", "test") + keyring.delete_password(test_service, "test") + print("[DropboxAuthManager] Using system keyring backend") + return True + except Exception as e: + print(f"[DropboxAuthManager] System keyring not available: {e}") + + # Try keyrings.alt file-based backends + try: + import keyrings.alt.file + import keyrings.alt.cryptfile + + # Try encrypted file keyring first (more secure) + try: + crypto_keyring = keyrings.alt.cryptfile.CryptFileKeyring() + # Set a simple password for the keyring file + crypto_keyring.keyring_key = "comfyui_dropbox_keyring_2024" + + # Set custom file location in the plugin directory + import os + plugin_dir = os.path.dirname(os.path.abspath(__file__)) + crypto_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring.cfg") + + keyring.set_keyring(crypto_keyring) + + # Test it works + test_service = "test_crypto_keyring" + keyring.set_password(test_service, "test", "test") + keyring.delete_password(test_service, "test") + + print(f"[DropboxAuthManager] Using encrypted file keyring: {crypto_keyring.filename}") + return True + + except Exception as crypto_error: + print(f"[DropboxAuthManager] Encrypted file keyring failed: {crypto_error}") + + # Fall back to plaintext file keyring + try: + file_keyring = keyrings.alt.file.PlaintextKeyring() + + # Set custom file location in the plugin directory + plugin_dir = os.path.dirname(os.path.abspath(__file__)) + file_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring_plaintext.cfg") + + keyring.set_keyring(file_keyring) + + # Test it works + test_service = "test_file_keyring" + keyring.set_password(test_service, "test", "test") + keyring.delete_password(test_service, "test") + + print(f"[DropboxAuthManager] Using plaintext file keyring: {file_keyring.filename}") + print(f"[DropboxAuthManager] WARNING: Credentials will be stored in plaintext") + return True + + except Exception as file_error: + print(f"[DropboxAuthManager] File keyring also failed: {file_error}") + return False + + except ImportError: + print("[DropboxAuthManager] keyrings.alt not available - install with: pip install keyrings.alt") + return False + + except Exception as e: + print(f"[DropboxAuthManager] All keyring backends failed: {e}") + return False + class DropboxAuthManager: def __init__(self, app_key=None, app_secret=None): - self.app_key = app_key if app_key is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") - self.app_secret = app_secret if app_secret is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") - self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + # Setup the best available keyring backend first + self.keyring_available = setup_keyring_backend() + + # Try to get from keyring, but handle backend unavailability gracefully + if self.keyring_available: + try: + self.app_key = app_key if app_key is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") + self.app_secret = app_secret if app_secret is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") + self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + except Exception as e: + print(f"[DropboxAuthManager] Error reading from keyring: {e}") + self.app_key = app_key + self.app_secret = app_secret + self.refresh_token = None + self.keyring_available = False + else: + print(f"[DropboxAuthManager] No keyring backend available - using provided credentials only") + self.app_key = app_key + self.app_secret = app_secret + self.refresh_token = None def is_connected(self): return bool(self.app_key and self.app_secret and self.refresh_token) def store_tokens(self, app_key, app_secret, refresh_token): + if not self.keyring_available: + raise RuntimeError("Keyring not available. Please use 'env_file' or 'display_only' storage method instead.") + try: keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_key", app_key) keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_secret", app_secret) @@ -126,18 +219,19 @@ class DropboxAuthManager: def reset(self): """Clear stored tokens""" - try: - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") - - # Clear instance variables - self.app_key = None - self.app_secret = None - self.refresh_token = None - except KeyringError: - # If passwords don't exist, that's fine - pass + if self.keyring_available: + try: + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") + keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + except KeyringError: + # If passwords don't exist, that's fine + pass + + # Clear instance variables regardless of keyring availability + self.app_key = None + self.app_secret = None + self.refresh_token = None def get_oauth_url(self, redirect_uri=None, state=None): """Generate OAuth URL for initial authorization""" diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index ac23f2c..3186c7d 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -18,6 +18,10 @@ class DropboxSetupNode: auth_manager = DropboxAuthManager() is_connected = auth_manager.is_connected() + # If keyring is not available, we know keyring storage won't work + if not auth_manager.keyring_available: + print(f"[DropboxSetup] Keyring not available in this environment") + # Also check for .env file credentials if not is_connected: node_dir = os.path.dirname(__file__) @@ -80,9 +84,17 @@ class DropboxSetupNode: "label": "Automatic OAuth (no manual auth code needed)", "default": True }) - inputs["optional"]["storage_method"] = (["keyring", "env_file", "display_only"], { + # Smart default based on keyring availability + if auth_manager.keyring_available: + default_storage = "keyring" + storage_options = ["keyring", "env_file", "display_only"] + else: + default_storage = "display_only" # Better for RunPod/Docker + storage_options = ["display_only", "env_file", "keyring"] + + inputs["optional"]["storage_method"] = (storage_options, { "label": "Credential Storage Method", - "default": "keyring" + "default": default_storage }) return inputs @@ -284,8 +296,14 @@ class DropboxSetupNode: # Handle different storage methods if storage_method == "keyring": # Store in system keyring (original behavior) - auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) - message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + try: + auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) + message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + except RuntimeError as e: + if "Keyring not available" in str(e): + message = f"โŒ All keyring backends failed in this environment.\n\n๐Ÿ’ก Please use 'env_file' or 'display_only' storage method instead.\n\n๐Ÿ”ง To enable file-based keyring, ensure 'keyrings.alt' is installed.\n\nTechnical details: {e}" + else: + raise e elif storage_method == "env_file": # Store in .env file From be753b5c76906b1188ebf301cef65e280f4ab7f5 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Mon, 18 Aug 2025 17:21:04 -0700 Subject: [PATCH 17/45] added output print to console on "display only", turned off automatic UI refresh on display_only path --- oauth_handler.py | 61 +++++++++++++++++++++++-------------------- setup_dropbox_node.py | 14 +++++----- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 711156f..cfb2ae5 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -79,6 +79,19 @@ class OAuthCallbackHandler: with open(display_marker_path, "w") as f: f.write("display_only_setup_completed") + # 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! โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” @@ -103,19 +116,22 @@ DROPBOX_FOLDER={self.dropbox_folder} auth_manager.store_tokens(self.app_key, self.app_secret, refresh_token) success_message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." - # Send WebSocket message to trigger ComfyUI refresh - 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}") + # 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 - no auto-refresh") # Call completion callback if provided if self.completion_callback: @@ -309,7 +325,7 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to
- ๐Ÿš€ ComfyUI will refresh automatically to hide the auth fields. Close this window when you're done copying credentials. + ๐Ÿ“‹ Copy the credentials above, then manually refresh ComfyUI to hide the auth fields. Close this window when done.
@@ -344,20 +360,9 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to }}); }}); - // Notify parent window (ComfyUI) about successful OAuth - if (window.opener) {{ - try {{ - window.opener.postMessage({{ - type: 'dropbox_oauth_complete', - success: true, - message: 'Dropbox connected successfully with display_only method', - session_id: '{session_id}' - }}, '*'); - console.log('Sent OAuth completion message to parent window'); - }} catch (e) {{ - console.error('Could not notify parent window:', e); - }} - }} + // For display_only, don't auto-refresh ComfyUI - let user copy credentials first + // The user can manually refresh when they're done + console.log('Display-only OAuth completed - not auto-refreshing ComfyUI'); // Don't auto-close for display_only - let user close manually after copying credentials // The user can close this window when they're done copying diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 3186c7d..4425fd8 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -351,16 +351,18 @@ DROPBOX_FOLDER={dropbox_dest_folder} # For display_only, ensure credentials are prominently shown in console if storage_method == "display_only": - print("\n" + "="*60) - print("๐Ÿ”ฅ DROPBOX CREDENTIALS READY FOR PRODUCTION ๐Ÿ”ฅ") - print("="*60) + 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("="*60) - print("๐Ÿ“‹ Copy these to your environment variables!") - print("="*60 + "\n") + print("="*80) + print("๐Ÿ“‹ Copy the lines above to your environment variables!") + print("๐Ÿš€ Perfect for RunPod, Docker, and production environments!") + print("๐Ÿ’ก Note: ComfyUI won't auto-refresh - refresh manually after copying!") + print("="*80 + "\n") # Use ComfyUI's dynamic return format for better UI integration return { From 202ac3ce1b5bac87529bb70ac8751ad41f87de07 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 16:11:22 -0700 Subject: [PATCH 18/45] RunPod OAuth setup --- RUNPOD_SETUP.md | 73 +++++++++++++++++++++++++++++++++++++++++++ oauth_handler.py | 56 +++++++++++++++++++++++++++++++-- requirements.txt | 1 + setup_dropbox_node.py | 8 +++-- 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 RUNPOD_SETUP.md diff --git a/RUNPOD_SETUP.md b/RUNPOD_SETUP.md new file mode 100644 index 0000000..07afcdc --- /dev/null +++ b/RUNPOD_SETUP.md @@ -0,0 +1,73 @@ +# RunPod OAuth Setup Guide + +## ๐Ÿš€ Quick Setup for RunPod + +The DropSend node now automatically detects RunPod environments, but for OAuth to work properly, you may need to configure your public URL. + +### Option 1: Automatic Detection (Try This First) + +The node will try to automatically detect your RunPod URL. Just run the OAuth flow and check the console for messages like: + +``` +[OAuthHandler] Detected server URL from request: https://your-runpod-url.runpod.net +``` + +### Option 2: Manual Configuration (If Auto-Detection Fails) + +If the automatic detection doesn't work, set one of these environment variables: + +#### Method A: Set PUBLIC_URL Environment Variable +```bash +export PUBLIC_URL="https://your-runpod-id.runpod.net:8188" +``` + +#### Method B: Set COMFYUI_PUBLIC_URL Environment Variable +```bash +export COMFYUI_PUBLIC_URL="https://your-runpod-id.runpod.net:8188" +``` + +#### Method C: Set RUNPOD_PUBLIC_IP (if available) +```bash +export RUNPOD_PUBLIC_IP="your-runpod-id.runpod.net" +``` + +### How to Find Your RunPod URL + +1. **From RunPod Dashboard**: Look for your pod's public URL +2. **From Browser**: Copy the URL you use to access ComfyUI +3. **From Console**: The node will log detected URLs during OAuth + +### Example URLs + +- `https://abc123-8188.proxy.runpod.net` +- `https://xyz789.runpod.net:8188` +- `https://your-custom-domain.com:8188` + +### Testing the Configuration + +1. Run the DropboxSetupNode +2. Check console output for detected URL +3. If URL looks wrong, set the environment variable and restart ComfyUI + +### Troubleshooting + +**Problem**: OAuth callback fails with "redirect_uri mismatch" +**Solution**: Check that the detected URL in the console matches how you access ComfyUI + +**Problem**: Automatic detection shows localhost +**Solution**: Set PUBLIC_URL or COMFYUI_PUBLIC_URL environment variable + +**Problem**: Still getting localhost after setting environment variables +**Solution**: Restart ComfyUI after setting environment variables + +### Storage Method Recommendations for RunPod + +1. **`keyring`**: Now works with encrypted file storage (recommended) +2. **`display_only`**: Best for temporary pods - copy credentials to environment +3. **`env_file`**: Good for persistent storage volumes + +## ๐Ÿ’ก Tips + +- The enhanced keyring backend now works on RunPod with encrypted file storage +- Use `display_only` for maximum flexibility in cloud environments +- Set environment variables in your RunPod template for automatic configuration \ No newline at end of file diff --git a/oauth_handler.py b/oauth_handler.py index cfb2ae5..c1b4831 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -371,6 +371,57 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to """, content_type='text/html') +def get_server_base_url(request=None): + """Dynamically detect the server base URL for OAuth callbacks""" + 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 + import socket + hostname = socket.gethostname() + + # Check for RunPod-specific hostname patterns + if 'runpod' in hostname.lower() or hostname.endswith('.runpod.net'): + # For RunPod, try to get the public URL + import os + 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}") + + # 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""" @@ -408,8 +459,9 @@ async def handle_oauth_callback(request): session_data = pending_oauth_sessions[session_id] handler = session_data['handler'] - # Process the callback with the redirect URI - callback_url = "http://localhost:8188/oauth/dropbox/callback" + # Process the callback with the dynamic redirect URI + base_url = get_server_base_url(request) + callback_url = f"{base_url}/oauth/dropbox/callback" success, message = await handler.process_callback(auth_code, redirect_uri=callback_url) # Clean up the session diff --git a/requirements.txt b/requirements.txt index edeadcd..f067d84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ python-dotenv watchdog dropbox keyring +keyrings.alt diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 4425fd8..bd62b39 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -7,7 +7,7 @@ import uuid from dotenv import load_dotenv, dotenv_values import urllib.parse from .dropbox_auth_manager import DropboxAuthManager -from .oauth_handler import OAuthCallbackHandler +from .oauth_handler import OAuthCallbackHandler, get_server_base_url class DropboxSetupNode: @@ -238,7 +238,8 @@ class DropboxSetupNode: if auto_oauth: # Automatic OAuth flow with callback session_id = str(uuid.uuid4()) - callback_url = "http://localhost:8188/oauth/dropbox/callback" + base_url = get_server_base_url() + callback_url = f"{base_url}/oauth/dropbox/callback" oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id) # Set up OAuth session for callback handling @@ -282,7 +283,8 @@ class DropboxSetupNode: # Get the tokens without storing them yet # Only pass redirect_uri if this was an automatic OAuth flow if auto_oauth: - callback_url = "http://localhost:8188/oauth/dropbox/callback" + base_url = get_server_base_url() + callback_url = f"{base_url}/oauth/dropbox/callback" print(f"[DropboxSetup] Using automatic OAuth with redirect_uri: {callback_url}") result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean, redirect_uri=callback_url) else: From 23044e535169492a73bfb5775eefb83c38d400fe Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 16:38:57 -0700 Subject: [PATCH 19/45] fix for os scoping issue --- RUNPOD_SETUP.md | 9 ++------- oauth_handler.py | 8 ++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/RUNPOD_SETUP.md b/RUNPOD_SETUP.md index 07afcdc..50ae18c 100644 --- a/RUNPOD_SETUP.md +++ b/RUNPOD_SETUP.md @@ -1,6 +1,6 @@ # RunPod OAuth Setup Guide -## ๐Ÿš€ Quick Setup for RunPod +## Quick Setup for RunPod The DropSend node now automatically detects RunPod environments, but for OAuth to work properly, you may need to configure your public URL. @@ -26,11 +26,6 @@ export PUBLIC_URL="https://your-runpod-id.runpod.net:8188" export COMFYUI_PUBLIC_URL="https://your-runpod-id.runpod.net:8188" ``` -#### Method C: Set RUNPOD_PUBLIC_IP (if available) -```bash -export RUNPOD_PUBLIC_IP="your-runpod-id.runpod.net" -``` - ### How to Find Your RunPod URL 1. **From RunPod Dashboard**: Look for your pod's public URL @@ -70,4 +65,4 @@ export RUNPOD_PUBLIC_IP="your-runpod-id.runpod.net" - The enhanced keyring backend now works on RunPod with encrypted file storage - Use `display_only` for maximum flexibility in cloud environments -- Set environment variables in your RunPod template for automatic configuration \ No newline at end of file +- Set environment variables in your RunPod template for automatic configuration diff --git a/oauth_handler.py b/oauth_handler.py index c1b4831..b3aa436 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -373,6 +373,9 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to 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 @@ -389,13 +392,12 @@ def get_server_base_url(request=None): port = getattr(PromptServer.instance, 'port', 8188) # Try to detect if we're on RunPod or similar cloud service - import socket 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 - import os runpod_public_ip = os.getenv('RUNPOD_PUBLIC_IP') if runpod_public_ip: base_url = f"https://{runpod_public_ip}:{port}" @@ -416,6 +418,8 @@ def get_server_base_url(request=None): except Exception as e: print(f"[OAuthHandler] Error detecting server URL: {e}") + import traceback + traceback.print_exc() # Final fallback base_url = "http://localhost:8188" From 787ae3ecf184577c8a9ba9d4d921cd4a79842295 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 16:50:07 -0700 Subject: [PATCH 20/45] oauth fixes --- dropbox_auth_manager.py | 5 +++-- oauth_handler.py | 17 +++++++++++++---- setup_dropbox_node.py | 4 +++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 76abe82..05ebb54 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -71,8 +71,9 @@ def setup_keyring_backend(): print(f"[DropboxAuthManager] File keyring also failed: {file_error}") return False - except ImportError: - print("[DropboxAuthManager] keyrings.alt not available - install with: pip install keyrings.alt") + except ImportError as e: + print(f"[DropboxAuthManager] keyrings.alt not available: {e}") + print("[DropboxAuthManager] Install with: pip install keyrings.alt") return False except Exception as e: diff --git a/oauth_handler.py b/oauth_handler.py index b3aa436..f5e755d 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -14,7 +14,7 @@ class OAuthCallbackHandler: 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"): + 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}") @@ -32,6 +32,7 @@ class OAuthCallbackHandler: '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 } @@ -463,9 +464,17 @@ async def handle_oauth_callback(request): session_data = pending_oauth_sessions[session_id] handler = session_data['handler'] - # Process the callback with the dynamic redirect URI - base_url = get_server_base_url(request) - callback_url = f"{base_url}/oauth/dropbox/callback" + # 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 diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index bd62b39..6a4da87 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -239,12 +239,14 @@ class DropboxSetupNode: # Automatic OAuth flow with callback session_id = str(uuid.uuid4()) base_url = get_server_base_url() + print(f"[DropboxSetup] Detected base URL: {base_url}") callback_url = f"{base_url}/oauth/dropbox/callback" + print(f"[DropboxSetup] Using callback URL: {callback_url}") oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id) # Set up OAuth session for callback handling oauth_handler = OAuthCallbackHandler() - oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder) + oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder, original_redirect_uri=callback_url) try: print(f"[DropboxSetup] Setting up automatic OAuth popup...") From 663b9b6168ade5d4cc4580bbcd8da5db9ef8c529 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 17:38:37 -0700 Subject: [PATCH 21/45] non-blocking keyring.alt --- dropbox_auth_manager.py | 142 +++++++++++++++++++--------------------- setup_dropbox_node.py | 15 +++-- 2 files changed, 80 insertions(+), 77 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 05ebb54..1ad758c 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -7,74 +7,57 @@ from keyring.errors import KeyringError DROPBOX_KEYRING_SERVICE = "comfyui_dropbox" def setup_keyring_backend(): - """Setup the best available keyring backend for the environment""" + """Setup the best available keyring backend for the environment - non-blocking""" try: - # Try to use the default backend first - keyring.get_keyring() + # Try to use the default backend first (non-interactive) + current_keyring = keyring.get_keyring() + keyring_name = current_keyring.__class__.__name__ + + # Skip interactive/password-prompting keyrings during startup + if 'crypt' in keyring_name.lower() or 'gnome' in keyring_name.lower(): + print(f"[DropboxAuthManager] Skipping interactive keyring ({keyring_name}) to avoid blocking startup") + raise Exception("Interactive keyring detected - using fallback") + + # Test non-interactively test_service = "test_keyring_availability" keyring.set_password(test_service, "test", "test") keyring.delete_password(test_service, "test") - print("[DropboxAuthManager] Using system keyring backend") + print(f"[DropboxAuthManager] Using system keyring backend: {keyring_name}") return True + except Exception as e: - print(f"[DropboxAuthManager] System keyring not available: {e}") + print(f"[DropboxAuthManager] System keyring not available or interactive: {e}") # Try keyrings.alt file-based backends try: import keyrings.alt.file - import keyrings.alt.cryptfile - # Try encrypted file keyring first (more secure) - try: - crypto_keyring = keyrings.alt.cryptfile.CryptFileKeyring() - # Set a simple password for the keyring file - crypto_keyring.keyring_key = "comfyui_dropbox_keyring_2024" - - # Set custom file location in the plugin directory - import os - plugin_dir = os.path.dirname(os.path.abspath(__file__)) - crypto_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring.cfg") - - keyring.set_keyring(crypto_keyring) - - # Test it works - test_service = "test_crypto_keyring" - keyring.set_password(test_service, "test", "test") - keyring.delete_password(test_service, "test") - - print(f"[DropboxAuthManager] Using encrypted file keyring: {crypto_keyring.filename}") - return True - - except Exception as crypto_error: - print(f"[DropboxAuthManager] Encrypted file keyring failed: {crypto_error}") - - # Fall back to plaintext file keyring - try: - file_keyring = keyrings.alt.file.PlaintextKeyring() - - # Set custom file location in the plugin directory - plugin_dir = os.path.dirname(os.path.abspath(__file__)) - file_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring_plaintext.cfg") - - keyring.set_keyring(file_keyring) - - # Test it works - test_service = "test_file_keyring" - keyring.set_password(test_service, "test", "test") - keyring.delete_password(test_service, "test") - - print(f"[DropboxAuthManager] Using plaintext file keyring: {file_keyring.filename}") - print(f"[DropboxAuthManager] WARNING: Credentials will be stored in plaintext") - return True - - except Exception as file_error: - print(f"[DropboxAuthManager] File keyring also failed: {file_error}") - return False + # Use plaintext file keyring (non-interactive, works reliably) + file_keyring = keyrings.alt.file.PlaintextKeyring() + + # Set custom file location in the plugin directory + import os + plugin_dir = os.path.dirname(os.path.abspath(__file__)) + file_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring_plaintext.cfg") + + keyring.set_keyring(file_keyring) + + # Test it works + test_service = "test_file_keyring" + keyring.set_password(test_service, "test", "test") + keyring.delete_password(test_service, "test") + + print(f"[DropboxAuthManager] Using plaintext file keyring: {file_keyring.filename}") + print(f"[DropboxAuthManager] NOTE: Credentials stored in plugin directory (not system keyring)") + return True except ImportError as e: print(f"[DropboxAuthManager] keyrings.alt not available: {e}") print("[DropboxAuthManager] Install with: pip install keyrings.alt") return False + except Exception as file_error: + print(f"[DropboxAuthManager] File keyring failed: {file_error}") + return False except Exception as e: print(f"[DropboxAuthManager] All keyring backends failed: {e}") @@ -82,32 +65,45 @@ def setup_keyring_backend(): class DropboxAuthManager: def __init__(self, app_key=None, app_secret=None): - # Setup the best available keyring backend first - self.keyring_available = setup_keyring_backend() + # Don't set up keyring during initialization - do it lazily when needed + self.keyring_available = None # Unknown until first access + self.keyring_tested = False - # Try to get from keyring, but handle backend unavailability gracefully - if self.keyring_available: - try: - self.app_key = app_key if app_key is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") - self.app_secret = app_secret if app_secret is not None else keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") - self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") - except Exception as e: - print(f"[DropboxAuthManager] Error reading from keyring: {e}") - self.app_key = app_key - self.app_secret = app_secret - self.refresh_token = None - self.keyring_available = False - else: - print(f"[DropboxAuthManager] No keyring backend available - using provided credentials only") - self.app_key = app_key - self.app_secret = app_secret - self.refresh_token = None + # Always store provided credentials + self.app_key = app_key + self.app_secret = app_secret + self.refresh_token = None + + # Try to load from keyring only if no credentials provided + if app_key is None or app_secret is None: + self._try_load_from_keyring() + + def _ensure_keyring_setup(self): + """Set up keyring backend only when needed (lazy initialization)""" + if not self.keyring_tested: + self.keyring_available = setup_keyring_backend() + self.keyring_tested = True + return self.keyring_available + + def _try_load_from_keyring(self): + """Try to load credentials from keyring, but don't block if unavailable""" + try: + if self._ensure_keyring_setup(): + if self.app_key is None: + self.app_key = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") + if self.app_secret is None: + self.app_secret = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") + if self.refresh_token is None: + self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + except Exception as e: + print(f"[DropboxAuthManager] Could not load from keyring: {e}") + # Don't fail - just continue without keyring credentials def is_connected(self): return bool(self.app_key and self.app_secret and self.refresh_token) def store_tokens(self, app_key, app_secret, refresh_token): - if not self.keyring_available: + if not self._ensure_keyring_setup(): raise RuntimeError("Keyring not available. Please use 'env_file' or 'display_only' storage method instead.") try: @@ -220,7 +216,7 @@ class DropboxAuthManager: def reset(self): """Clear stored tokens""" - if self.keyring_available: + if self._ensure_keyring_setup(): try: keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 6a4da87..b1ff4af 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -18,9 +18,16 @@ class DropboxSetupNode: auth_manager = DropboxAuthManager() is_connected = auth_manager.is_connected() - # If keyring is not available, we know keyring storage won't work - if not auth_manager.keyring_available: - print(f"[DropboxSetup] Keyring not available in this environment") + # Check keyring availability for smart defaults (non-blocking) + keyring_available = True # Assume available until proven otherwise + try: + # Only test keyring if we need to know for sure + keyring_available = auth_manager._ensure_keyring_setup() + if not keyring_available: + print(f"[DropboxSetup] Keyring not available in this environment") + except Exception as e: + print(f"[DropboxSetup] Keyring test failed: {e}") + keyring_available = False # Also check for .env file credentials if not is_connected: @@ -85,7 +92,7 @@ class DropboxSetupNode: "default": True }) # Smart default based on keyring availability - if auth_manager.keyring_available: + if keyring_available: default_storage = "keyring" storage_options = ["keyring", "env_file", "display_only"] else: From a949141127f1de4d6730ac18493c9bb5b3d62c58 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 17:54:52 -0700 Subject: [PATCH 22/45] updates to fix oauth staying connected now that we are using keyring --- dropbox_auth_manager.py | 38 ++++++++++++++++++++++++++++++++++++-- setup_dropbox_node.py | 6 +++--- web/dropbox_oauth.js | 17 ++++++++++++++--- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 1ad758c..ed5a4c6 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -214,8 +214,42 @@ class DropboxAuthManager: raise RuntimeError("Failed to retrieve access token.") return access_token - def reset(self): - """Clear stored tokens""" + def reset(self, revoke_token=True): + """Clear stored tokens and optionally revoke authorization with Dropbox""" + + # First, revoke the token with Dropbox if requested and possible + if revoke_token and 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 + + # Clear from keyring if self._ensure_keyring_setup(): try: keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index b1ff4af..5f4c22f 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -126,10 +126,10 @@ class DropboxSetupNode: # Handle reconnect/reset request if reconnect: - print("[DropboxSetup] Reconnect requested - clearing credentials") + print("[DropboxSetup] Reconnect requested - revoking token and clearing credentials") - # Clear keyring credentials - auth_manager.reset() + # Clear keyring credentials AND revoke token with Dropbox + auth_manager.reset(revoke_token=True) # Also clear .env file if it exists node_dir = os.path.dirname(__file__) diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index 8177768..43a3652 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -158,14 +158,25 @@ window.openDropboxOAuth = function(url) { // Focus the popup popup.focus(); - // Monitor popup closure (optional - for fallback UI updates) + 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"); + console.log("[DropSendNode] OAuth popup closed by user or completed"); clearInterval(checkClosed); oauthPopup = null; } - }, 1000); + }, 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 { From 22ad2d805a777bb6198ef60723eb8b11980df471 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:05:02 -0700 Subject: [PATCH 23/45] adding force reapprove to handle case where it is automatically re-authorizing without user input --- dropbox_auth_manager.py | 6 +++++- setup_dropbox_node.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index ed5a4c6..ad0fd5a 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -264,7 +264,7 @@ class DropboxAuthManager: self.app_secret = None self.refresh_token = None - def get_oauth_url(self, redirect_uri=None, state=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.") @@ -276,6 +276,10 @@ class DropboxAuthManager: if state: url += f"&state={state}" + + # Force reapproval to ensure user sees authorization screen + if force_reapprove: + url += "&force_reapprove=true" return url diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 5f4c22f..1470514 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -249,7 +249,7 @@ class DropboxSetupNode: print(f"[DropboxSetup] Detected base URL: {base_url}") callback_url = f"{base_url}/oauth/dropbox/callback" print(f"[DropboxSetup] Using callback URL: {callback_url}") - oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id) + oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id, force_reapprove=True) # Set up OAuth session for callback handling oauth_handler = OAuthCallbackHandler() @@ -258,7 +258,7 @@ class DropboxSetupNode: try: print(f"[DropboxSetup] Setting up automatic OAuth popup...") # We'll use JavaScript to open a proper popup window - message = f"๐Ÿš€ Automatic OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to open a small popup window for authorization:\n\n๐Ÿ”— {oauth_url}\n\nโœจ After authorization, the popup will close and ComfyUI will refresh automatically!" + message = f"๐Ÿš€ Automatic OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to open a small popup window for authorization:\n\n๐Ÿ”— {oauth_url}\n\nโœจ After authorization, the popup will close and ComfyUI will refresh automatically!\n\n๐Ÿ” DEBUG: Session {session_id[:8]}... created at {str(uuid.uuid4())[:8]}" print(f"[DropboxSetup] Session ID: {session_id}") print(f"[DropboxSetup] Callback URL: {callback_url}") print(f"[DropboxSetup] OAuth URL ready for popup: {oauth_url}") @@ -267,7 +267,7 @@ class DropboxSetupNode: message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, ComfyUI will refresh automatically!" else: # Manual OAuth flow (original behavior) - oauth_url = auth_temp.get_oauth_url() + oauth_url = auth_temp.get_oauth_url(force_reapprove=True) try: print(f"[DropboxSetup] Opening browser for manual OAuth flow...") From e4cd6552bef8580dd0669aaa7e1b178f2f51c62b Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:16:28 -0700 Subject: [PATCH 24/45] keyring lazy load so non-blocking --- dropbox_auth_manager.py | 17 ++++++++++++++++- setup_dropbox_node.py | 14 ++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index ad0fd5a..3ffacdb 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -75,8 +75,9 @@ class DropboxAuthManager: self.refresh_token = None # Try to load from keyring only if no credentials provided + # Skip during initialization to avoid blocking startup - will load lazily when needed if app_key is None or app_secret is None: - self._try_load_from_keyring() + pass # Load lazily in is_connected() or when actually needed def _ensure_keyring_setup(self): """Set up keyring backend only when needed (lazy initialization)""" @@ -89,17 +90,31 @@ class DropboxAuthManager: """Try to load credentials from keyring, but don't block if unavailable""" try: if self._ensure_keyring_setup(): + original_app_key = self.app_key + original_app_secret = self.app_secret + original_refresh_token = self.refresh_token + if self.app_key is None: self.app_key = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") if self.app_secret is None: self.app_secret = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") if self.refresh_token is None: self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + + # Debug: Show what was loaded + print(f"[DropboxAuthManager] Keyring load results:") + print(f" app_key: {original_app_key} -> {self.app_key}") + print(f" app_secret: {original_app_secret} -> {self.app_secret}") + print(f" refresh_token: {original_refresh_token} -> {bool(self.refresh_token)}") + except Exception as e: print(f"[DropboxAuthManager] Could not load from keyring: {e}") # Don't fail - just continue without keyring credentials def is_connected(self): + # Load from keyring lazily if we don't have credentials yet + if not (self.app_key and self.app_secret and self.refresh_token): + self._try_load_from_keyring() return bool(self.app_key and self.app_secret and self.refresh_token) def store_tokens(self, app_key, app_secret, refresh_token): diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 1470514..0f0d1ce 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -15,16 +15,22 @@ class DropboxSetupNode: def INPUT_TYPES(cls): # Check if credentials are already stored (keyring or .env file) try: + print(f"[DropboxSetup] INPUT_TYPES: Creating DropboxAuthManager for connection check...") auth_manager = DropboxAuthManager() is_connected = auth_manager.is_connected() + print(f"[DropboxSetup] INPUT_TYPES connection check results:") + print(f" is_connected: {is_connected}") + print(f" app_key present: {bool(auth_manager.app_key)}") + print(f" app_secret present: {bool(auth_manager.app_secret)}") + print(f" refresh_token present: {bool(auth_manager.refresh_token)}") + # Check keyring availability for smart defaults (non-blocking) keyring_available = True # Assume available until proven otherwise try: - # Only test keyring if we need to know for sure - keyring_available = auth_manager._ensure_keyring_setup() - if not keyring_available: - print(f"[DropboxSetup] Keyring not available in this environment") + # Skip keyring testing during INPUT_TYPES - just assume it works for UI defaults + # We'll test it properly when actually needed during setup() + pass except Exception as e: print(f"[DropboxSetup] Keyring test failed: {e}") keyring_available = False From a9300c1ecb0be50c3686472486b097acb1abd1d7 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:23:07 -0700 Subject: [PATCH 25/45] still trying to make keyring non-blocking... --- setup_dropbox_node.py | 59 +++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 0f0d1ce..c6ff972 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -13,42 +13,24 @@ from .oauth_handler import OAuthCallbackHandler, get_server_base_url class DropboxSetupNode: @classmethod def INPUT_TYPES(cls): - # Check if credentials are already stored (keyring or .env file) + # Check if credentials are already stored WITHOUT loading from keyring (non-blocking) try: - print(f"[DropboxSetup] INPUT_TYPES: Creating DropboxAuthManager for connection check...") - auth_manager = DropboxAuthManager() - is_connected = auth_manager.is_connected() + print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials (non-blocking)...") + is_connected = False + keyring_available = True # Assume available for UI defaults - print(f"[DropboxSetup] INPUT_TYPES connection check results:") - print(f" is_connected: {is_connected}") - print(f" app_key present: {bool(auth_manager.app_key)}") - print(f" app_secret present: {bool(auth_manager.app_secret)}") - print(f" refresh_token present: {bool(auth_manager.refresh_token)}") + # Check for .env file credentials first (no keyring access) + node_dir = os.path.dirname(__file__) + env_path = os.path.join(node_dir, ".env") + if os.path.exists(env_path): + from dotenv import dotenv_values + env_vars = dotenv_values(env_path) + if all([env_vars.get("DROPBOX_APP_KEY"), env_vars.get("DROPBOX_APP_SECRET"), env_vars.get("DROPBOX_REFRESH_TOKEN")]): + is_connected = True + print(f"[DropboxSetup] Found credentials in .env file") - # Check keyring availability for smart defaults (non-blocking) - keyring_available = True # Assume available until proven otherwise - try: - # Skip keyring testing during INPUT_TYPES - just assume it works for UI defaults - # We'll test it properly when actually needed during setup() - pass - except Exception as e: - print(f"[DropboxSetup] Keyring test failed: {e}") - keyring_available = False - - # Also check for .env file credentials + # Check for display_only completion marker with environment variables if not is_connected: - node_dir = os.path.dirname(__file__) - env_path = os.path.join(node_dir, ".env") - if os.path.exists(env_path): - from dotenv import dotenv_values - env_vars = dotenv_values(env_path) - if all([env_vars.get("DROPBOX_APP_KEY"), env_vars.get("DROPBOX_APP_SECRET"), env_vars.get("DROPBOX_REFRESH_TOKEN")]): - is_connected = True - print(f"[DropboxSetup] Found credentials in .env file") - - # Also check for display_only completion marker - but only if environment variables actually exist - if not is_connected: - node_dir = os.path.dirname(__file__) display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") if os.path.exists(display_marker_path): # For display_only, verify that the user actually set up environment variables @@ -63,10 +45,21 @@ class DropboxSetupNode: else: print(f"[DropboxSetup] Found display_only completion marker but environment variables not set yet") - print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected}") + # Check for keyring credentials ONLY by looking for keyring files (no keyring access) + if not is_connected: + keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] + for keyring_file in keyring_files: + keyring_path = os.path.join(node_dir, keyring_file) + if os.path.exists(keyring_path) and os.path.getsize(keyring_path) > 0: + is_connected = True + print(f"[DropboxSetup] Found keyring credential file: {keyring_file}") + break + + print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected} (no keyring access)") except Exception as e: print(f"[DropboxSetup] INPUT_TYPES error: {e}") is_connected = False + keyring_available = True # Base inputs that are always shown inputs = { From d3cfa57d554f1b0187f7b44a46465d3bde8d4ed5 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:30:23 -0700 Subject: [PATCH 26/45] still working on making keyring non-blocking --- dropbox_auth_manager.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 3ffacdb..dd34fc4 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -14,8 +14,10 @@ def setup_keyring_backend(): keyring_name = current_keyring.__class__.__name__ # Skip interactive/password-prompting keyrings during startup - if 'crypt' in keyring_name.lower() or 'gnome' in keyring_name.lower(): - print(f"[DropboxAuthManager] Skipping interactive keyring ({keyring_name}) to avoid blocking startup") + if ('crypt' in keyring_name.lower() or 'gnome' in keyring_name.lower() or + 'encrypted' in keyring_name.lower() or 'password' in keyring_name.lower() or + 'SecretService' in keyring_name or 'Keychain' in keyring_name): + print(f"[DropboxAuthManager] Skipping interactive keyring ({keyring_name}) to avoid blocking") raise Exception("Interactive keyring detected - using fallback") # Test non-interactively @@ -232,6 +234,11 @@ class DropboxAuthManager: def reset(self, revoke_token=True): """Clear stored tokens and optionally revoke authorization with Dropbox""" + # Load credentials from keyring first if we don't have them in memory + if revoke_token and not (self.refresh_token and self.app_key and self.app_secret): + print("[DropboxAuthManager] Loading credentials for token revocation...") + self._try_load_from_keyring() + # First, revoke the token with Dropbox if requested and possible if revoke_token and self.refresh_token and self.app_key and self.app_secret: try: @@ -264,15 +271,16 @@ class DropboxAuthManager: print(f"[DropboxAuthManager] Could not revoke token with Dropbox: {e}") # Continue with local cleanup even if revocation fails - # Clear from keyring - if self._ensure_keyring_setup(): - try: + # Clear from keyring - but don't block if keyring setup fails + try: + if self._ensure_keyring_setup(): keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") - except KeyringError: - # If passwords don't exist, that's fine - pass + print("[DropboxAuthManager] Cleared credentials from keyring") + except Exception as e: + print(f"[DropboxAuthManager] Could not clear keyring (continuing): {e}") + # Don't fail the reset - just continue with local cleanup # Clear instance variables regardless of keyring availability self.app_key = None From 898fd78f609b60c1b80206ecdeb53f2a47b7d1cd Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:36:16 -0700 Subject: [PATCH 27/45] remove blocking on keyring --- dropbox_auth_manager.py | 86 +++++++++++++++++---------------- setup_dropbox_node.py | 102 ++++++++++++++++++++++------------------ 2 files changed, 102 insertions(+), 86 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index dd34fc4..1635e33 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -234,53 +234,57 @@ class DropboxAuthManager: def reset(self, revoke_token=True): """Clear stored tokens and optionally revoke authorization with Dropbox""" - # Load credentials from keyring first if we don't have them in memory - if revoke_token and not (self.refresh_token and self.app_key and self.app_secret): - print("[DropboxAuthManager] Loading credentials for token revocation...") - self._try_load_from_keyring() - - # First, revoke the token with Dropbox if requested and possible - if revoke_token and 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}") + if revoke_token: + # Load credentials from keyring 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_keyring() + + # 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...") - except Exception as e: - print(f"[DropboxAuthManager] Could not revoke token with Dropbox: {e}") - # Continue with local cleanup even if revocation fails + # 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 from keyring - but don't block if keyring setup fails - try: - if self._ensure_keyring_setup(): + # Clear from keyring - but only if keyring is already available (don't force setup) + if self.keyring_tested and self.keyring_available: + try: keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") print("[DropboxAuthManager] Cleared credentials from keyring") - except Exception as e: - print(f"[DropboxAuthManager] Could not clear keyring (continuing): {e}") - # Don't fail the reset - just continue with local cleanup + except Exception as e: + print(f"[DropboxAuthManager] Could not clear keyring (continuing): {e}") + else: + print("[DropboxAuthManager] Keyring not accessed - will clear file-based keyring manually") # Clear instance variables regardless of keyring availability self.app_key = None diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index c6ff972..ae6ab09 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -12,49 +12,48 @@ from .oauth_handler import OAuthCallbackHandler, get_server_base_url class DropboxSetupNode: @classmethod - def INPUT_TYPES(cls): - # Check if credentials are already stored WITHOUT loading from keyring (non-blocking) + def _check_credentials_non_blocking(cls): + """Check for credentials without accessing keyring (non-blocking)""" try: - print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials (non-blocking)...") - is_connected = False - keyring_available = True # Assume available for UI defaults - - # Check for .env file credentials first (no keyring access) node_dir = os.path.dirname(__file__) + + # Check .env file env_path = os.path.join(node_dir, ".env") if os.path.exists(env_path): from dotenv import dotenv_values env_vars = dotenv_values(env_path) if all([env_vars.get("DROPBOX_APP_KEY"), env_vars.get("DROPBOX_APP_SECRET"), env_vars.get("DROPBOX_REFRESH_TOKEN")]): - is_connected = True - print(f"[DropboxSetup] Found credentials in .env file") + return True - # Check for display_only completion marker with environment variables - if not is_connected: - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - if os.path.exists(display_marker_path): - # For display_only, verify that the user actually set up environment variables - display_only_env_set = all([ - os.getenv("DROPBOX_APP_KEY"), - os.getenv("DROPBOX_APP_SECRET"), - os.getenv("DROPBOX_REFRESH_TOKEN") - ]) - if display_only_env_set: - is_connected = True - print(f"[DropboxSetup] Found display_only completion marker with valid environment variables") - else: - print(f"[DropboxSetup] Found display_only completion marker but environment variables not set yet") - - # Check for keyring credentials ONLY by looking for keyring files (no keyring access) - if not is_connected: - keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] - for keyring_file in keyring_files: - keyring_path = os.path.join(node_dir, keyring_file) - if os.path.exists(keyring_path) and os.path.getsize(keyring_path) > 0: - is_connected = True - print(f"[DropboxSetup] Found keyring credential file: {keyring_file}") - break + # Check display_only with environment variables + display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") + if os.path.exists(display_marker_path): + display_only_env_set = all([ + os.getenv("DROPBOX_APP_KEY"), + os.getenv("DROPBOX_APP_SECRET"), + os.getenv("DROPBOX_REFRESH_TOKEN") + ]) + if display_only_env_set: + return True + # Check keyring files existence + keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] + for keyring_file in keyring_files: + keyring_path = os.path.join(node_dir, keyring_file) + if os.path.exists(keyring_path) and os.path.getsize(keyring_path) > 0: + return True + + return False + except Exception: + return False + + @classmethod + def INPUT_TYPES(cls): + # Check if credentials are already stored WITHOUT loading from keyring (non-blocking) + try: + print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials (non-blocking)...") + is_connected = cls._check_credentials_non_blocking() + keyring_available = True # Assume available for UI defaults print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected} (no keyring access)") except Exception as e: print(f"[DropboxSetup] INPUT_TYPES error: {e}") @@ -119,30 +118,41 @@ class DropboxSetupNode: print(f" auto_oauth: {auto_oauth}") print(f" storage_method: {storage_method}") - # Initialize auth manager + # Initialize auth manager (no keyring access yet) auth_manager = DropboxAuthManager() - print(f"[DropboxSetup] Auth manager initialized. Is connected: {auth_manager.is_connected()}") + print(f"[DropboxSetup] Auth manager initialized (keyring access deferred)") # Handle reconnect/reset request if reconnect: - print("[DropboxSetup] Reconnect requested - revoking token and clearing credentials") + print("[DropboxSetup] Reconnect requested - clearing all credentials") - # Clear keyring credentials AND revoke token with Dropbox - auth_manager.reset(revoke_token=True) + # Skip token revocation to avoid keyring access - just clear everything + print("[DropboxSetup] Skipping token revocation to avoid keyring prompts") + auth_manager.reset(revoke_token=False) - # Also clear .env file if it exists + # 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) - # Also clear display_only marker if it exists + # Clear display_only marker if it exists display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") if os.path.exists(display_marker_path): print(f"[DropboxSetup] Removing display_only marker: {display_marker_path}") os.remove(display_marker_path) + # Clear keyring files manually (avoiding keyring API) + keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] + for keyring_file in keyring_files: + keyring_path = os.path.join(node_dir, keyring_file) + if os.path.exists(keyring_path): + print(f"[DropboxSetup] Removing keyring file: {keyring_file}") + os.remove(keyring_path) + # Send WebSocket message to trigger ComfyUI refresh after clearing credentials try: from server import PromptServer @@ -163,13 +173,15 @@ class DropboxSetupNode: "result": (message,) } - # Check if already connected (keyring has credentials) - if auth_manager.is_connected(): + # Check if already connected using the same non-blocking method as INPUT_TYPES + is_connected = self._check_credentials_non_blocking() + if is_connected: print("[DropboxSetup] Already connected - testing stored credentials") - # Test the stored credentials by getting an access token + # Only access keyring now when we know we need to test credentials try: + # This will trigger keyring access but only when we know credentials exist access_token = auth_manager.get_access_token() - message = "โœ… Dropbox already connected using stored keyring credentials. Ready to upload files." + message = "โœ… Dropbox already connected using stored credentials. Ready to upload files." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, From c2538ec8486f306b6dfb7dcde79942c3c09816b2 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 18:57:54 -0700 Subject: [PATCH 28/45] get this keyring to stop mucking about --- dropbox_auth_manager.py | 40 +++++++++++++++++++++++++++------------- oauth_handler.py | 16 +++++++++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 1635e33..ca1666c 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -20,17 +20,26 @@ def setup_keyring_backend(): print(f"[DropboxAuthManager] Skipping interactive keyring ({keyring_name}) to avoid blocking") raise Exception("Interactive keyring detected - using fallback") - # Test non-interactively - test_service = "test_keyring_availability" - keyring.set_password(test_service, "test", "test") - keyring.delete_password(test_service, "test") + # Test non-interactively with our actual service name to be more thorough + test_service = DROPBOX_KEYRING_SERVICE + test_key = "test_key_interactive_check" + test_value = "test_value" + + print(f"[DropboxAuthManager] Testing keyring ({keyring_name}) for interactive prompts...") + keyring.set_password(test_service, test_key, test_value) + retrieved = keyring.get_password(test_service, test_key) + keyring.delete_password(test_service, test_key) + + if retrieved != test_value: + raise Exception("Keyring test failed - retrieved value doesn't match") + print(f"[DropboxAuthManager] Using system keyring backend: {keyring_name}") return True except Exception as e: print(f"[DropboxAuthManager] System keyring not available or interactive: {e}") - # Try keyrings.alt file-based backends + # Force fallback to plaintext file keyring to avoid any interactive prompts try: import keyrings.alt.file @@ -44,7 +53,7 @@ def setup_keyring_backend(): keyring.set_keyring(file_keyring) - # Test it works + # Test the file keyring works test_service = "test_file_keyring" keyring.set_password(test_service, "test", "test") keyring.delete_password(test_service, "test") @@ -60,10 +69,6 @@ def setup_keyring_backend(): except Exception as file_error: print(f"[DropboxAuthManager] File keyring failed: {file_error}") return False - - except Exception as e: - print(f"[DropboxAuthManager] All keyring backends failed: {e}") - return False class DropboxAuthManager: def __init__(self, app_key=None, app_secret=None): @@ -120,10 +125,12 @@ class DropboxAuthManager: return bool(self.app_key and self.app_secret and self.refresh_token) def store_tokens(self, app_key, app_secret, refresh_token): - if not self._ensure_keyring_setup(): - raise RuntimeError("Keyring not available. Please use 'env_file' or 'display_only' storage method instead.") - + """Store tokens in keyring with fallback for interactive keyrings""" try: + if not self._ensure_keyring_setup(): + raise RuntimeError("Keyring not available. Please use 'env_file' or 'display_only' storage method instead.") + + print(f"[DropboxAuthManager] Storing tokens using keyring...") keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_key", app_key) keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_secret", app_secret) keyring.set_password(DROPBOX_KEYRING_SERVICE, "refresh_token", refresh_token) @@ -132,8 +139,15 @@ class DropboxAuthManager: self.app_key = app_key self.app_secret = app_secret self.refresh_token = refresh_token + print(f"[DropboxAuthManager] Tokens stored successfully in keyring") + except KeyringError as e: raise RuntimeError(f"Failed to store Dropbox tokens securely: {e}") + except Exception as e: + # Handle cases where keyring setup succeeded but storage fails (e.g., interactive prompts) + error_msg = f"Keyring storage failed - this may be due to interactive keyring prompts: {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): diff --git a/oauth_handler.py b/oauth_handler.py index f5e755d..0242c12 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -46,19 +46,29 @@ class OAuthCallbackHandler: print(f"[OAuthHandler] Redirect URI: {redirect_uri}") # 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") + 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 == "keyring": # Store in system keyring (original behavior) - auth_manager.store_tokens(self.app_key, self.app_secret, refresh_token) - success_message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + print(f"[OAuthHandler] Attempting to store tokens in keyring...") + try: + auth_manager.store_tokens(self.app_key, self.app_secret, refresh_token) + print(f"[OAuthHandler] Tokens stored in keyring successfully") + success_message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + except Exception as e: + print(f"[OAuthHandler] ERROR storing tokens in keyring: {e}") + # Fall back to display_only if keyring fails + success_message = f"โš ๏ธ Keyring storage failed: {e}. Please use 'display_only' or 'env_file' storage method." + return False, success_message elif self.storage_method == "env_file": # Store in .env file From ece9c68eb45d2ec01a1cab0893bdf84563d51334 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:05:18 -0700 Subject: [PATCH 29/45] removing keyring behavior --- dropbox_auth_manager.py | 172 +++++++++++----------------------------- oauth_handler.py | 28 +++---- setup_dropbox_node.py | 52 ++++-------- 3 files changed, 73 insertions(+), 179 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index ca1666c..8b2d204 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -1,151 +1,76 @@ # dropbox_auth_manager.py import os import requests -import keyring -from keyring.errors import KeyringError - -DROPBOX_KEYRING_SERVICE = "comfyui_dropbox" - -def setup_keyring_backend(): - """Setup the best available keyring backend for the environment - non-blocking""" - try: - # Try to use the default backend first (non-interactive) - current_keyring = keyring.get_keyring() - keyring_name = current_keyring.__class__.__name__ - - # Skip interactive/password-prompting keyrings during startup - if ('crypt' in keyring_name.lower() or 'gnome' in keyring_name.lower() or - 'encrypted' in keyring_name.lower() or 'password' in keyring_name.lower() or - 'SecretService' in keyring_name or 'Keychain' in keyring_name): - print(f"[DropboxAuthManager] Skipping interactive keyring ({keyring_name}) to avoid blocking") - raise Exception("Interactive keyring detected - using fallback") - - # Test non-interactively with our actual service name to be more thorough - test_service = DROPBOX_KEYRING_SERVICE - test_key = "test_key_interactive_check" - test_value = "test_value" - - print(f"[DropboxAuthManager] Testing keyring ({keyring_name}) for interactive prompts...") - keyring.set_password(test_service, test_key, test_value) - retrieved = keyring.get_password(test_service, test_key) - keyring.delete_password(test_service, test_key) - - if retrieved != test_value: - raise Exception("Keyring test failed - retrieved value doesn't match") - - print(f"[DropboxAuthManager] Using system keyring backend: {keyring_name}") - return True - - except Exception as e: - print(f"[DropboxAuthManager] System keyring not available or interactive: {e}") - - # Force fallback to plaintext file keyring to avoid any interactive prompts - try: - import keyrings.alt.file - - # Use plaintext file keyring (non-interactive, works reliably) - file_keyring = keyrings.alt.file.PlaintextKeyring() - - # Set custom file location in the plugin directory - import os - plugin_dir = os.path.dirname(os.path.abspath(__file__)) - file_keyring.filename = os.path.join(plugin_dir, ".dropbox_keyring_plaintext.cfg") - - keyring.set_keyring(file_keyring) - - # Test the file keyring works - test_service = "test_file_keyring" - keyring.set_password(test_service, "test", "test") - keyring.delete_password(test_service, "test") - - print(f"[DropboxAuthManager] Using plaintext file keyring: {file_keyring.filename}") - print(f"[DropboxAuthManager] NOTE: Credentials stored in plugin directory (not system keyring)") - return True - - except ImportError as e: - print(f"[DropboxAuthManager] keyrings.alt not available: {e}") - print("[DropboxAuthManager] Install with: pip install keyrings.alt") - return False - except Exception as file_error: - print(f"[DropboxAuthManager] File keyring failed: {file_error}") - return False +import json class DropboxAuthManager: def __init__(self, app_key=None, app_secret=None): - # Don't set up keyring during initialization - do it lazily when needed - self.keyring_available = None # Unknown until first access - self.keyring_tested = False - # Always store provided credentials self.app_key = app_key self.app_secret = app_secret self.refresh_token = None - # Try to load from keyring only if no credentials provided - # Skip during initialization to avoid blocking startup - will load lazily when needed + # Try to load from file storage if no credentials provided if app_key is None or app_secret is None: - pass # Load lazily in is_connected() or when actually needed + self._try_load_from_file() - def _ensure_keyring_setup(self): - """Set up keyring backend only when needed (lazy initialization)""" - if not self.keyring_tested: - self.keyring_available = setup_keyring_backend() - self.keyring_tested = True - return self.keyring_available + 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_keyring(self): - """Try to load credentials from keyring, but don't block if unavailable""" + def _try_load_from_file(self): + """Try to load credentials from JSON file""" try: - if self._ensure_keyring_setup(): - original_app_key = self.app_key - original_app_secret = self.app_secret - original_refresh_token = self.refresh_token + 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 = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_key") + self.app_key = creds.get("app_key") if self.app_secret is None: - self.app_secret = keyring.get_password(DROPBOX_KEYRING_SERVICE, "app_secret") + self.app_secret = creds.get("app_secret") if self.refresh_token is None: - self.refresh_token = keyring.get_password(DROPBOX_KEYRING_SERVICE, "refresh_token") + self.refresh_token = creds.get("refresh_token") - # Debug: Show what was loaded - print(f"[DropboxAuthManager] Keyring load results:") - print(f" app_key: {original_app_key} -> {self.app_key}") - print(f" app_secret: {original_app_secret} -> {self.app_secret}") - print(f" refresh_token: {original_refresh_token} -> {bool(self.refresh_token)}") + print(f"[DropboxAuthManager] Loaded credentials from file: {credentials_path}") except Exception as e: - print(f"[DropboxAuthManager] Could not load from keyring: {e}") - # Don't fail - just continue without keyring credentials + print(f"[DropboxAuthManager] Could not load from credentials file: {e}") def is_connected(self): - # Load from keyring lazily if we don't have credentials yet + # 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_keyring() + 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 keyring with fallback for interactive keyrings""" + """Store tokens in JSON file""" try: - if not self._ensure_keyring_setup(): - raise RuntimeError("Keyring not available. Please use 'env_file' or 'display_only' storage method instead.") + credentials_path = self._get_credentials_file_path() - print(f"[DropboxAuthManager] Storing tokens using keyring...") - keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_key", app_key) - keyring.set_password(DROPBOX_KEYRING_SERVICE, "app_secret", app_secret) - keyring.set_password(DROPBOX_KEYRING_SERVICE, "refresh_token", refresh_token) + 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 keyring") + print(f"[DropboxAuthManager] Tokens stored successfully in credentials file") - except KeyringError as e: - raise RuntimeError(f"Failed to store Dropbox tokens securely: {e}") except Exception as e: - # Handle cases where keyring setup succeeded but storage fails (e.g., interactive prompts) - error_msg = f"Keyring storage failed - this may be due to interactive keyring prompts: {e}" + error_msg = f"Failed to store Dropbox tokens: {e}" print(f"[DropboxAuthManager] {error_msg}") raise RuntimeError(error_msg) @@ -249,10 +174,10 @@ class DropboxAuthManager: """Clear stored tokens and optionally revoke authorization with Dropbox""" if revoke_token: - # Load credentials from keyring first if we don't have them in memory + # 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_keyring() + 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: @@ -288,19 +213,16 @@ class DropboxAuthManager: else: print("[DropboxAuthManager] Skipping token revocation (revoke_token=False)") - # Clear from keyring - but only if keyring is already available (don't force setup) - if self.keyring_tested and self.keyring_available: - try: - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_key") - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "app_secret") - keyring.delete_password(DROPBOX_KEYRING_SERVICE, "refresh_token") - print("[DropboxAuthManager] Cleared credentials from keyring") - except Exception as e: - print(f"[DropboxAuthManager] Could not clear keyring (continuing): {e}") - else: - print("[DropboxAuthManager] Keyring not accessed - will clear file-based keyring manually") + # 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 regardless of keyring availability + # Clear instance variables self.app_key = None self.app_secret = None self.refresh_token = None diff --git a/oauth_handler.py b/oauth_handler.py index 0242c12..7e768c6 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -57,20 +57,7 @@ class OAuthCallbackHandler: # Handle different storage methods success_message = None - if self.storage_method == "keyring": - # Store in system keyring (original behavior) - print(f"[OAuthHandler] Attempting to store tokens in keyring...") - try: - auth_manager.store_tokens(self.app_key, self.app_secret, refresh_token) - print(f"[OAuthHandler] Tokens stored in keyring successfully") - success_message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." - except Exception as e: - print(f"[OAuthHandler] ERROR storing tokens in keyring: {e}") - # Fall back to display_only if keyring fails - success_message = f"โš ๏ธ Keyring storage failed: {e}. Please use 'display_only' or 'env_file' storage method." - return False, success_message - - elif self.storage_method == "env_file": + if self.storage_method == "env_file": # Store in .env file import os node_dir = os.path.dirname(__file__) @@ -123,9 +110,16 @@ DROPBOX_FOLDER={self.dropbox_folder} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”""" else: - # Fallback to keyring - auth_manager.store_tokens(self.app_key, self.app_secret, refresh_token) - success_message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + # 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": diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index ae6ab09..3de63df 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -4,6 +4,7 @@ import os import requests import webbrowser import uuid +import json from dotenv import load_dotenv, dotenv_values import urllib.parse from .dropbox_auth_manager import DropboxAuthManager @@ -13,7 +14,7 @@ from .oauth_handler import OAuthCallbackHandler, get_server_base_url class DropboxSetupNode: @classmethod def _check_credentials_non_blocking(cls): - """Check for credentials without accessing keyring (non-blocking)""" + """Check for credentials without blocking operations""" try: node_dir = os.path.dirname(__file__) @@ -35,13 +36,6 @@ class DropboxSetupNode: ]) if display_only_env_set: return True - - # Check keyring files existence - keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] - for keyring_file in keyring_files: - keyring_path = os.path.join(node_dir, keyring_file) - if os.path.exists(keyring_path) and os.path.getsize(keyring_path) > 0: - return True return False except Exception: @@ -89,13 +83,9 @@ class DropboxSetupNode: "label": "Automatic OAuth (no manual auth code needed)", "default": True }) - # Smart default based on keyring availability - if keyring_available: - default_storage = "keyring" - storage_options = ["keyring", "env_file", "display_only"] - else: - default_storage = "display_only" # Better for RunPod/Docker - storage_options = ["display_only", "env_file", "keyring"] + # Simple storage options without keyring complexity + default_storage = "env_file" + storage_options = ["env_file", "display_only"] inputs["optional"]["storage_method"] = (storage_options, { "label": "Credential Storage Method", @@ -145,13 +135,6 @@ class DropboxSetupNode: print(f"[DropboxSetup] Removing display_only marker: {display_marker_path}") os.remove(display_marker_path) - # Clear keyring files manually (avoiding keyring API) - keyring_files = [".dropbox_keyring.cfg", ".dropbox_keyring_plaintext.cfg"] - for keyring_file in keyring_files: - keyring_path = os.path.join(node_dir, keyring_file) - if os.path.exists(keyring_path): - print(f"[DropboxSetup] Removing keyring file: {keyring_file}") - os.remove(keyring_path) # Send WebSocket message to trigger ComfyUI refresh after clearing credentials try: @@ -316,18 +299,7 @@ class DropboxSetupNode: print(f"[DropboxSetup] Using storage method: {storage_method}") # Handle different storage methods - if storage_method == "keyring": - # Store in system keyring (original behavior) - try: - auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) - message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." - except RuntimeError as e: - if "Keyring not available" in str(e): - message = f"โŒ All keyring backends failed in this environment.\n\n๐Ÿ’ก Please use 'env_file' or 'display_only' storage method instead.\n\n๐Ÿ”ง To enable file-based keyring, ensure 'keyrings.alt' is installed.\n\nTechnical details: {e}" - else: - raise e - - elif storage_method == "env_file": + if storage_method == "env_file": # Store in .env file node_dir = os.path.dirname(__file__) env_path = os.path.join(node_dir, ".env") @@ -365,9 +337,15 @@ DROPBOX_FOLDER={dropbox_dest_folder} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”""" else: - # Fallback to keyring - auth_manager_setup.store_tokens(app_key_clean, app_secret_clean, refresh_token) - message = "โœ… Dropbox connected successfully! Credentials stored securely in system keyring." + # 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}") From 1c148acf6982262f92d395e7013374589f79b87b Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:16:36 -0700 Subject: [PATCH 30/45] removing auto_oauth option - since that is default without using this node anyways --- setup_dropbox_node.py | 81 ++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 3de63df..7be10df 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -79,10 +79,6 @@ class DropboxSetupNode: "label": "Reset stored credentials", "default": False }) - inputs["optional"]["auto_oauth"] = ("BOOLEAN", { - "label": "Automatic OAuth (no manual auth code needed)", - "default": True - }) # Simple storage options without keyring complexity default_storage = "env_file" storage_options = ["env_file", "display_only"] @@ -98,14 +94,13 @@ class DropboxSetupNode: OUTPUT_NODE = True FUNCTION = "setup" - def setup(self, dropbox_dest_folder, app_key=None, app_secret=None, auth_code=None, reconnect=False, auto_oauth=True, storage_method="keyring"): + 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" auto_oauth: {auto_oauth}") print(f" storage_method: {storage_method}") # Initialize auth manager (no keyring access yet) @@ -231,45 +226,33 @@ class DropboxSetupNode: print(f"[DropboxSetup] {message}") return (message,) - # If no auth code, generate OAuth URL + # If no auth code, generate OAuth URL for automatic popup flow if not auth_code_clean: - print(f"[DropboxSetup] No auth code provided - generating OAuth URL") + print(f"[DropboxSetup] No auth code provided - generating OAuth URL for popup flow") auth_temp = DropboxAuthManager(app_key=app_key_clean) - if auto_oauth: - # Automatic OAuth flow with callback - session_id = str(uuid.uuid4()) - base_url = get_server_base_url() - print(f"[DropboxSetup] Detected base URL: {base_url}") - callback_url = f"{base_url}/oauth/dropbox/callback" - print(f"[DropboxSetup] Using callback URL: {callback_url}") - oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id, force_reapprove=True) - - # Set up OAuth session for callback handling - oauth_handler = OAuthCallbackHandler() - oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder, original_redirect_uri=callback_url) - - try: - print(f"[DropboxSetup] Setting up automatic OAuth popup...") - # We'll use JavaScript to open a proper popup window - message = f"๐Ÿš€ Automatic OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to open a small popup window for authorization:\n\n๐Ÿ”— {oauth_url}\n\nโœจ After authorization, the popup will close and ComfyUI will refresh automatically!\n\n๐Ÿ” DEBUG: Session {session_id[:8]}... created at {str(uuid.uuid4())[:8]}" - print(f"[DropboxSetup] Session ID: {session_id}") - print(f"[DropboxSetup] Callback URL: {callback_url}") - print(f"[DropboxSetup] OAuth URL ready for popup: {oauth_url}") - except Exception as e: - print(f"[DropboxSetup] Error setting up OAuth: {e}") - message = f"๐Ÿ”— Automatic OAuth Setup:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, ComfyUI will refresh automatically!" - else: - # Manual OAuth flow (original behavior) - oauth_url = auth_temp.get_oauth_url(force_reapprove=True) - - try: - print(f"[DropboxSetup] Opening browser for manual OAuth flow...") - webbrowser.open(oauth_url) - message = f"๐ŸŒ Browser opened for Dropbox authorization!\n\n๐Ÿ“‹ After authorizing, paste the code in the auth_code field:\n{oauth_url}" - except Exception as e: - print(f"[DropboxSetup] Could not auto-open browser: {e}") - message = f"๐Ÿ“‹ Visit this URL to authorize, then paste the auth code:\n{oauth_url}" + # Automatic OAuth flow with popup callback + session_id = str(uuid.uuid4()) + base_url = get_server_base_url() + print(f"[DropboxSetup] Detected base URL: {base_url}") + callback_url = f"{base_url}/oauth/dropbox/callback" + print(f"[DropboxSetup] Using callback URL: {callback_url}") + oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id, force_reapprove=True) + + # Set up OAuth session for callback handling + oauth_handler = OAuthCallbackHandler() + oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder, original_redirect_uri=callback_url) + + try: + print(f"[DropboxSetup] Setting up automatic OAuth popup...") + # JavaScript will automatically open a popup window + message = f"๐Ÿš€ Dropbox OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to authorize with Dropbox:\n\n๐Ÿ”— {oauth_url}\n\nโœจ A popup window will open, and after authorization, ComfyUI will refresh automatically!" + print(f"[DropboxSetup] Session ID: {session_id}") + print(f"[DropboxSetup] Callback URL: {callback_url}") + 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, ComfyUI will refresh automatically!" print(f"[DropboxSetup] OAuth URL: {oauth_url}") @@ -284,15 +267,11 @@ class DropboxSetupNode: auth_manager_setup = DropboxAuthManager(app_key_clean, app_secret_clean) # Get the tokens without storing them yet - # Only pass redirect_uri if this was an automatic OAuth flow - if auto_oauth: - base_url = get_server_base_url() - callback_url = f"{base_url}/oauth/dropbox/callback" - print(f"[DropboxSetup] Using automatic OAuth with redirect_uri: {callback_url}") - result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean, redirect_uri=callback_url) - else: - print(f"[DropboxSetup] Using manual OAuth (no redirect_uri)") - result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean) + # Always use automatic OAuth flow with callback URL + base_url = get_server_base_url() + callback_url = f"{base_url}/oauth/dropbox/callback" + print(f"[DropboxSetup] Using automatic OAuth with redirect_uri: {callback_url}") + result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean, redirect_uri=callback_url) refresh_token = result.get("refresh_token") print(f"[DropboxSetup] Auth code exchange successful") From 21c6476afabf508a75aa41eda385b1f585808f67 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:23:15 -0700 Subject: [PATCH 31/45] removing all references to keyring --- setup_dropbox_node.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 7be10df..f902eaa 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -43,16 +43,14 @@ class DropboxSetupNode: @classmethod def INPUT_TYPES(cls): - # Check if credentials are already stored WITHOUT loading from keyring (non-blocking) + # Check if credentials are already stored (non-blocking) try: - print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials (non-blocking)...") + print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials...") is_connected = cls._check_credentials_non_blocking() - keyring_available = True # Assume available for UI defaults - print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected} (no keyring access)") + print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected}") except Exception as e: print(f"[DropboxSetup] INPUT_TYPES error: {e}") is_connected = False - keyring_available = True # Base inputs that are always shown inputs = { @@ -103,16 +101,16 @@ class DropboxSetupNode: print(f" reconnect: {reconnect}") print(f" storage_method: {storage_method}") - # Initialize auth manager (no keyring access yet) + # Initialize auth manager auth_manager = DropboxAuthManager() - print(f"[DropboxSetup] Auth manager initialized (keyring access deferred)") + print(f"[DropboxSetup] Auth manager initialized") # Handle reconnect/reset request if reconnect: print("[DropboxSetup] Reconnect requested - clearing all credentials") - # Skip token revocation to avoid keyring access - just clear everything - print("[DropboxSetup] Skipping token revocation to avoid keyring prompts") + # 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 @@ -137,14 +135,14 @@ class DropboxSetupNode: message_data = { "type": "dropbox_reconnect_complete", "success": True, - "message": "๐Ÿ”„ All credentials cleared - ComfyUI will refresh to show auth fields" + "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 from all storage locations. ComfyUI will refresh to show auth fields..." + message = "๐Ÿ”„ Dropbox credentials cleared. ComfyUI will refresh to show auth fields..." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, From 6c7bb51fbedec4b02c309622d3dd922af99ce328 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:27:39 -0700 Subject: [PATCH 32/45] emoji removal, fix for oauth window trigger --- oauth_handler.py | 28 +++++++++++----------- setup_dropbox_node.py | 54 +++++++++++++++++++++---------------------- web/dropbox_oauth.js | 6 ++--- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 7e768c6..227fd24 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -67,7 +67,7 @@ class OAuthCallbackHandler: 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." + success_message = "Dropbox connected successfully! Credentials saved to .env file." elif self.storage_method == "display_only": # Display credentials for manual copying and create completion marker @@ -79,22 +79,22 @@ class OAuthCallbackHandler: # Print credentials to console for easy copy/paste print("\n" + "=" * 80) - print("๐Ÿ”ฅ DROPBOX CREDENTIALS READY FOR PRODUCTION - COPY FROM CONSOLE ๐Ÿ”ฅ") + 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("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! + success_message = f"""Dropbox Connected Successfully! -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -๐Ÿ“‹ ENVIRONMENT VARIABLES - Copy & Paste Ready -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +===================================================================== +ENVIRONMENT VARIABLES - Copy & Paste Ready +===================================================================== DROPBOX_APP_KEY={self.app_key} @@ -104,10 +104,10 @@ DROPBOX_REFRESH_TOKEN={refresh_token} DROPBOX_FOLDER={self.dropbox_folder} -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -๐Ÿ’ก Perfect for RunPod, Docker, and production environments! -๐Ÿš€ These credentials are ready to use immediately. -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”""" +===================================================================== +Perfect for RunPod, Docker, and production environments! +These credentials are ready to use immediately. +=====================================================================""" else: # Fallback to env_file @@ -119,7 +119,7 @@ DROPBOX_FOLDER={self.dropbox_folder} 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." + 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": @@ -145,7 +145,7 @@ DROPBOX_FOLDER={self.dropbox_folder} return True, success_message except Exception as e: - error_msg = f"โŒ OAuth callback failed: {e}" + error_msg = f"Error: OAuth callback failed: {e}" print(f"[OAuthHandler] ERROR: {error_msg}") if self.completion_callback: diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index f902eaa..62f9620 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -63,7 +63,7 @@ class DropboxSetupNode: if is_connected: # If connected, only show reconnect option inputs["optional"]["reconnect"] = ("BOOLEAN", { - "label": "๐Ÿ”„ Reconnect Dropbox (clears credentials & refreshes UI)", + "label": "Reconnect Dropbox (clears credentials & refreshes UI)", "default": False }) else: @@ -135,14 +135,14 @@ class DropboxSetupNode: message_data = { "type": "dropbox_reconnect_complete", "success": True, - "message": "๐Ÿ”„ Credentials cleared - ComfyUI will refresh to show auth fields" + "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..." + message = "Dropbox credentials cleared. ComfyUI will refresh to show auth fields..." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -157,14 +157,14 @@ class DropboxSetupNode: try: # This will trigger keyring access but only when we know credentials exist access_token = auth_manager.get_access_token() - message = "โœ… Dropbox already connected using stored credentials. Ready to upload files." + message = "Dropbox already connected using stored credentials. Ready to upload files." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, "result": (message,) } except Exception as e: - message = f"โš ๏ธ Stored credentials found but invalid: {e}. Use 'reconnect' to reset." + message = f"Warning: Stored credentials found but invalid: {e}. Use 'reconnect' to reset." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -182,7 +182,7 @@ class DropboxSetupNode: node_dir = os.path.dirname(__file__) display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") if os.path.exists(display_marker_path): - message = "โœ… Dropbox connected using environment variables (display_only setup). Ready to upload files." + message = "Dropbox connected using environment variables (display_only setup). Ready to upload files." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -190,7 +190,7 @@ class DropboxSetupNode: } else: # Legacy environment variables (not from display_only) - message = "โœ… Dropbox credentials found in system environment variables. Ready to upload files." + message = "Dropbox credentials found in system environment variables. Ready to upload files." print(f"[DropboxSetup] {message}") return { "ui": {"text": [message]}, @@ -203,7 +203,7 @@ class DropboxSetupNode: os.getenv("RUNPOD_SECRET_DROPBOX_REFRESH_TOKEN") ]) if runpod_env_set: - return ("โš ๏ธ Detected RunPod secrets. Using those instead of keyring.",) + return ("Warning: Detected RunPod secrets. Using those instead.",) # New setup flow using DropboxAuthManager print(f"[DropboxSetup] Starting new setup flow") @@ -220,7 +220,7 @@ class DropboxSetupNode: # Check if we have app credentials if not app_key_clean or not app_secret_clean: - message = "โŒ Missing App Key or App Secret. Please provide both." + message = "Error: Missing App Key or App Secret. Please provide both." print(f"[DropboxSetup] {message}") return (message,) @@ -244,13 +244,13 @@ class DropboxSetupNode: try: print(f"[DropboxSetup] Setting up automatic OAuth popup...") # JavaScript will automatically open a popup window - message = f"๐Ÿš€ Dropbox OAuth Ready!\n\n๐Ÿ–ฑ๏ธ Click the link below to authorize with Dropbox:\n\n๐Ÿ”— {oauth_url}\n\nโœจ A popup window will open, and after authorization, ComfyUI will refresh automatically!" + message = f"Dropbox OAuth Ready!\n\nClick the link below to authorize with Dropbox:\n\n{oauth_url}\n\nA popup window will open, and after authorization, ComfyUI will refresh automatically!" print(f"[DropboxSetup] Session ID: {session_id}") print(f"[DropboxSetup] Callback URL: {callback_url}") 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, ComfyUI will refresh automatically!" + message = f"Dropbox Authorization:\n\nPlease visit this URL to authorize:\n{oauth_url}\n\nAfter authorization, ComfyUI will refresh automatically!" print(f"[DropboxSetup] OAuth URL: {oauth_url}") @@ -285,7 +285,7 @@ class DropboxSetupNode: 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." + message = "Dropbox connected successfully! Credentials saved to .env file." elif storage_method == "display_only": # Display credentials for manual copying and create completion marker @@ -294,11 +294,11 @@ class DropboxSetupNode: with open(display_marker_path, "w") as f: f.write("display_only_setup_completed") - message = f"""โœ… Dropbox Connected Successfully! + message = f"""Dropbox Connected Successfully! -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -๐Ÿ“‹ ENVIRONMENT VARIABLES - Copy & Paste Ready -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” +===================================================================== +ENVIRONMENT VARIABLES - Copy & Paste Ready +===================================================================== DROPBOX_APP_KEY={app_key_clean} @@ -308,10 +308,10 @@ DROPBOX_REFRESH_TOKEN={refresh_token} DROPBOX_FOLDER={dropbox_dest_folder} -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” -๐Ÿ’ก Perfect for RunPod, Docker, and production environments! -๐Ÿš€ These credentials are ready to use immediately. -โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”""" +===================================================================== +Perfect for RunPod, Docker, and production environments! +These credentials are ready to use immediately. +=====================================================================""" else: # Fallback to env_file storage @@ -322,23 +322,23 @@ DROPBOX_FOLDER={dropbox_dest_folder} 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." + 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("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("๐Ÿ’ก Note: ComfyUI won't auto-refresh - refresh manually after copying!") + print("Copy the lines above to your environment variables!") + print("Perfect for RunPod, Docker, and production environments!") + print("Note: ComfyUI won't auto-refresh - refresh manually after copying!") print("="*80 + "\n") # Use ComfyUI's dynamic return format for better UI integration @@ -348,7 +348,7 @@ DROPBOX_FOLDER={dropbox_dest_folder} } except Exception as e: - message = f"โŒ Setup failed: {e}" + message = f"Error: Setup failed: {e}" print(f"[DropboxSetup] ERROR: {message}") return { "ui": {"text": [message]}, @@ -357,4 +357,4 @@ DROPBOX_FOLDER={dropbox_dest_folder} # 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"} diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index 43a3652..eccebf1 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -110,11 +110,11 @@ app.registerExtension({ // Check if this execution includes OAuth URL generation if (message && typeof message === 'object' && message.text && message.text[0]) { const text = message.text[0]; - if (text.includes('๐Ÿš€ Automatic OAuth Ready!')) { - console.log("[DropSendNode] Detected automatic OAuth ready message"); + 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]+)/); + const urlMatch = text.match(/(https:\/\/www\.dropbox\.com\/oauth2\/authorize[^\s]+)/); if (urlMatch) { const oauthUrl = urlMatch[1]; console.log("[DropSendNode] Extracted OAuth URL:", oauthUrl); From 49e4ffa8a9de0af22cbe079324c1644804058888 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:34:24 -0700 Subject: [PATCH 33/45] adding oauth window auto close on success --- oauth_handler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 227fd24..4d529bf 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -330,7 +330,7 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to
- ๐Ÿ“‹ Copy the credentials above, then manually refresh ComfyUI to hide the auth fields. Close this window when done. + Copy the credentials above, then manually refresh ComfyUI to hide the auth fields. Close this window when done.
@@ -500,7 +500,7 @@ async def handle_oauth_callback(request): app_secret = session_data['app_secret'] dropbox_folder = session_data['dropbox_folder'] - # Special success page with textarea fields for credentials + # 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 @@ -513,7 +513,7 @@ async def handle_oauth_callback(request): """, content_type='text/html', status=500) else: - # Standard success page for other storage methods + # Standard success page for env_file storage method (auto-closes quickly) return web.Response(text=f""" @@ -578,7 +578,7 @@ async def handle_oauth_callback(request):
โœ…

Authorization Successful!

{message}

-

ComfyUI will refresh automatically

+

This window will close automatically

""", content_type='text/html') From 532e3ab77d970e1141b6cdfffac76f0d4a7dbed0 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:38:51 -0700 Subject: [PATCH 34/45] upping delay before closing oauth window --- oauth_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 4d529bf..ff87779 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -596,10 +596,10 @@ async def handle_oauth_callback(request): }} }} - // Close this popup quickly for env_file since credentials are saved automatically + // Close this popup after showing success confirmation for env_file setTimeout(() => {{ window.close(); - }}, 1500); + }}, 3000); """, content_type='text/html') From 8127641d3949dc73b7944a44bb0f18349a453297 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:44:32 -0700 Subject: [PATCH 35/45] upping that time limit --- oauth_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth_handler.py b/oauth_handler.py index ff87779..11e435d 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -599,7 +599,7 @@ async def handle_oauth_callback(request): // Close this popup after showing success confirmation for env_file setTimeout(() => {{ window.close(); - }}, 3000); + }}, 8000); """, content_type='text/html') From bf699037923a0fd1b355fcee6164e43972d5a7bf Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:49:36 -0700 Subject: [PATCH 36/45] adding close button to oauth window for situations where it doesnt auto-close --- oauth_handler.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/oauth_handler.py b/oauth_handler.py index 11e435d..e75fc07 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -557,6 +557,20 @@ async def handle_oauth_callback(request): 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; @@ -575,10 +589,10 @@ async def handle_oauth_callback(request):
-
โœ…
+
โœ“

Authorization Successful!

{message}

-

This window will close automatically

+
""", content_type='text/html') From 6db833cb08f4da54b60c748d02a2da0d6175735f Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 19:56:24 -0700 Subject: [PATCH 37/45] fixing auto-close delay on oauth success --- web/dropbox_oauth.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index eccebf1..9bd5786 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -52,11 +52,8 @@ app.registerExtension({ }, handleOAuthSuccess() { - // Close OAuth popup if it's open - if (oauthPopup && !oauthPopup.closed) { - oauthPopup.close(); - console.log("[DropSendNode] Closed OAuth popup window"); - } + // 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 before refreshing this.resetReconnectFields(); @@ -65,7 +62,7 @@ app.registerExtension({ setTimeout(() => { console.log("[DropSendNode] Refreshing ComfyUI interface after OAuth success"); window.location.reload(); - }, 1500); // Small delay to ensure OAuth processing is complete + }, 5000); // Longer delay to allow users to see success message and use manual close button }, resetReconnectFields() { From de49b4741844b9938354de19a1770ad11416e30f Mon Sep 17 00:00:00 2001 From: adamhaley Date: Tue, 19 Aug 2025 20:02:59 -0700 Subject: [PATCH 38/45] comfyUI reloading without delay --- web/dropbox_oauth.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index 9bd5786..d049757 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -55,14 +55,12 @@ app.registerExtension({ // 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 before refreshing + // Reset any reconnect checkboxes this.resetReconnectFields(); - // Refresh ComfyUI interface to update node UI - setTimeout(() => { - console.log("[DropSendNode] Refreshing ComfyUI interface after OAuth success"); - window.location.reload(); - }, 5000); // Longer delay to allow users to see success message and use manual close button + // Refresh ComfyUI interface immediately to update node UI + console.log("[DropSendNode] Refreshing ComfyUI interface after OAuth success"); + window.location.reload(); }, resetReconnectFields() { From 21aed02ef2fbfb2bcf99d39ff11279a22f4d5fa2 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 14:10:39 -0700 Subject: [PATCH 39/45] removing emojis and references to keyring --- RUNPOD_SETUP.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RUNPOD_SETUP.md b/RUNPOD_SETUP.md index 50ae18c..1756bb3 100644 --- a/RUNPOD_SETUP.md +++ b/RUNPOD_SETUP.md @@ -61,8 +61,7 @@ export COMFYUI_PUBLIC_URL="https://your-runpod-id.runpod.net:8188" 2. **`display_only`**: Best for temporary pods - copy credentials to environment 3. **`env_file`**: Good for persistent storage volumes -## ๐Ÿ’ก Tips +## Tips -- The enhanced keyring backend now works on RunPod with encrypted file storage - Use `display_only` for maximum flexibility in cloud environments - Set environment variables in your RunPod template for automatic configuration From a76934b093eda0c2bad8a9e0a7205de86246fb9a Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 14:34:53 -0700 Subject: [PATCH 40/45] added manual step using return_type=code to give the user the auth code and having them put in --- setup_dropbox_node.py | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 62f9620..2d2e7cc 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -224,33 +224,22 @@ class DropboxSetupNode: print(f"[DropboxSetup] {message}") return (message,) - # If no auth code, generate OAuth URL for automatic popup flow + # 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 popup flow") + print(f"[DropboxSetup] No auth code provided - generating OAuth URL for manual flow") auth_temp = DropboxAuthManager(app_key=app_key_clean) - # Automatic OAuth flow with popup callback - session_id = str(uuid.uuid4()) - base_url = get_server_base_url() - print(f"[DropboxSetup] Detected base URL: {base_url}") - callback_url = f"{base_url}/oauth/dropbox/callback" - print(f"[DropboxSetup] Using callback URL: {callback_url}") - oauth_url = auth_temp.get_oauth_url(redirect_uri=callback_url, state=session_id, force_reapprove=True) - - # Set up OAuth session for callback handling - oauth_handler = OAuthCallbackHandler() - oauth_handler.start_oauth_session(session_id, app_key_clean, app_secret_clean, storage_method=storage_method, dropbox_folder=dropbox_dest_folder, original_redirect_uri=callback_url) + # 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 automatic OAuth popup...") + 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, and after authorization, ComfyUI will refresh automatically!" - print(f"[DropboxSetup] Session ID: {session_id}") - print(f"[DropboxSetup] Callback URL: {callback_url}") + 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, ComfyUI will refresh automatically!" + 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}") @@ -265,11 +254,9 @@ class DropboxSetupNode: auth_manager_setup = DropboxAuthManager(app_key_clean, app_secret_clean) # Get the tokens without storing them yet - # Always use automatic OAuth flow with callback URL - base_url = get_server_base_url() - callback_url = f"{base_url}/oauth/dropbox/callback" - print(f"[DropboxSetup] Using automatic OAuth with redirect_uri: {callback_url}") - result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean, redirect_uri=callback_url) + # Use manual OAuth flow without redirect_uri (auth codes from manual flow) + print(f"[DropboxSetup] Using manual OAuth flow without redirect_uri") + result = auth_manager_setup.exchange_auth_code_raw(auth_code_clean) refresh_token = result.get("refresh_token") print(f"[DropboxSetup] Auth code exchange successful") From 4622d8bb1ea2e77645fd64d02c59e0ac810babed Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 14:39:35 -0700 Subject: [PATCH 41/45] removing auto-refresh functionality --- web/dropbox_oauth.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index d049757..600b2d8 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -58,9 +58,8 @@ app.registerExtension({ // Reset any reconnect checkboxes this.resetReconnectFields(); - // Refresh ComfyUI interface immediately to update node UI - console.log("[DropSendNode] Refreshing ComfyUI interface after OAuth success"); - window.location.reload(); + // Note: No auto-refresh - user can manually refresh ComfyUI when ready + console.log("[DropSendNode] OAuth success handled - no auto-refresh, user can manually refresh ComfyUI"); }, resetReconnectFields() { @@ -85,11 +84,8 @@ app.registerExtension({ }, handleReconnectSuccess() { - // Refresh ComfyUI interface to show auth fields again - setTimeout(() => { - console.log("[DropSendNode] Refreshing ComfyUI interface after reconnect"); - window.location.reload(); - }, 1000); // Shorter delay for reconnect + // Note: No auto-refresh - user can manually refresh ComfyUI to see auth fields + console.log("[DropSendNode] Reconnect success handled - no auto-refresh, user can manually refresh ComfyUI"); }, // Override node execution to detect and handle OAuth URLs From 5cfb0fd6060f2e50850eaa7cd3d1e50255780d21 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 14:59:13 -0700 Subject: [PATCH 42/45] removing hidden fields functionality , simplifying --- setup_dropbox_node.py | 106 ++++++------------------------------------ 1 file changed, 15 insertions(+), 91 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 2d2e7cc..6972f7f 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -12,81 +12,27 @@ from .oauth_handler import OAuthCallbackHandler, get_server_base_url class DropboxSetupNode: - @classmethod - def _check_credentials_non_blocking(cls): - """Check for credentials without blocking operations""" - try: - node_dir = os.path.dirname(__file__) - - # Check .env file - env_path = os.path.join(node_dir, ".env") - if os.path.exists(env_path): - from dotenv import dotenv_values - env_vars = dotenv_values(env_path) - if all([env_vars.get("DROPBOX_APP_KEY"), env_vars.get("DROPBOX_APP_SECRET"), env_vars.get("DROPBOX_REFRESH_TOKEN")]): - return True - - # Check display_only with environment variables - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - if os.path.exists(display_marker_path): - display_only_env_set = all([ - os.getenv("DROPBOX_APP_KEY"), - os.getenv("DROPBOX_APP_SECRET"), - os.getenv("DROPBOX_REFRESH_TOKEN") - ]) - if display_only_env_set: - return True - - return False - except Exception: - return False - @classmethod def INPUT_TYPES(cls): - # Check if credentials are already stored (non-blocking) - try: - print(f"[DropboxSetup] INPUT_TYPES: Checking for credentials...") - is_connected = cls._check_credentials_non_blocking() - print(f"[DropboxSetup] INPUT_TYPES check - is_connected: {is_connected}") - except Exception as e: - print(f"[DropboxSetup] INPUT_TYPES error: {e}") - is_connected = False - - # Base inputs that are always shown - inputs = { + # Always show all fields for simplicity - no dynamic field hiding + return { "required": { + "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": {} + "optional": { + "reconnect": ("BOOLEAN", { + "label": "Reset stored credentials", + "default": False + }), + "storage_method": (["env_file", "display_only"], { + "label": "Credential Storage Method", + "default": "env_file" + }) + } } - - if is_connected: - # If connected, only show reconnect option - inputs["optional"]["reconnect"] = ("BOOLEAN", { - "label": "Reconnect Dropbox (clears credentials & refreshes UI)", - "default": False - }) - else: - # If not connected, show setup fields - inputs["required"].update({ - "app_key": ("STRING", {"default": "", "multiline": False}), - "app_secret": ("STRING", {"default": "", "multiline": False}), - "auth_code": ("STRING", {"default": "", "multiline": False}), - }) - inputs["optional"]["reconnect"] = ("BOOLEAN", { - "label": "Reset stored credentials", - "default": False - }) - # Simple storage options without keyring complexity - default_storage = "env_file" - storage_options = ["env_file", "display_only"] - - inputs["optional"]["storage_method"] = (storage_options, { - "label": "Credential Storage Method", - "default": default_storage - }) - - return inputs RETURN_TYPES = ("STRING",) OUTPUT_NODE = True @@ -149,28 +95,6 @@ class DropboxSetupNode: "result": (message,) } - # Check if already connected using the same non-blocking method as INPUT_TYPES - is_connected = self._check_credentials_non_blocking() - if is_connected: - print("[DropboxSetup] Already connected - testing stored credentials") - # Only access keyring now when we know we need to test credentials - try: - # This will trigger keyring access but only when we know credentials exist - access_token = auth_manager.get_access_token() - message = "Dropbox already connected using stored credentials. Ready to upload files." - print(f"[DropboxSetup] {message}") - return { - "ui": {"text": [message]}, - "result": (message,) - } - except Exception as e: - message = f"Warning: Stored credentials found but invalid: {e}. Use 'reconnect' to reset." - print(f"[DropboxSetup] {message}") - return { - "ui": {"text": [message]}, - "result": (message,) - } - # Check for display_only environment variables (user set them up after OAuth flow) display_only_env_set = all([ os.getenv("DROPBOX_APP_KEY"), From e0fcc56aff565e2f40dcdfd585bb1ae668068173 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 15:30:23 -0700 Subject: [PATCH 43/45] cleaned up references to older functionality --- dropbox_auth_manager.py | 8 ++++---- oauth_handler.py | 14 ++++++-------- setup_dropbox_node.py | 6 +++--- test_dropbox_auth.py | 2 +- web/dropbox_oauth.js | 14 +++++++------- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/dropbox_auth_manager.py b/dropbox_auth_manager.py index 8b2d204..e8151aa 100644 --- a/dropbox_auth_manager.py +++ b/dropbox_auth_manager.py @@ -128,10 +128,10 @@ class DropboxAuthManager: 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 redirect_uri, try without it - # This handles cases where the auth code was obtained without the redirect_uri + # 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 redirect_uri, trying without redirect_uri...") + print(f"[DropboxAuthManager] 400 error with callback URL, trying without callback URL...") data_without_redirect = { "code": auth_code, @@ -140,7 +140,7 @@ class DropboxAuthManager: "client_secret": self.app_secret } - print(f"[DropboxAuthManager] Retry request data (no redirect_uri): {data_without_redirect}") + 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) diff --git a/oauth_handler.py b/oauth_handler.py index e75fc07..5027e1e 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -43,7 +43,7 @@ class OAuthCallbackHandler: try: print(f"[OAuthHandler] Processing callback for session: {self.session_id}") print(f"[OAuthHandler] Auth code received: {auth_code[:10]}...") - print(f"[OAuthHandler] Redirect URI: {redirect_uri}") + print(f"[OAuthHandler] Processing callback for auth code exchange") # Use DropboxAuthManager to exchange the code print(f"[OAuthHandler] Creating DropboxAuthManager for token exchange...") @@ -136,7 +136,7 @@ These credentials are ready to use immediately. 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 - no auto-refresh") + print(f"[OAuthHandler] Skipping WebSocket notification for display_only method") # Call completion callback if provided if self.completion_callback: @@ -330,7 +330,7 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to
- Copy the credentials above, then manually refresh ComfyUI to hide the auth fields. Close this window when done. + Copy the credentials above, then close this window when done.
@@ -365,12 +365,10 @@ def create_display_only_success_page(session_id, app_key, app_secret, refresh_to }}); }}); - // For display_only, don't auto-refresh ComfyUI - let user copy credentials first - // The user can manually refresh when they're done - console.log('Display-only OAuth completed - not auto-refreshing ComfyUI'); + // For display_only, let user copy credentials first + console.log('Display-only OAuth completed - credentials ready for copying'); - // Don't auto-close for display_only - let user close manually after copying credentials - // The user can close this window when they're done copying + // User can close this window when done copying credentials diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 6972f7f..90ceb75 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -178,8 +178,8 @@ class DropboxSetupNode: auth_manager_setup = DropboxAuthManager(app_key_clean, app_secret_clean) # Get the tokens without storing them yet - # Use manual OAuth flow without redirect_uri (auth codes from manual flow) - print(f"[DropboxSetup] Using manual OAuth flow without redirect_uri") + # 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") @@ -249,7 +249,7 @@ These credentials are ready to use immediately. print("="*80) print("Copy the lines above to your environment variables!") print("Perfect for RunPod, Docker, and production environments!") - print("Note: ComfyUI won't auto-refresh - refresh manually after copying!") + print("Note: Refresh ComfyUI manually after copying credentials!") print("="*80 + "\n") # Use ComfyUI's dynamic return format for better UI integration diff --git a/test_dropbox_auth.py b/test_dropbox_auth.py index 7f8a9a1..228a510 100644 --- a/test_dropbox_auth.py +++ b/test_dropbox_auth.py @@ -78,7 +78,7 @@ def test_auth_code_exchange(): auth.exchange_auth_code(test_auth_code) print("โœ… Auth code exchange successful") - print("โœ… Credentials stored in keyring") + print("โœ… Credentials stored successfully") return True except Exception as e: diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index 600b2d8..bc2972f 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -18,7 +18,7 @@ app.registerExtension({ const data = event.detail; if (data.success) { - console.log("[DropSendNode] OAuth successful - refreshing interface"); + console.log("[DropSendNode] OAuth successful"); this.handleOAuthSuccess(); } else { console.error("[DropSendNode] OAuth failed:", data.message); @@ -31,7 +31,7 @@ app.registerExtension({ const data = event.detail; if (data.success) { - console.log("[DropSendNode] Reconnect successful - refreshing interface"); + console.log("[DropSendNode] Reconnect successful"); this.handleReconnectSuccess(); } }); @@ -42,7 +42,7 @@ app.registerExtension({ console.log("[DropSendNode] Received OAuth completion notification via postMessage", event.data); if (event.data.success) { - console.log("[DropSendNode] OAuth successful - refreshing interface"); + console.log("[DropSendNode] OAuth successful"); this.handleOAuthSuccess(); } else { console.error("[DropSendNode] OAuth failed:", event.data.message); @@ -58,8 +58,8 @@ app.registerExtension({ // Reset any reconnect checkboxes this.resetReconnectFields(); - // Note: No auto-refresh - user can manually refresh ComfyUI when ready - console.log("[DropSendNode] OAuth success handled - no auto-refresh, user can manually refresh ComfyUI"); + // Note: Manual refresh required if user wants to update UI + console.log("[DropSendNode] OAuth success handled - manual refresh available"); }, resetReconnectFields() { @@ -84,8 +84,8 @@ app.registerExtension({ }, handleReconnectSuccess() { - // Note: No auto-refresh - user can manually refresh ComfyUI to see auth fields - console.log("[DropSendNode] Reconnect success handled - no auto-refresh, user can manually refresh ComfyUI"); + // Note: Manual refresh required to see auth fields + console.log("[DropSendNode] Reconnect success handled - manual refresh available"); }, // Override node execution to detect and handle OAuth URLs From 66d4104f6dba8ed3027019bc45a51d8d07c92fc4 Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 15:39:51 -0700 Subject: [PATCH 44/45] serup auto -refresh for reconnect path ONLY. removed log messages pertaining to auto-refresh --- setup_dropbox_node.py | 1 - web/dropbox_oauth.js | 21 +++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 90ceb75..7af41a6 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -249,7 +249,6 @@ These credentials are ready to use immediately. print("="*80) print("Copy the lines above to your environment variables!") print("Perfect for RunPod, Docker, and production environments!") - print("Note: Refresh ComfyUI manually after copying credentials!") print("="*80 + "\n") # Use ComfyUI's dynamic return format for better UI integration diff --git a/web/dropbox_oauth.js b/web/dropbox_oauth.js index bc2972f..2a15311 100644 --- a/web/dropbox_oauth.js +++ b/web/dropbox_oauth.js @@ -84,8 +84,17 @@ app.registerExtension({ }, handleReconnectSuccess() { - // Note: Manual refresh required to see auth fields - console.log("[DropSendNode] Reconnect success handled - manual refresh available"); + // 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 @@ -101,6 +110,8 @@ app.registerExtension({ // 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"); @@ -115,8 +126,14 @@ app.registerExtension({ 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) { From c7665e8d6b4fc38a85872322faf67e42c92c85cb Mon Sep 17 00:00:00 2001 From: adamhaley Date: Wed, 20 Aug 2025 18:33:56 -0700 Subject: [PATCH 45/45] cleanup --- RUNPOD_SETUP.md | 67 --------- generate_base64.py | 10 -- get_refresh_token.py | 27 ---- oauth_handler.py | 6 +- requirements.txt | 2 - setup_dropbox_node.py | 43 ++---- test_dropbox_auth.py | 140 ------------------ .../test_dropbox_auth.py | 86 +++++------ 8 files changed, 55 insertions(+), 326 deletions(-) delete mode 100644 RUNPOD_SETUP.md delete mode 100644 generate_base64.py delete mode 100644 get_refresh_token.py delete mode 100644 test_dropbox_auth.py rename test_dropbox_auth_simple.py => tests/test_dropbox_auth.py (59%) diff --git a/RUNPOD_SETUP.md b/RUNPOD_SETUP.md deleted file mode 100644 index 1756bb3..0000000 --- a/RUNPOD_SETUP.md +++ /dev/null @@ -1,67 +0,0 @@ -# RunPod OAuth Setup Guide - -## Quick Setup for RunPod - -The DropSend node now automatically detects RunPod environments, but for OAuth to work properly, you may need to configure your public URL. - -### Option 1: Automatic Detection (Try This First) - -The node will try to automatically detect your RunPod URL. Just run the OAuth flow and check the console for messages like: - -``` -[OAuthHandler] Detected server URL from request: https://your-runpod-url.runpod.net -``` - -### Option 2: Manual Configuration (If Auto-Detection Fails) - -If the automatic detection doesn't work, set one of these environment variables: - -#### Method A: Set PUBLIC_URL Environment Variable -```bash -export PUBLIC_URL="https://your-runpod-id.runpod.net:8188" -``` - -#### Method B: Set COMFYUI_PUBLIC_URL Environment Variable -```bash -export COMFYUI_PUBLIC_URL="https://your-runpod-id.runpod.net:8188" -``` - -### How to Find Your RunPod URL - -1. **From RunPod Dashboard**: Look for your pod's public URL -2. **From Browser**: Copy the URL you use to access ComfyUI -3. **From Console**: The node will log detected URLs during OAuth - -### Example URLs - -- `https://abc123-8188.proxy.runpod.net` -- `https://xyz789.runpod.net:8188` -- `https://your-custom-domain.com:8188` - -### Testing the Configuration - -1. Run the DropboxSetupNode -2. Check console output for detected URL -3. If URL looks wrong, set the environment variable and restart ComfyUI - -### Troubleshooting - -**Problem**: OAuth callback fails with "redirect_uri mismatch" -**Solution**: Check that the detected URL in the console matches how you access ComfyUI - -**Problem**: Automatic detection shows localhost -**Solution**: Set PUBLIC_URL or COMFYUI_PUBLIC_URL environment variable - -**Problem**: Still getting localhost after setting environment variables -**Solution**: Restart ComfyUI after setting environment variables - -### Storage Method Recommendations for RunPod - -1. **`keyring`**: Now works with encrypted file storage (recommended) -2. **`display_only`**: Best for temporary pods - copy credentials to environment -3. **`env_file`**: Good for persistent storage volumes - -## Tips - -- Use `display_only` for maximum flexibility in cloud environments -- Set environment variables in your RunPod template for automatic configuration diff --git a/generate_base64.py b/generate_base64.py deleted file mode 100644 index 05ed4a9..0000000 --- a/generate_base64.py +++ /dev/null @@ -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}") diff --git a/get_refresh_token.py b/get_refresh_token.py deleted file mode 100644 index 5c7fafb..0000000 --- a/get_refresh_token.py +++ /dev/null @@ -1,27 +0,0 @@ -# ComfyUI_DropSendNode/get_refresh_token.py - -import base64 -import requests - -APP_KEY = input("Enter your APP KEY: ") -APP_SECRET = input("Enter your APP SECRET: ") -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": "https://localhost" - }, - 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) diff --git a/oauth_handler.py b/oauth_handler.py index 5027e1e..ded42c1 100644 --- a/oauth_handler.py +++ b/oauth_handler.py @@ -70,12 +70,8 @@ class OAuthCallbackHandler: success_message = "Dropbox connected successfully! Credentials saved to .env file." elif self.storage_method == "display_only": - # Display credentials for manual copying and create completion marker + # Display credentials for manual copying import os - node_dir = os.path.dirname(__file__) - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - with open(display_marker_path, "w") as f: - f.write("display_only_setup_completed") # Print credentials to console for easy copy/paste print("\n" + "=" * 80) diff --git a/requirements.txt b/requirements.txt index f067d84..a41eb3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,3 @@ requests python-dotenv watchdog dropbox -keyring -keyrings.alt diff --git a/setup_dropbox_node.py b/setup_dropbox_node.py index 7af41a6..ed86c81 100644 --- a/setup_dropbox_node.py +++ b/setup_dropbox_node.py @@ -68,13 +68,6 @@ class DropboxSetupNode: print(f"[DropboxSetup] Removing .env file: {env_path}") os.remove(env_path) - # Clear display_only marker if it exists - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - if os.path.exists(display_marker_path): - print(f"[DropboxSetup] Removing display_only marker: {display_marker_path}") - os.remove(display_marker_path) - - # Send WebSocket message to trigger ComfyUI refresh after clearing credentials try: from server import PromptServer @@ -95,31 +88,19 @@ class DropboxSetupNode: "result": (message,) } - # Check for display_only environment variables (user set them up after OAuth flow) - display_only_env_set = all([ + # 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 display_only_env_set: - # Check if this came from display_only flow - node_dir = os.path.dirname(__file__) - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - if os.path.exists(display_marker_path): - message = "Dropbox connected using environment variables (display_only setup). Ready to upload files." - print(f"[DropboxSetup] {message}") - return { - "ui": {"text": [message]}, - "result": (message,) - } - else: - # Legacy environment variables (not from display_only) - message = "Dropbox credentials found in system environment variables. Ready to upload files." - print(f"[DropboxSetup] {message}") - return { - "ui": {"text": [message]}, - "result": (message,) - } + 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,) + } # Check for RunPod secrets runpod_env_set = all([ @@ -199,11 +180,7 @@ class DropboxSetupNode: message = "Dropbox connected successfully! Credentials saved to .env file." elif storage_method == "display_only": - # Display credentials for manual copying and create completion marker - node_dir = os.path.dirname(__file__) - display_marker_path = os.path.join(node_dir, ".dropbox_display_complete") - with open(display_marker_path, "w") as f: - f.write("display_only_setup_completed") + # Display credentials for manual copying message = f"""Dropbox Connected Successfully! diff --git a/test_dropbox_auth.py b/test_dropbox_auth.py deleted file mode 100644 index 228a510..0000000 --- a/test_dropbox_auth.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# test_dropbox_auth_simple.py - Non-interactive testing script for DropboxAuthManager - -from dropbox_auth_manager import DropboxAuthManager - -def test_oauth_url_generation(): - """Test OAuth URL generation""" - print("๐Ÿงช TEST 1: OAuth URL generation") - print("-" * 40) - - # Use a dummy app key for testing - test_app_key = "test_app_key_12345" - - try: - auth = DropboxAuthManager(app_key=test_app_key) - oauth_url = auth.get_oauth_url() - print(f"โœ… Generated OAuth URL:") - print(f" {oauth_url}") - expected_url = f"https://www.dropbox.com/oauth2/authorize?response_type=code&client_id={test_app_key}&token_access_type=offline" - if oauth_url == expected_url: - print("โœ… URL format is correct") - return True - else: - print("โŒ URL format mismatch") - return False - except Exception as e: - print(f"โŒ Error generating OAuth URL: {e}") - return False - -def test_stored_credentials(): - """Test retrieving stored credentials""" - print("\n๐Ÿงช TEST 2: Check for stored credentials") - print("-" * 40) - - try: - auth = DropboxAuthManager() - - print(f"App key stored: {bool(auth.app_key)}") - print(f"App secret stored: {bool(auth.app_secret)}") - print(f"Refresh token stored: {bool(auth.refresh_token)}") - print(f"Is connected: {auth.is_connected()}") - - if auth.is_connected(): - print("โœ… Stored credentials found - testing access token...") - try: - access_token = auth.get_access_token() - print(f"โœ… Got access token: {access_token[:10]}...") - return True - except Exception as e: - print(f"โŒ Error getting access token: {e}") - return False - else: - print("โ„น๏ธ No stored credentials found (this is expected for first run)") - return True - - except Exception as e: - print(f"โŒ Error checking stored credentials: {e}") - return False - -def test_auth_code_exchange(): - """Test auth code exchange with placeholder values""" - print("\n๐Ÿงช TEST 3: Auth code exchange test (with dummy values)") - print("-" * 40) - - # These are dummy values - replace with real ones to test - test_app_key = "your_app_key_here" - test_app_secret = "your_app_secret_here" - test_auth_code = "your_auth_code_here" - - if test_app_key == "your_app_key_here": - print("โ„น๏ธ Skipping auth code exchange test - placeholder values detected") - print("โ„น๏ธ To test this, replace the dummy values in this function with real credentials") - return True - - try: - auth = DropboxAuthManager(test_app_key, test_app_secret) - print(f"Created DropboxAuthManager with app_key: {bool(auth.app_key)}, app_secret: {bool(auth.app_secret)}") - - auth.exchange_auth_code(test_auth_code) - print("โœ… Auth code exchange successful") - print("โœ… Credentials stored successfully") - return True - - except Exception as e: - print(f"โŒ Error during auth code exchange: {e}") - return False - -def test_manager_initialization(): - """Test DropboxAuthManager initialization with different parameters""" - print("\n๐Ÿงช TEST 4: Manager initialization tests") - print("-" * 40) - - try: - # Test 1: Empty initialization - auth1 = DropboxAuthManager() - print(f"โœ… Empty init - app_key: {auth1.app_key}, app_secret: {auth1.app_secret}") - - # Test 2: With parameters - auth2 = DropboxAuthManager("test_key", "test_secret") - print(f"โœ… With params - app_key: {auth2.app_key}, app_secret: {auth2.app_secret}") - - # Test 3: With empty strings (this was causing the bug) - auth3 = DropboxAuthManager("", "") - print(f"โœ… Empty strings - app_key: '{auth3.app_key}', app_secret: '{auth3.app_secret}'") - - return True - except Exception as e: - print(f"โŒ Error during initialization tests: {e}") - return False - -def main(): - print("๐Ÿš€ Dropbox Auth Manager Test Suite (Non-Interactive)") - print("=" * 60) - - # Run all tests - tests = [ - test_oauth_url_generation, - test_stored_credentials, - test_manager_initialization, - test_auth_code_exchange - ] - - results = [] - for test in tests: - result = test() - results.append(result) - - print("\n" + "=" * 60) - print("๐Ÿ“Š TEST SUMMARY:") - passed = sum(results) - total = len(results) - print(f"โœ… Passed: {passed}/{total}") - - if passed == total: - print("๐ŸŽ‰ All tests passed!") - else: - print("โš ๏ธ Some tests failed - check output above") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_dropbox_auth_simple.py b/tests/test_dropbox_auth.py similarity index 59% rename from test_dropbox_auth_simple.py rename to tests/test_dropbox_auth.py index df32107..e10e407 100644 --- a/test_dropbox_auth_simple.py +++ b/tests/test_dropbox_auth.py @@ -8,8 +8,11 @@ Usage: This script tests the DropboxAuthManager without requiring user input. """ -from dropbox_auth_manager import DropboxAuthManager +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""" @@ -21,44 +24,43 @@ def test_basic_functionality(): # Test initialization without credentials print("\n1. Testing initialization without credentials...") auth = DropboxAuthManager() - print(f"โœ… DropboxAuthManager initialized") + 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"โœ… DropboxAuthManager initialized with test credentials") + 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"โœ… Basic OAuth URL: {oauth_url[:80]}...") + print(f"PASS: Basic OAuth URL: {oauth_url[:80]}...") - # Test OAuth URL with redirect_uri - callback_url = "http://localhost:8188/oauth/dropbox/callback" - oauth_url_with_redirect = test_auth.get_oauth_url(redirect_uri=callback_url, state="test-session-123") - print(f"โœ… OAuth URL with redirect: {oauth_url_with_redirect[:100]}...") + # 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 redirect_uri is included - if "redirect_uri=" in oauth_url_with_redirect and callback_url in oauth_url_with_redirect: - print("โœ… redirect_uri properly included in OAuth URL") + # 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("โŒ redirect_uri not found in OAuth URL") + print("FAIL: OAuth URL missing required parameters") - # Test that state is included - if "state=test-session-123" in oauth_url_with_redirect: - print("โœ… state parameter properly included in OAuth URL") + # 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("โŒ state parameter not found in OAuth URL") + print("FAIL: state parameter not found in OAuth URL") print("\n" + "=" * 50) - print("โœ… All basic functionality tests passed!") + print("PASS: All basic functionality tests passed!") print("=" * 50) except Exception as e: - print(f"โŒ Error during basic functionality test: {e}") + print(f"FAIL: Error during basic functionality test: {e}") raise def test_error_handling(): @@ -73,24 +75,24 @@ def test_error_handling(): auth_no_key = DropboxAuthManager() try: auth_no_key.get_oauth_url() - print("โŒ Should have raised ValueError") + print("FAIL: Should have raised ValueError") except ValueError as e: - print(f"โœ… Correctly raised ValueError: {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("โŒ Should have raised ValueError") + print("FAIL: Should have raised ValueError") except ValueError as e: - print(f"โœ… Correctly raised ValueError: {e}") + print(f"PASS: Correctly raised ValueError: {e}") print("\n" + "=" * 50) - print("โœ… All error handling tests passed!") + print("PASS: All error handling tests passed!") print("=" * 50) except Exception as e: - print(f"โŒ Unexpected error during error handling test: {e}") + print(f"FAIL: Unexpected error during error handling test: {e}") raise def test_token_exchange_retry_logic(): @@ -107,46 +109,46 @@ def test_token_exchange_retry_logic(): 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", 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"โœ… Method executed correctly (expected API failure): {type(e).__name__}") - else: - print(f"โŒ Unexpected error type: {e}") - - # Test method without redirect_uri 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"โœ… Method executed correctly without redirect_uri (expected API failure): {type(e).__name__}") + print(f"PASS: Method executed correctly (expected API failure): {type(e).__name__}") else: - print(f"โŒ Unexpected error type: {e}") + 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("โœ… Token exchange retry logic tests completed!") + print("PASS: Token exchange retry logic tests completed!") print("=" * 50) except Exception as e: - print(f"โŒ Error during retry logic test: {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") + 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("\n๐ŸŽ‰ All tests completed successfully!") - print("๐Ÿ’ก To test with real Dropbox credentials, use the interactive flow in ComfyUI") + print("\nAll tests completed successfully!") + print("To test with real Dropbox credentials, use the interactive flow in ComfyUI") except Exception as e: - print(f"\n๐Ÿ’ฅ Test failed: {e}") + print(f"\nTest failed: {e}") return 1 return 0