ge-tool/backend/services/nas_sharing_api/auth.py

374 lines
12 KiB
Python
Raw Normal View History

2025-12-10 06:41:43 +00:00
"""
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 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