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..6af447f 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:
@@ -29,3 +29,8 @@ Steps for 2):
## Screenshots

+
+## Development
+
+- Use `yarn test` to run all style, lint and unit tests
+- Use `yarn fix` to fix the coding style
diff --git a/package.json b/package.json
index 2bb3cd4..09db13f 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2",
+ "jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.0"
},
"dependencies": {
diff --git a/src/App.js b/src/App.js
index b3c40c3..a7fc125 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..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 c65cd12..a6889b2 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,
@@ -23,6 +24,7 @@ import {
TextField,
TextInput,
ReferenceField,
+ ReferenceManyField,
SelectInput,
BulkDeleteButton,
DeleteButton,
@@ -36,6 +38,7 @@ import {
sanitizeListRestProps,
} from "react-admin";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
+import { DeviceRemoveButton } from "./devices";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles({
@@ -205,6 +208,7 @@ const UserTitle = ({ record }) => {
};
export const UserEdit = props => {
const classes = useStyles();
+ const translate = useTranslate();
return (
}>
}>
@@ -254,6 +258,37 @@ export const UserEdit = props => {
+ }
+ path="devices"
+ >
+
+
+
+
+
+
+
+
+
+
}
diff --git a/src/i18n/de.js b/src/i18n/de.js
index 560e310..0243041 100644
--- a/src/i18n/de.js
+++ b/src/i18n/de.js
@@ -37,7 +37,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",
@@ -51,6 +51,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.",
@@ -107,6 +112,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 12ba1ae..f6501c9 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -35,7 +35,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",
@@ -49,6 +49,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",
@@ -105,6 +110,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/setupTests.js b/src/setupTests.js
index 6f413a4..b1a6b6d 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -1,4 +1,6 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
+import fetchMock from "jest-fetch-mock";
configure({ adapter: new Adapter() });
+fetchMock.enableMocks();
diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js
index cebc413..d4c8bcd 100644
--- a/src/synapse/dataProvider.js
+++ b/src/synapse/dataProvider.js
@@ -45,8 +45,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",
}),
@@ -67,6 +67,19 @@ const resourceMap = {
return json.total_rooms;
},
},
+ 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 => ({
@@ -166,30 +179,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),
}));
},
@@ -276,11 +277,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,
}));
@@ -305,11 +306,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 => ({
diff --git a/src/synapse/dataProvider.test.js b/src/synapse/dataProvider.test.js
new file mode 100644
index 0000000..a6563b7
--- /dev/null
+++ b/src/synapse/dataProvider.test.js
@@ -0,0 +1,78 @@
+import dataProvider from "./dataProvider";
+
+beforeEach(() => {
+ fetch.resetMocks();
+});
+
+describe("dataProvider", () => {
+ localStorage.setItem("base_url", "http://localhost");
+ localStorage.setItem("access_token", "access_token");
+
+ it("fetches all users", async () => {
+ fetch.mockResponseOnce(
+ JSON.stringify({
+ users: [
+ {
+ name: "user_id1",
+ password_hash: "password_hash1",
+ is_guest: 0,
+ admin: 0,
+ user_type: null,
+ deactivated: 0,
+ displayname: "User One",
+ },
+ {
+ name: "user_id2",
+ password_hash: "password_hash2",
+ is_guest: 0,
+ admin: 1,
+ user_type: null,
+ deactivated: 0,
+ displayname: "User Two",
+ },
+ ],
+ next_token: "100",
+ total: 200,
+ })
+ );
+
+ const users = await dataProvider.getList("users", {
+ pagination: { page: 1, perPage: 5 },
+ sort: { field: "title", order: "ASC" },
+ filter: { author_id: 12 },
+ });
+
+ expect(users["data"][0]["id"]).toEqual("user_id1");
+ expect(users["total"]).toEqual(200);
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
+
+ it("fetches one user", async () => {
+ fetch.mockResponseOnce(
+ JSON.stringify({
+ name: "user_id1",
+ password: "user_password",
+ displayname: "User",
+ threepids: [
+ {
+ medium: "email",
+ address: "user@mail_1.com",
+ },
+ {
+ medium: "email",
+ address: "user@mail_2.com",
+ },
+ ],
+ avatar_url: "mxc://localhost/user1",
+ admin: false,
+ deactivated: false,
+ })
+ );
+
+ const user = await dataProvider.getOne("users", { id: "user_id1" });
+
+ expect(user["data"]["id"]).toEqual("user_id1");
+ expect(user["data"]["displayname"]).toEqual("User");
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 7607589..b20f394 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3502,6 +3502,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+cross-fetch@^3.0.4:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.5.tgz#2739d2981892e7ab488a7ad03b92df2816e03f4c"
+ integrity sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew==
+ dependencies:
+ node-fetch "2.6.0"
+
cross-spawn@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
@@ -6447,6 +6454,14 @@ jest-environment-node@^24.9.0:
jest-mock "^24.9.0"
jest-util "^24.9.0"
+jest-fetch-mock@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
+ integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
+ dependencies:
+ cross-fetch "^3.0.4"
+ promise-polyfill "^8.1.3"
+
jest-get-type@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
@@ -7650,6 +7665,11 @@ no-case@^3.0.3:
lower-case "^2.0.1"
tslib "^1.10.0"
+node-fetch@2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+ integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@@ -9131,6 +9151,11 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+promise-polyfill@^8.1.3:
+ version "8.1.3"
+ resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
+ integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==
+
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"