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 VerifReq
pull/28788/head^2
Kerry 2022-09-14 14:37:36 +02:00 committed by GitHub
parent 41960b164b
commit f20d86b7b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 113 additions and 16 deletions

View File

@ -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;

View File

@ -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;
} }

View File

@ -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'

View File

@ -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} />
</> </>

View File

@ -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>;
}; };

View File

@ -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";

View File

@ -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 &&

View File

@ -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",

View File

@ -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 =>

View File

@ -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>,
] ]
`; `;

View File

@ -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>

View File

@ -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);
});
});
}); });

View File

@ -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"