Compare commits

..

18 Commits

Author SHA1 Message Date
Manuel Stahl ab649fbf70 Merge branch 'master' into amp.chat
Change-Id: I6141964157bcb7218e2e6368a3ca8d20eb4855e9
2020-05-05 13:44:06 +02:00
Timo Paulssen 880223e5de Offer invitations in room creation
Turns the "Create Room" form into a tabbed form with
tabs that mimic the room display. In the "Members" tab
an AutocompleteArrayInput allows selecting multiple
users by their displayname.

The displayname is also what is displayed ìn the
invitations list.

Creating the room immediately sends out the invitations
as well.

Change-Id: I3915144114ffe4c629848363c9cb7917634d04d8
2020-04-28 19:36:15 +02:00
Manuel Stahl 76fdc80e3e Merge branch 'master' into amp.chat
Change-Id: I08a7a34e041993c29bb12fff52d07534374cda4e
2020-04-28 16:35:40 +02:00
Manuel Stahl 375649756f Add page to show room details
Change-Id: Iec4f402c4322d775cc14c567069a3295ad383b44
2020-04-28 16:30:47 +02:00
Manuel Stahl 662735a91f Adapt for changes in v2/users API
Change-Id: I927b81882fa20e5b3de3d9fc216e2136f7036bba
2020-04-28 16:30:47 +02:00
Manuel Stahl 0823976edd Cleanup room creation
Change-Id: Ieb5189513d21606f8d0bea5692112350a68f2e14
2020-04-23 16:31:26 +02:00
Michael Albert d3cd2e9e33 Fix localStorage entry of homeserver url
Change-Id: I206e3b4428df1f51d4281ad4db26bd64bdffb85d
2020-04-21 17:42:43 +02:00
Timo Paulssen 24abcd4e4a Normalize alias a little, display initial sigil
turns all whitespace into underscores, shows leading
sigil if the alias is non-empty, so the user doesn't get
confused about whether they have to input a # or not.

Change-Id: Ic81e69cc3f0074d63a67b976c9bda32f8de025de
2020-04-20 19:31:33 +02:00
Timo Paulssen c1c32e3268 Offer "alias" field in room create form
Tries its best to not allow aliases that are too
long (full alias including leading #, : in the
middle, and homeserver domain name must not exceed
255 bytes.

Change-Id: I1e784a94cb599eca7e30736d666b20e37aad5444
2020-04-20 19:31:33 +02:00
Timo Paulssen ca15435625 Offer room creation form
A choice of public or private is offered, which maps to matrix'
visibility parameter. A name can also be provided.

Change-Id: I34d99acbc4624a9ed54ca6f6609573d5fc1049da
2020-04-20 19:31:33 +02:00
Michael Albert e9c3901b68 Merge branch 'master' into amp.chat
Change-Id: Iac4e56401aab3f7f39b623b617990ec1952f8cd0
2020-04-20 16:57:23 +02:00
Michael Albert 7aec6f9369 Allow searching for users
Change-Id: Icf4a3b05b24c66971f55b22e7540a1dc904a3a92
2020-04-20 11:22:06 +00:00
Michael Albert d2a3f07a59 Fix QR code creation
Change-Id: Ib6bbd1be6d4dca1f617043c3c2338924b2321ea3
2020-04-20 12:15:52 +02:00
Manuel Stahl bf7867f106 Merge branch 'master' into amp.chat
Change-Id: I45b7a6db27456aaa2eca66b406cdaa49e492e61e
2020-02-11 18:56:53 +01:00
Michael Albert f0e32abc4f Fix QR code creation
Change-Id: If05856a6fdafa43a93c6b57963820710db188d42
2020-02-11 17:35:19 +00:00
Michael Albert 61b1580735 Fix redirect after create/edit user
Change-Id: Icdb797bf6b1a47cbeff901b1952672584b2e8e8f
2020-02-11 17:34:32 +00:00
Manuel Stahl 0f7e4c1909 Create PDF with QR code on user create/edit
Change-Id: Ib89b68e956d96002ddbf6ac5ddcaea73b5b3e3fb
2020-02-10 13:10:08 +01:00
Michael Albert c9bce409d2 Prefill user_id and password on user creation
Change-Id: I3f604f38c1842f155f3b39da20ba45992ba522be
2020-02-10 13:10:08 +01:00
18 changed files with 1793 additions and 2166 deletions
+1
View File
@@ -1,4 +1,5 @@
# Exclude a bunch of stuff which can make the build context a larger than it needs to be # Exclude a bunch of stuff which can make the build context a larger than it needs to be
.git/
tests/ tests/
build/ build/
lib/ lib/
+2 -24
View File
@@ -4,28 +4,6 @@
This project is built using [react-admin](https://marmelab.com/react-admin/). This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.15.0 for all functions to work as expected! Use `yarn install` after cloning this repo.
## Step-By-Step install: Use `yarn start` to launch the webserver.
You have two options:
1. Download the source code from github and run using nodejs
2. Run the Docker container
Steps for 1):
- make sure you have installed the following: git, yarn, nodejs
- download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git`
- change into downloaded directory: `cd synapse-admin`
- download dependencies: `yarn install`
- start web server: `yarn start`
Steps for 2):
- run the Docker container: `docker run -p 8080:80 awesometechnologies/synapse-admin`
- browse to http://localhost:8080
## Screenshots
![Screenshots](./screenshots.jpg)
+8 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.2.1", "version": "0.1.0",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH", "author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -12,7 +12,7 @@
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.1.1", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^10.0.2", "@testing-library/react": "^10.0.2",
"@testing-library/user-event": "^12.0.11", "@testing-library/user-event": "^10.0.1",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-react-16": "^1.15.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
@@ -21,16 +21,19 @@
"prettier": "^2.0.0" "prettier": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@progress/kendo-drawing": "^1.6.0",
"@progress/kendo-react-pdf": "^3.10.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-german": "^2.1.2", "ra-language-german": "^2.1.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-admin": "^3.7.0", "react-admin": "^3.4.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "^3.4.1" "react-scripts": "^3.4.1"
}, },
"scripts": { "scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start", "start": "react-scripts start",
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build", "build": "react-scripts build",
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix", "fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other", "fix": "yarn fix:code && yarn fix:other",
Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

+1 -8
View File
@@ -38,12 +38,5 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<footer
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
Synapse-Admin <b>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
</a>
</footer>
</body> </body>
</html> </html>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

+13 -3
View File
@@ -4,12 +4,14 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users"; import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList, RoomShow } from "./components/rooms"; import { RoomList, RoomCreate, RoomShow } from "./components/rooms";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group"; import UserIcon from "@material-ui/icons/Group";
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList"; import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
import ShowUserPdf from "./components/ShowUserPdf";
import { Route } from "react-router-dom";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {
@@ -27,6 +29,9 @@ const App = () => (
authProvider={authProvider} authProvider={authProvider}
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]}
> >
<Resource <Resource
name="users" name="users"
@@ -35,9 +40,14 @@ const App = () => (
edit={UserEdit} edit={UserEdit}
icon={UserIcon} icon={UserIcon}
/> />
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} /> <Resource
name="rooms"
list={RoomList}
create={RoomCreate}
show={RoomShow}
icon={RoomIcon}
/>
<Resource name="connections" /> <Resource name="connections" />
<Resource name="devices" />
<Resource name="servernotices" /> <Resource name="servernotices" />
</Admin> </Admin>
); );
+2 -35
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState } from "react";
import { import {
fetchUtils, fetchUtils,
FormDataConsumer, FormDataConsumer,
@@ -29,7 +29,7 @@ const useStyles = makeStyles(theme => ({
main: { main: {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
minHeight: "calc(100vh - 1em)", minHeight: "100vh",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)", background: "url(./images/floating-cogs.svg)",
@@ -40,7 +40,6 @@ const useStyles = makeStyles(theme => ({
card: { card: {
minWidth: "30em", minWidth: "30em",
marginTop: "6em", marginTop: "6em",
marginBottom: "6em",
}, },
avatar: { avatar: {
margin: "1em", margin: "1em",
@@ -65,12 +64,6 @@ const useStyles = makeStyles(theme => ({
actions: { actions: {
padding: "0 1em 1em 1em", padding: "0 1em 1em 1em",
}, },
serverVersion: {
color: "#9e9e9e",
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginBottom: "1em",
marginLeft: "0.5em",
},
})); }));
const LoginPage = ({ theme }) => { const LoginPage = ({ theme }) => {
@@ -144,7 +137,6 @@ const LoginPage = ({ theme }) => {
const UserData = ({ formData }) => { const UserData = ({ formData }) => {
const form = useForm(); const form = useForm();
const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => { const handleUsernameChange = _ => {
if (formData.base_url) return; if (formData.base_url) return;
@@ -165,30 +157,6 @@ const LoginPage = ({ theme }) => {
} }
}; };
useEffect(
_ => {
if (
!formData.base_url ||
!formData.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/)
)
return;
const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`;
fetchUtils
.fetchJson(versionUrl, { method: "GET" })
.then(({ json }) => {
setServerVersion(
`${translate("synapseadmin.auth.server_version")} ${
json["server_version"]
}`
);
})
.catch(_ => {
setServerVersion("");
});
},
[formData.base_url]
);
return ( return (
<div> <div>
<div className={classes.input}> <div className={classes.input}>
@@ -221,7 +189,6 @@ const LoginPage = ({ theme }) => {
fullWidth fullWidth
/> />
</div> </div>
<div className={classes.serverVersion}>{serverVersion}</div>
</div> </div>
); );
}; };
+35
View File
@@ -0,0 +1,35 @@
import React, { useCallback } from "react";
import { SaveButton, useCreate, useRedirect, useNotify } from "react-admin";
const SaveQrButton = props => {
const [create] = useCreate("users");
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath } = props;
const handleSave = useCallback(
(values, redirect) => {
create(
{
payload: { data: { ...values } },
},
{
onSuccess: ({ data: newRecord }) => {
notify("ra.notification.created", "info", {
smart_count: 1,
});
redirectTo(redirect, basePath, newRecord.id, {
password: values.password,
...newRecord,
});
},
}
);
},
[create, notify, redirectTo, basePath]
);
return <SaveButton {...props} onSave={handleSave} />;
};
export default SaveQrButton;
-48
View File
@@ -7,10 +7,8 @@ import {
Toolbar, Toolbar,
required, required,
useCreate, useCreate,
useMutation,
useNotify, useNotify,
useTranslate, useTranslate,
useUnselectAll,
} from "react-admin"; } from "react-admin";
import MessageIcon from "@material-ui/icons/Message"; import MessageIcon from "@material-ui/icons/Message";
import IconCancel from "@material-ui/icons/Cancel"; import IconCancel from "@material-ui/icons/Cancel";
@@ -100,49 +98,3 @@ export const ServerNoticeButton = ({ record }) => {
</Fragment> </Fragment>
); );
}; };
export const ServerNoticeBulkButton = ({ selectedIds }) => {
const [open, setOpen] = useState(false);
const notify = useNotify();
const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleSend = values => {
createMany(
{
type: "createMany",
resource: "servernotices",
payload: { ids: selectedIds, data: values },
},
{
onSuccess: ({ data }) => {
notify("resources.servernotices.action.send_success");
unselectAll("users");
handleDialogClose();
},
onFailure: error =>
notify("resources.servernotices.action.send_failure", "error"),
}
);
};
return (
<Fragment>
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={loading}
>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={handleDialogClose}
onSend={handleSend}
/>
</Fragment>
);
};
+132
View File
@@ -0,0 +1,132 @@
import React from "react";
import { Title, Button } from "react-admin";
import { makeStyles } from "@material-ui/core/styles";
import { PDFExport } from "@progress/kendo-react-pdf";
import QRCode from "qrcode.react";
function xor(a, b) {
var res = "";
for (var i = 0; i < a.length; i++) {
res += String.fromCharCode(a.charCodeAt(i) ^ b.charCodeAt(i % b.length));
}
return res;
}
function calculateQrString(serverUrl, username, password) {
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
var urlString = "user=" + username + "&password=" + password;
urlString = xor(urlString, magicString); // xor with magic string
urlString = btoa(urlString); // to base64
return serverUrl + "/#" + urlString;
}
const ShowUserPdf = props => {
const useStyles = makeStyles(theme => ({
page: {
height: 800,
width: 566,
padding: "none",
backgroundColor: "white",
boxShadow: "5px 5px 5px black",
margin: "auto",
overflowX: "hidden",
overflowY: "hidden",
},
header: {
height: 144,
width: 534,
marginLeft: 32,
marginTop: 15,
},
name: {
width: 233,
fontSize: 40,
float: "left",
marginTop: 15,
},
logo: {
width: 90,
marginTop: 20,
marginRight: 32,
float: "left",
},
code: {
marginLeft: 330,
marginTop: 86,
},
qr: {
marginRight: 40,
float: "right",
},
note: {
fontSize: 18,
marginTop: 100,
marginLeft: 32,
marginRight: 32,
},
}));
const classes = useStyles();
var resume;
const exportPDF = () => {
resume.save();
};
var qrCode = "";
var displayname = "";
if (
props.location.state &&
props.location.state.id &&
props.location.state.password
) {
const { id, password } = props.location.state;
const username = id.substring(1, id.indexOf(":"));
const serverUrl = "https://" + id.substring(id.indexOf(":") + 1);
const qrString = calculateQrString(serverUrl, username, password);
qrCode = <QRCode value={qrString} size={128} />;
displayname = props.location.state.displayname;
}
return (
<div>
<Title title="PDF" />
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
<PDFExport
paperSize={"A4"}
fileName="User.pdf"
title=""
subject=""
keywords=""
ref={r => (resume = r)}
>
<div className={classes.page}>
<div className={classes.code}>Ihr persönlicher Anmeldecode:</div>
<div className={classes.header}>
<div className={classes.name}>{displayname}</div>
<img className={classes.logo} alt="Logo" src="images/logo.png" />
<div className={classes.qr}>{qrCode}</div>
</div>
<div className={classes.note}>
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
notieren:
<br />
<br />
<br />
<hr />
</div>
</div>
</PDFExport>
</div>
);
};
export default ShowUserPdf;
-89
View File
@@ -1,89 +0,0 @@
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>
);
};
+137 -194
View File
@@ -1,224 +1,167 @@
import React from "react"; import React from "react";
import { connect } from "react-redux";
import { import {
BooleanField, AutocompleteArrayInput,
BooleanInput,
Create,
Datagrid, Datagrid,
Filter, FormTab,
List, List,
Pagination, Pagination,
SelectField, ReferenceArrayField,
ReferenceArrayInput,
Show, Show,
Tab, Tab,
TabbedForm,
TabbedShowLayout, TabbedShowLayout,
TextField, TextField,
TextInput,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get";
import { Tooltip, Typography, Chip } 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 ViewListIcon from "@material-ui/icons/ViewList"; import ViewListIcon from "@material-ui/icons/ViewList";
import VisibilityIcon from "@material-ui/icons/Visibility"; import UserIcon from "@material-ui/icons/Group";
const RoomPagination = props => ( const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const EncryptionField = ({ source, record = {}, emptyText }) => { export const RoomList = props => (
const translate = useTranslate(); <List {...props} pagination={<RoomPagination />}>
const value = get(record, source); <Datagrid rowClick="show">
let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true"; <TextField source="room_id" />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="joined_members" />
</Datagrid>
</List>
);
if (value === false || value === true) { const validateDisplayName = fieldval =>
return ( fieldval === undefined
<Typography component="span" variant="body2"> ? "synapseadmin.rooms.room_name_required"
<Tooltip title={translate(ariaLabel, { _: ariaLabel })}> : fieldval.length === 0
{value === true ? ( ? "synapseadmin.rooms.room_name_required"
<HttpsIcon data-testid="true" htmlColor="limegreen" /> : undefined;
) : (
<NoEncryptionIcon data-testid="false" color="error" /> function approximateAliasLength(alias, homeserver) {
)} /* TODO maybe handle punycode in homeserver name */
</Tooltip>
</Typography> var te;
);
// Support for TextEncoder is quite widespread, but the polyfill is
// pretty large; We will only underestimate the size with the regular
// length attribute of String, so we never prevent the user from using
// an alias that is short enough for the server, but too long for our
// heuristic.
try {
te = new TextEncoder();
} catch (err) {
if (err instanceof ReferenceError) {
te = undefined;
}
} }
return ( const aliasLength = te === undefined ? alias.length : te.encode(alias).length;
<Typography component="span" variant="body2">
{emptyText} return "#".length + aliasLength + ":".length + homeserver.length;
</Typography> }
);
const validateAlias = fieldval => {
if (fieldval === undefined) {
return undefined;
}
const homeserver = localStorage.getItem("home_server");
if (approximateAliasLength(fieldval, homeserver) > 255) {
return "synapseadmin.rooms.alias_too_long";
}
}; };
const removeLeadingWhitespace = fieldVal =>
fieldVal === undefined ? undefined : fieldVal.trimStart();
const replaceAllWhitespace = fieldVal =>
fieldVal === undefined ? undefined : fieldVal.replace(/\s/, "_");
const removeLeadingSigil = fieldVal =>
fieldVal === undefined
? undefined
: fieldVal.startsWith("#")
? fieldVal.substr(1)
: fieldVal;
const validateHasAliasIfPublic = formdata => {
let errors = {};
if (formdata.public) {
if (
formdata.canonical_alias === undefined ||
formdata.canonical_alias.trim().length === 0
) {
errors.canonical_alias = "synapseadmin.rooms.alias_required_if_public";
}
}
return errors;
};
export const RoomCreate = props => (
<Create {...props}>
<TabbedForm validate={validateHasAliasIfPublic}>
<FormTab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
<TextInput
source="name"
parse={removeLeadingWhitespace}
validate={validateDisplayName}
/>
<TextInput
source="canonical_alias"
parse={fv => replaceAllWhitespace(removeLeadingSigil(fv))}
validate={validateAlias}
placeholder="#"
/>
<BooleanInput source="public" label="synapseadmin.rooms.make_public" />
</FormTab>
<FormTab
label="resources.rooms.fields.invite_members"
icon={<UserIcon />}
>
<ReferenceArrayInput
reference="users"
source="invitees"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteArrayInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceArrayInput>
</FormTab>
</TabbedForm>
</Create>
);
const RoomTitle = ({ record }) => { const RoomTitle = ({ record }) => {
const translate = useTranslate(); const translate = useTranslate();
var name = "";
if (record) {
name = record.name !== "" ? record.name : record.id;
}
return ( return (
<span> <span>
{translate("resources.rooms.name", 1)} {name} {translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
</span> </span>
); );
}; };
export const RoomShow = props => (
export const RoomShow = props => { <Show {...props} title={<RoomTitle />}>
const translate = useTranslate(); <TabbedShowLayout>
return ( <Tab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
<Show {...props} title={<RoomTitle />}> <TextField source="id" disabled />
<TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="creator" />
</Tab>
<Tab
label="synapseadmin.rooms.tabs.detail"
icon={<PageviewIcon />}
path="detail"
>
<TextField source="joined_members" />
<TextField source="joined_local_members" />
<TextField source="state_events" />
<TextField source="version" />
<TextField
source="encryption"
emptyText={translate("resources.rooms.enums.unencrypted")}
/>
</Tab>
<Tab
label="synapseadmin.rooms.tabs.permission"
icon={<VisibilityIcon />}
path="permission"
>
<BooleanField source="federatable" />
<BooleanField source="public" />
<SelectField
source="join_rules"
choices={[
{ id: "public", name: "resources.rooms.enums.join_rules.public" },
{ id: "knock", name: "resources.rooms.enums.join_rules.knock" },
{ id: "invite", name: "resources.rooms.enums.join_rules.invite" },
{
id: "private",
name: "resources.rooms.enums.join_rules.private",
},
]}
/>
<SelectField
source="guest_access"
choices={[
{
id: "can_join",
name: "resources.rooms.enums.guest_access.can_join",
},
{
id: "forbidden",
name: "resources.rooms.enums.guest_access.forbidden",
},
]}
/>
<SelectField
source="history_visibility"
choices={[
{
id: "invited",
name: "resources.rooms.enums.history_visibility.invited",
},
{
id: "joined",
name: "resources.rooms.enums.history_visibility.joined",
},
{
id: "shared",
name: "resources.rooms.enums.history_visibility.shared",
},
{
id: "world_readable",
name: "resources.rooms.enums.history_visibility.world_readable",
},
]}
/>
</Tab>
</TabbedShowLayout>
</Show>
);
};
const RoomFilter = ({ ...props }) => {
const translate = useTranslate();
return (
<Filter {...props}>
<Chip
label={translate("resources.rooms.fields.joined_local_members")}
source="joined_local_members"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.state_events")}
source="state_events"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.version")}
source="version"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
<Chip
label={translate("resources.rooms.fields.federatable")}
source="federatable"
defaultValue={false}
style={{ marginBottom: 8 }}
/>
</Filter>
);
};
const FilterableRoomList = ({ ...props }) => {
const filter = props.roomFilters;
const localMembersFilter =
filter && filter.joined_local_members ? true : false;
const stateEventsFilter = filter && filter.state_events ? true : false;
const versionFilter = filter && filter.version ? true : false;
const federateableFilter = filter && filter.federatable ? true : false;
return (
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
filters={<RoomFilter />}
>
<Datagrid rowClick="show">
<EncryptionField
source="is_encrypted"
sortBy="encryption"
label={<HttpsIcon />}
/>
<TextField source="name" /> <TextField source="name" />
<TextField source="joined_members" /> <TextField source="canonical_alias" />
{localMembersFilter && <TextField source="joined_local_members" />} <TextField source="join_rules" />
{stateEventsFilter && <TextField source="state_events" />} <TextField source="guest_access" />
{versionFilter && <TextField source="version" />} </Tab>
{federateableFilter && <BooleanField source="federatable" />} <Tab label="resources.rooms.fields.joined_members" icon={<UserIcon />}>
<BooleanField source="public" /> <ReferenceArrayField reference="users" source="members">
</Datagrid> <Datagrid>
</List> <TextField source="id" />
); <TextField source="displayname" />
}; </Datagrid>
</ReferenceArrayField>
function mapStateToProps(state) { </Tab>
return { </TabbedShowLayout>
roomFilters: state.admin.resources.rooms.list.params.displayedFilters, </Show>
}; );
}
export const RoomList = connect(mapStateToProps)(FilterableRoomList);
+150 -208
View File
@@ -1,8 +1,5 @@
import React, { cloneElement, Fragment } from "react"; import React, { Fragment } from "react";
import Avatar from "@material-ui/core/Avatar";
import PersonPinIcon from "@material-ui/icons/PersonPin"; 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 SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import { import {
ArrayInput, ArrayInput,
@@ -20,11 +17,12 @@ import {
FormTab, FormTab,
BooleanField, BooleanField,
BooleanInput, BooleanInput,
ImageField,
PasswordInput, PasswordInput,
TextField, TextField,
TextInput, TextInput,
SearchInput,
ReferenceField, ReferenceField,
ReferenceManyField,
SelectInput, SelectInput,
BulkDeleteButton, BulkDeleteButton,
DeleteButton, DeleteButton,
@@ -32,65 +30,9 @@ import {
regex, regex,
useTranslate, useTranslate,
Pagination, Pagination,
CreateButton,
ExportButton,
TopToolbar,
sanitizeListRestProps,
} from "react-admin"; } from "react-admin";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import SaveQrButton from "./SaveQrButton";
import { DeviceRemoveButton } from "./devices"; import { ServerNoticeButton } from "./ServerNotices";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles({
small: {
height: "40px",
width: "40px",
},
large: {
height: "120px",
width: "120px",
float: "right",
},
});
const UserListActions = ({
currentSort,
className,
resource,
filters,
displayedFilters,
exporter, // you can hide ExportButton if exporter = (null || false)
filterValues,
permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false
basePath,
selectedIds,
onUnselectItems,
showFilter,
maxResults,
total,
...rest
}) => (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
cloneElement(filters, {
resource,
showFilter,
displayedFilters,
filterValues,
context: "button",
})}
<CreateButton basePath={basePath} />
<ExportButton
disabled={total === 0}
resource={resource}
sort={currentSort}
filter={{ ...filterValues, ...permanentFilter }}
exporter={exporter}
maxResults={maxResults}
/>
</TopToolbar>
);
const UserPagination = props => ( const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@@ -98,6 +40,7 @@ const UserPagination = props => (
const UserFilter = props => ( const UserFilter = props => (
<Filter {...props}> <Filter {...props}>
<SearchInput source="user_id" alwaysOn />
<BooleanInput source="guests" alwaysOn /> <BooleanInput source="guests" alwaysOn />
<BooleanInput <BooleanInput
label="resources.users.fields.show_deactivated" label="resources.users.fields.show_deactivated"
@@ -111,7 +54,6 @@ const UserBulkActionButtons = props => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Fragment> <Fragment>
<ServerNoticeBulkButton {...props} />
<BulkDeleteButton <BulkDeleteButton
{...props} {...props}
label="resources.users.action.erase" label="resources.users.action.erase"
@@ -121,37 +63,94 @@ const UserBulkActionButtons = props => {
); );
}; };
const AvatarField = ({ source, className, record = {} }) => ( export const UserList = props => (
<Avatar src={record[source]} className={className} /> <List
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<ImageField source="avatar_url" title="displayname" />
<TextField source="id" sortable={false} />
<TextField source="displayname" />
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid>
</List>
); );
export const UserList = props => { function generateRandomUser() {
const classes = useStyles(); const homeserver = localStorage.getItem("home_server");
return ( const user_id =
<List "@" +
{...props} Array(8)
filters={<UserFilter />} .fill("0123456789abcdefghijklmnopqrstuvwxyz")
filterDefaultValues={{ guests: true, deactivated: false }} .map(
actions={<UserListActions maxResults={10000} />} x =>
bulkActionButtons={<UserBulkActionButtons />} x[
pagination={<UserPagination />} Math.floor(
> (crypto.getRandomValues(new Uint32Array(1))[0] /
<Datagrid rowClick="edit"> (0xffffffff + 1)) *
<AvatarField x.length
source="avatar_src" )
sortable={false} ]
className={classes.small} )
/> .join("") +
<TextField source="id" sortable={false} /> ":" +
<TextField source="displayname" sortable={false} /> homeserver;
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} /> const password = Array(20)
<BooleanField source="deactivated" sortable={false} /> .fill(
</Datagrid> "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
</List> )
); .map(
x =>
x[
Math.floor(
(crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)) *
x.length
)
]
)
.join("");
return {
id: user_id,
password: password,
};
}
// redirect to the related Author show page
const redirect = (basePath, id, data) => {
return {
pathname: "/showpdf",
state: {
id: data.id,
displayname: data.displayname,
password: data.password,
},
};
}; };
const UserCreateToolbar = props => (
<Toolbar {...props}>
<SaveQrButton
label="synapseadmin.action.save_and_show"
redirect={redirect}
submitOnEnter={true}
/>
<SaveButton
label="synapseadmin.action.save_only"
redirect="list"
submitOnEnter={false}
variant="text"
/>
</Toolbar>
);
// https://matrix.org/docs/spec/appendices#user-identifiers // https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex( const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/, /^@[a-z0-9._=\-/]+:.*/,
@@ -162,7 +161,17 @@ const UserEditToolbar = props => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton submitOnEnter={true} /> <SaveQrButton
label="synapseadmin.action.save_and_show"
redirect={redirect}
submitOnEnter={true}
/>
<SaveButton
label="synapseadmin.action.save_only"
redirect="list"
submitOnEnter={false}
variant="text"
/>
<DeleteButton <DeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
title={translate("resources.users.helper.erase")} title={translate("resources.users.helper.erase")}
@@ -173,8 +182,8 @@ const UserEditToolbar = props => {
}; };
export const UserCreate = props => ( export const UserCreate = props => (
<Create {...props}> <Create record={generateRandomUser()} {...props}>
<SimpleForm> <SimpleForm toolbar={<UserCreateToolbar />}>
<TextInput source="id" autoComplete="off" validate={validateUser} /> <TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" /> <TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" /> <PasswordInput source="password" autoComplete="new-password" />
@@ -199,81 +208,49 @@ const UserTitle = ({ record }) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<span> <span>
{translate("resources.users.name", { {translate("resources.users.name")}{" "}
smart_count: 1,
})}{" "}
{record ? `"${record.displayname}"` : ""} {record ? `"${record.displayname}"` : ""}
</span> </span>
); );
}; };
export const UserEdit = props => { export const UserEdit = props => (
const classes = useStyles(); <Edit {...props} title={<UserTitle />}>
const translate = useTranslate(); <TabbedForm toolbar={<UserEditToolbar />}>
return ( <FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<Edit {...props} title={<UserTitle />}> <TextInput source="id" disabled />
<TabbedForm toolbar={<UserEditToolbar />}> <TextInput source="displayname" />
<FormTab label="resources.users.name" icon={<PersonPinIcon />}> <PasswordInput source="password" autoComplete="new-password" />
<AvatarField <BooleanInput source="admin" />
source="avatar_src" <BooleanInput
sortable={false} source="deactivated"
className={classes.large} helperText="resources.users.helper.deactivate"
/> />
<TextInput source="id" disabled /> <ArrayInput source="threepids">
<TextInput source="displayname" /> <SimpleFormIterator>
<PasswordInput source="password" autoComplete="new-password" /> <SelectInput
<BooleanInput source="admin" /> source="medium"
<BooleanInput choices={[
source="deactivated" { id: "email", name: "resources.users.email" },
helperText="resources.users.helper.deactivate" { id: "msisdn", name: "resources.users.msisdn" },
/> ]}
<DateField />
source="creation_ts_ms" <TextInput source="address" />
showTime </SimpleFormIterator>
options={{ </ArrayInput>
year: "numeric", </FormTab>
month: "2-digit", <FormTab
day: "2-digit", label="resources.connections.name"
hour: "2-digit", icon={<SettingsInputComponentIcon />}
minute: "2-digit", >
second: "2-digit", <ReferenceField reference="connections" source="id" addLabel={false}>
}} <ArrayField
/> source="devices[].sessions[0].connections"
<TextField source="consent_version" /> label="resources.connections.name"
</FormTab>
<FormTab
label="resources.users.threepid"
icon={<ContactMailIcon />}
path="threepid"
>
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<TextInput source="address" />
</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%" }}> <Datagrid style={{ width: "100%" }}>
<TextField source="device_id" sortable={false} /> <TextField source="ip" sortable={false} />
<TextField source="display_name" sortable={false} />
<TextField source="last_seen_ip" sortable={false} />
<DateField <DateField
source="last_seen_ts" source="last_seen"
showTime showTime
options={{ options={{
year: "numeric", year: "numeric",
@@ -285,50 +262,15 @@ export const UserEdit = props => {
}} }}
sortable={false} sortable={false}
/> />
<DeviceRemoveButton /> <TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid> </Datagrid>
</ReferenceManyField> </ArrayField>
</FormTab> </ReferenceField>
<FormTab </FormTab>
label="resources.connections.name" </TabbedForm>
icon={<SettingsInputComponentIcon />} </Edit>
path="connections" );
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
>
<Datagrid style={{ width: "100%" }}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
</TabbedForm>
</Edit>
);
};
+26 -58
View File
@@ -6,23 +6,28 @@ export default {
auth: { auth: {
base_url: "Heimserver URL", base_url: "Heimserver URL",
welcome: "Willkommen bei Synapse-admin", welcome: "Willkommen bei Synapse-admin",
server_version: "Synapse Version",
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'", username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL", url_error: "Keine gültige Matrix Server URL",
}, },
action: {
save_and_show: "Speichern und QR Code erzeugen",
save_only: "Nur speichern",
download_pdf: "PDF speichern",
},
users: { users: {
invalid_user_id: invalid_user_id:
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver", "Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
}, },
rooms: { rooms: {
details: "Raumdetails", details: "Raumdetails",
tabs: { room_name: "Raumname",
basic: "Allgemein", make_public: "Öffentlicher Raum",
members: "Mitglieder", room_name_required: "Muss angegeben werden",
detail: "Details", alias_required_if_public: "Muss für öffentliche Räume angegeben werden.",
permission: "Berechtigungen", alias: "Alias",
}, alias_too_long:
"Darf zusammen mit der Domain des Homeservers 255 bytes nicht überschreiten",
}, },
}, },
resources: { resources: {
@@ -31,13 +36,12 @@ export default {
name: "Benutzer", name: "Benutzer",
email: "E-Mail", email: "E-Mail",
msisdn: "Telefon", msisdn: "Telefon",
threepid: "E-Mail / Telefon",
fields: { fields: {
avatar: "Avatar", avatar: "Avatar",
id: "Benutzer-ID", id: "Benutzer-ID",
name: "Name", name: "Name",
is_guest: "Gast", is_guest: "Gast",
admin: "Server Administrator", admin: "Admin",
deactivated: "Deaktiviert", deactivated: "Deaktiviert",
guests: "Zeige Gäste", guests: "Zeige Gäste",
show_deactivated: "Zeige deaktivierte Benutzer", show_deactivated: "Zeige deaktivierte Benutzer",
@@ -45,17 +49,9 @@ export default {
displayname: "Anzeigename", displayname: "Anzeigename",
password: "Passwort", password: "Passwort",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Adresse", 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: { helper: {
deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.",
@@ -72,36 +68,8 @@ export default {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
joined_local_members: "Lokale Mitglieder", invite_members: "Mitglieder einladen",
state_events: "Ereignisse", invitees: "Einladungen",
version: "Version",
is_encrypted: "Verschlüsselt",
encryption: "Verschlüsselungs-Algorithmus",
federatable: "Fö­de­rierbar",
public: "Öffentlich",
creator: "Ersteller",
join_rules: "Beitrittsregeln",
guest_access: "Gastzugriff",
history_visibility: "Historie-Sichtbarkeit",
},
enums: {
join_rules: {
public: "Öffentlich",
knock: "Auf Anfrage",
invite: "Nur auf Einladung",
private: "Privat",
},
guest_access: {
can_join: "Gäste können beitreten",
forbidden: "Gäste können nicht beitreten",
},
history_visibility: {
invited: "Ab Einladung",
joined: "Ab Beitritt",
shared: "Ab Setzen der Einstellung",
world_readable: "Jeder",
},
unencrypted: "Nicht verschlüsselt",
}, },
}, },
connections: { connections: {
@@ -112,17 +80,6 @@ export default {
user_agent: "User Agent", 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: { servernotices: {
name: "Serverbenachrichtigungen", name: "Serverbenachrichtigungen",
send: "Servernachricht versenden", send: "Servernachricht versenden",
@@ -159,4 +116,15 @@ export default {
logged_out: "Abgemeldet", logged_out: "Abgemeldet",
}, },
}, },
ra: {
...germanMessages.ra,
input: {
...germanMessages.ra.input,
password: {
...germanMessages.ra.input.password,
toggle_hidden: "Anzeigen",
toggle_visible: "Verstecken",
},
},
},
}; };
+17 -57
View File
@@ -10,17 +10,24 @@ export default {
protocol_error: "URL has to start with 'http://' or 'https://'", protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL", url_error: "Not a valid Matrix server URL",
}, },
action: {
save_and_show: "Create QR code",
save_only: "Save",
download_pdf: "Download PDF",
},
users: { users: {
invalid_user_id: invalid_user_id:
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver", "Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
}, },
rooms: { rooms: {
tabs: { details: "Room Details",
basic: "Basic", room_name: "Room Name",
members: "Members", make_public: "Make room public",
detail: "Details", room_name_required: "Must be provided",
permission: "Permissions", alias_required_if_public: "Must be provided for a public room",
}, alias: "Alias",
alias_too_long:
"Must not exceed 255 bytes including the domain of the homeserver.",
}, },
}, },
resources: { resources: {
@@ -29,13 +36,12 @@ export default {
name: "User |||| Users", name: "User |||| Users",
email: "Email", email: "Email",
msisdn: "Phone", msisdn: "Phone",
threepid: "Email / Phone",
fields: { fields: {
avatar: "Avatar", avatar: "Avatar",
id: "User-ID", id: "User-ID",
name: "Name", name: "Name",
is_guest: "Guest", is_guest: "Guest",
admin: "Server Administrator", admin: "Admin",
deactivated: "Deactivated", deactivated: "Deactivated",
guests: "Show guests", guests: "Show guests",
show_deactivated: "Show deactivated users", show_deactivated: "Show deactivated users",
@@ -43,17 +49,9 @@ export default {
displayname: "Displayname", displayname: "Displayname",
password: "Password", password: "Password",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Address", 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: { helper: {
deactivate: "Deactivated users cannot be reactivated", deactivate: "Deactivated users cannot be reactivated",
@@ -70,36 +68,9 @@ export default {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Members", joined_members: "Members",
joined_local_members: "local members", invite_members: "Invite Members",
state_events: "State events",
version: "Version", invitees: "Invitations",
is_encrypted: "Encrypted",
encryption: "Encryption",
federatable: "Federatable",
public: "Public",
creator: "Creator",
join_rules: "Join rules",
guest_access: "Guest access",
history_visibility: "History visibility",
},
enums: {
join_rules: {
public: "Public",
knock: "Knock",
invite: "Invite",
private: "Private",
},
guest_access: {
can_join: "Guests can join",
forbidden: "Guests can not join",
},
history_visibility: {
invited: "Since invited",
joined: "Since joined",
shared: "Since shared",
world_readable: "Anyone",
},
unencrypted: "Unencrypted",
}, },
}, },
connections: { connections: {
@@ -110,17 +81,6 @@ export default {
user_agent: "User agent", 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: { servernotices: {
name: "Server Notices", name: "Server Notices",
send: "Send server notices", send: "Send server notices",
+39 -81
View File
@@ -14,29 +14,16 @@ const jsonClient = (url, options = {}) => {
return fetchUtils.fetchJson(url, options); return fetchUtils.fetchJson(url, options);
}; };
const mxcUrlToHttp = mxcUrl => {
const homeserver = localStorage.getItem("base_url");
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
var ret = re.exec(mxcUrl);
console.log("mxcClient " + ret);
if (ret == null) return null;
const serverName = ret[1];
const mediaId = ret[2];
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
};
const resourceMap = { const resourceMap = {
users: { users: {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
map: u => ({ map: u => ({
...u, ...u,
id: u.name, id: u.name,
avatar_src: mxcUrlToHttp(u.avatar_url),
is_guest: !!u.is_guest, is_guest: !!u.is_guest,
admin: !!u.admin, admin: !!u.admin,
deactivated: !!u.deactivated, deactivated: !!u.deactivated,
// need timestamp in milliseconds displayname: u.display_name || u.displayname,
creation_ts_ms: u.creation_ts * 1000,
}), }),
data: "users", data: "users",
total: json => json.total, total: json => json.total,
@@ -45,8 +32,8 @@ const resourceMap = {
body: data, body: data,
method: "PUT", method: "PUT",
}), }),
delete: params => ({ delete: id => ({
endpoint: `/_synapse/admin/v1/deactivate/${params.id}`, endpoint: `/_synapse/admin/v1/deactivate/${id}`,
body: { erase: true }, body: { erase: true },
method: "POST", method: "POST",
}), }),
@@ -56,29 +43,22 @@ const resourceMap = {
map: r => ({ map: r => ({
...r, ...r,
id: r.room_id, id: r.room_id,
alias: r.canonical_alias,
members: r.joined_members,
is_encrypted: !!r.encryption,
federatable: !!r.federatable,
public: !!r.public,
}), }),
data: "rooms", data: "rooms",
total: json => { total: json => json.total_rooms,
return json.total_rooms; create: data => ({
}, endpoint: "/_matrix/client/r0/createRoom",
}, body: {
devices: { name: data.name,
map: d => ({ room_alias_name: data.canonical_alias,
...d, visibility: data.public ? "public" : "private",
id: d.device_id, invite:
}), Array.isArray(data.invitees) && data.invitees.length > 0
data: "devices", ? data.invitees
reference: id => ({ : undefined,
endpoint: `/_synapse/admin/v2/users/${id}/devices`, },
}), method: "POST",
delete: params => ({ })
endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
}),
}, },
connections: { connections: {
path: "/_synapse/admin/v1/whois", path: "/_synapse/admin/v1/whois",
@@ -112,20 +92,11 @@ function filterNullValues(key, value) {
return value; return value;
} }
function getSearchOrder(order) {
if (order === "DESC") {
return "b";
} else {
return "f";
}
}
const dataProvider = { const dataProvider = {
getList: (resource, params) => { getList: (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { user_id, guests, deactivated } = params.filter; const { user_id, guests, deactivated } = params.filter;
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage; const from = (page - 1) * perPage;
const query = { const query = {
from: from, from: from,
@@ -133,8 +104,6 @@ const dataProvider = {
user_id: user_id, user_id: user_id,
guests: guests, guests: guests,
deactivated: deactivated, deactivated: deactivated,
order_by: field,
dir: getSearchOrder(order),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@@ -179,18 +148,30 @@ const dataProvider = {
}, },
getManyReference: (resource, params) => { getManyReference: (resource, params) => {
// FIXME
console.log("getManyReference " + resource); 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"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const ref = res["reference"](params.id); const endpoint_url = homeserver + res.path;
const endpoint_url = homeserver + ref.endpoint; const url = `${endpoint_url}?${stringify(query)}`;
return jsonClient(endpoint_url).then(({ headers, json }) => ({ return jsonClient(url).then(({ headers, json }) => ({
data: json[res.data].map(res.map), data: json,
total: parseInt(headers.get("content-range").split("/").pop(), 10),
})); }));
}, },
@@ -246,29 +227,6 @@ const dataProvider = {
})); }));
}, },
createMany: (resource, params) => {
console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource];
if (!("create" in res)) return Promise.reject();
return Promise.all(
params.ids.map(id => {
params.data.id = id;
const cre = res["create"](params.data);
const endpoint_url = homeserver + cre.endpoint;
return jsonClient(endpoint_url, {
method: cre.method,
body: JSON.stringify(cre.body, filterNullValues),
});
})
).then(responses => ({
data: responses.map(({ json }) => json),
}));
},
delete: (resource, params) => { delete: (resource, params) => {
console.log("delete " + resource); console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
@@ -277,11 +235,11 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if ("delete" in res) { if ("delete" in res) {
const del = res["delete"](params); const del = res["delete"](params.id);
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE", method: del.method,
body: "body" in del ? JSON.stringify(del.body) : null, body: JSON.stringify(del.body),
}).then(({ json }) => ({ }).then(({ json }) => ({
data: json, data: json,
})); }));
@@ -306,11 +264,11 @@ const dataProvider = {
if ("delete" in res) { if ("delete" in res) {
return Promise.all( return Promise.all(
params.ids.map(id => { params.ids.map(id => {
const del = res["delete"]({ ...params, id: id }); const del = res["delete"](id);
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
return jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE", method: del.method,
body: "body" in del ? JSON.stringify(del.body) : null, body: JSON.stringify(del.body),
}); });
}) })
).then(responses => ({ ).then(responses => ({
+1230 -1356
View File
File diff suppressed because it is too large Load Diff