fix(ui): try/finally in handleOidcLogin; logout redirects to /login (not ?local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ vi.mock('./auth-store', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { api as apiClient } from '../api/client';
|
import { api as apiClient } from '../api/client';
|
||||||
|
import { useAuthStore } from './auth-store';
|
||||||
import { LoginPage } from './LoginPage';
|
import { LoginPage } from './LoginPage';
|
||||||
|
|
||||||
function wrapper(initialEntries: string[]) {
|
function wrapper(initialEntries: string[]) {
|
||||||
@@ -127,4 +128,29 @@ describe('LoginPage', () => {
|
|||||||
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
|
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('SSO button click: when /auth/oidc/config fails, button unlocks and error is set', async () => {
|
||||||
|
const setStateMock = vi.fn();
|
||||||
|
const useAuthStoreMock = vi.mocked(useAuthStore) as unknown as { setState: typeof setStateMock };
|
||||||
|
useAuthStoreMock.setState = setStateMock;
|
||||||
|
|
||||||
|
(apiClient.GET as any).mockImplementation((path: string) => {
|
||||||
|
if (path === '/auth/capabilities') return Promise.resolve({
|
||||||
|
data: { oidc: { enabled: true, providerName: 'Logto', primary: true }, localAccounts: { enabled: true, adminRecoveryOnly: true } },
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
if (path === '/auth/oidc/config') return Promise.reject(new Error('network down'));
|
||||||
|
return Promise.resolve({ data: undefined, error: { message: 'unexpected' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<LoginPage />, { wrapper: wrapper(['/login']) });
|
||||||
|
const btn = await screen.findByRole('button', { name: /sign in with logto/i });
|
||||||
|
fireEvent.click(btn);
|
||||||
|
|
||||||
|
await waitFor(() => expect(setStateMock).toHaveBeenCalled());
|
||||||
|
const errorPayload = setStateMock.mock.calls[0][0];
|
||||||
|
expect(errorPayload.error).toMatch(/OIDC configuration unavailable/i);
|
||||||
|
// Button should not stay locked in "Redirecting…"
|
||||||
|
await waitFor(() => expect(btn).not.toHaveTextContent(/redirecting/i));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,27 +68,32 @@ export function LoginPage() {
|
|||||||
|
|
||||||
const handleOidcLogin = async () => {
|
const handleOidcLogin = async () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
const { data } = await api.GET('/auth/oidc/config');
|
try {
|
||||||
if (!data?.authorizationEndpoint || !data?.clientId) {
|
const { data } = await api.GET('/auth/oidc/config');
|
||||||
setOidcLoading(false);
|
if (!data?.authorizationEndpoint || !data?.clientId) {
|
||||||
|
useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.endSessionEndpoint) {
|
||||||
|
localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
|
||||||
|
}
|
||||||
|
const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`;
|
||||||
|
const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(data.additionalScopes || [])];
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: data.clientId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
scope: scopes.join(' '),
|
||||||
|
});
|
||||||
|
if (data.resource) params.set('resource', data.resource);
|
||||||
|
// Note: NO prompt=none. Per RFC 9700 §4.4, that's silent re-auth only;
|
||||||
|
// for first-time login it returns login_required and traps users on a local form.
|
||||||
|
window.location.href = `${data.authorizationEndpoint}?${params}`;
|
||||||
|
} catch {
|
||||||
useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
|
useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
|
||||||
return;
|
} finally {
|
||||||
|
setOidcLoading(false);
|
||||||
}
|
}
|
||||||
if (data.endSessionEndpoint) {
|
|
||||||
localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
|
|
||||||
}
|
|
||||||
const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`;
|
|
||||||
const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(data.additionalScopes || [])];
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
response_type: 'code',
|
|
||||||
client_id: data.clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
scope: scopes.join(' '),
|
|
||||||
});
|
|
||||||
if (data.resource) params.set('resource', data.resource);
|
|
||||||
// Note: NO prompt=none. Per RFC 9700 §4.4, that's silent re-auth only;
|
|
||||||
// for first-time login it returns login_required and traps users on a local form.
|
|
||||||
window.location.href = `${data.authorizationEndpoint}?${params}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -154,11 +154,11 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
const loginUrl = `${config.basePath}login?local`;
|
const loginUrl = `${config.basePath}login`;
|
||||||
if (endSessionEndpoint && idToken) {
|
if (endSessionEndpoint && idToken) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
id_token_hint: idToken,
|
id_token_hint: idToken,
|
||||||
post_logout_redirect_uri: `${window.location.origin}${config.basePath}login?local`,
|
post_logout_redirect_uri: `${window.location.origin}${config.basePath}login`,
|
||||||
});
|
});
|
||||||
fetch(`${endSessionEndpoint}?${params}`, { mode: 'no-cors' }).finally(() => {
|
fetch(`${endSessionEndpoint}?${params}`, { mode: 'no-cors' }).finally(() => {
|
||||||
window.location.href = loginUrl;
|
window.location.href = loginUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user