Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f56bac356 | ||
![]() |
bbbca0c57c | ||
![]() |
314906657f | ||
![]() |
78e7c5f391 | ||
![]() |
1074178e31 | ||
![]() |
352ab1290a |
12 changed files with 296 additions and 28 deletions
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -37,6 +37,7 @@ const App = () => (
|
|||
/>
|
||||
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
|
||||
<Resource name="connections" />
|
||||
<Resource name="devices" />
|
||||
<Resource name="servernotices" />
|
||||
</Admin>
|
||||
);
|
||||
|
|
89
src/components/devices.js
Normal file
89
src/components/devices.js
Normal file
|
@ -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 (
|
||||
<Fragment>
|
||||
<Button
|
||||
label="ra.action.remove"
|
||||
onClick={handleClick}
|
||||
className={classnames("ra-delete-button", classes.deleteButton)}
|
||||
>
|
||||
<ActionDelete />
|
||||
</Button>
|
||||
<Confirm
|
||||
isOpen={open}
|
||||
loading={loading}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleDialogClose}
|
||||
title="resources.devices.action.erase.title"
|
||||
content="resources.devices.action.erase.content"
|
||||
translateOptions={{
|
||||
id: record.id,
|
||||
name: record.display_name ? record.display_name : record.id,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<Edit {...props} title={<UserTitle />}>
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
|
@ -254,6 +258,37 @@ export const UserEdit = props => {
|
|||
</SimpleFormIterator>
|
||||
</ArrayInput>
|
||||
</FormTab>
|
||||
<FormTab
|
||||
label={translate("resources.devices.name", { smart_count: 2 })}
|
||||
icon={<DevicesIcon />}
|
||||
path="devices"
|
||||
>
|
||||
<ReferenceManyField
|
||||
reference="devices"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<TextField source="device_id" sortable={false} />
|
||||
<TextField source="display_name" sortable={false} />
|
||||
<TextField source="last_seen_ip" sortable={false} />
|
||||
<DateField
|
||||
source="last_seen_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
sortable={false}
|
||||
/>
|
||||
<DeviceRemoveButton />
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</FormTab>
|
||||
<FormTab
|
||||
label="resources.connections.name"
|
||||
icon={<SettingsInputComponentIcon />}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
78
src/synapse/dataProvider.test.js
Normal file
78
src/synapse/dataProvider.test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
25
yarn.lock
25
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue