Device manage - handle sessions that don't support encryption (#9717)
* add handling for unverifiable sessions * test * update types for filtervariation * strict fixes * avoid setting up cross signing in device man tests
This commit is contained in:
parent
6150b86421
commit
888e69f39a
12 changed files with 200 additions and 105 deletions
|
@ -32,6 +32,7 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
|
|||
[DeviceSecurityVariation.Inactive]: InactiveIcon,
|
||||
[DeviceSecurityVariation.Verified]: VerifiedIcon,
|
||||
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
|
||||
[DeviceSecurityVariation.Unverifiable]: UnverifiedIcon,
|
||||
};
|
||||
|
||||
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
|
||||
|
|
|
@ -56,6 +56,26 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
|||
</p>
|
||||
</>,
|
||||
},
|
||||
// unverifiable uses single-session case
|
||||
// because it is only ever displayed on a single session detail
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t('Unverified session'),
|
||||
description: <>
|
||||
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
|
||||
)
|
||||
}
|
||||
</p><p>
|
||||
{ _t(
|
||||
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</>,
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t('Inactive sessions'),
|
||||
description: <>
|
||||
|
|
|
@ -30,18 +30,33 @@ interface Props {
|
|||
onVerifyDevice?: () => void;
|
||||
}
|
||||
|
||||
export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||
device,
|
||||
onVerifyDevice,
|
||||
}) => {
|
||||
const securityCardProps = device.isVerified ? {
|
||||
variation: DeviceSecurityVariation.Verified,
|
||||
heading: _t('Verified session'),
|
||||
description: <>
|
||||
{ _t('This session is ready for secure messaging.') }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||
</>,
|
||||
} : {
|
||||
const getCardProps = (device: ExtendedDevice): {
|
||||
variation: DeviceSecurityVariation;
|
||||
heading: string;
|
||||
description: React.ReactNode;
|
||||
} => {
|
||||
if (device.isVerified) {
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Verified,
|
||||
heading: _t('Verified session'),
|
||||
description: <>
|
||||
{ _t('This session is ready for secure messaging.') }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||
</>,
|
||||
};
|
||||
}
|
||||
if (device.isVerified === null) {
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t('Unverified session'),
|
||||
description: <>
|
||||
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
|
||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
|
||||
</>,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variation: DeviceSecurityVariation.Unverified,
|
||||
heading: _t('Unverified session'),
|
||||
description: <>
|
||||
|
@ -49,10 +64,19 @@ export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
|||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||
</>,
|
||||
};
|
||||
};
|
||||
|
||||
export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||
device,
|
||||
onVerifyDevice,
|
||||
}) => {
|
||||
const securityCardProps = getCardProps(device);
|
||||
|
||||
return <DeviceSecurityCard
|
||||
{...securityCardProps}
|
||||
>
|
||||
{ !device.isVerified && !!onVerifyDevice &&
|
||||
{ /* check for explicit false to exclude unverifiable devices */ }
|
||||
{ device.isVerified === false && !!onVerifyDevice &&
|
||||
<AccessibleButton
|
||||
kind='primary'
|
||||
onClick={onVerifyDevice}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
|
|||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import {
|
||||
filterDevicesBySecurityRecommendation,
|
||||
FilterVariation,
|
||||
INACTIVE_DEVICE_AGE_DAYS,
|
||||
} from './filter';
|
||||
import SelectableDeviceTile from './SelectableDeviceTile';
|
||||
|
@ -47,8 +48,8 @@ interface Props {
|
|||
expandedDeviceIds: ExtendedDevice['device_id'][];
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][];
|
||||
filter?: DeviceSecurityVariation;
|
||||
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
|
||||
filter?: FilterVariation;
|
||||
onFilterChange: (filter: FilterVariation | undefined) => void;
|
||||
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
saveDeviceName: DevicesState['saveDeviceName'];
|
||||
|
@ -68,12 +69,12 @@ const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right:
|
|||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|
||||
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
|
||||
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
|
||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
||||
.sort(sortDevicesByLatestActivityThenDisplayName);
|
||||
|
||||
const ALL_FILTER_ID = 'ALL';
|
||||
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
||||
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
|
||||
|
||||
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||
title: string;
|
||||
|
@ -90,6 +91,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
|||
`sign out from those you don't recognize or use anymore.`,
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Unverifiable]: {
|
||||
title: _t('Unverified session'),
|
||||
description: _t(
|
||||
`This session doesn't support encryption and thus can't be verified.`,
|
||||
),
|
||||
},
|
||||
[DeviceSecurityVariation.Inactive]: {
|
||||
title: _t('Inactive sessions'),
|
||||
description: _t(
|
||||
|
@ -100,8 +107,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
|||
},
|
||||
};
|
||||
|
||||
const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation =>
|
||||
Object.values<string>(DeviceSecurityVariation).includes(filter);
|
||||
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
|
||||
!!filter && ([
|
||||
DeviceSecurityVariation.Inactive,
|
||||
DeviceSecurityVariation.Unverified,
|
||||
DeviceSecurityVariation.Verified,
|
||||
] as string[]).includes(filter);
|
||||
|
||||
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
||||
if (isSecurityVariation(filter)) {
|
||||
|
@ -124,7 +135,7 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
|
|||
return null;
|
||||
};
|
||||
|
||||
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
||||
const getNoResultsMessage = (filter?: FilterVariation): string => {
|
||||
switch (filter) {
|
||||
case DeviceSecurityVariation.Verified:
|
||||
return _t('No verified sessions found.');
|
||||
|
@ -136,7 +147,7 @@ const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
|||
return _t('No sessions found.');
|
||||
}
|
||||
};
|
||||
interface NoResultsProps { filter?: DeviceSecurityVariation, clearFilter: () => void}
|
||||
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
|
||||
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
||||
<div className='mx_FilteredDeviceList_noResults'>
|
||||
{ getNoResultsMessage(filter) }
|
||||
|
@ -273,7 +284,7 @@ export const FilteredDeviceList =
|
|||
];
|
||||
|
||||
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
|
||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
|
||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
|
||||
};
|
||||
|
||||
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
||||
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton from '../../elements/AccessibleButton';
|
|||
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
ExtendedDevice,
|
||||
|
@ -31,7 +31,7 @@ import {
|
|||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
currentDeviceId: ExtendedDevice['device_id'];
|
||||
goToFilteredList: (filter: DeviceSecurityVariation) => void;
|
||||
goToFilteredList: (filter: FilterVariation) => void;
|
||||
}
|
||||
|
||||
const SecurityRecommendations: React.FC<Props> = ({
|
||||
|
|
|
@ -22,10 +22,14 @@ const MS_DAY = 24 * 60 * 60 * 1000;
|
|||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
|
||||
|
||||
export type FilterVariation = DeviceSecurityVariation.Verified
|
||||
| DeviceSecurityVariation.Inactive
|
||||
| DeviceSecurityVariation.Unverified;
|
||||
|
||||
export const isDeviceInactive: DeviceFilterCondition = device =>
|
||||
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
|
||||
|
||||
const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
||||
const filters: Record<FilterVariation, DeviceFilterCondition> = {
|
||||
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
|
||||
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
|
||||
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
|
||||
|
@ -33,7 +37,7 @@ const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
|||
|
||||
export const filterDevicesBySecurityRecommendation = (
|
||||
devices: ExtendedDevice[],
|
||||
securityVariations: DeviceSecurityVariation[],
|
||||
securityVariations: FilterVariation[],
|
||||
) => {
|
||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||
if (!activeFilters.length) {
|
||||
|
|
|
@ -32,4 +32,7 @@ export enum DeviceSecurityVariation {
|
|||
Verified = 'Verified',
|
||||
Unverified = 'Unverified',
|
||||
Inactive = 'Inactive',
|
||||
// sessions that do not support encryption
|
||||
// eg a session that logged in via api to get an access token
|
||||
Unverifiable = 'Unverifiable'
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
|
|||
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
|
||||
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
|
||||
import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
||||
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
|
||||
import { ExtendedDevice } from '../../devices/types';
|
||||
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
||||
|
@ -37,6 +37,7 @@ import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
|||
import SettingsStore from '../../../../../settings/SettingsStore';
|
||||
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
||||
import QuestionDialog from '../../../dialogs/QuestionDialog';
|
||||
import { FilterVariation } from '../../devices/filter';
|
||||
|
||||
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
|
@ -123,7 +124,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
setPushNotifications,
|
||||
supportsMSC3881,
|
||||
} = useOwnDevices();
|
||||
const [filter, setFilter] = useState<DeviceSecurityVariation>();
|
||||
const [filter, setFilter] = useState<FilterVariation>();
|
||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -142,7 +143,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
|
||||
const onGoToFilteredList = (filter: FilterVariation) => {
|
||||
setFilter(filter);
|
||||
clearTimeout(scrollIntoViewTimeoutRef.current);
|
||||
// wait a tick for the filtered section to rerender with different height
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue