Device manager - logout current session (PSG-743) (#9275)
* trigger verification of other devices * add sign out of current device section in device details * fix classname * lint * strict type fix * fix test * improve mocked VerifReqpull/28788/head^2
parent
41960b164b
commit
f20d86b7b8
|
@ -34,6 +34,7 @@ limitations under the License.
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: $spacing-16;
|
grid-gap: $spacing-16;
|
||||||
|
justify-content: left;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
@ -46,7 +47,7 @@ limitations under the License.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mxDeviceDetails_metadataTable {
|
.mx_DeviceDetails_metadataTable {
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
|
|
||||||
|
|
|
@ -138,15 +138,25 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_link,
|
&.mx_AccessibleButton_kind_link,
|
||||||
&.mx_AccessibleButton_kind_link_inline {
|
&.mx_AccessibleButton_kind_link_inline,
|
||||||
color: $accent;
|
&.mx_AccessibleButton_kind_danger_inline {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_kind_link,
|
||||||
&.mx_AccessibleButton_kind_link_inline {
|
&.mx_AccessibleButton_kind_link_inline {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_kind_danger_inline {
|
||||||
|
color: $alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_AccessibleButton_kind_link_inline,
|
||||||
|
&.mx_AccessibleButton_kind_danger_inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ type AccessibleButtonKind = | 'primary'
|
||||||
| 'danger'
|
| 'danger'
|
||||||
| 'danger_outline'
|
| 'danger_outline'
|
||||||
| 'danger_sm'
|
| 'danger_sm'
|
||||||
|
| 'danger_inline'
|
||||||
| 'link'
|
| 'link'
|
||||||
| 'link_inline'
|
| 'link_inline'
|
||||||
| 'link_sm'
|
| 'link_sm'
|
||||||
|
|
|
@ -29,10 +29,14 @@ interface Props {
|
||||||
device?: DeviceWithVerification;
|
device?: DeviceWithVerification;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onVerifyCurrentDevice: () => void;
|
onVerifyCurrentDevice: () => void;
|
||||||
|
onSignOutCurrentDevice: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CurrentDeviceSection: React.FC<Props> = ({
|
const CurrentDeviceSection: React.FC<Props> = ({
|
||||||
device, isLoading, onVerifyCurrentDevice,
|
device,
|
||||||
|
isLoading,
|
||||||
|
onVerifyCurrentDevice,
|
||||||
|
onSignOutCurrentDevice,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
@ -51,7 +55,12 @@ const CurrentDeviceSection: React.FC<Props> = ({
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
/>
|
/>
|
||||||
</DeviceTile>
|
</DeviceTile>
|
||||||
{ isExpanded && <DeviceDetails device={device} /> }
|
{ isExpanded &&
|
||||||
|
<DeviceDetails
|
||||||
|
device={device}
|
||||||
|
onSignOutDevice={onSignOutCurrentDevice}
|
||||||
|
/>
|
||||||
|
}
|
||||||
<br />
|
<br />
|
||||||
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
|
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyCurrentDevice} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { formatDate } from '../../../../DateUtils';
|
import { formatDate } from '../../../../DateUtils';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
import Heading from '../../typography/Heading';
|
import Heading from '../../typography/Heading';
|
||||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||||
import { DeviceWithVerification } from './types';
|
import { DeviceWithVerification } from './types';
|
||||||
|
@ -25,6 +26,9 @@ import { DeviceWithVerification } from './types';
|
||||||
interface Props {
|
interface Props {
|
||||||
device: DeviceWithVerification;
|
device: DeviceWithVerification;
|
||||||
onVerifyDevice?: () => void;
|
onVerifyDevice?: () => void;
|
||||||
|
// @TODO(kerry) optional while signout only implemented
|
||||||
|
// for current device (PSG-744)
|
||||||
|
onSignOutDevice?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MetadataTable {
|
interface MetadataTable {
|
||||||
|
@ -35,6 +39,7 @@ interface MetadataTable {
|
||||||
const DeviceDetails: React.FC<Props> = ({
|
const DeviceDetails: React.FC<Props> = ({
|
||||||
device,
|
device,
|
||||||
onVerifyDevice,
|
onVerifyDevice,
|
||||||
|
onSignOutDevice,
|
||||||
}) => {
|
}) => {
|
||||||
const metadata: MetadataTable[] = [
|
const metadata: MetadataTable[] = [
|
||||||
{
|
{
|
||||||
|
@ -64,7 +69,7 @@ const DeviceDetails: React.FC<Props> = ({
|
||||||
<section className='mx_DeviceDetails_section'>
|
<section className='mx_DeviceDetails_section'>
|
||||||
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
|
<p className='mx_DeviceDetails_sectionHeading'>{ _t('Session details') }</p>
|
||||||
{ metadata.map(({ heading, values }, index) => <table
|
{ metadata.map(({ heading, values }, index) => <table
|
||||||
className='mxDeviceDetails_metadataTable'
|
className='mx_DeviceDetails_metadataTable'
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
{ heading &&
|
{ heading &&
|
||||||
|
@ -82,6 +87,15 @@ const DeviceDetails: React.FC<Props> = ({
|
||||||
</table>,
|
</table>,
|
||||||
) }
|
) }
|
||||||
</section>
|
</section>
|
||||||
|
{ !!onSignOutDevice && <section className='mx_DeviceDetails_section'>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onSignOutDevice}
|
||||||
|
kind='danger_inline'
|
||||||
|
data-testid='device-detail-sign-out-cta'
|
||||||
|
>
|
||||||
|
{ _t('Sign out of this session') }
|
||||||
|
</AccessibleButton>
|
||||||
|
</section> }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
|
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { MatrixError } from "matrix-js-sdk/src/matrix";
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||||
|
|
|
@ -27,6 +27,7 @@ import SettingsTab from '../SettingsTab';
|
||||||
import Modal from '../../../../../Modal';
|
import Modal from '../../../../../Modal';
|
||||||
import SetupEncryptionDialog from '../../../dialogs/security/SetupEncryptionDialog';
|
import SetupEncryptionDialog from '../../../dialogs/security/SetupEncryptionDialog';
|
||||||
import VerificationRequestDialog from '../../../dialogs/VerificationRequestDialog';
|
import VerificationRequestDialog from '../../../dialogs/VerificationRequestDialog';
|
||||||
|
import LogoutDialog from '../../../dialogs/LogoutDialog';
|
||||||
|
|
||||||
const SessionManagerTab: React.FC = () => {
|
const SessionManagerTab: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
|
@ -90,6 +91,15 @@ const SessionManagerTab: React.FC = () => {
|
||||||
});
|
});
|
||||||
}, [requestDeviceVerification, refreshDevices, currentUserMember]);
|
}, [requestDeviceVerification, refreshDevices, currentUserMember]);
|
||||||
|
|
||||||
|
const onSignOutCurrentDevice = () => {
|
||||||
|
if (!currentDevice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.createDialog(LogoutDialog,
|
||||||
|
/* props= */{}, /* className= */undefined,
|
||||||
|
/* isPriority= */false, /* isStatic= */true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
clearTimeout(scrollIntoViewTimeoutRef.current);
|
clearTimeout(scrollIntoViewTimeoutRef.current);
|
||||||
}, [scrollIntoViewTimeoutRef]);
|
}, [scrollIntoViewTimeoutRef]);
|
||||||
|
@ -104,6 +114,7 @@ const SessionManagerTab: React.FC = () => {
|
||||||
device={currentDevice}
|
device={currentDevice}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onVerifyCurrentDevice={onVerifyCurrentDevice}
|
onVerifyCurrentDevice={onVerifyCurrentDevice}
|
||||||
|
onSignOutCurrentDevice={onSignOutCurrentDevice}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
shouldShowOtherSessions &&
|
shouldShowOtherSessions &&
|
||||||
|
|
|
@ -1712,6 +1712,7 @@
|
||||||
"Device": "Device",
|
"Device": "Device",
|
||||||
"IP address": "IP address",
|
"IP address": "IP address",
|
||||||
"Session details": "Session details",
|
"Session details": "Session details",
|
||||||
|
"Sign out of this session": "Sign out of this session",
|
||||||
"Toggle device details": "Toggle device details",
|
"Toggle device details": "Toggle device details",
|
||||||
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
||||||
"Verified": "Verified",
|
"Verified": "Verified",
|
||||||
|
|
|
@ -35,6 +35,7 @@ describe('<CurrentDeviceSection />', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
device: alicesVerifiedDevice,
|
device: alicesVerifiedDevice,
|
||||||
onVerifyCurrentDevice: jest.fn(),
|
onVerifyCurrentDevice: jest.fn(),
|
||||||
|
onSignOutCurrentDevice: jest.fn(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
};
|
};
|
||||||
const getComponent = (props = {}): React.ReactElement =>
|
const getComponent = (props = {}): React.ReactElement =>
|
||||||
|
|
|
@ -50,7 +50,7 @@ HTMLCollection [
|
||||||
Session details
|
Session details
|
||||||
</p>
|
</p>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -78,7 +78,7 @@ HTMLCollection [
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -101,6 +101,18 @@ HTMLCollection [
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
<section
|
||||||
|
class="mx_DeviceDetails_section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||||
|
data-testid="device-detail-sign-out-cta"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Sign out of this session
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>,
|
</div>,
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -50,7 +50,7 @@ exports[`<DeviceDetails /> renders a verified device 1`] = `
|
||||||
Session details
|
Session details
|
||||||
</p>
|
</p>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -78,7 +78,7 @@ exports[`<DeviceDetails /> renders a verified device 1`] = `
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -155,7 +155,7 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
|
||||||
Session details
|
Session details
|
||||||
</p>
|
</p>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -185,7 +185,7 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -264,7 +264,7 @@ exports[`<DeviceDetails /> renders device without metadata 1`] = `
|
||||||
Session details
|
Session details
|
||||||
</p>
|
</p>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -292,7 +292,7 @@ exports[`<DeviceDetails /> renders device without metadata 1`] = `
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table
|
<table
|
||||||
class="mxDeviceDetails_metadataTable"
|
class="mx_DeviceDetails_metadataTable"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
mockClientMethodsUser,
|
mockClientMethodsUser,
|
||||||
} from '../../../../../test-utils';
|
} from '../../../../../test-utils';
|
||||||
import Modal from '../../../../../../src/Modal';
|
import Modal from '../../../../../../src/Modal';
|
||||||
|
import LogoutDialog from '../../../../../../src/components/views/dialogs/LogoutDialog';
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ describe('<SessionManagerTab />', () => {
|
||||||
const mockCrossSigningInfo = {
|
const mockCrossSigningInfo = {
|
||||||
checkDeviceTrust: jest.fn(),
|
checkDeviceTrust: jest.fn(),
|
||||||
};
|
};
|
||||||
const mockVerificationRequest = { cancel: jest.fn() } as unknown as VerificationRequest;
|
const mockVerificationRequest = { cancel: jest.fn(), on: jest.fn() } as unknown as VerificationRequest;
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
...mockClientMethodsUser(aliceId),
|
...mockClientMethodsUser(aliceId),
|
||||||
getStoredCrossSigningForUser: jest.fn().mockReturnValue(mockCrossSigningInfo),
|
getStoredCrossSigningForUser: jest.fn().mockReturnValue(mockCrossSigningInfo),
|
||||||
|
@ -374,4 +375,29 @@ describe('<SessionManagerTab />', () => {
|
||||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Sign out', () => {
|
||||||
|
it('Signs out of current device', async () => {
|
||||||
|
const modalSpy = jest.spyOn(Modal, 'createDialog');
|
||||||
|
|
||||||
|
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] });
|
||||||
|
const { getByTestId } = render(getComponent());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// open device detail
|
||||||
|
const tile1 = getByTestId(`device-tile-${alicesDevice.device_id}`);
|
||||||
|
const toggle1 = tile1.querySelector('[aria-label="Toggle device details"]') as Element;
|
||||||
|
fireEvent.click(toggle1);
|
||||||
|
|
||||||
|
const signOutButton = getByTestId('device-detail-sign-out-cta');
|
||||||
|
expect(signOutButton).toMatchSnapshot();
|
||||||
|
fireEvent.click(signOutButton);
|
||||||
|
|
||||||
|
// logout dialog opened
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(LogoutDialog, {}, undefined, false, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||||
|
data-testid="device-detail-sign-out-cta"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Sign out of this session
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<SessionManagerTab /> goes to filtered list from security recommendations 1`] = `
|
exports[`<SessionManagerTab /> goes to filtered list from security recommendations 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_FilteredDeviceList_header"
|
class="mx_FilteredDeviceList_header"
|
||||||
|
|
Loading…
Reference in New Issue