diff --git a/.dockerignore b/.dockerignore index b2a9091..a9f7bc4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ # Exclude a bunch of stuff which can make the build context a larger than it needs to be -.git/ tests/ build/ lib/ diff --git a/README.md b/README.md index 4bae8a4..7ee8b21 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/). -It needs at least Synapse v1.14.0 for all functions to work as expected! +It needs at least Synapse v1.15.0 for all functions to work as expected! ## Step-By-Step install: diff --git a/src/App.js b/src/App.js index c8cc6aa..d92ec30 100644 --- a/src/App.js +++ b/src/App.js @@ -48,6 +48,7 @@ const App = () => ( icon={RoomIcon} /> + ); diff --git a/src/components/devices.js b/src/components/devices.js new file mode 100644 index 0000000..ee9292c --- /dev/null +++ b/src/components/devices.js @@ -0,0 +1,89 @@ +import React, { Fragment, useState } from "react"; +import { + Button, + useMutation, + useNotify, + 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"; + +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" } +); + +export const DeviceRemoveButton = props => { + const { record } = props; + const classes = useStyles(props); + const [open, setOpen] = useState(false); + const refresh = useRefresh(); + const notify = useNotify(); + + const [removeDevice, { loading }] = useMutation(); + + if (!record) return null; + + const handleClick = () => setOpen(true); + const handleDialogClose = () => setOpen(false); + + const handleConfirm = () => { + removeDevice( + { + type: "delete", + resource: "devices", + payload: { + id: record.id, + user_id: record.user_id, + }, + }, + { + onSuccess: () => { + notify("resources.devices.action.erase.success"); + refresh(); + }, + onFailure: () => + notify("resources.devices.action.erase.failure", "error"), + } + ); + setOpen(false); + }; + + return ( + + + + + ); +}; diff --git a/src/components/users.js b/src/components/users.js index 405635a..fdeb52a 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -2,6 +2,7 @@ import React, { cloneElement, Fragment } from "react"; import Avatar from "@material-ui/core/Avatar"; 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, @@ -24,6 +25,7 @@ import { TextInput, SearchInput, ReferenceField, + ReferenceManyField, SelectInput, BulkDeleteButton, DeleteButton, @@ -38,6 +40,7 @@ import { } from "react-admin"; import SaveQrButton from "./SaveQrButton"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; +import { DeviceRemoveButton } from "./devices"; import { makeStyles } from "@material-ui/core/styles"; const useStyles = makeStyles({ @@ -288,6 +291,7 @@ const UserTitle = ({ record }) => { export const UserEdit = props => { const classes = useStyles(); + const translate = useTranslate(); return ( }> }> @@ -337,6 +341,37 @@ export const UserEdit = props => { + } + path="devices" + > + + + + + + + + + + } diff --git a/src/i18n/de.js b/src/i18n/de.js index bee641b..56b2ab4 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -50,7 +50,7 @@ export default { id: "Benutzer-ID", name: "Name", is_guest: "Gast", - admin: "Admin", + admin: "Server Administrator", deactivated: "Deaktiviert", guests: "Zeige Gäste", show_deactivated: "Zeige deaktivierte Benutzer", @@ -64,6 +64,11 @@ export default { address: "Adresse", creation_ts_ms: "Zeitpunkt der Erstellung", consent_version: "Zugestimmte Geschäftsbedingungen", + // Devices: + device_id: "Geräte-ID", + display_name: "Gerätename", + last_seen_ts: "Zeitstempel", + last_seen_ip: "IP-Adresse", }, helper: { deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", @@ -122,6 +127,17 @@ export default { user_agent: "User Agent", }, }, + devices: { + name: "Gerät |||| Geräte", + action: { + erase: { + title: "Entferne %{id}", + content: 'Möchten Sie das Gerät "%{name}" wirklich entfernen?', + success: "Gerät erfolgreich entfernt.", + 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 84c82a8..8cb73e8 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -50,7 +50,7 @@ export default { id: "User-ID", name: "Name", is_guest: "Guest", - admin: "Admin", + admin: "Server Administrator", deactivated: "Deactivated", guests: "Show guests", show_deactivated: "Show deactivated users", @@ -64,6 +64,11 @@ export default { address: "Address", creation_ts_ms: "Creation timestamp", consent_version: "Consent version", + // Devices: + device_id: "Device-ID", + display_name: "Device name", + last_seen_ts: "Timestamp", + last_seen_ip: "IP address", }, helper: { deactivate: "Deactivated users cannot be reactivated", @@ -122,6 +127,17 @@ export default { user_agent: "User agent", }, }, + devices: { + name: "Device |||| Devices", + action: { + erase: { + title: "Removing %{id}", + content: 'Are you sure you want to remove the device "%{name}"?', + success: "Device successfully removed.", + 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 54c06d7..e4a3eb9 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -46,8 +46,8 @@ const resourceMap = { body: data, method: "PUT", }), - delete: id => ({ - endpoint: `/_synapse/admin/v1/deactivate/${id}`, + delete: params => ({ + endpoint: `/_synapse/admin/v1/deactivate/${params.id}`, body: { erase: true }, method: "POST", }), @@ -90,6 +90,19 @@ const resourceMap = { method: "POST", }), }, + devices: { + map: d => ({ + ...d, + id: d.device_id, + }), + data: "devices", + reference: id => ({ + endpoint: `/_synapse/admin/v2/users/${id}/devices`, + }), + delete: params => ({ + endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`, + }), + }, connections: { path: "/_synapse/admin/v1/whois", map: c => ({ @@ -189,30 +202,18 @@ const dataProvider = { }, getManyReference: (resource, params) => { - // FIXME console.log("getManyReference " + resource); - const { page, perPage } = params.pagination; - const { field, order } = params.sort; - const query = { - sort: JSON.stringify([field, order]), - range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]), - filter: JSON.stringify({ - ...params.filter, - [params.target]: params.id, - }), - }; const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) return Promise.reject(); const res = resourceMap[resource]; - const endpoint_url = homeserver + res.path; - const url = `${endpoint_url}?${stringify(query)}`; + const ref = res["reference"](params.id); + const endpoint_url = homeserver + ref.endpoint; - return jsonClient(url).then(({ headers, json }) => ({ - data: json, - total: parseInt(headers.get("content-range").split("/").pop(), 10), + return jsonClient(endpoint_url).then(({ headers, json }) => ({ + data: json[res.data].map(res.map), })); }, @@ -299,11 +300,11 @@ const dataProvider = { const res = resourceMap[resource]; if ("delete" in res) { - const del = res["delete"](params.id); + const del = res["delete"](params); const endpoint_url = homeserver + del.endpoint; return jsonClient(endpoint_url, { - method: del.method, - body: JSON.stringify(del.body), + method: "method" in del ? del.method : "DELETE", + body: "body" in del ? JSON.stringify(del.body) : null, }).then(({ json }) => ({ data: json, })); @@ -328,11 +329,11 @@ const dataProvider = { if ("delete" in res) { return Promise.all( params.ids.map(id => { - const del = res["delete"](id); + const del = res["delete"]({ ...params, id: id }); const endpoint_url = homeserver + del.endpoint; return jsonClient(endpoint_url, { - method: del.method, - body: JSON.stringify(del.body), + method: "method" in del ? del.method : "DELETE", + body: "body" in del ? JSON.stringify(del.body) : null, }); }) ).then(responses => ({