Skip to content

Commit 769d753

Browse files
authored
feat: Logging out (#1244)
- Write auth plugin components as wrappers - Allows plugins to always be mounted and control the transition to loading the app - Needed to ensure keycloak plugin can wipe token on logout - Refactor auth plugins to just use `AuthPluginBase` - There was a lot of the same code, just refactored it into one common base - Also added a `useContextOrThrow` hook - Added login screen for `AuthPluginPsk` - Took the `LoginAnimation` from Enterprise, renamed to `RandomAreaPlotAnimation` - Added a `Login` and `LoginForm` components to allow others to reuse if desired - Also stores the pre-shared key as a cookie after successful login - Add broadcast channel for notifying other tabs about login/logout - Notifies after successful login, or when logout button is pressed - Added permission for `canLogout` (since you can't logout when anonymous - Add context providers for overriding user info and permissions (used by auth plugins)
1 parent 0e286bd commit 769d753

83 files changed

Lines changed: 2227 additions & 518 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 55 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app-utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
"@deephaven/auth-plugins": "file:../auth-plugins",
3232
"@deephaven/components": "file:../components",
3333
"@deephaven/jsapi-bootstrap": "file:../jsapi-bootstrap",
34+
"@deephaven/jsapi-components": "file:../jsapi-components",
3435
"@deephaven/jsapi-types": "file:../jsapi-types",
36+
"@deephaven/jsapi-utils": "file:../jsapi-utils",
3537
"@deephaven/log": "file:../log",
36-
"@deephaven/redux": "file:../redux",
38+
"@deephaven/react-hooks": "file:../react-hooks",
3739
"@paciolan/remote-component": "2.13.0",
3840
"@paciolan/remote-module-loader": "^3.0.2",
3941
"fira": "mozilla/fira#4.202"

packages/app-utils/src/components/AppBootstrap.test.tsx

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { AUTH_HANDLER_TYPE_ANONYMOUS } from '@deephaven/auth-plugins';
33
import { ApiContext } from '@deephaven/jsapi-bootstrap';
4+
import { BROADCAST_LOGIN_MESSAGE } from '@deephaven/jsapi-utils';
45
import {
56
CoreClient,
67
IdeConnection,
@@ -19,13 +20,42 @@ jest.mock('../plugins', () => ({
1920
loadModulePlugins: jest.fn(() => mockPluginsPromise),
2021
}));
2122

23+
const mockChannel = {
24+
postMessage: jest.fn(),
25+
};
26+
jest.mock('@deephaven/jsapi-components', () => ({
27+
...jest.requireActual('@deephaven/jsapi-components'),
28+
RefreshTokenBootstrap: jest.fn(({ children }) => children),
29+
useBroadcastChannel: jest.fn(() => mockChannel),
30+
useBroadcastLoginListener: jest.fn(),
31+
}));
32+
2233
const mockChildText = 'Mock Child';
2334
const mockChild = <div>{mockChildText}</div>;
2435

2536
function expectMockChild() {
2637
return expect(screen.queryByText(mockChildText));
2738
}
2839

40+
function renderComponent(client: CoreClient) {
41+
const api = TestUtils.createMockProxy<DhType>({
42+
CoreClient: (jest
43+
.fn()
44+
.mockImplementation(() => client) as unknown) as CoreClient,
45+
});
46+
return render(
47+
<ApiContext.Provider value={api}>
48+
<AppBootstrap apiUrl={API_URL} pluginsUrl={PLUGINS_URL}>
49+
{mockChild}
50+
</AppBootstrap>
51+
</ApiContext.Provider>
52+
);
53+
}
54+
55+
beforeEach(() => {
56+
jest.clearAllMocks();
57+
});
58+
2959
it('should throw if api has not been bootstrapped', () => {
3060
expect(() =>
3161
render(
@@ -49,19 +79,7 @@ it('should display an error if no login plugin matches the provided auth handler
4979
getAuthConfigValues: mockGetAuthConfigValues,
5080
login: mockLogin,
5181
});
52-
const api = TestUtils.createMockProxy<DhType>({
53-
CoreClient: (jest
54-
.fn()
55-
.mockImplementation(() => client) as unknown) as CoreClient,
56-
});
57-
58-
render(
59-
<ApiContext.Provider value={api}>
60-
<AppBootstrap apiUrl={API_URL} pluginsUrl={PLUGINS_URL}>
61-
{mockChild}
62-
</AppBootstrap>
63-
</ApiContext.Provider>
64-
);
82+
renderComponent(client);
6583
expectMockChild().toBeNull();
6684
expect(mockGetAuthConfigValues).toHaveBeenCalled();
6785

@@ -105,19 +123,8 @@ it('should log in automatically when the anonymous handler is supported', async
105123
login: mockLogin,
106124
getAsIdeConnection: mockGetAsConnection,
107125
});
108-
const api = TestUtils.createMockProxy<DhType>({
109-
CoreClient: (jest
110-
.fn()
111-
.mockImplementation(() => client) as unknown) as CoreClient,
112-
});
113126

114-
render(
115-
<ApiContext.Provider value={api}>
116-
<AppBootstrap apiUrl={API_URL} pluginsUrl={PLUGINS_URL}>
117-
<div>{mockChild}</div>
118-
</AppBootstrap>
119-
</ApiContext.Provider>
120-
);
127+
renderComponent(client);
121128

122129
expectMockChild().toBeNull();
123130
expect(mockLogin).not.toHaveBeenCalled();
@@ -127,25 +134,31 @@ it('should log in automatically when the anonymous handler is supported', async
127134
await mockPluginsPromise;
128135
});
129136

137+
expect(mockChannel.postMessage).not.toHaveBeenCalled();
130138
expectMockChild().toBeNull();
131139
expect(mockLogin).toHaveBeenCalled();
132-
expect(screen.queryByTestId('auth-anonymous-loading')).not.toBeNull();
140+
expect(screen.queryByTestId('auth-base-loading')).not.toBeNull();
133141

134142
// Wait for login to complete
135143
await act(async () => {
136144
mockLoginResolve();
137145
});
138146

139-
expect(screen.queryByTestId('auth-anonymous-loading')).toBeNull();
147+
expect(screen.queryByTestId('auth-base-loading')).toBeNull();
140148
expect(screen.queryByTestId('connection-bootstrap-loading')).not.toBeNull();
141149
expect(screen.queryByText(mockChildText)).toBeNull();
150+
expect(mockChannel.postMessage).toHaveBeenCalledWith(
151+
expect.objectContaining({
152+
message: BROADCAST_LOGIN_MESSAGE,
153+
})
154+
);
142155

143156
// Wait for IdeConnection to resolve
144157
await act(async () => {
145158
mockConnectionResolve(mockConnection);
146159
});
147160

148-
expect(screen.queryByTestId('auth-anonymous-loading')).toBeNull();
161+
expect(screen.queryByTestId('auth-base-loading')).toBeNull();
149162
expect(screen.queryByTestId('connection-bootstrap-loading')).toBeNull();
150163
expectMockChild().not.toBeNull();
151164
});

packages/app-utils/src/components/AppBootstrap.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import React from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import '@deephaven/components/scss/BaseStyleSheet.scss';
3+
import { ClientBootstrap } from '@deephaven/jsapi-bootstrap';
4+
import {
5+
RefreshTokenBootstrap,
6+
useBroadcastLoginListener,
7+
} from '@deephaven/jsapi-components';
38
import FontBootstrap from './FontBootstrap';
4-
import ClientBootstrap from './ClientBootstrap';
59
import PluginsBootstrap from './PluginsBootstrap';
610
import AuthBootstrap from './AuthBootstrap';
711
import ConnectionBootstrap from './ConnectionBootstrap';
@@ -35,18 +39,32 @@ export function AppBootstrap({
3539
children,
3640
}: AppBootstrapProps) {
3741
const serverUrl = getBaseUrl(apiUrl).origin;
38-
const clientOptions = getConnectOptions();
42+
const clientOptions = useMemo(() => getConnectOptions(), []);
43+
44+
// On logout, we reset the client and have user login again
45+
const [logoutCount, setLogoutCount] = useState(0);
46+
const onLogin = useCallback(() => undefined, []);
47+
const onLogout = useCallback(() => {
48+
setLogoutCount(value => value + 1);
49+
}, []);
50+
useBroadcastLoginListener(onLogin, onLogout);
3951
return (
4052
<FontBootstrap fontClassNames={fontClassNames}>
41-
<ClientBootstrap serverUrl={serverUrl} options={clientOptions}>
42-
<PluginsBootstrap pluginsUrl={pluginsUrl}>
43-
<AuthBootstrap>
44-
<ConnectionBootstrap>
45-
<FontsLoaded>{children}</FontsLoaded>
46-
</ConnectionBootstrap>
47-
</AuthBootstrap>
48-
</PluginsBootstrap>
49-
</ClientBootstrap>
53+
<PluginsBootstrap pluginsUrl={pluginsUrl}>
54+
<ClientBootstrap
55+
serverUrl={serverUrl}
56+
options={clientOptions}
57+
key={logoutCount}
58+
>
59+
<RefreshTokenBootstrap>
60+
<AuthBootstrap>
61+
<ConnectionBootstrap>
62+
<FontsLoaded>{children}</FontsLoaded>
63+
</ConnectionBootstrap>
64+
</AuthBootstrap>
65+
</RefreshTokenBootstrap>
66+
</ClientBootstrap>
67+
</PluginsBootstrap>
5068
</FontBootstrap>
5169
);
5270
}

0 commit comments

Comments
 (0)