mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
Refactor login functionality: Remove Remember Me feature and update tests for deep link support
This commit is contained in:
@@ -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 -->
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user