""" NAS Sharing Auth Module - Login flow với OTP modal support EXTRACTED từ download_link.py, adapted cho modal OTP pattern """ import os import time from typing import Optional, Tuple, TYPE_CHECKING from selenium import webdriver from selenium.webdriver.common.by import By if TYPE_CHECKING: from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def get_dsm_credentials() -> Tuple[str, str]: """ Get DSM credentials from environment Returns: (username, password) tuple Raises: ValueError: If credentials not set in .env.local """ username = os.getenv("NAS_USERNAME") password = os.getenv("NAS_PASSWORD") if not username or not password: raise ValueError("NAS_USERNAME and NAS_PASSWORD must be set in .env.local") return username, password def perform_login( driver: webdriver.Chrome, username: Optional[str] = None, password: Optional[str] = None, otp_callback=None ) -> bool: """ Perform DSM login with OTP modal support EXTRACTED từ download_link.py DSMSeleniumLogin.login() MODIFIED: Dùng callback cho OTP thay vì manual input Args: driver: Selenium WebDriver instance username: DSM username (default: from env) password: DSM password (default: from env) otp_callback: Function() -> Optional[str] to get OTP code from modal Should return None if timeout/cancelled Returns: True if login successful """ if username is None or password is None: username, password = get_dsm_credentials() try: # Get DSM URL from env dsm_url = os.getenv("NAS_DSM_URL") if not dsm_url: raise ValueError("NAS_DSM_URL must be set in .env.local") print(f"\n🌐 Đang truy cập: {dsm_url}") print(" (Trang có thể mất 30-60s để load...)") # Navigate to DSM try: driver.get(dsm_url) except Exception as e: print(f"⚠️ Timeout khi load trang, nhưng tiếp tục thử...") time.sleep(5) wait = WebDriverWait(driver, 30) # === STEP 1: Enter USERNAME === print("🔍 BƯỚC 1: Đang tìm form username...") # NO FALLBACK - Throw error nếu selector không match username_input = wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "input#login_username, input[type='text'][name='username'], input.syno-ux-textfield[type='text']")) ) print(f"📝 Nhập username: {username}") username_input.click() time.sleep(0.5) username_input.clear() time.sleep(0.5) username_input.send_keys(username) time.sleep(1) # Click Next button print("🖱️ Tìm nút Next...") next_button = _find_button(driver, [ "div[syno-id='account-panel-next-btn']", # DSM 7.x "div.login-btn[role='button']", "button#login-btn", "button[type='submit']", ]) if not next_button: raise RuntimeError("Không tìm thấy nút Next") print("🖱️ Click nút Next...") next_button.click() time.sleep(3) # === STEP 2: Enter PASSWORD === print("\n🔍 BƯỚC 2: Đang tìm form password...") try: password_input = wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "input[syno-id='password']")) ) except: try: password_input = wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "input[type='password'][name='current-password']")) ) except: password_input = wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, "input[type='password']")) ) print("🔑 Nhập password...") password_input.click() time.sleep(0.5) password_input.clear() time.sleep(0.5) password_input.send_keys(password) time.sleep(1) # Tick "Stay signed in" checkbox print("☑️ Tick checkbox 'Stay signed in'...") try: stay_signed_checkbox = driver.find_element(By.CSS_SELECTOR, "div.login-checkbox input[type='checkbox']") if not stay_signed_checkbox.is_selected(): checkbox_label = driver.find_element(By.CSS_SELECTOR, "div.login-checkbox label.box") checkbox_label.click() print(" ✅ Đã tick 'Stay signed in'") time.sleep(0.5) else: print(" ℹ️ Checkbox đã được tick sẵn") except Exception as e: print(f" ⚠️ Không tìm thấy checkbox (không sao): {e}") time.sleep(0.5) # Click Sign In button print("🖱️ Tìm nút Sign In...") signin_button = _find_button(driver, [ "div[syno-id='password-panel-next-btn']", # DSM 7.x password panel "div[syno-id='account-panel-next-btn']", "div.login-btn[role='button']", "button#login-btn", "button[type='submit']", ]) if not signin_button: raise RuntimeError("Không tìm thấy nút Sign In") print("🖱️ Click nút Sign In...") signin_button.click() time.sleep(3) # === STEP 3: Handle OTP if needed === otp_required = detect_otp_modal(driver) if otp_required: print("\n" + "=" * 70) print("🔐 PHÁT HIỆN YÊU CẦU OTP (2-FACTOR AUTHENTICATION)") print("=" * 70) if otp_callback: print("⏳ Đang đợi OTP từ frontend modal...") otp_code = otp_callback() if not otp_code: print("❌ Không nhận được OTP (timeout hoặc cancelled)") return False # Submit OTP print(f"✅ Nhận OTP: {otp_code[:2]}***") if not submit_otp_code(driver, otp_code): print("❌ Lỗi submit OTP") return False time.sleep(3) else: print("⚠️ Không có OTP callback, bỏ qua...") return False else: print("ℹ️ Không phát hiện yêu cầu OTP") # === STEP 4: Wait for login success === print("⏳ Đang chờ đăng nhập hoàn tất...") return wait_for_login_success(driver, timeout=15) except Exception as e: print(f"❌ Lỗi trong quá trình đăng nhập: {e}") import traceback traceback.print_exc() return False def detect_otp_modal(driver: webdriver.Chrome) -> bool: """ Detect if OTP modal is shown EXTRACTED từ download_link.py logic Returns: True if OTP input is visible """ try: # Method 1: Find title "Enter verification code" otp_title = driver.find_element(By.XPATH, "//*[contains(text(), 'Enter verification code') or contains(text(), 'verification code')]") if otp_title.is_displayed(): return True except: pass try: # Method 2: Find input with name='one-time-code' otp_input = driver.find_element(By.CSS_SELECTOR, "input[name='one-time-code']") if otp_input.is_displayed(): return True except: pass try: # Method 3: Find OTP button with syno-id otp_button = driver.find_element(By.CSS_SELECTOR, "div[syno-id='otp-panel-next-btn']") if otp_button.is_displayed(): return True except: pass return False def submit_otp_code(driver: webdriver.Chrome, otp_code: str) -> bool: """ Submit OTP code to form Args: driver: Selenium WebDriver otp_code: 6-digit OTP code Returns: True if submitted successfully """ try: # Find OTP input otp_input = driver.find_element(By.CSS_SELECTOR, "input[name='one-time-code']") if not otp_input.is_displayed(): print("❌ OTP input không visible") return False # Clear and enter OTP otp_input.clear() otp_input.send_keys(otp_code) time.sleep(0.5) # Click submit or press Enter try: otp_button = driver.find_element(By.CSS_SELECTOR, "div[syno-id='otp-panel-next-btn']") otp_button.click() except: # Fallback: press Enter otp_input.send_keys("\n") print("✅ OTP đã submit") return True except Exception as e: print(f"❌ Lỗi submit OTP: {e}") return False def wait_for_login_success(driver: webdriver.Chrome, timeout: int = 15) -> bool: """ Wait for login to complete EXTRACTED từ download_link.py is_logged_in() check Args: driver: Selenium WebDriver timeout: Max seconds to wait Returns: True if "Synology Drive" div detected (indicates successful login) Raises: RuntimeError: If login not detected after timeout """ for i in range(timeout): try: # Check for "Synology Drive" text - only appears after login elem = driver.find_element(By.XPATH, "//div[contains(text(), 'Synology Drive')]") if elem is not None: print("✅ Đăng nhập thành công!") return True except Exception as e: # Log specific error for debugging if i == 0: # Only log on first attempt print(f"🔍 Chưa thấy 'Synology Drive', đang đợi... (Error: {type(e).__name__})") time.sleep(1) # Timeout - throw error thay vì return False print("❌ KHÔNG phát hiện đăng nhập thành công sau timeout") raise RuntimeError( f"Login verification failed: 'Synology Drive' element not found after {timeout}s. " f"Login may have failed or page structure changed." ) def is_logged_in(driver: webdriver.Chrome) -> bool: """ Quick check if already logged in EXACT COPY từ download_link.py DSMSeleniumLogin.is_logged_in() Returns: True if "Synology Drive" div found """ if not driver: return False try: elem = driver.find_element(By.XPATH, "//div[contains(text(), 'Synology Drive')]") return elem is not None except: return False def _find_button(driver: webdriver.Chrome, selectors: list) -> Optional['WebElement']: """ Helper: Find first visible button from selector list Args: driver: Selenium WebDriver selectors: List of CSS selectors to try Returns: WebElement if found, None otherwise """ for selector in selectors: try: if selector.startswith("//"): button = driver.find_element(By.XPATH, selector) else: button = driver.find_element(By.CSS_SELECTOR, selector) if button and button.is_displayed(): return button except: continue return None