From 2ae87fca389a6e30d51502f503b0788e5a6e05b3 Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:54:34 +0000 Subject: [PATCH] Refactor login functionality: Remove Remember Me feature and update tests for deep link support --- front/index.php | 91 +++++------- test/ui/test_ui_login.py | 308 +++++++++------------------------------ 2 files changed, 104 insertions(+), 295 deletions(-) diff --git a/front/index.php b/front/index.php index ca5a8188..a3e5c071 100755 --- a/front/index.php +++ b/front/index.php @@ -13,7 +13,6 @@ require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php'; session_start(); -const COOKIE_NAME = 'NetAlertX_SaveLogin'; const DEFAULT_REDIRECT = '/devices.php'; /* ===================================================== @@ -42,9 +41,39 @@ function validate_local_path(?string $encoded): string { return $decoded; } +function extract_hash_from_path(string $path): array { + /* + Split a path into path and hash components. + + For deep links encoded in the 'next' parameter like /devices.php#device-123, + extract the hash fragment so it can be properly included in the redirect. + + Args: + path: Full path potentially with hash (e.g., "/devices.php#device-123") + + Returns: + Array with keys 'path' (without hash) and 'hash' (with # prefix, or empty string) + */ + $parts = explode('#', $path, 2); + return [ + 'path' => $parts[0], + 'hash' => !empty($parts[1]) ? '#' . $parts[1] : '' + ]; +} + function append_hash(string $url): string { + // First check if the URL already has a hash from the deep link + $parts = extract_hash_from_path($url); + if (!empty($parts['hash'])) { + return $parts['path'] . $parts['hash']; + } + + // Fall back to POST url_hash (for browser-captured hashes) if (!empty($_POST['url_hash'])) { - return $url . preg_replace('/[^#a-zA-Z0-9_\-]/', '', $_POST['url_hash']); + $sanitized = preg_replace('/[^#a-zA-Z0-9_\-]/', '', $_POST['url_hash']); + if (str_starts_with($sanitized, '#')) { + return $url . $sanitized; + } } return $url; } @@ -134,14 +163,6 @@ function call_api(string $endpoint, array $data = []): ?array { function logout_user(): void { $_SESSION = []; session_destroy(); - - setcookie(COOKIE_NAME,'',[ - 'expires'=>time()-3600, - 'path'=>'/', - 'secure'=>is_https_request(), - 'httponly'=>true, - 'samesite'=>'Strict' - ]); } /* ===================================================== @@ -173,28 +194,7 @@ if (!empty($_POST['loginpassword'])) { login_user(); - // Handle "Remember Me" if checked - if (!empty($_POST['PWRemember'])) { - // Generate random token (64-byte hex = 128 chars, use 64 chars) - $token = bin2hex(random_bytes(32)); - - // Call API to save token hash to Parameters table - $save_response = call_api('/auth/remember-me/save', [ - 'token' => $token - ]); - - // If API call successful, set persistent cookie - if ($save_response && isset($save_response['success']) && $save_response['success']) { - setcookie(COOKIE_NAME, $token, [ - 'expires' => time() + 604800, - 'path' => '/', - 'secure' => is_https_request(), - 'httponly' => true, - 'samesite' => 'Strict' - ]); - } - } - + // Redirect to target page, preserving deep link hash if present safe_redirect(append_hash($redirectTo)); } } @@ -203,20 +203,6 @@ if (!empty($_POST['loginpassword'])) { Remember Me Validation ===================================================== */ -if (!is_authenticated() && !empty($_COOKIE[COOKIE_NAME])) { - - // Call API to validate token against stored hash - $validate_response = call_api('/auth/validate-remember', [ - 'token' => $_COOKIE[COOKIE_NAME] - ]); - - // If API returns valid token, authenticate and redirect - if ($validate_response && isset($validate_response['valid']) && $validate_response['valid'] === true) { - login_user(); - safe_redirect(append_hash($redirectTo)); - } -} - /* ===================================================== Already Logged In ===================================================== */ @@ -289,18 +275,7 @@ if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923
-
-
- -
-
- -
+
diff --git a/test/ui/test_ui_login.py b/test/ui/test_ui_login.py index ee944846..1de1ba5c 100644 --- a/test/ui/test_ui_login.py +++ b/test/ui/test_ui_login.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Login Page UI Tests -Tests login functionality, Remember Me, and deep link support +Tests login functionality and deep link support after login """ import sys @@ -20,7 +20,7 @@ from .test_helpers import BASE_URL, wait_for_page_load, wait_for_element_by_css def get_login_password(): """Get login password from config file or environment - + Returns the plaintext password that should be used for login. For test/dev environments, tries common test passwords and defaults. Returns None if password cannot be determined (will skip test). @@ -28,20 +28,20 @@ def get_login_password(): # Try environment variable first (for testing) if os.getenv("LOGIN_PASSWORD"): return os.getenv("LOGIN_PASSWORD") - + # SHA256 hash of "password" - the default test password (from index.php) DEFAULT_PASSWORD_HASH = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92' - + # List of passwords to try in order passwords_to_try = ["123456", "password", "test", "admin"] - + # Try common config file locations config_paths = [ "/data/config/app.conf", "/app/back/app.conf", os.path.expanduser("~/.netalertx/app.conf") ] - + for config_path in config_paths: try: if os.path.exists(config_path): @@ -55,7 +55,7 @@ def get_login_password(): # Remove quotes value = value.strip('"').strip("'") print(f"✓ Found password config: {value[:32]}...") - + # If it's the default, use the default password if value == DEFAULT_PASSWORD_HASH: print(f" Using default password: '123456'") @@ -69,10 +69,10 @@ def get_login_password(): except (FileNotFoundError, IOError, PermissionError) as e: print(f"⚠ Error reading {config_path}: {e}") continue - + # If we couldn't determine the password from config, try default password print(f"ℹ Password not determinable from config, trying default passwords...") - + # For now, return first test password to try # Tests will skip if login fails return None @@ -80,20 +80,20 @@ def get_login_password(): def perform_login(driver, password=None): """Helper function to perform login with optional password fallback - + Args: driver: Selenium WebDriver password: Password to try. If None, will try default test password """ if password is None: password = "123456" # Default test password - + password_input = driver.find_element(By.NAME, "loginpassword") password_input.send_keys(password) - + submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']") submit_button.click() - + # Wait for page to respond to form submission # This might either redirect or show login error time.sleep(1) @@ -104,11 +104,11 @@ def test_login_page_loads(driver): """Test: Login page loads successfully""" driver.get(f"{BASE_URL}/index.php") wait_for_page_load(driver) - + # Check that login form is present password_field = driver.find_element(By.NAME, "loginpassword") assert password_field, "Password field should be present" - + submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']") assert submit_button, "Submit button should be present" @@ -118,295 +118,129 @@ def test_login_redirects_to_devices(driver): import pytest password = get_login_password() # Use password if found, otherwise helper will use default "password" - + driver.get(f"{BASE_URL}/index.php") wait_for_page_load(driver) - + perform_login(driver, password) - + # Wait for redirect to complete (server-side redirect is usually instant) time.sleep(1) - + # Should be redirected to devices page if '/devices.php' not in driver.current_url: pytest.skip(f"Login failed or not configured. URL: {driver.current_url}") - + assert '/devices.php' in driver.current_url, \ f"Expected redirect to devices.php, got {driver.current_url}" def test_login_with_deep_link_preserves_hash(driver): - """Test: Login with deep link (?next=...) preserves the URL fragment hash""" + """Test: Login with deep link (?next=...) preserves the URL fragment hash + + When a user logs in from a deep link URL (e.g., ?next=base64(devices.php%23device-123)), + they should be redirected to the target page with the hash fragment intact. + """ import base64 import pytest - + password = get_login_password() - + # Create a deep link to devices.php#device-123 deep_link_path = "/devices.php#device-123" encoded_path = base64.b64encode(deep_link_path.encode()).decode() - + # Navigate to login with deep link driver.get(f"{BASE_URL}/index.php?next={encoded_path}") wait_for_page_load(driver) - + perform_login(driver, password) - - # Wait for JavaScript redirect to complete (up to 5 seconds) - for i in range(50): - current_url = driver.current_url - if '/devices.php' in current_url or '/index.php' not in current_url: - break - time.sleep(0.1) - + + # Wait for redirect to complete (server-side redirect + potential JS handling) + time.sleep(2) + # Check that we're on the right page with the hash preserved current_url = driver.current_url - if '/devices.php' not in current_url: - pytest.skip(f"Login failed or not configured. URL: {current_url}") + print(f"URL after login with deep link: {current_url}") + if '/devices.php' not in current_url: + pytest.skip(f"Login failed or redirect not configured. URL: {current_url}") + + # Verify the hash fragment is preserved assert '#device-123' in current_url, f"Expected #device-123 hash in URL, got {current_url}" -def test_login_with_deep_link_to_device_tree(driver): - """Test: Login with deep link to network tree page""" +def test_login_with_deep_link_to_network_page(driver): + """Test: Login with deep link to network.php page preserves hash + + User can login with a deep link to the network page (e.g., network.php#settings-panel), + and should be redirected to that page with the hash fragment intact. + """ import base64 import pytest - + password = get_login_password() - + # Create a deep link to network.php#settings-panel deep_link_path = "/network.php#settings-panel" encoded_path = base64.b64encode(deep_link_path.encode()).decode() - + # Navigate to login with deep link driver.get(f"{BASE_URL}/index.php?next={encoded_path}") wait_for_page_load(driver) - + perform_login(driver, password) - - # Wait for JavaScript redirect to complete (up to 5 seconds) - for i in range(50): - current_url = driver.current_url - if '/network.php' in current_url or '/index.php' not in current_url: - break - time.sleep(0.1) - + + # Wait for redirect to complete + time.sleep(2) + # Check that we're on the right page with the hash preserved current_url = driver.current_url - if '/network.php' not in current_url: - pytest.skip(f"Login failed or not configured. URL: {current_url}") + print(f"URL after login with network.php deep link: {current_url}") + if '/network.php' not in current_url: + pytest.skip(f"Login failed or redirect not configured. URL: {current_url}") + + # Verify the hash fragment is preserved assert '#settings-panel' in current_url, f"Expected #settings-panel hash in URL, got {current_url}" -def test_remember_me_checkbox_present(driver): - """Test: Remember Me checkbox is present on login form""" - driver.get(f"{BASE_URL}/index.php") - wait_for_page_load(driver) - - remember_me_checkbox = driver.find_element(By.NAME, "PWRemember") - assert remember_me_checkbox, "Remember Me checkbox should be present" -def test_remember_me_login_creates_cookie(driver): - """Test: Login with Remember Me checkbox creates persistent cookie - - Remember Me now uses a simple cookie-based approach (no API calls). - When logged in with Remember Me checked, a NetAlertX_SaveLogin cookie - is set with a 7-day expiration. On next page load, the cookie - automatically authenticates the user without requiring password re-entry. - """ - import pytest - password = get_login_password() - - driver.get(f"{BASE_URL}/index.php") - wait_for_page_load(driver) - - # Use JavaScript to check the checkbox reliably - checkbox = driver.find_element(By.NAME, "PWRemember") - driver.execute_script("arguments[0].checked = true;", checkbox) - driver.execute_script("arguments[0].click();", checkbox) # Trigger any change handlers - - # Verify checkbox is actually checked after clicking - time.sleep(0.5) - is_checked = checkbox.is_selected() - print(f"✓ Checkbox checked via JavaScript: {is_checked}") - - if not is_checked: - pytest.skip("Could not check Remember Me checkbox") - - perform_login(driver, password) - - # Wait for redirect - time.sleep(2) - - # Main assertion: login should work with Remember Me checked - assert '/devices.php' in driver.current_url or '/network.php' in driver.current_url, \ - f"Login with Remember Me should redirect to app, got {driver.current_url}" - - # Secondary check: verify Remember Me cookie (NetAlertX_SaveLogin) was set - cookies = driver.get_cookies() - cookie_names = [cookie['name'] for cookie in cookies] - - print(f"Cookies found: {cookie_names}") - - # Check for the Remember Me cookie - remember_me_cookie = None - for cookie in cookies: - if cookie['name'] == 'NetAlertX_SaveLogin': - remember_me_cookie = cookie - break - - if remember_me_cookie: - print(f"✓ Remember Me cookie successfully set: {remember_me_cookie['name']}") - print(f" Value (truncated): {remember_me_cookie['value'][:32]}...") - print(f" Expires: {remember_me_cookie.get('expiry', 'Not set')}") - print(f" HttpOnly: {remember_me_cookie.get('httpOnly', False)}") - print(f" Secure: {remember_me_cookie.get('secure', False)}") - print(f" SameSite: {remember_me_cookie.get('sameSite', 'Not set')}") - else: - print("ℹ Remember Me cookie (NetAlertX_SaveLogin) not set in test environment") - print(" This is expected if Remember Me checkbox was not properly checked") -def test_remember_me_with_deep_link_preserves_hash(driver): - """Test: Remember Me persistent login preserves URL fragments via cookies - - Remember Me now uses cookies only (no API validation required): - 1. Login with Remember Me checkbox → NetAlertX_SaveLogin cookie set - 2. Browser stores cookie persistently (7 days) - 3. On next page load, cookie presence auto-authenticates user - 4. Deep link with hash fragment preserved through redirect - - This simulates browser restart by clearing the session cookie (keeping Remember Me cookie). - """ - import base64 - import pytest - - password = get_login_password() - - # First, set up a Remember Me session - driver.get(f"{BASE_URL}/index.php") - wait_for_page_load(driver) - - # Use JavaScript to check the checkbox reliably - checkbox = driver.find_element(By.NAME, "PWRemember") - driver.execute_script("arguments[0].checked = true;", checkbox) - driver.execute_script("arguments[0].click();", checkbox) # Trigger any change handlers - - # Verify checkbox is actually checked - time.sleep(0.5) - is_checked = checkbox.is_selected() - print(f"Checkbox checked for Remember Me test: {is_checked}") - - if not is_checked: - pytest.skip("Could not check Remember Me checkbox") - - perform_login(driver, password) - - # Wait and check if login succeeded - time.sleep(2) - if '/index.php' in driver.current_url and '/devices.php' not in driver.current_url: - pytest.skip(f"Initial login failed. Cannot test Remember Me.") - - # Verify Remember Me cookie was set - cookies = driver.get_cookies() - remember_me_found = False - for cookie in cookies: - if cookie['name'] == 'NetAlertX_SaveLogin': - remember_me_found = True - print(f"✓ Remember Me cookie found: {cookie['name']}") - break - - if not remember_me_found: - pytest.skip("Remember Me cookie was not set during login") - - # Simulate browser restart by clearing session cookies (but keep Remember Me cookie) - # Get all cookies, filter out session-related ones, keep Remember Me cookie - remember_me_cookie = None - for cookie in cookies: - if cookie['name'] == 'NetAlertX_SaveLogin': - remember_me_cookie = cookie - break - - # Clear all cookies - driver.delete_all_cookies() - - # Restore Remember Me cookie to simulate browser restart - if remember_me_cookie: - try: - driver.add_cookie({ - 'name': remember_me_cookie['name'], - 'value': remember_me_cookie['value'], - 'path': remember_me_cookie.get('path', '/'), - 'secure': remember_me_cookie.get('secure', False), - 'domain': remember_me_cookie.get('domain', None), - 'httpOnly': remember_me_cookie.get('httpOnly', False), - 'sameSite': remember_me_cookie.get('sameSite', 'Strict') - }) - except Exception as e: - pytest.skip(f"Could not restore Remember Me cookie: {e}") - - # Now test deep link with Remember Me cookie (simulated browser restart) - deep_link_path = "/devices.php#device-456" - encoded_path = base64.b64encode(deep_link_path.encode()).decode() - - driver.get(f"{BASE_URL}/index.php?next={encoded_path}") - wait_for_page_load(driver) - - # Wait a moment for Remember Me cookie validation and redirect - time.sleep(2) - - # Check current URL - should be on devices with hash - current_url = driver.current_url - print(f"Current URL after Remember Me auto-login: {current_url}") - - # Verify we're logged in and on the right page - assert '/index.php' not in current_url or '/devices.php' in current_url or '/network.php' in current_url, \ - f"Expected app page after Remember Me auto-login, got {current_url}" - def test_login_without_next_parameter(driver): """Test: Login without ?next parameter defaults to devices.php""" import pytest password = get_login_password() - + driver.get(f"{BASE_URL}/index.php") wait_for_page_load(driver) - + perform_login(driver, password) - + # Wait for redirect to complete time.sleep(1) - + # Should redirect to default devices page current_url = driver.current_url if '/devices.php' not in current_url: pytest.skip(f"Login failed or not configured. URL: {current_url}") - + assert '/devices.php' in current_url, f"Expected default redirect to devices.php, got {current_url}" -def test_url_hash_hidden_input_populated(driver): - """Test: URL fragment hash is populated in hidden url_hash input field""" - import base64 +def test_url_hash_hidden_input_present(driver): + """Test: URL fragment hash field is present in login form - # Create a deep link - deep_link_path = "/devices.php#device-789" - encoded_path = base64.b64encode(deep_link_path.encode()).decode() - - # Navigate to login with deep link - driver.get(f"{BASE_URL}/index.php?next={encoded_path}") + The hidden url_hash input field is used to capture and preserve + URL hash fragments during form submission and redirect. + """ + driver.get(f"{BASE_URL}/index.php") wait_for_page_load(driver) - - # Wait a bit for JavaScript to execute and populate the hash - time.sleep(1) - - # Get the hidden input value - note: this tests JavaScript functionality + + # Verify the hidden input field exists url_hash_input = driver.find_element(By.ID, "url_hash") - url_hash_value = url_hash_input.get_attribute("value") - - # The JavaScript should have populated this with window.location.hash - # However, since we're navigating to index.php, the hash won't be present at page load - # So this test verifies the mechanism exists and would work assert url_hash_input, "Hidden url_hash input field should be present" + assert url_hash_input.get_attribute("type") == "hidden", "url_hash should be a hidden input field"