Merge pull request 'fix: reset the header and return home when a customer logs out (#21)' (#28) from fix/logout-customer-reset into main
Reviewed-on: #28
This commit was merged in pull request #28.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Typography, Switch, Button, Table, Modal, message, Space, Divider } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchMyOrders, OrderHistoryItem, updateConsent, exportMyData, deleteMyAccount, logoutCustomer } from './customerApi';
|
||||
import { fetchMyOrders, OrderHistoryItem, updateConsent, exportMyData, deleteMyAccount } from './customerApi';
|
||||
import { useCustomerAuth } from './CustomerAuthContext';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Account() {
|
||||
const { customer, loading, refresh } = useCustomerAuth();
|
||||
const { customer, loading, refresh, logout } = useCustomerAuth();
|
||||
const [orders, setOrders] = useState<OrderHistoryItem[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -28,8 +28,15 @@ export default function Account() {
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await logoutCustomer();
|
||||
navigate('/');
|
||||
try {
|
||||
await logout();
|
||||
} catch (err) {
|
||||
message.error(`Couldn't log out — ${(err as Error).message}`);
|
||||
return;
|
||||
}
|
||||
// replace, so Back doesn't return to the account page — which would only
|
||||
// bounce to /login now that the session is gone.
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { Customer, fetchMe } from './customerApi';
|
||||
import { Customer, fetchMe, logoutCustomer } from './customerApi';
|
||||
|
||||
interface CustomerAuthValue {
|
||||
customer: Customer | null;
|
||||
loading: boolean;
|
||||
refresh: () => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const CustomerAuthContext = createContext<CustomerAuthValue>({ customer: null, loading: true, refresh: () => {} });
|
||||
const CustomerAuthContext = createContext<CustomerAuthValue>({
|
||||
customer: null,
|
||||
loading: true,
|
||||
refresh: () => {},
|
||||
logout: async () => {}
|
||||
});
|
||||
|
||||
export function useCustomerAuth() {
|
||||
return useContext(CustomerAuthContext);
|
||||
@@ -24,8 +30,22 @@ export function CustomerAuthProvider({ children }: { children: React.ReactNode }
|
||||
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
|
||||
// Logging out has to clear the context, not just call the endpoint —
|
||||
// otherwise `customer` stays set and the header keeps offering "My Account"
|
||||
// until something happens to refetch. Clearing here rather than calling
|
||||
// refresh() avoids a window where the session is gone but the UI still shows
|
||||
// the customer as signed in.
|
||||
//
|
||||
// Rejects if the server did not accept the logout, leaving the customer
|
||||
// signed in, because the session cookie is still valid in that case.
|
||||
const logout = useCallback(async () => {
|
||||
await logoutCustomer();
|
||||
setCustomer(null);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CustomerAuthContext.Provider value={{ customer, loading, refresh }}>
|
||||
<CustomerAuthContext.Provider value={{ customer, loading, refresh, logout }}>
|
||||
{children}
|
||||
</CustomerAuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,16 @@ export function verifyEmail(token: string): Promise<{ status: string }> {
|
||||
}
|
||||
|
||||
export function logoutCustomer(): Promise<void> {
|
||||
return fetch('/api/customers/logout', { method: 'POST' }).then(() => undefined);
|
||||
// Not routed through handle(): logout answers 204 with no body, so parsing
|
||||
// JSON would throw on success. The failure case still has to reject — the
|
||||
// session cookie survives a failed logout, so reporting success would leave
|
||||
// the customer logged in and silently signed back in on their next reload.
|
||||
return fetch('/api/customers/logout', { method: 'POST' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Request failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchMe(): Promise<Customer | null> {
|
||||
|
||||
@@ -35,4 +35,73 @@ test.describe('Customer accounts', () => {
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await expect(page.getByText('invalid email or password')).toBeVisible();
|
||||
});
|
||||
|
||||
test('logging out returns to the home page and resets the header', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await page.goto('/register');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill('supersecret123');
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
await expect(page).toHaveURL(/\/account/);
|
||||
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign up' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeHidden();
|
||||
});
|
||||
|
||||
test('the logged-out header survives a reload', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await page.goto('/register');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill('supersecret123');
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
await expect(page).toHaveURL(/\/account/);
|
||||
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible();
|
||||
|
||||
// Proves the server session was actually destroyed, rather than the header
|
||||
// merely being repainted from stale client state.
|
||||
await page.reload();
|
||||
await expect(page.getByRole('button', { name: 'Log in' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeHidden();
|
||||
});
|
||||
|
||||
test('logging out does not leave the account page on the back stack', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await page.goto('/register');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill('supersecret123');
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
await expect(page).toHaveURL(/\/account/);
|
||||
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
|
||||
await page.goBack();
|
||||
await expect(page).not.toHaveURL(/\/account/);
|
||||
});
|
||||
|
||||
test('a failed logout says so instead of appearing to succeed', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await page.goto('/register');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill('supersecret123');
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
await expect(page).toHaveURL(/\/account/);
|
||||
|
||||
await page.route('**/api/customers/logout', (route) =>
|
||||
route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"internal error"}' })
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
|
||||
// The session cookie is still valid, so pretending to be logged out would
|
||||
// silently log the customer back in on their next reload.
|
||||
await expect(page.getByText(/couldn't log out/i)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/account/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user