From f92173b1cbd757cc4277a4b3fae755507edcb138 Mon Sep 17 00:00:00 2001 From: dklimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 6 Jul 2020 22:15:25 +0200 Subject: [PATCH] Add management of devices to EditUser Allows to view user devices and logout individual devices from those. Add a new tab to UserEdit (detail view). --- src/App.js | 1 + src/components/devices.js | 96 +++++++++++++++++ src/components/users.js | 210 +++++++++++++++++++++--------------- src/i18n/de.js | 16 +++ src/i18n/en.js | 16 +++ src/synapse/dataProvider.js | 44 +++++++- 6 files changed, 296 insertions(+), 87 deletions(-) create mode 100644 src/components/devices.js diff --git a/src/App.js b/src/App.js index f943e3f..ed78de1 100644 --- a/src/App.js +++ b/src/App.js @@ -37,6 +37,7 @@ const App = () => ( /> + ); diff --git a/src/components/devices.js b/src/components/devices.js new file mode 100644 index 0000000..196f3d8 --- /dev/null +++ b/src/components/devices.js @@ -0,0 +1,96 @@ +import React, { Fragment, useState } from "react"; +import { + Button, + useMutation, + useNotify, + useTranslate, + Confirm, + useRefresh, +} from "react-admin"; +import ActionDelete from "@material-ui/icons/Delete"; +import { makeStyles } from "@material-ui/core/styles"; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import classnames from "classnames"; + +export const RemoveDeviceButton = props => { + const { record } = props; + const classes = useStyles(props); + const [open, setOpen] = useState(false); + const translate = useTranslate(); + const refresh = useRefresh(); + const notify = useNotify(); + + const [removeDevice, { loading }] = useMutation(); + const handleSend = values => { + removeDevice( + { + type: "removeDevice", + resource: "devices", + payload: { + user_id: record.user_id, + device_id: record.device_id, + }, + }, + { + onSuccess: () => { + notify("resources.devices.action.remove_success"); + refresh(); + }, + onFailure: () => + notify("resources.devices.action.remove_failure", "error"), + } + ); + }; + + const handleClick = () => setOpen(true); + const handleDialogClose = () => setOpen(false); + + const handleConfirm = () => { + handleSend(); + setOpen(false); + }; + + return ( + + + + + ); +}; + +const useStyles = makeStyles( + theme => ({ + deleteButton: { + color: theme.palette.error.main, + "&:hover": { + backgroundColor: fade(theme.palette.error.main, 0.12), + // Reset on mouse devices + "@media (hover: none)": { + backgroundColor: "transparent", + }, + }, + }, + }), + { name: "RaDeleteDeviceButton" } +); diff --git a/src/components/users.js b/src/components/users.js index ff21436..7ae40a1 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -1,6 +1,7 @@ import React, { Fragment } from "react"; import PersonPinIcon from "@material-ui/icons/PersonPin"; import ContactMailIcon from "@material-ui/icons/ContactMail"; +import DevicesIcon from "@material-ui/icons/Devices"; import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent"; import { ArrayInput, @@ -32,6 +33,7 @@ import { Pagination, } from "react-admin"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; +import { RemoveDeviceButton } from "./devices"; const UserPagination = props => ( @@ -148,89 +150,129 @@ const UserTitle = ({ record }) => { ); }; -export const UserEdit = props => ( - }> - }> - }> - - - - - - - - - } - path="threepid" - > - - - - - - - - } - path="connections" - > - { + const translate = useTranslate(); + return ( + }> + }> + } > - + + + + + + + + } + path="threepid" + > + + + + + + + + } + path="devices" + > + - - - - - - - - - - -); + + + + + + + + + + + + } + path="connections" + > + + + + + + + + + + + + + ); +}; diff --git a/src/i18n/de.js b/src/i18n/de.js index 5ec9947..066bc64 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -73,6 +73,22 @@ export default { user_agent: "User Agent", }, }, + devices: { + name: "Gerät |||| Geräte", + fields: { + device_id: "Geräte-ID", + display_name: "Anzeigename", + last_seen_ts: "Zeitstempel", + last_seen_ip: "IP-Adresse", + }, + action: { + remove_title: "Entferne %{name}: %{id}", + remove_content: + "Möchten Sie dieses %{name} wirklich entfernen? %{display_name}: %{displayname}", + remove_success: "Gerät erfolgreich entfernt.", + remove_failure: "Beim Entfernen ist ein Fehler aufgetreten.", + }, + }, servernotices: { name: "Serverbenachrichtigungen", send: "Servernachricht versenden", diff --git a/src/i18n/en.js b/src/i18n/en.js index 2b29701..88b4b29 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -72,6 +72,22 @@ export default { user_agent: "User agent", }, }, + devices: { + name: "Device |||| Devices", + fields: { + device_id: "Device-ID", + display_name: "Displayname", + last_seen_ts: "Timestamp", + last_seen_ip: "IP address", + }, + action: { + remove_title: "Remove %{name} #%{id}", + remove_content: + "Are you sure you want to remove this %{name}? %{display_name}: %{displayname}", + remove_success: "Device successfully removed.", + remove_failure: "An error has occurred.", + }, + }, servernotices: { name: "Server Notices", send: "Send server notices", diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 21748fd..d728d4f 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -32,6 +32,9 @@ const resourceMap = { ? parseInt(json.next_token, 10) + perPage : from + json.users.length; }, + getMany: id => ({ + endpoint: `/_synapse/admin/v2/users/${id}`, + }), create: data => ({ endpoint: `/_synapse/admin/v2/users/${data.id}`, body: data, @@ -59,13 +62,26 @@ const resourceMap = { return json.total_rooms; }, }, + devices: { + path: "/_synapse/admin/v2/users", + map: d => ({ + ...d, + id: d.devices[0].user_id, + }), + data: "devices", + getMany: id => ({ + endpoint: `/_synapse/admin/v2/users/${id}/devices`, + }), + }, connections: { - path: "/_synapse/admin/v1/whois", map: c => ({ ...c, id: c.user_id, }), data: "connections", + getMany: id => ({ + endpoint: `/_synapse/admin/v1/whois/${id}`, + }), }, servernotices: { map: n => ({ id: n.event_id }), @@ -148,10 +164,14 @@ const dataProvider = { if (!homeserver || !(resource in resourceMap)) return Promise.reject(); const res = resourceMap[resource]; + if (!("getMany" in res)) return Promise.reject(); - const endpoint_url = homeserver + res.path; return Promise.all( - params.ids.map(id => jsonClient(`${endpoint_url}/${id}`)) + params.ids.map(id => { + const getMany = res["getMany"](id); + const endpoint_url = homeserver + getMany.endpoint; + return jsonClient(endpoint_url); + }) ).then(responses => ({ data: responses.map(({ json }) => res.map(json)), })); @@ -321,6 +341,24 @@ const dataProvider = { })); } }, + + removeDevice: (resource, params) => { + console.log("removeDevice " + resource); + const homeserver = localStorage.getItem("base_url"); + if (!homeserver || !(resource in resourceMap)) return Promise.reject(); + + const res = resourceMap[resource]; + const endpoint_url = homeserver + res.path; + + return jsonClient( + `${endpoint_url}/${params.user_id}/devices/${params.device_id}`, + { + method: "DELETE", + } + ).then(({ json }) => ({ + data: json, + })); + }, }; export default dataProvider;