16 Commits

Author SHA1 Message Date
Manuel Stahl
ac0657c428 Add hint about required synapse version
Change-Id: I8152681d695ded78d0f9b9d82bc038aa67723fbc
2020-07-08 09:55:52 +02:00
Michael Albert
ab709aee3e Add screenshots and install instructions
Change-Id: Ibfede12d700924b7b3b01af5f3a3624f21a93862
2020-07-08 07:49:42 +00:00
dklimpel
6da3c8b885 Bugfix translation of plural in UserTitle 2020-07-08 08:50:00 +02:00
Manuel Stahl
ab04db5baf Get avatar_url and displayname from v2/users API
API was added by synapse v1.13.0.

Change-Id: I927b81882fa20e5b3de3d9fc216e2136f7036bba
2020-07-07 18:51:35 +02:00
Dirk Klimpel
8282a3caf8 Move threepids in UserEdit to a separate tab (#51)
Separates information into individual tabs for a better overview.
2020-07-06 12:35:26 +02:00
Michael Albert
2fc75cd6fc Merge pull request #43 from dklimpel/extend_room_list
Extend the room list with further attributes
2020-07-03 19:32:00 +02:00
Dirk Klimpel
3fd615943c Shows encrypted status with icons 2020-07-01 22:36:15 +02:00
dklimpel
aaf1ebb909 Change field creation_ts * 1000 to creation_ts_ms 2020-06-16 10:15:22 +02:00
dklimpel
627f3d2917 Add creation timestamp and consent version to UserEdit
Add information about the user to UserEdit
- creation timestamp
- consent version
2020-06-16 10:15:22 +02:00
dklimpel
168e249296 Bugfix removes the ability to click on individual connections.
If you click on a connection in UserEdit, you will get an empty page.
This solves the problem.
2020-06-16 09:45:43 +02:00
dklimpel
5bdfb80db7 Bugfix sort users by user_id
Users are not sortable by `user_id`.
Set `sortable={false}`.
2020-06-10 13:11:35 +02:00
dependabot[bot]
b7c3684b80 Bump websocket-extensions from 0.1.3 to 0.1.4 (#49)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-09 14:50:58 +02:00
Manuel Stahl
0ada5287d7 Show Synapse server version on login page
Change-Id: Id9b0d2adf83590524069d308f4fe9e5d14147295
2020-05-25 13:29:15 +02:00
dklimpel
3884c50012 Extend the room list with further attributes
Add further attributes:
- is_encrypted
- federatable
- public
- state_events
- version
- joined_local_members

Also add the ability to sort.

API was added by synapse v1.13.0.
2020-05-23 17:43:33 +02:00
Michael Albert
300e22a537 Show version of Synapse-Admin
Change-Id: I354e3f3b3e4f45e5ca72318ce70b66bee433f3d6
2020-05-15 13:28:51 +02:00
Dirk Klimpel
009ce803e2 Add ServerNoticeButton to UserBulkActionButtons (#41)
This adds the button to send "Server Notices" to many users at once.
2020-05-06 09:03:33 +02:00
16 changed files with 392 additions and 600 deletions

View File

@@ -4,6 +4,28 @@
This project is built using [react-admin](https://marmelab.com/react-admin/).
Use `yarn install` after cloning this repo.
It needs at least Synapse v1.13.0 for all functions to work as expected!
Use `yarn start` to launch the webserver.
## Step-By-Step install:
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)

View File

@@ -1,6 +1,6 @@
{
"name": "synapse-admin",
"version": "0.1.0",
"version": "0.2.1",
"description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0",
@@ -21,10 +21,7 @@
"prettier": "^2.0.0"
},
"dependencies": {
"@progress/kendo-drawing": "^1.6.0",
"@progress/kendo-react-pdf": "^3.10.1",
"prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-german": "^2.1.2",
"react": "^16.13.1",
"react-admin": "^3.4.0",
@@ -32,8 +29,8 @@
"react-scripts": "^3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
"build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
"fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

View File

@@ -38,5 +38,12 @@
To begin the development, run `npm start` or `yarn start`.
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>
</html>
</html>

BIN
screenshots.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -4,14 +4,12 @@ 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 } from "./components/rooms";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
import germanMessages from "./i18n/de";
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?
const messages = {
@@ -29,9 +27,6 @@ const App = () => (
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
customRoutes={[
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]}
>
<Resource
name="users"
@@ -40,13 +35,7 @@ const App = () => (
edit={UserEdit}
icon={UserIcon}
/>
<Resource
name="rooms"
list={RoomList}
create={RoomCreate}
show={RoomShow}
icon={RoomIcon}
/>
<Resource name="rooms" list={RoomList} icon={RoomIcon} />
<Resource name="connections" />
<Resource name="servernotices" />
</Admin>

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
fetchUtils,
FormDataConsumer,
@@ -29,7 +29,7 @@ const useStyles = makeStyles(theme => ({
main: {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
minHeight: "calc(100vh - 1em)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
@@ -40,6 +40,7 @@ const useStyles = makeStyles(theme => ({
card: {
minWidth: "30em",
marginTop: "6em",
marginBottom: "6em",
},
avatar: {
margin: "1em",
@@ -64,6 +65,12 @@ const useStyles = makeStyles(theme => ({
actions: {
padding: "0 1em 1em 1em",
},
serverVersion: {
color: "#9e9e9e",
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginBottom: "1em",
marginLeft: "0.5em",
},
}));
const LoginPage = ({ theme }) => {
@@ -137,6 +144,7 @@ const LoginPage = ({ theme }) => {
const UserData = ({ formData }) => {
const form = useForm();
const [serverVersion, setServerVersion] = useState("");
const handleUsernameChange = _ => {
if (formData.base_url) return;
@@ -157,6 +165,30 @@ 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 (
<div>
<div className={classes.input}>
@@ -189,6 +221,7 @@ const LoginPage = ({ theme }) => {
fullWidth
/>
</div>
<div className={classes.serverVersion}>{serverVersion}</div>
</div>
);
};

View File

@@ -1,35 +0,0 @@
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;

View File

@@ -7,8 +7,10 @@ import {
Toolbar,
required,
useCreate,
useMutation,
useNotify,
useTranslate,
useUnselectAll,
} from "react-admin";
import MessageIcon from "@material-ui/icons/Message";
import IconCancel from "@material-ui/icons/Cancel";
@@ -98,3 +100,49 @@ export const ServerNoticeButton = ({ record }) => {
</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>
);
};

View File

@@ -1,132 +0,0 @@
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;

View File

@@ -1,167 +1,68 @@
import React from "react";
import {
AutocompleteArrayInput,
BooleanInput,
Create,
Datagrid,
FormTab,
List,
Pagination,
ReferenceArrayField,
ReferenceArrayInput,
Show,
Tab,
TabbedForm,
TabbedShowLayout,
TextField,
TextInput,
Pagination,
BooleanField,
useTranslate,
} from "react-admin";
import ViewListIcon from "@material-ui/icons/ViewList";
import UserIcon from "@material-ui/icons/Group";
import get from "lodash/get";
import { Tooltip, Typography } from "@material-ui/core";
import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const EncryptionField = ({ source, record = {}, emptyText }) => {
const translate = useTranslate();
const value = get(record, source);
let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true";
if (value === false || value === true) {
return (
<Typography component="span" variant="body2">
<Tooltip title={translate(ariaLabel, { _: ariaLabel })}>
{value === true ? (
<HttpsIcon data-testid="true" htmlColor="limegreen" />
) : (
<NoEncryptionIcon data-testid="false" color="error" />
)}
</Tooltip>
</Typography>
);
}
return (
<Typography component="span" variant="body2">
{emptyText}
</Typography>
);
};
export const RoomList = props => (
<List {...props} pagination={<RoomPagination />}>
<Datagrid rowClick="show">
<TextField source="room_id" />
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
>
<Datagrid>
<EncryptionField
source="is_encrypted"
sortBy="encryption"
label={<HttpsIcon />}
/>
<TextField source="room_id" sortable={false} />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="joined_members" />
<TextField source="joined_local_members" />
<TextField source="state_events" />
<TextField source="version" />
<BooleanField source="federatable" />
<BooleanField source="public" />
</Datagrid>
</List>
);
const validateDisplayName = fieldval =>
fieldval === undefined
? "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 */
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;
}
}
const aliasLength = te === undefined ? alias.length : te.encode(alias).length;
return "#".length + aliasLength + ":".length + homeserver.length;
}
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 translate = useTranslate();
return (
<span>
{translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
</span>
);
};
export const RoomShow = props => (
<Show {...props} title={<RoomTitle />}>
<TabbedShowLayout>
<Tab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
<TextField source="id" disabled />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="join_rules" />
<TextField source="guest_access" />
</Tab>
<Tab label="resources.rooms.fields.joined_members" icon={<UserIcon />}>
<ReferenceArrayField reference="users" source="members">
<Datagrid>
<TextField source="id" />
<TextField source="displayname" />
</Datagrid>
</ReferenceArrayField>
</Tab>
</TabbedShowLayout>
</Show>
);

View File

@@ -1,5 +1,7 @@
import React, { 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 SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import {
ArrayInput,
@@ -17,11 +19,9 @@ import {
FormTab,
BooleanField,
BooleanInput,
ImageField,
PasswordInput,
TextField,
TextInput,
SearchInput,
ReferenceField,
SelectInput,
BulkDeleteButton,
@@ -31,8 +31,20 @@ import {
useTranslate,
Pagination,
} from "react-admin";
import SaveQrButton from "./SaveQrButton";
import { ServerNoticeButton } from "./ServerNotices";
import { ServerNoticeButton, ServerNoticeBulkButton } 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 UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
@@ -40,7 +52,6 @@ const UserPagination = props => (
const UserFilter = props => (
<Filter {...props}>
<SearchInput source="user_id" alwaysOn />
<BooleanInput source="guests" alwaysOn />
<BooleanInput
label="resources.users.fields.show_deactivated"
@@ -54,6 +65,7 @@ const UserBulkActionButtons = props => {
const translate = useTranslate();
return (
<Fragment>
<ServerNoticeBulkButton {...props} />
<BulkDeleteButton
{...props}
label="resources.users.action.erase"
@@ -63,94 +75,36 @@ const UserBulkActionButtons = props => {
);
};
export const UserList = props => (
<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>
const AvatarField = ({ source, className, record = {} }) => (
<Avatar src={record[source]} className={className} />
);
function generateRandomUser() {
const homeserver = localStorage.getItem("home_server");
const user_id =
"@" +
Array(8)
.fill("0123456789abcdefghijklmnopqrstuvwxyz")
.map(
x =>
x[
Math.floor(
(crypto.getRandomValues(new Uint32Array(1))[0] /
(0xffffffff + 1)) *
x.length
)
]
)
.join("") +
":" +
homeserver;
const password = Array(20)
.fill(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
)
.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,
},
};
export const UserList = props => {
const classes = useStyles();
return (
<List
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit">
<AvatarField
source="avatar_src"
sortable={false}
className={classes.small}
/>
<TextField source="id" sortable={false} />
<TextField source="displayname" sortable={false} />
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid>
</List>
);
};
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
const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/,
@@ -161,17 +115,7 @@ const UserEditToolbar = props => {
const translate = useTranslate();
return (
<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"
/>
<SaveButton submitOnEnter={true} />
<DeleteButton
label="resources.users.action.erase"
title={translate("resources.users.helper.erase")}
@@ -182,8 +126,8 @@ const UserEditToolbar = props => {
};
export const UserCreate = props => (
<Create record={generateRandomUser()} {...props}>
<SimpleForm toolbar={<UserCreateToolbar />}>
<Create {...props}>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
@@ -208,69 +152,104 @@ const UserTitle = ({ record }) => {
const translate = useTranslate();
return (
<span>
{translate("resources.users.name")}{" "}
{translate("resources.users.name", {
smart_count: 1,
})}{" "}
{record ? `"${record.displayname}"` : ""}
</span>
);
};
export const UserEdit = props => (
<Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<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="resources.connections.name"
icon={<SettingsInputComponentIcon />}
>
<ReferenceField reference="connections" source="id" addLabel={false}>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
export const UserEdit = props => {
const classes = useStyles();
return (
<Edit {...props} title={<UserTitle />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<AvatarField
source="avatar_src"
sortable={false}
className={classes.large}
/>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<DateField
source="creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
/>
<TextField source="consent_version" />
</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="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
>
<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>
);
<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>
);
};

View File

@@ -6,29 +6,15 @@ export default {
auth: {
base_url: "Heimserver URL",
welcome: "Willkommen bei Synapse-admin",
server_version: "Synapse Version",
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
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: {
invalid_user_id:
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
},
rooms: {
details: "Raumdetails",
room_name: "Raumname",
make_public: "Öffentlicher Raum",
room_name_required: "Muss angegeben werden",
alias_required_if_public: "Muss für öffentliche Räume angegeben werden.",
alias: "Alias",
alias_too_long:
"Darf zusammen mit der Domain des Homeservers 255 bytes nicht überschreiten",
},
},
resources: {
users: {
@@ -36,6 +22,7 @@ export default {
name: "Benutzer",
email: "E-Mail",
msisdn: "Telefon",
threepid: "E-Mail / Telefon",
fields: {
avatar: "Avatar",
id: "Benutzer-ID",
@@ -49,9 +36,12 @@ export default {
displayname: "Anzeigename",
password: "Passwort",
avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium",
threepids: "3PIDs",
address: "Adresse",
creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen",
},
helper: {
deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.",
@@ -68,8 +58,12 @@ export default {
name: "Name",
canonical_alias: "Alias",
joined_members: "Mitglieder",
invite_members: "Mitglieder einladen",
invitees: "Einladungen",
joined_local_members: "lokale Mitglieder",
state_events: "Ereignisse",
version: "Version",
is_encrypted: "Verschlüsselt",
federatable: "Fö­de­riert",
public: "Öffentlich",
},
},
connections: {
@@ -116,15 +110,4 @@ export default {
logged_out: "Abgemeldet",
},
},
ra: {
...germanMessages.ra,
input: {
...germanMessages.ra.input,
password: {
...germanMessages.ra.input.password,
toggle_hidden: "Anzeigen",
toggle_visible: "Verstecken",
},
},
},
};

View File

@@ -10,25 +10,10 @@ export default {
protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL",
},
action: {
save_and_show: "Create QR code",
save_only: "Save",
download_pdf: "Download PDF",
},
users: {
invalid_user_id:
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
},
rooms: {
details: "Room Details",
room_name: "Room Name",
make_public: "Make room public",
room_name_required: "Must be provided",
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: {
users: {
@@ -36,6 +21,7 @@ export default {
name: "User |||| Users",
email: "Email",
msisdn: "Phone",
threepid: "Email / Phone",
fields: {
avatar: "Avatar",
id: "User-ID",
@@ -49,9 +35,12 @@ export default {
displayname: "Displayname",
password: "Password",
avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium",
threepids: "3PIDs",
address: "Address",
creation_ts_ms: "Creation timestamp",
consent_version: "Consent version",
},
helper: {
deactivate: "Deactivated users cannot be reactivated",
@@ -68,9 +57,12 @@ export default {
name: "Name",
canonical_alias: "Alias",
joined_members: "Members",
invite_members: "Invite Members",
invitees: "Invitations",
joined_local_members: "local members",
state_events: "State events",
version: "Version",
is_encrypted: "Encrypted",
federatable: "Federatable",
public: "Public",
},
},
connections: {

View File

@@ -14,16 +14,29 @@ const jsonClient = (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 = {
users: {
path: "/_synapse/admin/v2/users",
map: u => ({
...u,
id: u.name,
avatar_src: mxcUrlToHttp(u.avatar_url),
is_guest: !!u.is_guest,
admin: !!u.admin,
deactivated: !!u.deactivated,
displayname: u.display_name || u.displayname,
// need timestamp in milliseconds
creation_ts_ms: u.creation_ts * 1000,
}),
data: "users",
total: json => json.total,
@@ -43,22 +56,16 @@ const resourceMap = {
map: r => ({
...r,
id: r.room_id,
alias: r.canonical_alias,
members: r.joined_members,
is_encrypted: !!r.encryption,
federatable: !!r.federatable,
public: !!r.public,
}),
data: "rooms",
total: json => json.total_rooms,
create: data => ({
endpoint: "/_matrix/client/r0/createRoom",
body: {
name: data.name,
room_alias_name: data.canonical_alias,
visibility: data.public ? "public" : "private",
invite:
Array.isArray(data.invitees) && data.invitees.length > 0
? data.invitees
: undefined,
},
method: "POST",
})
total: json => {
return json.total_rooms;
},
},
connections: {
path: "/_synapse/admin/v1/whois",
@@ -92,11 +99,20 @@ function filterNullValues(key, value) {
return value;
}
function getSearchOrder(order) {
if (order === "DESC") {
return "b";
} else {
return "f";
}
}
const dataProvider = {
getList: (resource, params) => {
console.log("getList " + resource);
const { user_id, guests, deactivated } = params.filter;
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage;
const query = {
from: from,
@@ -104,6 +120,8 @@ const dataProvider = {
user_id: user_id,
guests: guests,
deactivated: deactivated,
order_by: field,
dir: getSearchOrder(order),
};
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@@ -227,6 +245,29 @@ 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) => {
console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url");

View File

@@ -1246,25 +1246,6 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@progress/kendo-drawing@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@progress/kendo-drawing/-/kendo-drawing-1.6.0.tgz#66e9df431f52c7dd9fd5567be80dcbfa3a162281"
integrity sha512-9dGlFvW9fMgqAgcbLi+SfTeMUpNYdoVthwNxwAtsRQ+QwcgXJcdzFpLrLBXp17pXpDDFpiOyMqiwjffNGwtc3w==
dependencies:
pako "^1.0.5"
"@progress/kendo-file-saver@^1.0.1":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@progress/kendo-file-saver/-/kendo-file-saver-1.0.7.tgz#5b602115d1b0b5e26f3e52451a3ed7c29ed76c51"
integrity sha512-8tsho/+DATzfTW4BBaHrkF3C3jqH2/bQ+XbjqA0KfmTiBRVK6ygK+tkvkYeDhFlQBbJ02MmJlEC6OmXvXRFkUg==
"@progress/kendo-react-pdf@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@progress/kendo-react-pdf/-/kendo-react-pdf-3.10.1.tgz#348517daaddb366bbe840a92ec2fffbfd07ac2d2"
integrity sha512-2EKfQCwLFEa+mgCLKQ70iWVu7q2Dh/wJl6pPJ6Ix42BA7SiA2k5UmDk819FLY9pnMgv7WDxwqBP+8CvdkLoP5w==
dependencies:
"@progress/kendo-file-saver" "^1.0.1"
"@redux-saga/core@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
@@ -7938,7 +7919,7 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pako@^1.0.5, pako@~1.0.5:
pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@@ -9106,20 +9087,6 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
qrcode.react@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.0.tgz#7e8889db3b769e555e8eb463d4c6de221c36d5de"
integrity sha512-jBXleohRTwvGBe1ngV+62QvEZ/9IZqQivdwzo9pJM4LQMoCM2VnvNBnKdjvGnKyDZ/l0nCDgsPod19RzlPvm/Q==
dependencies:
loose-envify "^1.4.0"
prop-types "^15.6.0"
qr.js "0.0.0"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -11477,9 +11444,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
version "1.0.5"