diff --git a/README.md b/README.md index a2e0190..722d54b 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.18.0 for all functions to work as expected! +It needs at least Synapse v1.23.0 for all functions to work as expected! You get your server version with the request `/_synapse/admin/v1/server_version`. See also [Synapse version API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst). diff --git a/package.json b/package.json index 6e599ee..cb9132c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "synapse-admin", - "version": "0.2.1", + "version": "0.7.0", "description": "Admin GUI for the Matrix.org server Synapse", "author": "Awesome Technologies Innovationslabor GmbH", "license": "Apache-2.0", diff --git a/src/App.js b/src/App.js index 6ad0b22..3afdff8 100644 --- a/src/App.js +++ b/src/App.js @@ -5,9 +5,13 @@ import authProvider from "./synapse/authProvider"; import dataProvider from "./synapse/dataProvider"; import { UserList, UserCreate, UserEdit } from "./components/users"; import { RoomList, RoomShow } from "./components/rooms"; +import { ReportList, ReportShow } from "./components/EventReports"; import LoginPage from "./components/LoginPage"; import UserIcon from "@material-ui/icons/Group"; -import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList"; +import EqualizerIcon from "@material-ui/icons/Equalizer"; +import { UserMediaStatsList } from "./components/statistics"; +import RoomIcon from "@material-ui/icons/ViewList"; +import ReportIcon from "@material-ui/icons/Warning"; import { ImportFeature } from "./components/ImportFeature"; import { Route } from "react-router-dom"; import germanMessages from "./i18n/de"; @@ -25,6 +29,7 @@ const i18nProvider = polyglotI18nProvider( const App = () => ( ( icon={UserIcon} /> + + + + + ); diff --git a/src/components/EventReports.js b/src/components/EventReports.js new file mode 100644 index 0000000..c73647f --- /dev/null +++ b/src/components/EventReports.js @@ -0,0 +1,135 @@ +import React from "react"; +import { + Datagrid, + DateField, + List, + NumberField, + Pagination, + ReferenceField, + Show, + Tab, + TabbedShowLayout, + TextField, + useTranslate, +} from "react-admin"; +import PageviewIcon from "@material-ui/icons/Pageview"; +import ViewListIcon from "@material-ui/icons/ViewList"; + +const ReportPagination = props => ( + +); + +export const ReportShow = props => { + const translate = useTranslate(); + return ( + + + } + > + + + + + + + + + + + + + + } + path="detail" + > + {" "} + + + + + + + + + + + + + + + + + ); +}; + +export const ReportList = ({ ...props }) => { + return ( + } + sort={{ field: "received_ts", order: "DESC" }} + bulkActionButtons={false} + > + + + + + + + + + ); +}; diff --git a/src/components/rooms.js b/src/components/rooms.js index 10dc987..e1a44e6 100644 --- a/src/components/rooms.js +++ b/src/components/rooms.js @@ -4,6 +4,7 @@ import { BooleanField, BulkDeleteWithConfirmButton, Datagrid, + DeleteButton, Filter, List, Pagination, @@ -15,6 +16,7 @@ import { Tab, TabbedShowLayout, TextField, + TopToolbar, useTranslate, } from "react-admin"; import get from "lodash/get"; @@ -70,10 +72,26 @@ const RoomTitle = ({ record }) => { ); }; +const RoomShowActions = ({ basePath, data, resource }) => { + const translate = useTranslate(); + return ( + + + + ); +}; + export const RoomShow = props => { const translate = useTranslate(); return ( - }> + } title={}> }> diff --git a/src/components/statistics.js b/src/components/statistics.js new file mode 100644 index 0000000..f14a2fa --- /dev/null +++ b/src/components/statistics.js @@ -0,0 +1,42 @@ +import React from "react"; +import { + Datagrid, + Filter, + List, + NumberField, + TextField, + SearchInput, + Pagination, +} from "react-admin"; + +const UserMediaStatsPagination = props => ( + +); + +const UserMediaStatsFilter = props => ( + + + +); + +export const UserMediaStatsList = props => { + return ( + } + pagination={} + sort={{ field: "media_length", order: "DESC" }} + bulkActionButtons={false} + > + "/users/" + id + "/media"}> + + + + + + + ); +}; diff --git a/src/components/users.js b/src/components/users.js index 5f8caf8..e66eb44 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -5,6 +5,9 @@ import ContactMailIcon from "@material-ui/icons/ContactMail"; import DevicesIcon from "@material-ui/icons/Devices"; import GetAppIcon from "@material-ui/icons/GetApp"; import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent"; +import NotificationsIcon from "@material-ui/icons/Notifications"; +import PermMediaIcon from "@material-ui/icons/PermMedia"; +import ViewListIcon from "@material-ui/icons/ViewList"; import { ArrayInput, ArrayField, @@ -40,6 +43,7 @@ import { ExportButton, TopToolbar, sanitizeListRestProps, + NumberField, } from "react-admin"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { DeviceRemoveButton } from "./devices"; @@ -312,6 +316,7 @@ export const UserEdit = props => { /> + } @@ -330,6 +335,7 @@ export const UserEdit = props => { + } @@ -361,6 +367,7 @@ export const UserEdit = props => { + } @@ -400,6 +407,111 @@ export const UserEdit = props => { + + } + path="media" + > + } + perPage={50} + > + + + + + + + + + + + + + + + } + path="rooms" + > + + "/rooms/" + id + "/show"} + > + + + + + + + + + } + path="pushers" + > + + + + + + + + + + + + + ); diff --git a/src/i18n/de.js b/src/i18n/de.js index 7e73b63..269a4f8 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -29,6 +29,7 @@ export default { "Sind Sie sicher dass Sie den Raum löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten und Medien, die der Raum beinhaltet werden vom Server gelöscht!", }, }, + reports: { tabs: { basic: "Allgemein", detail: "Details" } }, }, import_users: { error: { @@ -126,7 +127,8 @@ export default { consent_version: "Zugestimmte Geschäftsbedingungen", }, helper: { - deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", + deactivate: + "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.", erase: "DSGVO konformes Löschen der Benutzerdaten", }, action: { @@ -173,6 +175,30 @@ export default { unencrypted: "Nicht verschlüsselt", }, }, + reports: { + name: "Ereignisbericht |||| Ereignisberichte", + fields: { + id: "ID", + received_ts: "Meldezeit", + user_id: "Meldender", + name: "Raumname", + score: "Wert", + reason: "Grund", + event_id: "Event-ID", + event_json: { + origin: "Ursprungsserver", + origin_server_ts: "Sendezeit", + type: "Eventtyp", + content: { + msgtype: "Inhaltstyp", + body: "Nachrichteninhalt", + format: "Nachrichtenformat", + formatted_body: "Formatierter Nachrichteninhalt", + algorithm: "Verschlüsselungsalgorithmus", + }, + }, + }, + }, connections: { name: "Verbindungen", fields: { @@ -198,6 +224,33 @@ export default { }, }, }, + users_media: { + name: "Medien", + fields: { + media_id: "Medien ID", + media_length: "Größe", + media_type: "Typ", + upload_name: "Dateiname", + quarantined_by: "Zur Quarantäne hinzugefügt", + safe_from_quarantine: "Geschützt vor Quarantäne", + created_ts: "Erstellt", + last_access_ts: "Letzter Zugriff", + }, + }, + pushers: { + name: "Pusher |||| Pushers", + fields: { + app: "App", + app_display_name: "App-Anzeigename", + app_id: "App ID", + device_display_name: "Geräte-Anzeigename", + kind: "Art", + lang: "Sprache", + profile_tag: "Profil-Tag", + pushkey: "Pushkey", + data: { url: "URL" }, + }, + }, servernotices: { name: "Serverbenachrichtigungen", send: "Servernachricht versenden", @@ -214,9 +267,20 @@ export default { 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.', }, }, + user_media_statistics: { + name: "Dateien je Benutzer", + fields: { + media_count: "Anzahl der Dateien", + media_length: "Größe der Dateien", + }, + }, }, ra: { ...germanMessages.ra, + action: { + ...germanMessages.ra.action, + unselect: "Abwählen", + }, auth: { ...germanMessages.ra.auth, auth_check_error: "Anmeldung fehlgeschlagen", diff --git a/src/i18n/en.js b/src/i18n/en.js index 8ec1903..c322fd1 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -6,6 +6,7 @@ export default { auth: { base_url: "Homeserver URL", welcome: "Welcome to Synapse-admin", + server_version: "Synapse version", username_error: "Please enter fully qualified user ID: '@user:domain'", protocol_error: "URL has to start with 'http://' or 'https://'", url_error: "Not a valid Matrix server URL", @@ -27,6 +28,7 @@ export default { "Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!", }, }, + reports: { tabs: { basic: "Basic", detail: "Details" } }, }, import_users: { error: { @@ -124,7 +126,7 @@ export default { consent_version: "Consent version", }, helper: { - deactivate: "Deactivated users cannot be reactivated", + deactivate: "You must provide a password to re-activate an account.", erase: "Mark the user as GDPR-erased", }, action: { @@ -138,8 +140,8 @@ export default { name: "Name", canonical_alias: "Alias", joined_members: "Members", - joined_local_members: "local members", - joined_local_devices: "local devices", + joined_local_members: "Local members", + joined_local_devices: "Local devices", state_events: "State events", version: "Version", is_encrypted: "Encrypted", @@ -171,6 +173,30 @@ export default { unencrypted: "Unencrypted", }, }, + reports: { + name: "Reported event |||| Reported events", + fields: { + id: "ID", + received_ts: "report time", + user_id: "announcer", + name: "name of the room", + score: "score", + reason: "reason", + event_id: "event ID", + event_json: { + origin: "origin server", + origin_server_ts: "time of send", + type: "event typ", + content: { + msgtype: "content type", + body: "content", + format: "format", + formatted_body: "formatted content", + algorithm: "algorithm", + }, + }, + }, + }, connections: { name: "Connections", fields: { @@ -196,6 +222,33 @@ export default { }, }, }, + users_media: { + name: "Media", + fields: { + media_id: "Media ID", + media_length: "Lenght", + media_type: "Type", + upload_name: "File name", + quarantined_by: "Quarantined by", + safe_from_quarantine: "Safe from quarantine", + created_ts: "Created", + last_access_ts: "Last access", + }, + }, + pushers: { + name: "Pusher |||| Pushers", + fields: { + app: "App", + app_display_name: "App display name", + app_id: "App ID", + device_display_name: "Device display name", + kind: "Kind", + lang: "Language", + profile_tag: "Profile tag", + pushkey: "Pushkey", + data: { url: "URL" }, + }, + }, servernotices: { name: "Server Notices", send: "Send server notices", @@ -212,5 +265,12 @@ export default { 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.', }, }, + user_media_statistics: { + name: "Users' media", + fields: { + media_count: "Media count", + media_length: "Media length", + }, + }, }, }; diff --git a/src/synapse/authProvider.js b/src/synapse/authProvider.js index 30ce600..972d599 100644 --- a/src/synapse/authProvider.js +++ b/src/synapse/authProvider.js @@ -10,6 +10,7 @@ const authProvider = { type: "m.login.password", user: username, password: password, + initial_device_display_name: "Synapse Admin", }), }; @@ -30,8 +31,26 @@ const authProvider = { }, // called when the user clicks on the logout button logout: () => { - console.log("logout "); - localStorage.removeItem("access_token"); + console.log("logout"); + + const logout_api_url = + localStorage.getItem("base_url") + "/_matrix/client/r0/logout"; + const access_token = localStorage.getItem("access_token"); + + const options = { + method: "POST", + user: { + authenticated: true, + token: `Bearer ${access_token}`, + }, + }; + + if (typeof access_token === "string") { + fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => { + localStorage.removeItem("access_token"); + localStorage.removeItem("device_id"); + }); + } return Promise.resolve(); }, // called when the API returns an error @@ -46,7 +65,7 @@ const authProvider = { checkAuth: () => { const access_token = localStorage.getItem("access_token"); console.log("checkAuth " + access_token); - return typeof access_token == "string" + return typeof access_token === "string" ? Promise.resolve() : Promise.reject(); }, diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 6d5f78d..d928462 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -72,13 +72,24 @@ const resourceMap = { method: "POST", }), }, + reports: { + path: "/_synapse/admin/v1/event_reports", + map: er => ({ + ...er, + id: er.id, + }), + data: "event_reports", + total: json => json.total, + }, devices: { map: d => ({ ...d, id: d.device_id, }), data: "devices", - total: json => json.devices.length, + total: json => { + return json.total; + }, reference: id => ({ endpoint: `/_synapse/admin/v2/users/${id}/devices`, }), @@ -102,7 +113,52 @@ const resourceMap = { endpoint: `/_synapse/admin/v1/rooms/${id}/members`, }), data: "members", - total: json => json.members.length, + total: json => { + return json.total; + }, + }, + pushers: { + map: p => ({ + ...p, + id: p.pushkey, + }), + reference: id => ({ + endpoint: `/_synapse/admin/v1/users/${id}/pushers`, + }), + data: "pushers", + total: json => { + return json.total; + }, + }, + joined_rooms: { + map: jr => ({ + id: jr, + }), + reference: id => ({ + endpoint: `/_synapse/admin/v1/users/${id}/joined_rooms`, + }), + data: "joined_rooms", + total: json => { + return json.total; + }, + }, + users_media: { + map: um => ({ + ...um, + id: um.media_id, + }), + reference: id => ({ + endpoint: `/_synapse/admin/v1/users/${id}/media`, + }), + data: "media", + total: json => { + return json.total; + }, + delete: params => ({ + endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( + "home_server" + )}/${params.id}`, + }), }, servernotices: { map: n => ({ id: n.event_id }), @@ -118,6 +174,17 @@ const resourceMap = { method: "POST", }), }, + user_media_statistics: { + path: "/_synapse/admin/v1/statistics/users/media", + map: usms => ({ + ...usms, + id: usms.user_id, + }), + data: "users", + total: json => { + return json.total; + }, + }, }; function filterNullValues(key, value) { @@ -201,6 +268,10 @@ const dataProvider = { console.log("getManyReference " + resource); const { page, perPage } = params.pagination; const from = (page - 1) * perPage; + const query = { + from: from, + limit: perPage, + }; const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) return Promise.reject(); @@ -208,7 +279,7 @@ const dataProvider = { const res = resourceMap[resource]; const ref = res["reference"](params.id); - const endpoint_url = homeserver + ref.endpoint; + const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`; return jsonClient(endpoint_url).then(({ headers, json }) => ({ data: json[res.data].map(res.map), diff --git a/yarn.lock b/yarn.lock index cad6848..0a394ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2643,10 +2643,10 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== bn.js@^5.0.0, bn.js@^5.1.1: version "5.1.3" @@ -2717,7 +2717,7 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= @@ -4207,17 +4207,17 @@ electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.591: integrity sha512-nLO2Wd2yU42eSoNJVQKNf89CcEGqeFZd++QsnN2XIgje1s/19AgctfjLIbPORlvcCO8sYjLwX4iUgDdusOY8Sg== elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" + bn.js "^4.11.9" + brorand "^1.1.0" hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" emoji-regex@^7.0.1, emoji-regex@^7.0.2: version "7.0.3" @@ -5528,7 +5528,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= @@ -5855,9 +5855,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== inquirer@7.0.4: version "7.0.4" @@ -7468,7 +7468,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: +minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=