Refactor login functionality: Remove Remember Me feature and update tests for deep link support

This commit is contained in:
Jokob @NetAlertX
2026-02-22 04:54:34 +00:00
parent 8224363c45
commit 2ae87fca38
2 changed files with 104 additions and 295 deletions

View File

@@ -13,7 +13,6 @@ require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php';
session_start(); session_start();
const COOKIE_NAME = 'NetAlertX_SaveLogin';
const DEFAULT_REDIRECT = '/devices.php'; const DEFAULT_REDIRECT = '/devices.php';
/* ===================================================== /* =====================================================
@@ -42,9 +41,39 @@ function validate_local_path(?string $encoded): string {
return $decoded; 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 { 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'])) { 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; return $url;
} }
@@ -134,14 +163,6 @@ function call_api(string $endpoint, array $data = []): ?array {
function logout_user(): void { function logout_user(): void {
$_SESSION = []; $_SESSION = [];
session_destroy(); 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(); login_user();
// Handle "Remember Me" if checked // Redirect to target page, preserving deep link hash if present
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'
]);
}
}
safe_redirect(append_hash($redirectTo)); safe_redirect(append_hash($redirectTo));
} }
} }
@@ -203,20 +203,6 @@ if (!empty($_POST['loginpassword'])) {
Remember Me Validation 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 Already Logged In
===================================================== */ ===================================================== */
@@ -289,18 +275,7 @@ if ($nax_Password === '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923
<span class="glyphicon glyphicon-lock form-control-feedback"></span> <span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-8"> <div class="col-xs-12">
<div class="checkbox icheck">
<label>
<input type="checkbox" name="PWRemember">
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
<?= lang('Login_Remember');?><br><span style="font-size: smaller"><?= lang('Login_Remember_small');?></span>
</div>
</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4" style="padding-top: 10px;">
<button type="submit" class="btn btn-primary btn-block btn-flat"><?= lang('Login_Submit');?></button> <button type="submit" class="btn btn-primary btn-block btn-flat"><?= lang('Login_Submit');?></button>
</div> </div>
<!-- /.col --> <!-- /.col -->

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Login Page UI Tests Login Page UI Tests
Tests login functionality, Remember Me, and deep link support Tests login functionality and deep link support after login
""" """
import sys import sys
@@ -136,7 +136,11 @@ def test_login_redirects_to_devices(driver):
def test_login_with_deep_link_preserves_hash(driver): 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 base64
import pytest import pytest
@@ -152,23 +156,26 @@ def test_login_with_deep_link_preserves_hash(driver):
perform_login(driver, password) perform_login(driver, password)
# Wait for JavaScript redirect to complete (up to 5 seconds) # Wait for redirect to complete (server-side redirect + potential JS handling)
for i in range(50): time.sleep(2)
current_url = driver.current_url
if '/devices.php' in current_url or '/index.php' not in current_url:
break
time.sleep(0.1)
# Check that we're on the right page with the hash preserved # Check that we're on the right page with the hash preserved
current_url = driver.current_url current_url = driver.current_url
if '/devices.php' not in current_url: print(f"URL after login with deep link: {current_url}")
pytest.skip(f"Login failed or not configured. URL: {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}" 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): def test_login_with_deep_link_to_network_page(driver):
"""Test: Login with deep link to network tree page""" """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 base64
import pytest import pytest
@@ -184,186 +191,23 @@ def test_login_with_deep_link_to_device_tree(driver):
perform_login(driver, password) perform_login(driver, password)
# Wait for JavaScript redirect to complete (up to 5 seconds) # Wait for redirect to complete
for i in range(50): time.sleep(2)
current_url = driver.current_url
if '/network.php' in current_url or '/index.php' not in current_url:
break
time.sleep(0.1)
# Check that we're on the right page with the hash preserved # Check that we're on the right page with the hash preserved
current_url = driver.current_url current_url = driver.current_url
if '/network.php' not in current_url: print(f"URL after login with network.php deep link: {current_url}")
pytest.skip(f"Login failed or not configured. URL: {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}" 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): def test_login_without_next_parameter(driver):
@@ -387,26 +231,16 @@ def test_login_without_next_parameter(driver):
assert '/devices.php' in current_url, f"Expected default redirect to devices.php, got {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): def test_url_hash_hidden_input_present(driver):
"""Test: URL fragment hash is populated in hidden url_hash input field""" """Test: URL fragment hash field is present in login form
import base64
# Create a deep link The hidden url_hash input field is used to capture and preserve
deep_link_path = "/devices.php#device-789" URL hash fragments during form submission and redirect.
encoded_path = base64.b64encode(deep_link_path.encode()).decode() """
driver.get(f"{BASE_URL}/index.php")
# Navigate to login with deep link
driver.get(f"{BASE_URL}/index.php?next={encoded_path}")
wait_for_page_load(driver) wait_for_page_load(driver)
# Wait a bit for JavaScript to execute and populate the hash # Verify the hidden input field exists
time.sleep(1)
# Get the hidden input value - note: this tests JavaScript functionality
url_hash_input = driver.find_element(By.ID, "url_hash") 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, "Hidden url_hash input field should be present"
assert url_hash_input.get_attribute("type") == "hidden", "url_hash should be a hidden input field"