From 270d48607ac6e3e9d93dd9d67cfb79198d149852 Mon Sep 17 00:00:00 2001 From: Elshad Shirinov Date: Thu, 12 Nov 2020 14:56:17 +0100 Subject: [PATCH] Allow server admin to create rooms for other users and change user power levels Change-Id: Ie96e9e0102454835536b6f42d247f9e714e28480 --- src/App.js | 3 +- src/components/rooms.js | 284 +++++++++++++++++++++++++++++++++++- src/i18n/de.js | 6 + src/i18n/en.js | 6 + src/synapse/dataProvider.js | 44 +++++- 5 files changed, 333 insertions(+), 10 deletions(-) diff --git a/src/App.js b/src/App.js index a70176a..ccc3e5e 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot"; import authProvider from "./synapse/authProvider"; import dataProvider from "./synapse/dataProvider"; import { UserList, UserCreate, UserEdit } from "./components/users"; -import { RoomList, RoomCreate, RoomShow } from "./components/rooms"; +import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms"; import LoginPage from "./components/LoginPage"; import UserIcon from "@material-ui/icons/Group"; import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList"; @@ -45,6 +45,7 @@ const App = () => ( list={RoomList} create={RoomCreate} show={RoomShow} + edit={RoomEdit} icon={RoomIcon} /> diff --git a/src/components/rooms.js b/src/components/rooms.js index 9484ac7..bfd3921 100644 --- a/src/components/rooms.js +++ b/src/components/rooms.js @@ -1,10 +1,14 @@ import React from "react"; import { connect } from "react-redux"; +import { Route, Link } from "react-router-dom"; import { AutocompleteArrayInput, + AutocompleteInput, BooleanInput, BooleanField, + Button, Create, + Edit, Datagrid, Filter, FormTab, @@ -12,24 +16,39 @@ import { Pagination, ReferenceArrayInput, ReferenceField, + ReferenceInput, ReferenceManyField, SelectField, Show, + SimpleForm, Tab, TabbedForm, TabbedShowLayout, TextField, TextInput, + Toolbar, + useDataProvider, + useRefresh, useTranslate, } from "react-admin"; import get from "lodash/get"; -import { Tooltip, Typography, Chip } from "@material-ui/core"; +import { + Tooltip, + Typography, + Chip, + Drawer, + styled, + withStyles, + Select, + MenuItem, +} from "@material-ui/core"; import HttpsIcon from "@material-ui/icons/Https"; import NoEncryptionIcon from "@material-ui/icons/NoEncryption"; import PageviewIcon from "@material-ui/icons/Pageview"; import UserIcon from "@material-ui/icons/Group"; import ViewListIcon from "@material-ui/icons/ViewList"; import VisibilityIcon from "@material-ui/icons/Visibility"; +import ContentSave from "@material-ui/icons/Save"; const RoomPagination = props => ( @@ -61,12 +80,13 @@ const EncryptionField = ({ source, record = {}, emptyText }) => { ); }; -const validateDisplayName = fieldval => - fieldval === undefined +const validateDisplayName = fieldval => { + return fieldval == null ? "synapseadmin.rooms.room_name_required" : fieldval.length === 0 ? "synapseadmin.rooms.room_name_required" : undefined; +}; function approximateAliasLength(alias, homeserver) { /* TODO maybe handle punycode in homeserver name */ @@ -141,6 +161,16 @@ export const RoomCreate = props => ( validate={validateAlias} placeholder="#" /> + ({ user_id: searchText })} + > + + { ); }; +// Explicitely passing "to" prop +// Toolbar adds all kinds of unsupported props to its children :( +const StyledLink = styles => { + const Styled = styled(Link)(styles); + return ({ to, children }) => {children}; +}; + +const RoomMemberEditToolbar = ({ backLink, translate, onSave, ...props }) => { + const SaveLink = StyledLink({ + textDecoration: "none", + }); + const CancelLink = StyledLink({ + textDecoration: "none", + marginLeft: "1em", + }); + const SaveIcon = styled(ContentSave)({ + width: "1rem", + marginRight: "0.25em", + }); + + return ( + + + + + + + + + ); +}; + +const RoomMemberIdField = ({ memberId, data = {} }) => { + const value = get(data[memberId], "id"); + + return ( + + {value} + + ); +}; + +const RoomMemberRoleInput = ({ memberId, data = {}, translate, onChange }) => { + const roleValue = get(data[memberId], "role"); + const [role, setRole] = React.useState(roleValue); + + React.useEffect(() => { + onChange(roleValue); + }, [onChange, roleValue]); + + return ( + + + + ); +}; + +const RoomMemberEdit = ({ backLink, memberId, ...props }) => { + const translate = useTranslate(); + const refresh = useRefresh(); + const dataProvider = useDataProvider(); + + const [role, setRole] = React.useState(); + + const { id } = props; + + return ( + + { + dataProvider + .update("rooms", { + data: { + id, + member_roles: [{ member_id: memberId, role }], + }, + }) + .then(() => { + refresh(); + }); + }} + /> + } + > + + + + + + + + + ); +}; + +const drawerStyles = { + paper: { + width: 300, + }, +}; +const StyledDrawer = withStyles(drawerStyles)(({ classes, ...props }) => ( + +)); + +export const RoomEdit = props => { + const translate = useTranslate(); + + return ( + + }> + + }> + ({ user_id: searchText })} + > + + + + + + `/rooms/${encodeURIComponent( + record.parentId + )}/${encodeURIComponent(id)}` + } + > + + + + + + + + + + + + {({ match }) => { + const isMatch = !!match && !!match.params; + + return ( + + {isMatch ? ( + + ) : ( +
+ )} + + ); + }} + + + ); +}; + export const RoomShow = props => { const translate = useTranslate(); return ( @@ -227,6 +493,18 @@ export const RoomShow = props => { > + diff --git a/src/i18n/de.js b/src/i18n/de.js index 085dcfa..a47afd5 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -70,6 +70,7 @@ export default { display_name: "Gerätename", last_seen_ts: "Zeitstempel", last_seen_ip: "IP-Adresse", + role: "Rolle", }, type: { default: "Standard", @@ -83,6 +84,11 @@ export default { action: { erase: "Lösche Benutzerdaten", }, + roles: { + user: "Nutzer", + mod: "Moderator", + admin: "Administrator", + }, }, rooms: { name: "Raum |||| Räume", diff --git a/src/i18n/en.js b/src/i18n/en.js index 0c4ee44..e648dd5 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -69,6 +69,7 @@ export default { display_name: "Device name", last_seen_ts: "Timestamp", last_seen_ip: "IP address", + role: "Role", }, type: { default: "Standard", @@ -82,6 +83,11 @@ export default { action: { erase: "Erase user data", }, + roles: { + user: "User", + mod: "Moderator", + admin: "Administrator", + }, }, rooms: { name: "Room |||| Rooms", diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index bdff9b7..1a71196 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -25,6 +25,16 @@ const mxcUrlToHttp = mxcUrl => { return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; }; +const powerLevelToRole = powerLevel => + powerLevel < 100 ? (powerLevel < 50 ? "user" : "mod") : "admin"; + +const POWER_LEVELS = { + admin: 100, + mod: 50, + user: 0, +}; +const roleToPowerLevel = role => POWER_LEVELS[role] || 0; + const resourceMap = { users: { path: "/_synapse/admin/v2/users", @@ -66,8 +76,9 @@ const resourceMap = { data: "rooms", total: json => json.total_rooms, create: data => ({ - endpoint: "/_matrix/client/r0/createRoom", + endpoint: "/_synapse/admin/v1/rooms", body: { + owner: data.owner, name: data.name, room_alias_name: data.canonical_alias, visibility: data.public ? "public" : "private", @@ -89,6 +100,20 @@ const resourceMap = { }, method: "POST", }), + delete: params => ({ + endpoint: `/_synapse/admin/v1/rooms/${params.id}/delete`, + body: { erase: true }, + method: "POST", + }), + transformBeforeUpdate: data => { + return { + ...data, + member_roles: (data.member_roles || []).map(member => ({ + member_id: member.member_id, + power_level: roleToPowerLevel(member.role), + })), + }; + }, }, devices: { map: d => ({ @@ -113,10 +138,11 @@ const resourceMap = { }, room_members: { map: m => ({ - id: m, + role: powerLevelToRole(m.power_level), + id: m.user_id, }), reference: id => ({ - endpoint: `/_synapse/admin/v1/rooms/${id}/members`, + endpoint: `/_synapse/admin/v1/rooms/${id}/power_levels`, }), data: "members", }, @@ -183,7 +209,7 @@ const dataProvider = { }, getOne: (resource, params) => { - console.log("getOne " + resource); + console.log("getOne " + resource, params); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) return Promise.reject(); @@ -222,7 +248,10 @@ const dataProvider = { const endpoint_url = homeserver + ref.endpoint; return jsonClient(endpoint_url).then(({ headers, json }) => ({ - data: json[res.data].map(res.map), + data: json[res.data].map(res.map).map(element => ({ + ...element, + parentId: params.id, + })), })); }, @@ -233,10 +262,13 @@ const dataProvider = { const res = resourceMap[resource]; + const transform = res.transformBeforeUpdate || (x => x); + const data = transform(params.data); + const endpoint_url = homeserver + res.path; return jsonClient(`${endpoint_url}/${params.data.id}`, { method: "PUT", - body: JSON.stringify(params.data, filterNullValues), + body: JSON.stringify(data, filterNullValues), }).then(({ json }) => ({ data: res.map(json), }));