ge-tool/backend/services/nas_sharing_api/auth.py
2025-12-10 13:41:43 +07:00

374 lines
12 KiB
Python
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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