diff --git a/docs/issues/frontend-auth-guard-reload.md b/docs/issues/frontend-auth-guard-reload.md new file mode 100644 index 00000000..ecc4fe49 --- /dev/null +++ b/docs/issues/frontend-auth-guard-reload.md @@ -0,0 +1,72 @@ +# [Frontend] Add Auth Guard on Page Reload + +## Summary +The frontend does not validate authentication state on page load/reload. When a user's session expires or authentication tokens are cleared, reloading the page should redirect to `/login`, but currently it does not. + +## Failing Test +- **File**: `tests/core/authentication.spec.ts` +- **Test**: `should redirect to login when session expires` +- **Line**: ~310 + +## Steps to Reproduce +1. Log in to the application +2. Open browser dev tools +3. Clear localStorage and cookies +4. Reload the page +5. **Expected**: Redirect to `/login` +6. **Actual**: Page remains on current route (e.g., `/dashboard`) + +## Root Cause +The frontend auth context/provider does not check for valid authentication tokens on initial page load. Auth validation only occurs when API calls return 401 errors. + +## Proposed Solution + +### Option 1: Auth Guard in Route Protection +Add an auth check in the route protection layer that runs on initial load: + +```typescript +// In AuthProvider or route guard +useEffect(() => { + const token = localStorage.getItem('auth_token'); + if (!token) { + navigate('/login'); + return; + } + + // Optionally validate token with backend + validateToken(token).catch(() => { + clearAuth(); + navigate('/login'); + }); +}, []); +``` + +### Option 2: API Client Interceptor +Add a 401 handler in the API client that redirects to login: + +```typescript +// In API client setup +client.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401) { + clearAuth(); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); +``` + +## Priority +**Medium** - Security improvement but not critical since API calls still require valid auth. + +## Labels +- frontend +- security +- auth +- enhancement + +## Related +- Fixes E2E test: `should redirect to login when session expires` +- Part of Phase 1 E2E testing backlog diff --git a/tests/core/authentication.spec.ts b/tests/core/authentication.spec.ts index 0390c791..3703808a 100644 --- a/tests/core/authentication.spec.ts +++ b/tests/core/authentication.spec.ts @@ -352,9 +352,18 @@ test.describe('Authentication Flows', () => { await test.step('Trigger an API call by navigating', async () => { await page.goto('/proxy-hosts'); + // Wait for the 401 response to be processed and UI to react + await page.waitForTimeout(2000); }); await test.step('Verify redirect to login or error message', async () => { + // Wait for potential redirect to login page + try { + await page.waitForURL(/\/login/, { timeout: 5000 }); + } catch { + // If no redirect, check for session expired message + } + // Should either redirect to login or show session expired message const isLoginPage = page.url().includes('/login'); const hasSessionExpiredMessage = await page diff --git a/tests/core/proxy-hosts.spec.ts b/tests/core/proxy-hosts.spec.ts index adbc30f3..50f64dfc 100644 --- a/tests/core/proxy-hosts.spec.ts +++ b/tests/core/proxy-hosts.spec.ts @@ -21,6 +21,19 @@ import { generateProxyHost, type ProxyHostConfig, } from '../fixtures/proxy-hosts'; +import type { Page } from '@playwright/test'; + +/** + * Helper to dismiss the "New Base Domain Detected" dialog if it appears. + * This dialog asks if the user wants to save the domain to their domain list. + */ +async function dismissDomainDialog(page: Page): Promise { + const noThanksBtn = page.getByRole('button', { name: /No, thanks/i }); + if (await noThanksBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await noThanksBtn.click(); + await page.waitForTimeout(300); + } +} test.describe('Proxy Hosts - CRUD Operations', () => { test.beforeEach(async ({ page, adminUser }) => { @@ -254,6 +267,9 @@ test.describe('Proxy Hosts - CRUD Operations', () => { const domainInput = page.locator('#domain-names'); await domainInput.fill(hostConfig.domain); + // Dismiss the "New Base Domain Detected" dialog if it appears after domain input + await dismissDomainDialog(page); + // Forward Host const forwardHostInput = page.locator('#forward-host'); await forwardHostInput.fill(hostConfig.forwardHost); @@ -265,9 +281,15 @@ test.describe('Proxy Hosts - CRUD Operations', () => { }); await test.step('Submit form', async () => { + // Dismiss the "New Base Domain Detected" dialog if it appears + await dismissDomainDialog(page); + const saveButton = getSaveButton(page); await saveButton.click(); + // Dismiss domain dialog again in case it appeared after Save click + await dismissDomainDialog(page); + // Handle "Unsaved changes" confirmation dialog if it appears const confirmDialog = page.getByRole('button', { name: /yes.*save/i }); if (await confirmDialog.isVisible({ timeout: 2000 }).catch(() => false)) { @@ -319,6 +341,9 @@ test.describe('Proxy Hosts - CRUD Operations', () => { }); await test.step('Submit and verify', async () => { + // Dismiss the "New Base Domain Detected" dialog if it appears + await dismissDomainDialog(page); + await getSaveButton(page).click(); // Handle "Unsaved changes" confirmation dialog if it appears @@ -358,6 +383,9 @@ test.describe('Proxy Hosts - CRUD Operations', () => { }); await test.step('Submit and verify', async () => { + // Dismiss the "New Base Domain Detected" dialog if it appears + await dismissDomainDialog(page); + await getSaveButton(page).click(); // Handle "Unsaved changes" confirmation dialog if it appears diff --git a/tests/fixtures/auth-fixtures.ts b/tests/fixtures/auth-fixtures.ts index d640e798..4ef5bcb1 100644 --- a/tests/fixtures/auth-fixtures.ts +++ b/tests/fixtures/auth-fixtures.ts @@ -165,24 +165,16 @@ export async function loginUser( * @param page - Playwright Page instance */ export async function logoutUser(page: import('@playwright/test').Page): Promise { - // Try common logout patterns - const logoutButton = page.getByRole('button', { name: /logout|sign out/i }); - const logoutLink = page.getByRole('link', { name: /logout|sign out/i }); - const userMenu = page.getByRole('button', { name: /user|profile|account/i }); + // Use text-based selector that handles emoji prefix (🚪 Logout) + // The button text contains "Logout" - use case-insensitive text matching + const logoutButton = page.getByText(/logout/i).first(); - // If there's a user menu, click it first - if (await userMenu.isVisible()) { - await userMenu.click(); - } + // Wait for the logout button to be visible and click it + await logoutButton.waitFor({ state: 'visible', timeout: 10000 }); + await logoutButton.click(); - // Click logout - if (await logoutButton.isVisible()) { - await logoutButton.click(); - } else if (await logoutLink.isVisible()) { - await logoutLink.click(); - } - - await page.waitForURL('/login'); + // Wait for redirect to login page + await page.waitForURL(/\/login/, { timeout: 15000 }); } /**