Compare commits

..

13 Commits

Author SHA1 Message Date
Michael Albert b9e81b2278 Bump version and update dependencies
Change-Id: I0c0349b4429ce06bea51453c092f0a11156aaa05
2021-12-17 20:38:18 +01:00
sakkiii f6f437b17a version tag on docker hub (#187)
* version tag on docker hub

* resolve name collision

* master branch added for latest tag

* prettier-ignore fix
2021-12-08 22:02:43 +01:00
Dirk Klimpel 91af8f1c04 Add sorting users by creation timestamp (#174)
* Add `creation_ts` to list users

* remove filter

* Bring back origin columns sort order
2021-12-08 21:59:09 +01:00
Aaron R abc9d5154e Switch Dockerfile to use current LTS version of Node (#205)
Node 17 current fails due to https://github.com/webpack/webpack/issues/14532. It probably makes sense to use the current LTS version of Node instead of the absolute latest version of Node so these kinds of bleeding edge issues are less likely to happen.
2021-11-15 21:35:23 +01:00
dependabot[bot] 8228d7d2c2 Bump tar from 6.1.8 to 6.1.11 (#207)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.8 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.8...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:18:58 +01:00
Dirk Klimpel 4adc20f80d replace undoable prop with mutationMode prop (#202) 2021-11-15 21:18:29 +01:00
Dirk Klimpel a5c7d7dd22 Make items in "Room directory" are clickable (#199) 2021-11-15 21:15:11 +01:00
dependabot[bot] dc5c2c1d68 Bump tmpl from 1.0.4 to 1.0.5 (#193)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:11:49 +01:00
Dirk Klimpel 42b3252353 Add pristine to UserEdit and ServerNotice (#185) 2021-11-15 21:02:47 +01:00
Dirk Klimpel 1a17d3e69b Automatically set the homeserver for a new user (#184)
and enhance form validation
2021-11-15 20:57:38 +01:00
Dirk Klimpel 79ef38ee6b Enable modify user external_ids (#179)
* Enable modify user `external_ids`

* add input validation
2021-11-15 20:40:05 +01:00
Manuel Stahl 0ff4b30d71 Remove update repo stop from docker release action
See https://github.com/peter-evans/dockerhub-description/issues/10

Change-Id: I42d2d4e1a28117be1419591f7d43653591182f0c
2021-08-26 10:43:11 +02:00
Manuel Stahl 6c4ff6c791 Add write permissions to github release action
Change-Id: Ie7db1e7410bbc1c0fccbc2d00119363629e10f22
2021-08-26 10:40:11 +02:00
23 changed files with 1765 additions and 2785 deletions
+26 -11
View File
@@ -2,8 +2,14 @@ name: Create docker image(s) and push to docker hub
on:
push:
tags:
- '[0-9]+\.[0-9]+\.[0-9]+'
# Sequence of patterns matched against refs/heads
# prettier-ignore
branches:
# Push events on master branch
- master
# Sequence of patterns matched against refs/tags
tags:
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
jobs:
docker:
@@ -21,16 +27,25 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
- name: Calculate docker image tag
id: set-tag
run: |
case "${GITHUB_REF}" in
refs/heads/master|refs/heads/main)
tag=latest
;;
refs/tags/*)
tag=${GITHUB_REF#refs/tags/}
;;
*)
tag=${GITHUB_SHA}
;;
esac
echo "::set-output name=tag::$tag"
- name: Build and Push Tag
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: awesometechnologies/synapse-admin:latest
- name: Update repo description
uses: peter-evans/dockerhub-description@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: awesometechnologies/synapse-admin
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
platforms: linux/amd64,linux/arm64
+3
View File
@@ -8,6 +8,9 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -1,5 +1,5 @@
# Builder
FROM node:current as builder
FROM node:lts as builder
WORKDIR /src
+1 -1
View File
@@ -5,7 +5,7 @@
This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.38.0 for all functions to work as expected!
It needs at least Synapse v1.41.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "synapse-admin",
"version": "AMP/2021.08",
"version": "0.8.4",
"description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0",
@@ -21,12 +21,8 @@
"ra-test": "^3.15.0"
},
"dependencies": {
"@progress/kendo-drawing": "^1.6.0",
"@progress/kendo-react-pdf": "^3.10.1",
"babel-preset-jest": "^24.9.0",
"papaparse": "^5.2.0",
"prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-chinese": "^2.0.10",
"ra-language-german": "^3.13.4",
"react": "^17.0.0",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

+1 -27
View File
@@ -9,32 +9,6 @@
name="description"
content="Synapse-Admin"
/>
<style>
@font-face {
font-family: "DejaVu Sans";
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-weight: bold;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Bold.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-style: italic;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans";
font-weight: bold;
font-style: italic;
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
}
@font-face {
font-family: "DejaVu Sans Mono";
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Mono.ttf") format("truetype");
}
</style>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -72,4 +46,4 @@
</a>
</footer>
</body>
</html>
</html>
+4 -13
View File
@@ -4,9 +4,8 @@ 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, RoomEdit } from "./components/rooms";
import { RoomList, RoomShow } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports";
import ImportFeature from "./components/ImportFeature";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
import EqualizerIcon from "@material-ui/icons/Equalizer";
@@ -14,12 +13,12 @@ import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { ImportFeature } from "./components/ImportFeature";
import { RoomDirectoryList } from "./components/RoomDirectory";
import { Route } from "react-router-dom";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import chineseMessages from "./i18n/zh";
import ShowUserPdf from "./components/ShowUserPdf";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
@@ -40,8 +39,7 @@ const App = () => (
dataProvider={dataProvider}
i18nProvider={i18nProvider}
customRoutes={[
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
<Route key="userImport" path="/import_users" component={ImportFeature} />,
]}
>
<Resource
@@ -51,14 +49,7 @@ const App = () => (
edit={UserEdit}
icon={UserIcon}
/>
<Resource
name="rooms"
list={RoomList}
create={RoomCreate}
show={RoomShow}
edit={RoomEdit}
icon={RoomIcon}
/>
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
<Resource
name="user_media_statistics"
list={UserMediaStatsList}
+14 -69
View File
@@ -20,7 +20,6 @@ import {
import { useTranslate } from "ra-core";
import Container from "@material-ui/core/Container/Container";
import { generateRandomUser } from "./users";
import ShowUserPdf from "./ShowUserPdf";
const LOGGING = true;
@@ -60,8 +59,6 @@ const FilePicker = props => {
const [progress, setProgress] = useState(null);
const [pdfRecords, setPdfRecords] = useState(null);
const [importResults, setImportResults] = useState(null);
const [skippedRecords, setSkippedRecords] = useState(null);
@@ -69,23 +66,17 @@ const FilePicker = props => {
const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("ignore");
const [showingPdf, setShowingPdf] = useState(false);
const translate = useTranslate();
const notify = useNotify();
const dataProvider = useDataProvider();
const onFileChange = async e => {
if (progress !== null) {
return;
}
if (LOGGING) console.log("onFileChange was called");
if (progress !== null) return;
setValues(null);
setError(null);
setStats(null);
setPdfRecords(null);
setImportResults(null);
const file = e.target.files ? e.target.files[0] : null;
/* Let's refuse some unreasonably big files instead of freezing
@@ -135,11 +126,6 @@ const FilePicker = props => {
});
if (eF.length !== 0) {
if (LOGGING) {
console.log(meta.fields);
console.log(eF);
console.log(oF);
}
setError(
translate("import_users.error.required_field", { field: eF[0] })
);
@@ -240,9 +226,6 @@ const FilePicker = props => {
setProgress,
setError
);
setPdfRecords(results.recordsForPdf);
setImportResults(results);
// offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful
@@ -268,8 +251,6 @@ const FilePicker = props => {
let skippedRecords = [];
let erroredRecords = [];
let succeededRecords = [];
let recordsForPdf = [];
let changeStats = {
toAdmin: 0,
toGuest: 0,
@@ -384,14 +365,6 @@ const FilePicker = props => {
await dataProvider.create("users", { data: recordData });
}
succeededRecords.push(recordData);
if (recordData.password !== undefined) {
recordsForPdf.push({
id: recordData.id,
password: recordData.password,
displayname: recordData.displayname,
});
}
}
);
};
@@ -416,7 +389,6 @@ const FilePicker = props => {
erroredRecords,
succeededRecords,
totalRecordCount: entriesCount,
recordsForPdf,
changeStats,
wasDryRun: dryRun,
};
@@ -646,10 +618,6 @@ const FilePicker = props => {
<br />,
]
: ""}
{translate(
"import_users.cards.results.for_print",
importResults.recordsForPdf.length
)}
<br />
{importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"),
@@ -687,43 +655,20 @@ const FilePicker = props => {
</CardActions>
);
let pdfDisplay =
pdfRecords && showingPdf && pdfRecords.length ? (
<ShowUserPdf records={pdfRecords} />
) : null;
let allCards = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
if (conflictCards) allCards.push(conflictCards);
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
let pdfActions = pdfRecords ? (
<CardActions>
<Button
size="large"
onClick={e => {
setShowingPdf(true);
}}
>
{translate("import_users.goToPdf")}
</Button>
</CardActions>
) : null;
let cardContainer = <Card>{allCards}</Card>;
if (pdfRecords && showingPdf) {
return <Card>{pdfDisplay}</Card>;
} else {
let allCards = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
if (conflictCards) allCards.push(conflictCards);
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
if (pdfActions) allCards.push(pdfActions);
let cardContainer = <Card>{allCards}</Card>;
return [
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
}
return [
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
};
export const ImportFeature = FilePicker;
+2 -2
View File
@@ -59,7 +59,7 @@ export const RoomDirectoryBulkDeleteButton = props => (
<BulkDeleteButton
{...props}
label="resources.room_directory.action.erase"
undoable={false}
mutationMode="pessimistic"
confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content"
resource="room_directory"
@@ -191,7 +191,7 @@ export const FilterableRoomDirectoryList = ({
filters={<RoomDirectoryFilter />}
perPage={100}
>
<Datagrid>
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
<AvatarField
source="avatar_src"
sortable={false}
-35
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;
+4 -1
View File
@@ -24,7 +24,10 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
const ServerNoticeToolbar = props => (
<Toolbar {...props}>
<SaveButton label="resources.servernotices.action.send" />
<SaveButton
label="resources.servernotices.action.send"
disabled={props.pristine}
/>
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
-263
View File
@@ -1,263 +0,0 @@
import React, { useRef } 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";
import { string, any } from "prop-types";
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";
const origUrlString = "user=" + username + "&password=" + password;
var urlString = xor(origUrlString, magicString); // xor with magic string
if (origUrlString !== xor(urlString, magicString)) {
console.error(
"xoring this url string with magicString twice gave different results:",
origUrlString,
urlString,
xor(urlString, magicString)
);
}
urlString = btoa(urlString); // to base64
return serverUrl + "/#" + urlString;
}
UserPdfPage.propTypes = {
classes: any,
displayname: string,
qrCode: any,
serverUrl: string,
username: string,
password: string,
};
function UserPdfPage({
classes,
displayname,
qrCode,
serverUrl,
username,
password,
}) {
return (
<div className={classes.page}>
<div className={classes.header}>
<div className={classes.name}>{displayname}</div>
<img className={classes.logo} alt="Logo" src="images/logo.png" />
</div>
<div className={classes.body}>
<table>
<tbody>
<tr>
<td width="200px">
<div className={classes.code_note}>
Ihr persönlicher Anmeldecode:
</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_note}>
Ihre persönlichen Zugangsdaten:
</div>
</td>
</tr>
<tr>
<td>
<div className={classes.qr}>{qrCode}</div>
</td>
<td className={classes.table_cell}>
<div className={classes.credentials_text}>
<br />
<table>
<tbody>
<tr>
<td>Heimserver:</td>
<td>
<span className={classes.credentials}>
{serverUrl}
</span>
</td>
</tr>
<tr>
<td>Benutzername:</td>
<td>
<span className={classes.credentials}>
{username}
</span>
</td>
</tr>
<tr>
<td>Passwort:</td>
<td>
<span className={classes.credentials}>
{password}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div className={classes.note}>
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
notieren:
<br />
<br />
<br />
<hr />
</div>
</div>
</div>
);
}
const useStyles = makeStyles(theme => ({
page: {
height: 800,
width: 566,
padding: "none",
backgroundColor: "white",
boxShadow: "5px 5px 5px black",
margin: "auto",
overflowX: "hidden",
overflowY: "hidden",
fontFamily: "DejaVu Sans, Sans-Serif",
fontSize: 15,
},
header: {
height: 144,
width: 534,
marginLeft: 32,
marginTop: 15,
},
name: {
width: 240,
fontSize: 35,
float: "left",
marginTop: 100,
},
logo: {
width: 90,
marginTop: 50,
marginRight: 70,
float: "right",
},
body: {
clear: "both",
},
table_cell: {
verticalAlign: "top",
},
code_note: {
marginLeft: 32,
marginTop: 86,
},
qr: {
marginTop: 15,
marginLeft: 32,
},
credentials_note: {
marginTop: 86,
marginLeft: 10,
},
credentials_text: {
marginLeft: 10,
fontSize: 12,
},
credentials: {
fontFamily: "DejaVu Sans Mono, monospace",
},
note: {
fontSize: 18,
marginTop: 100,
marginLeft: 32,
marginRight: 32,
},
}));
const ShowUserPdf = props => {
const classes = useStyles();
const userPdf = useRef(null);
const exportPDF = () => {
userPdf.current.save();
};
let userRecords;
if (props.records) {
userRecords = props.records;
}
if (
props.location &&
props.location.state &&
props.location.state.id &&
props.location.state.password
) {
userRecords = [
{
id: props.location.state.id,
password: props.location.state.password,
displayname: props.location.state.displayname,
},
];
}
return (
<div>
<Title title="PDF" />
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
<PDFExport
paperSize={"A4"}
fileName="Users.pdf"
title=""
subject=""
keywords=""
ref={userPdf}
//ref={r => (resume = r)}
>
{userRecords.map(record => {
if (record.id && record.password) {
const username = record.id.substring(1, record.id.indexOf(":"));
const serverUrl =
"https://" + record.id.substring(record.id.indexOf(":") + 1);
const qrString = calculateQrString(
serverUrl,
username,
record.password
);
const qrCode = <QRCode value={qrString} size={128} />;
return (
<UserPdfPage
classes={classes}
displayname={record.displayname}
qrCode={qrCode}
serverUrl={serverUrl}
username={username}
password={record.password}
/>
);
} else {
/* Skip empty PDF pages */
return null;
}
})}
</PDFExport>
</div>
);
};
export default ShowUserPdf;
+9 -383
View File
@@ -1,56 +1,31 @@
import React, { Fragment } from "react";
import { connect } from "react-redux";
import { Route, Link } from "react-router-dom";
import {
AutocompleteArrayInput,
AutocompleteInput,
BooleanInput,
BooleanField,
BulkDeleteButton,
Button,
Create,
Edit,
Datagrid,
DateField,
Datagrid,
DeleteButton,
Filter,
FormTab,
List,
NumberField,
Pagination,
ReferenceArrayInput,
ReferenceField,
ReferenceInput,
ReferenceManyField,
SearchInput,
SelectField,
Show,
SimpleForm,
Tab,
TabbedForm,
TabbedShowLayout,
TextField,
TextInput,
Toolbar,
TopToolbar,
useDataProvider,
useRecordContext,
useRefresh,
useTranslate,
} from "react-admin";
import get from "lodash/get";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Tooltip,
Typography,
Chip,
Drawer,
styled,
withStyles,
Select,
MenuItem,
} from "@material-ui/core";
import { Tooltip, Typography, Chip } from "@material-ui/core";
import FastForwardIcon from "@material-ui/icons/FastForward";
import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
@@ -58,7 +33,6 @@ 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";
import EventIcon from "@material-ui/icons/Event";
import {
RoomDirectoryBulkDeleteButton,
@@ -104,368 +78,20 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
);
};
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 */
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="#"
/>
<ReferenceInput
reference="users"
source="owner"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceInput>
<BooleanInput source="public" label="synapseadmin.rooms.make_public" />
<BooleanInput
source="encrypt"
initialValue={true}
label="synapseadmin.rooms.encrypt"
/>
</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();
var name = "";
if (record) {
name = record.name !== "" ? record.name : record.id;
}
return (
<span>
{translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
{translate("resources.rooms.name", 1)} {name}
</span>
);
};
// 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 }) => <Styled to={to}>{children}</Styled>;
};
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 (
<Toolbar {...props}>
<SaveLink to={backLink}>
<Button onClick={onSave} variant="contained">
<React.Fragment>
<SaveIcon />
{translate("ra.action.save")}
</React.Fragment>
</Button>
</SaveLink>
<CancelLink to={backLink}>
<Button>
<React.Fragment>{translate("ra.action.cancel")}</React.Fragment>
</Button>
</CancelLink>
</Toolbar>
);
};
const RoomMemberIdField = ({ memberId, data = {} }) => {
const value = get(data[memberId], "id");
return (
<Typography component="span" variant="body2">
{value}
</Typography>
);
};
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 (
<React.Fragment>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={role}
onChange={event => {
setRole(event.target.value);
onChange(event.target.value);
}}
>
<MenuItem value={"user"}>
{translate("resources.users.roles.user")}
</MenuItem>
<MenuItem value={"mod"}>
{translate("resources.users.roles.mod")}
</MenuItem>
<MenuItem value={"admin"}>
{translate("resources.users.roles.admin")}
</MenuItem>
</Select>
</React.Fragment>
);
};
const RoomMemberEdit = ({ backLink, memberId, ...props }) => {
const translate = useTranslate();
const refresh = useRefresh();
const dataProvider = useDataProvider();
const [role, setRole] = React.useState();
const { id } = props;
return (
<Edit title=" " {...props}>
<SimpleForm
toolbar={
<RoomMemberEditToolbar
backLink={backLink}
translate={translate}
onSave={() => {
dataProvider
.update("rooms", {
data: {
id,
member_roles: [{ member_id: memberId, role }],
},
})
.then(() => {
refresh();
});
}}
/>
}
>
<ReferenceManyField
reference="room_members"
target="room_id"
label="resources.users.fields.id"
>
<RoomMemberIdField memberId={memberId} />
</ReferenceManyField>
<ReferenceManyField
reference="room_members"
target="room_id"
label="resources.users.fields.role"
>
<RoomMemberRoleInput
memberId={memberId}
translate={translate}
onChange={setRole}
/>
</ReferenceManyField>
</SimpleForm>
</Edit>
);
};
const drawerStyles = {
paper: {
width: 300,
},
};
const StyledDrawer = withStyles(drawerStyles)(({ classes, ...props }) => (
<Drawer {...props} classes={classes} />
));
export const RoomEdit = props => {
const translate = useTranslate();
return (
<React.Fragment>
<Edit {...props} title={<RoomTitle />}>
<TabbedForm>
<FormTab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
<ReferenceArrayInput
reference="users"
source="invitees"
filterToQuery={searchText => ({ user_id: searchText })}
>
<AutocompleteArrayInput
optionText="displayname"
suggestionText="displayname"
/>
</ReferenceArrayInput>
<ReferenceManyField
reference="room_members"
target="room_id"
addLabel={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, basePath, record) =>
`/rooms/${encodeURIComponent(
record.parentId
)}/${encodeURIComponent(id)}`
}
>
<TextField
source="id"
sortable={false}
label="resources.users.fields.id"
/>
<ReferenceField
label="resources.users.fields.displayname"
source="id"
reference="users"
sortable={false}
link=""
>
<TextField source="displayname" sortable={false} />
</ReferenceField>
<SelectField
source="role"
label="resources.users.fields.role"
choices={[
{
id: "user",
name: translate("resources.users.roles.user"),
},
{ id: "mod", name: translate("resources.users.roles.mod") },
{
id: "admin",
name: translate("resources.users.roles.admin"),
},
]}
/>
</Datagrid>
</ReferenceManyField>
</FormTab>
</TabbedForm>
</Edit>
<Route path="/rooms/:roomId/:memberId">
{({ match }) => {
const isMatch = !!match && !!match.params;
return (
<StyledDrawer open={isMatch} anchor="right">
{isMatch ? (
<RoomMemberEdit
{...props}
memberId={
isMatch ? decodeURIComponent(match.params.memberId) : null
}
backLink={`/rooms/${match.params.roomId}`}
/>
) : (
<div />
)}
</StyledDrawer>
);
}}
</Route>
</React.Fragment>
);
};
const RoomShowActions = ({ basePath, data, resource }) => {
var roomDirectoryStatus = "";
if (data) {
@@ -689,7 +315,7 @@ const RoomBulkActionButtons = props => (
{...props}
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
undoable={false}
mutationMode="pessimistic"
/>
</Fragment>
);
+59 -89
View File
@@ -13,12 +13,10 @@ import {
ArrayInput,
ArrayField,
Button,
CreateButton,
Datagrid,
DateField,
Create,
Edit,
ExportButton,
List,
Filter,
Toolbar,
@@ -31,31 +29,33 @@ import {
PasswordInput,
TextField,
TextInput,
SearchInput,
ReferenceField,
ReferenceManyField,
SelectField,
SearchInput,
SelectInput,
BulkDeleteButton,
DeleteButton,
SaveButton,
maxLength,
regex,
required,
useTranslate,
Pagination,
CreateButton,
ExportButton,
TopToolbar,
sanitizeListRestProps,
NumberField,
} from "react-admin";
import SaveQrButton from "./SaveQrButton";
import { Link } from "react-router-dom";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
import { makeStyles } from "@material-ui/core/styles";
import { Link } from "react-router-dom";
const redirect = () => {
return {
pathname: "/importcsv",
pathname: "/import_users",
};
};
@@ -144,7 +144,7 @@ const UserBulkActionButtons = props => (
{...props}
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
undoable={false}
mutationMode="pessimistic"
/>
</Fragment>
);
@@ -175,53 +175,36 @@ export const UserList = props => {
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<SelectField
source="user_type"
choices={[
{ id: null, name: "resources.users.type.default" },
{ id: "free", name: "resources.users.type.free" },
{ id: "limited", name: "resources.users.type.limited" },
]}
/>
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
/>
</Datagrid>
</List>
);
};
// redirect to the related Author show page
const redirectToPdf = (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={redirectToPdf}
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._=\-/]+:.*/,
"synapseadmin.users.invalid_user_id"
);
// here only local part of user_id
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
// localStorage.getItem("home_server").length is not valid here
const validateUser = [
required(),
maxLength(253),
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
];
const validateAddress = [required(), maxLength(255)];
export function generateRandomUser() {
const homeserver = localStorage.getItem("home_server");
@@ -268,17 +251,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} disabled={props.pristine} />
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
@@ -292,20 +265,16 @@ 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" />
<BooleanInput source="admin" />
<SelectInput
source="user_type"
choices={[
{ id: null, name: "resources.users.type.default" },
{ id: "free", name: "resources.users.type.free" },
{ id: "limited", name: "resources.users.type.limited" },
]}
<TextInput source="displayname" validate={maxLength(256)} />
<PasswordInput
source="password"
autoComplete="new-password"
validate={maxLength(512)}
/>
<BooleanInput source="admin" />
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
@@ -314,8 +283,19 @@ export const UserCreate = props => (
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
validate={required()}
/>
<TextInput source="address" validate={validateAddress} />
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/>
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
@@ -333,7 +313,6 @@ const UserTitle = ({ record }) => {
</span>
);
};
export const UserEdit = props => {
const classes = useStyles();
const translate = useTranslate();
@@ -352,15 +331,6 @@ export const UserEdit = props => {
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<SelectInput
source="user_type"
choices={[
{ id: null, name: "resources.users.type.default" },
{ id: "free", name: "resources.users.type.free" },
{ id: "limited", name: "resources.users.type.limited" },
]}
emptyText="resources.users.type.default"
/>
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
@@ -405,16 +375,16 @@ export const UserEdit = props => {
icon={<AssignmentIndIcon />}
path="sso"
>
<ArrayField source="external_ids" label={false}>
<Datagrid style={{ width: "100%" }}>
<TextField source="auth_provider" sortable={false} />
<TextField
<ArrayInput source="external_ids" label={false}>
<SimpleFormIterator>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
sortable={false}
validate={required()}
/>
</Datagrid>
</ArrayField>
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
+1 -36
View File
@@ -11,26 +11,12 @@ const de = {
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",
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
tabs: { sso: "SSO" },
},
rooms: {
details: "Raumdetails",
room_name: "Raumname",
make_public: "Öffentlicher Raum",
encrypt: "Verschlüsselter 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",
tabs: {
basic: "Allgemein",
members: "Mitglieder",
@@ -105,8 +91,6 @@ const de = {
with_error:
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
simulated_only: "Import-Vorgang war nur simuliert",
for_print:
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
},
},
},
@@ -137,18 +121,6 @@ const de = {
creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen",
auth_provider: "Provider",
user_type: "Kontotyp",
// Devices:
device_id: "Geräte-ID",
display_name: "Gerätename",
last_seen_ts: "Zeitstempel",
last_seen_ip: "IP-Adresse",
role: "Rolle",
},
type: {
default: "Standard",
free: "Basic",
limited: "Eingeschränkt",
},
helper: {
deactivate:
@@ -158,11 +130,6 @@ const de = {
action: {
erase: "Lösche Benutzerdaten",
},
roles: {
user: "Nutzer",
mod: "Moderator",
admin: "Administrator",
},
},
rooms: {
name: "Raum |||| Räume",
@@ -171,8 +138,6 @@ const de = {
name: "Name",
canonical_alias: "Alias",
joined_members: "Mitglieder",
invite_members: "Mitglieder einladen",
invitees: "Einladungen",
joined_local_members: "Lokale Mitglieder",
joined_local_devices: "Lokale Endgeräte",
state_events: "Zustandsereignisse / Komplexität",
+1 -36
View File
@@ -11,26 +11,11 @@ const en = {
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",
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
tabs: { sso: "SSO" },
},
rooms: {
details: "Room Details",
room_name: "Room Name",
make_public: "Make room public",
encrypt: "Encrypt room",
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.",
tabs: {
basic: "Basic",
members: "Members",
@@ -105,8 +90,6 @@ const en = {
with_error:
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated",
for_print:
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
},
},
},
@@ -137,17 +120,6 @@ const en = {
creation_ts_ms: "Creation timestamp",
consent_version: "Consent version",
auth_provider: "Provider",
// Devices:
device_id: "Device-ID",
display_name: "Device name",
last_seen_ts: "Timestamp",
last_seen_ip: "IP address",
role: "Role",
},
type: {
default: "Standard",
free: "Basic",
limited: "Limited",
},
helper: {
deactivate: "You must provide a password to re-activate an account.",
@@ -156,11 +128,6 @@ const en = {
action: {
erase: "Erase user data",
},
roles: {
user: "User",
mod: "Moderator",
admin: "Administrator",
},
},
rooms: {
name: "Room |||| Rooms",
@@ -169,8 +136,6 @@ const en = {
name: "Name",
canonical_alias: "Alias",
joined_members: "Members",
invite_members: "Invite Members",
invitees: "Invitations",
joined_local_members: "Local members",
joined_local_devices: "Local devices",
state_events: "State events / Complexity",
+7 -48
View File
@@ -25,13 +25,6 @@ const mxcUrlToHttp = mxcUrl => {
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
};
const POWER_LEVELS = {
admin: 100,
mod: 50,
user: 0,
};
const roleToPowerLevel = role => POWER_LEVELS[role] || 0;
const resourceMap = {
users: {
path: "/_synapse/admin/v2/users",
@@ -42,14 +35,15 @@ const resourceMap = {
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,
create: data => ({
endpoint: `/_synapse/admin/v2/users/${data.id}`,
endpoint: `/_synapse/admin/v2/users/@${data.id}:${localStorage.getItem(
"home_server"
)}`,
body: data,
method: "PUT",
}),
@@ -71,40 +65,8 @@ const resourceMap = {
public: !!r.public,
}),
data: "rooms",
total: json => json.total_rooms,
create: data => ({
endpoint: "/_synapse/admin/v1/rooms",
body: {
owner: data.owner,
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,
initial_state: data.encrypt
? [
{
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
]
: undefined,
},
method: "POST",
}),
transformBeforeUpdate: data => {
return {
...data,
member_roles: (data.member_roles || []).map(member => ({
member_id: member.member_id,
power_level: roleToPowerLevel(member.role),
})),
};
total: json => {
return json.total_rooms;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
@@ -364,7 +326,7 @@ const dataProvider = {
},
getOne: (resource, params) => {
console.log("getOne " + resource, params);
console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@@ -425,13 +387,10 @@ 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(data, filterNullValues),
body: JSON.stringify(params.data, filterNullValues),
}).then(({ json }) => ({
data: res.map(json),
}));
+1631 -1765
View File
File diff suppressed because it is too large Load Diff