Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 0c5ad45

Browse files
author
Kerry
authored
Device manager - security recommendations section (PSG-639) (#9179)
* display inactive status on device tile * unify DeviceSecurityVariation type, add correct icon to inactive ui * move types into type file * move DeviceSecurityVariation into types * add security recommendations section * add view all stubbed buttons * undeo debug * test security recs * remove debug * use css for card spacing
1 parent 9eaf48b commit 0c5ad45

File tree

15 files changed

+518
-16
lines changed

15 files changed

+518
-16
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
@import "./components/views/settings/devices/_DeviceSecurityCard.pcss";
3232
@import "./components/views/settings/devices/_DeviceTile.pcss";
3333
@import "./components/views/settings/devices/_FilteredDeviceList.pcss";
34+
@import "./components/views/settings/devices/_SecurityRecommendations.pcss";
3435
@import "./components/views/settings/devices/_SelectableDeviceTile.pcss";
3536
@import "./components/views/settings/shared/_SettingsSubsection.pcss";
3637
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_SecurityRecommendations_spacing {
18+
height: $spacing-16;
19+
}

src/components/views/settings/devices/DeviceSecurityCard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import React from 'react';
2020
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
2121
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
2222
import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg';
23-
import { DeviceSecurityVariation } from './filter';
23+
import { DeviceSecurityVariation } from './types';
2424
interface Props {
2525
variation: DeviceSecurityVariation;
2626
heading: string;

src/components/views/settings/devices/DeviceTile.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import TooltipTarget from "../../elements/TooltipTarget";
2323
import { Alignment } from "../../elements/Tooltip";
2424
import Heading from "../../typography/Heading";
2525
import { INACTIVE_DEVICE_AGE_MS, isDeviceInactive } from "./filter";
26-
import { DeviceWithVerification } from "./useOwnDevices";
26+
import { DeviceWithVerification } from "./types";
2727
export interface DeviceTileProps {
2828
device: DeviceWithVerification;
2929
children?: React.ReactNode;

src/components/views/settings/devices/FilteredDeviceList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React from 'react';
1818

1919
import DeviceTile from './DeviceTile';
2020
import { filterDevicesBySecurityRecommendation } from './filter';
21-
import { DevicesDictionary, DeviceWithVerification } from './useOwnDevices';
21+
import { DevicesDictionary, DeviceWithVerification } from './types';
2222

2323
interface Props {
2424
devices: DevicesDictionary;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
import { _t } from '../../../../languageHandler';
20+
import AccessibleButton from '../../elements/AccessibleButton';
21+
import SettingsSubsection from '../shared/SettingsSubsection';
22+
import DeviceSecurityCard from './DeviceSecurityCard';
23+
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_MS } from './filter';
24+
import { DevicesDictionary, DeviceSecurityVariation } from './types';
25+
26+
interface Props {
27+
devices: DevicesDictionary;
28+
}
29+
const MS_DAY = 24 * 60 * 60 * 1000;
30+
31+
const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
32+
const devicesArray = Object.values(devices);
33+
34+
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
35+
devicesArray,
36+
[DeviceSecurityVariation.Unverified],
37+
).length;
38+
const inactiveDevicesCount = filterDevicesBySecurityRecommendation(
39+
devicesArray,
40+
[DeviceSecurityVariation.Inactive],
41+
).length;
42+
43+
if (!(unverifiedDevicesCount | inactiveDevicesCount)) {
44+
return null;
45+
}
46+
47+
const inactiveAgeDays = INACTIVE_DEVICE_AGE_MS / MS_DAY;
48+
49+
// TODO(kerrya) stubbed until PSG-640/652
50+
const noop = () => {};
51+
52+
return <SettingsSubsection
53+
heading={_t('Security recommendations')}
54+
description={_t('Improve your account security by following these recommendations')}
55+
data-testid='security-recommendations-section'
56+
>
57+
{
58+
!!unverifiedDevicesCount &&
59+
<DeviceSecurityCard
60+
variation={DeviceSecurityVariation.Unverified}
61+
heading={_t('Unverified sessions')}
62+
description={_t(
63+
`Verify your sessions for enhanced secure messaging` +
64+
` or sign out from those you don't recognize or use anymore.`,
65+
)}
66+
>
67+
<AccessibleButton
68+
kind='link_inline'
69+
onClick={noop}
70+
>
71+
{ _t('View all') + ` (${unverifiedDevicesCount})` }
72+
</AccessibleButton>
73+
</DeviceSecurityCard>
74+
}
75+
{
76+
!!inactiveDevicesCount &&
77+
<>
78+
{ !!unverifiedDevicesCount && <div className='mx_SecurityRecommendations_spacing' /> }
79+
<DeviceSecurityCard
80+
variation={DeviceSecurityVariation.Inactive}
81+
heading={_t('Inactive sessions')}
82+
description={_t(
83+
`Consider signing out from old sessions ` +
84+
`(%(inactiveAgeDays)s days or older) you don't use anymore`,
85+
{ inactiveAgeDays },
86+
)}
87+
>
88+
<AccessibleButton
89+
kind='link_inline'
90+
onClick={noop}
91+
>
92+
{ _t('View all') + ` (${inactiveDevicesCount})` }
93+
</AccessibleButton>
94+
</DeviceSecurityCard>
95+
</>
96+
}
97+
</SettingsSubsection>;
98+
};
99+
100+
export default SecurityRecommendations;

src/components/views/settings/devices/filter.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { DeviceWithVerification } from "./useOwnDevices";
18-
19-
export enum DeviceSecurityVariation {
20-
Verified = 'Verified',
21-
Unverified = 'Unverified',
22-
Inactive = 'Inactive',
23-
}
17+
import { DeviceWithVerification, DeviceSecurityVariation } from "./types";
2418

2519
type DeviceFilterCondition = (device: DeviceWithVerification) => boolean;
2620

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { IMyDevice } from "matrix-js-sdk/src/matrix";
18+
19+
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
20+
export type DevicesDictionary = Record<DeviceWithVerification['device_id'], DeviceWithVerification>;
21+
22+
export enum DeviceSecurityVariation {
23+
Verified = 'Verified',
24+
Unverified = 'Unverified',
25+
Inactive = 'Inactive',
26+
}

src/components/views/settings/devices/useOwnDevices.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
2020
import { logger } from "matrix-js-sdk/src/logger";
2121

2222
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
23-
24-
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
23+
import { DevicesDictionary } from "./types";
2524

2625
const isDeviceVerified = (
2726
matrixClient: MatrixClient,
@@ -56,11 +55,11 @@ const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise
5655

5756
return devicesDict;
5857
};
58+
5959
export enum OwnDevicesError {
6060
Unsupported = 'Unsupported',
6161
Default = 'Default',
6262
}
63-
export type DevicesDictionary = Record<DeviceWithVerification['device_id'], DeviceWithVerification>;
6463
type DevicesState = {
6564
devices: DevicesDictionary;
6665
currentDeviceId: string;

src/components/views/settings/tabs/user/SessionManagerTab.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import DeviceTile from '../../devices/DeviceTile';
2323
import DeviceSecurityCard from '../../devices/DeviceSecurityCard';
2424
import SettingsSubsection from '../../shared/SettingsSubsection';
2525
import FilteredDeviceList from '../../devices/FilteredDeviceList';
26-
import { DeviceSecurityVariation } from '../../devices/filter';
26+
import { DeviceSecurityVariation } from '../../devices/types';
27+
import SecurityRecommendations from '../../devices/SecurityRecommendations';
2728
import SettingsTab from '../SettingsTab';
2829

2930
const SessionManagerTab: React.FC = () => {
@@ -43,6 +44,7 @@ const SessionManagerTab: React.FC = () => {
4344
};
4445

4546
return <SettingsTab heading={_t('Sessions')}>
47+
<SecurityRecommendations devices={devices} />
4648
<SettingsSubsection
4749
heading={_t('Current session')}
4850
data-testid='current-session-section'

src/i18n/strings/en_EN.json

+7
Original file line numberDiff line numberDiff line change
@@ -1708,6 +1708,13 @@
17081708
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
17091709
"Verified": "Verified",
17101710
"Unverified": "Unverified",
1711+
"Security recommendations": "Security recommendations",
1712+
"Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
1713+
"Unverified sessions": "Unverified sessions",
1714+
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
1715+
"View all": "View all",
1716+
"Inactive sessions": "Inactive sessions",
1717+
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore",
17111718
"Unable to remove contact information": "Unable to remove contact information",
17121719
"Remove %(email)s?": "Remove %(email)s?",
17131720
"Invalid Email Address": "Invalid Email Address",

test/components/views/settings/devices/DeviceSecurityCard-test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { render } from '@testing-library/react';
1818
import React from 'react';
1919

2020
import DeviceSecurityCard from '../../../../../src/components/views/settings/devices/DeviceSecurityCard';
21-
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/filter';
21+
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';
2222

2323
describe('<DeviceSecurityCard />', () => {
2424
const defaultProps = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import { render } from '@testing-library/react';
19+
20+
import SecurityRecommendations from '../../../../../src/components/views/settings/devices/SecurityRecommendations';
21+
22+
const MS_DAY = 24 * 60 * 60 * 1000;
23+
describe('<SecurityRecommendations />', () => {
24+
const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false };
25+
const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true };
26+
const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) };
27+
const hundredDaysOldUnverified = {
28+
device_id: 'unverified-100-days-old',
29+
isVerified: false,
30+
last_seen_ts: Date.now() - (MS_DAY * 100),
31+
};
32+
33+
const defaultProps = {
34+
devices: {},
35+
};
36+
const getComponent = (props = {}) =>
37+
(<SecurityRecommendations {...defaultProps} {...props} />);
38+
39+
it('renders null when no devices', () => {
40+
const { container } = render(getComponent());
41+
expect(container.firstChild).toBeNull();
42+
});
43+
44+
it('renders unverified devices section when user has unverified devices', () => {
45+
const devices = {
46+
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
47+
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
48+
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
49+
};
50+
const { container } = render(getComponent({ devices }));
51+
expect(container).toMatchSnapshot();
52+
});
53+
54+
it('renders inactive devices section when user has inactive devices', () => {
55+
const devices = {
56+
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
57+
[hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified,
58+
};
59+
const { container } = render(getComponent({ devices }));
60+
expect(container).toMatchSnapshot();
61+
});
62+
63+
it('renders both cards when user has both unverified and inactive devices', () => {
64+
const devices = {
65+
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
66+
[hundredDaysOld.device_id]: hundredDaysOld,
67+
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
68+
};
69+
const { container } = render(getComponent({ devices }));
70+
expect(container).toMatchSnapshot();
71+
});
72+
});

0 commit comments

Comments
 (0)