Add support for managing sessions (#415)
* Allow node type prop in setting tile Signed-off-by: Ajay Bura <ajbura@gmail.com> * Update popup window max height Signed-off-by: Ajay Bura <ajbura@gmail.com> * Add device management setting Signed-off-by: Ajay Bura <ajbura@gmail.com> * Add password based login Signed-off-by: Ajay Bura <ajbura@gmail.com> * truncate long list of verified devices Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
fe997d8b01
commit
005434f79b
6 changed files with 251 additions and 7 deletions
|
@ -1,7 +1,7 @@
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.pw-model {
|
.pw-model {
|
||||||
--modal-height: 656px;
|
--modal-height: 774px;
|
||||||
max-height: var(--modal-height) !important;
|
max-height: var(--modal-height) !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,11 @@ function SettingTile({ title, options, content }) {
|
||||||
<div className="setting-tile">
|
<div className="setting-tile">
|
||||||
<div className="setting-tile__content">
|
<div className="setting-tile__content">
|
||||||
<div className="setting-tile__title">
|
<div className="setting-tile__title">
|
||||||
<Text variant="b1">{title}</Text>
|
{
|
||||||
|
typeof title === 'string'
|
||||||
|
? <Text variant="b1">{title}</Text>
|
||||||
|
: title
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +28,7 @@ SettingTile.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingTile.propTypes = {
|
SettingTile.propTypes = {
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
content: PropTypes.node,
|
content: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
219
src/app/organisms/settings/DeviceManage.jsx
Normal file
219
src/app/organisms/settings/DeviceManage.jsx
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './DeviceManage.scss';
|
||||||
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
|
||||||
|
import Text from '../../atoms/text/Text';
|
||||||
|
import Button from '../../atoms/button/Button';
|
||||||
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
|
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
||||||
|
|
||||||
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||||
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
function useDeviceList() {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const [deviceList, setDeviceList] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const updateDevices = () => mx.getDevices().then((data) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
setDeviceList(data.devices || []);
|
||||||
|
});
|
||||||
|
updateDevices();
|
||||||
|
|
||||||
|
const handleDevicesUpdate = (users) => {
|
||||||
|
if (users.includes(mx.getUserId())) {
|
||||||
|
updateDevices();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
||||||
|
return () => {
|
||||||
|
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return deviceList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCrossVerified(deviceId) {
|
||||||
|
try {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
|
||||||
|
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
|
||||||
|
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
|
||||||
|
return deviceTrust.isCrossSigningVerified();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeviceManage() {
|
||||||
|
const TRUNCATED_COUNT = 4;
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const deviceList = useDeviceList();
|
||||||
|
const [processing, setProcessing] = useState([]);
|
||||||
|
const [truncated, setTruncated] = useState(true);
|
||||||
|
const mountStore = useStore();
|
||||||
|
mountStore.setItem(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProcessing([]);
|
||||||
|
}, [deviceList]);
|
||||||
|
|
||||||
|
const addToProcessing = (device) => {
|
||||||
|
const old = [...processing];
|
||||||
|
old.push(device.device_id);
|
||||||
|
setProcessing(old);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromProcessing = () => {
|
||||||
|
setProcessing([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (deviceList === null) {
|
||||||
|
return (
|
||||||
|
<div className="device-manage">
|
||||||
|
<div className="device-manage__loading">
|
||||||
|
<Spinner size="small" />
|
||||||
|
<Text>Loading devices...</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRename = async (device) => {
|
||||||
|
const newName = window.prompt('Edit session name', device.display_name);
|
||||||
|
if (newName === null || newName.trim() === '') return;
|
||||||
|
if (newName.trim() === device.display_name) return;
|
||||||
|
addToProcessing(device);
|
||||||
|
try {
|
||||||
|
await mx.setDeviceDetails(device.device_id, {
|
||||||
|
display_name: newName,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
removeFromProcessing(device);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async (device, auth = undefined) => {
|
||||||
|
if (auth === undefined
|
||||||
|
? window.confirm(`You are about to logout "${device.display_name}" session?`)
|
||||||
|
: true
|
||||||
|
) {
|
||||||
|
addToProcessing(device);
|
||||||
|
try {
|
||||||
|
await mx.deleteDevice(device.device_id, auth);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.httpStatus === 401 && e.data?.flows) {
|
||||||
|
const { flows } = e.data;
|
||||||
|
const flow = flows.find((f) => f.stages.includes('m.login.password'));
|
||||||
|
if (flow) {
|
||||||
|
const password = window.prompt('Please enter account password', '');
|
||||||
|
if (password && password.trim() !== '') {
|
||||||
|
handleRemove(device, {
|
||||||
|
session: e.data.session,
|
||||||
|
type: 'm.login.password',
|
||||||
|
password,
|
||||||
|
identifier: {
|
||||||
|
type: 'm.id.user',
|
||||||
|
user: mx.getUserId(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.alert('Failed to remove session!');
|
||||||
|
if (!mountStore.getItem()) return;
|
||||||
|
removeFromProcessing(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDevice = (device, isVerified) => {
|
||||||
|
const deviceId = device.device_id;
|
||||||
|
const displayName = device.display_name;
|
||||||
|
const lastIP = device.last_seen_ip;
|
||||||
|
const lastTS = device.last_seen_ts;
|
||||||
|
return (
|
||||||
|
<SettingTile
|
||||||
|
key={deviceId}
|
||||||
|
title={(
|
||||||
|
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
|
||||||
|
{displayName}
|
||||||
|
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
options={
|
||||||
|
processing.includes(deviceId)
|
||||||
|
? <Spinner size="small" />
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
|
||||||
|
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content={(
|
||||||
|
<Text variant="b3">
|
||||||
|
Last activity
|
||||||
|
<span style={{ color: 'var(--tc-surface-normal)' }}>
|
||||||
|
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
|
||||||
|
</span>
|
||||||
|
{lastIP ? ` at ${lastIP}` : ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unverified = [];
|
||||||
|
const verified = [];
|
||||||
|
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
|
||||||
|
if (isCrossVerified(device.device_id)) verified.push(device);
|
||||||
|
else unverified.push(device);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="device-manage">
|
||||||
|
<div>
|
||||||
|
<MenuHeader>Unverified sessions</MenuHeader>
|
||||||
|
{
|
||||||
|
unverified.length > 0
|
||||||
|
? unverified.map((device) => renderDevice(device, false))
|
||||||
|
: <Text className="device-manage__info">No unverified session</Text>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<MenuHeader>Verified sessions</MenuHeader>
|
||||||
|
{
|
||||||
|
verified.length > 0
|
||||||
|
? verified.map((device, index) => {
|
||||||
|
if (truncated && index >= TRUNCATED_COUNT) return null;
|
||||||
|
return renderDevice(device, true);
|
||||||
|
})
|
||||||
|
: <Text className="device-manage__info">No verified session</Text>
|
||||||
|
}
|
||||||
|
{ verified.length > TRUNCATED_COUNT && (
|
||||||
|
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
||||||
|
{truncated ? `View ${verified.length - 4} more` : 'View less'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{ deviceList.length > 0 && (
|
||||||
|
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeviceManage;
|
18
src/app/organisms/settings/DeviceManage.scss
Normal file
18
src/app/organisms/settings/DeviceManage.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@use '../../partials/flex';
|
||||||
|
|
||||||
|
.device-manage {
|
||||||
|
&__loading {
|
||||||
|
@extend .cp-fx__row--c-c;
|
||||||
|
padding: var(--sp-extra-loose) var(--sp-normal);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0 var(--sp-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__info {
|
||||||
|
margin: var(--sp-normal);
|
||||||
|
}
|
||||||
|
& .setting-tile:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/Impor
|
||||||
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
|
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
|
||||||
|
|
||||||
import ProfileEditor from '../profile-editor/ProfileEditor';
|
import ProfileEditor from '../profile-editor/ProfileEditor';
|
||||||
|
import DeviceManage from './DeviceManage';
|
||||||
|
|
||||||
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
|
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
|
||||||
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
|
||||||
|
@ -167,15 +168,16 @@ function SecuritySection() {
|
||||||
return (
|
return (
|
||||||
<div className="settings-security">
|
<div className="settings-security">
|
||||||
<div className="settings-security__card">
|
<div className="settings-security__card">
|
||||||
<MenuHeader>Device Info</MenuHeader>
|
<MenuHeader>Session Info</MenuHeader>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
|
title={`Session ID: ${initMatrix.matrixClient.getDeviceId()}`}
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
title={`Session key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
||||||
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
|
content={<Text variant="b3">Use this session ID-key combo to verify or manage this session.</Text>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<DeviceManage />
|
||||||
<div className="settings-security__card">
|
<div className="settings-security__card">
|
||||||
<MenuHeader>Encryption</MenuHeader>
|
<MenuHeader>Encryption</MenuHeader>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
.settings-appearance__card,
|
.settings-appearance__card,
|
||||||
.settings-notifications,
|
.settings-notifications,
|
||||||
.settings-security__card,
|
.settings-security__card,
|
||||||
|
.settings-security .device-manage,
|
||||||
.settings-about__card {
|
.settings-about__card {
|
||||||
@extend .settings-window__card;
|
@extend .settings-window__card;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue