diff --git a/README.md b/README.md index ea4919a7..360e1088 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,8 @@ The following features can be configured using environment variables: * **COMFYUI_PATH**: The installation path of ComfyUI * **GITHUB_ENDPOINT**: Reverse proxy configuration for environments with limited access to GitHub * **HF_ENDPOINT**: Reverse proxy configuration for environments with limited access to Hugging Face +* **COMFY_MANAGER_PASSKEY**: User password to access ComfyUI-Manager +* **COMFY_MODLES**: Reverse models download location ### Example 1: diff --git a/glob/manager_server.py b/glob/manager_server.py index eff7c032..dfb1f905 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -20,6 +20,7 @@ import cm_global import logging import asyncio import queue +import hmac import manager_downloader import manager_migration @@ -312,11 +313,59 @@ def security_403_response(): return web.json_response({"error": "security_level"}, status=403) -def get_model_dir(data, show_log=False): +def get_manager_passkey(): + passkey = os.environ.get("COMFY_MANAGER_PASSKEY") + if passkey is None: + return None + + passkey = passkey.strip() + return passkey if passkey else None + + +@routes.get("/manager/auth") +async def manager_auth_status(request): + return web.json_response({"required": get_manager_passkey() is not None}, status=200) + + +@routes.post("/manager/auth") +async def manager_auth_verify(request): + expected_passkey = get_manager_passkey() + if expected_passkey is None: + return web.Response(status=200) + + try: + payload = await request.json() + provided_passkey = payload.get("password", "") + except Exception: + provided_passkey = await request.text() + + if not isinstance(provided_passkey, str): + return web.Response(status=400, text="Invalid password payload") + + if hmac.compare_digest(provided_passkey, expected_passkey): + return web.Response(status=200) + + return web.Response(status=403, text="Invalid manager passkey") + + +def get_models_base_dir(): + override_path = os.environ.get("COMFY_MODLES") + if not override_path: + # Backward-compatible fallback for the correctly spelled variable. + override_path = os.environ.get("COMFY_MODELS") + + if override_path: + return os.path.abspath(os.path.expandvars(os.path.expanduser(override_path))) + if 'download_model_base' in folder_paths.folder_names_and_paths: - models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0] - else: - models_base = folder_paths.models_dir + return folder_paths.folder_names_and_paths['download_model_base'][0][0] + + return folder_paths.models_dir + + +def get_model_dir(data, show_log=False): + models_base_override = os.environ.get("COMFY_MODLES") or os.environ.get("COMFY_MODELS") + models_base = get_models_base_dir() # NOTE: Validate to prevent path traversal. if any(char in data['filename'] for char in {'/', '\\', ':'}): @@ -355,7 +404,9 @@ def get_model_dir(data, show_log=False): base_model = os.path.join(models_base, data['save_path']) else: model_dir_name = model_dir_name_map.get(data['type'].lower()) - if model_dir_name is not None: + if models_base_override: + base_model = os.path.join(models_base, model_dir_name if model_dir_name is not None else "etc") + elif model_dir_name is not None: base_model = folder_paths.folder_names_and_paths[model_dir_name][0][0] else: base_model = os.path.join(models_base, "etc") @@ -922,6 +973,9 @@ async def fetch_customnode_alternatives(request): def check_model_installed(json_obj): + models_base_override = os.environ.get("COMFY_MODLES") or os.environ.get("COMFY_MODELS") + models_base = get_models_base_dir() + def is_exists(model_dir_name, filename, url): if filename == '': filename = os.path.basename(url) @@ -932,6 +986,10 @@ def check_model_installed(json_obj): if os.path.exists(os.path.join(x, filename)): return True + if models_base_override: + if os.path.exists(os.path.join(models_base, model_dir_name, filename)): + return True + return False model_dir_names = ['checkpoints', 'loras', 'vae', 'text_encoders', 'diffusion_models', 'clip_vision', 'embeddings', @@ -963,14 +1021,8 @@ def check_model_installed(json_obj): item['installed'] = 'True' if 'installed' not in item: - if item['filename'] == '': - filename = os.path.basename(item['url']) - else: - filename = item['filename'] - - fullpath = os.path.join(folder_paths.models_dir, item['save_path'], filename) - - item['installed'] = 'True' if os.path.exists(fullpath) else 'False' + fullpath = get_model_path(item) + item['installed'] = 'True' if fullpath is not None and os.path.exists(fullpath) else 'False' with concurrent.futures.ThreadPoolExecutor(8) as executor: for item in json_obj['models']: diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index bcf7e9e5..7e48017c 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -239,6 +239,123 @@ var update_policy_combo = null; let share_option = 'all'; var is_updating = false; +let managerAccessVerified = false; + + +function promptManagerPasskey() { + return new Promise((resolve) => { + const overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.zIndex = "1102"; + + const dialog = document.createElement("div"); + dialog.style.backgroundColor = "#222"; + dialog.style.color = "#fff"; + dialog.style.padding = "18px"; + dialog.style.borderRadius = "8px"; + dialog.style.width = "min(420px, 90vw)"; + dialog.style.boxSizing = "border-box"; + + const title = document.createElement("h3"); + title.textContent = "Manager Access Required"; + title.style.margin = "0 0 12px 0"; + + const message = document.createElement("div"); + message.textContent = "Enter the manager passkey."; + message.style.marginBottom = "10px"; + + const input = document.createElement("input"); + input.type = "password"; + input.placeholder = "Passkey"; + input.style.width = "100%"; + input.style.padding = "8px"; + input.style.marginBottom = "12px"; + input.style.boxSizing = "border-box"; + + const buttonRow = document.createElement("div"); + buttonRow.style.display = "flex"; + buttonRow.style.justifyContent = "flex-end"; + buttonRow.style.gap = "8px"; + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + + const confirmButton = document.createElement("button"); + confirmButton.textContent = "Unlock"; + + const close = (value) => { + document.body.removeChild(overlay); + resolve(value); + }; + + cancelButton.onclick = () => close(null); + confirmButton.onclick = () => close(input.value); + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + close(input.value); + } + }); + + buttonRow.append(cancelButton, confirmButton); + dialog.append(title, message, input, buttonRow); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + input.focus(); + }); +} + + +async function ensureManagerAccess() { + if (managerAccessVerified) { + return true; + } + + let res; + try { + res = await api.fetchApi("/manager/auth"); + } catch { + // Fallback to legacy behavior when auth endpoint isn't available. + return true; + } + + if (res.status !== 200) { + return true; + } + + const info = await res.json(); + if (!info?.required) { + managerAccessVerified = true; + return true; + } + + while (true) { + const password = await promptManagerPasskey(); + if (password === null) { + return false; + } + + const verifyRes = await api.fetchApi("/manager/auth", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + + if (verifyRes.status === 200) { + managerAccessVerified = true; + return true; + } + + customAlert("Invalid manager passkey."); + } +} // copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts @@ -1487,7 +1604,11 @@ app.registerExtension({ id: "Comfy.Manager.Menu.ToggleVisibility", label: "Toggle Manager Menu Visibility", icon: "mdi mdi-puzzle", - function: () => { + function: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if (!manager_instance) { setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); @@ -1500,7 +1621,11 @@ app.registerExtension({ id: "Comfy.Manager.CustomNodesManager.ToggleVisibility", label: "Toggle Custom Nodes Manager Visibility", icon: "pi pi-server", - function: () => { + function: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if (CustomNodesManager.instance?.isVisible) { CustomNodesManager.instance.close(); return; @@ -1570,7 +1695,11 @@ app.registerExtension({ let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "puzzle", - action: () => { + action: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); @@ -1581,7 +1710,11 @@ app.registerExtension({ }).element, new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "star", - action: () => { + action: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); @@ -1638,7 +1771,11 @@ app.registerExtension({ // old style Manager button const managerButton = document.createElement("button"); managerButton.textContent = "Manager"; - managerButton.onclick = () => { + managerButton.onclick = async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); manager_instance.show();