Compare commits

..

13 Commits

Author SHA1 Message Date
Manuel Stahl 9c36ed6566 Increment version
Change-Id: I48fb812632dc2e498ad267477809a16fc882cf4c
2021-08-25 10:42:34 +02:00
dklimpel 8536f552d4 Add button to quarantine media (#180)
Change-Id: I6496826fdf75ab8b7b3ed5a9056abf86a50caea3
2021-08-25 10:42:34 +02:00
Manuel Stahl aaf782d24f Use .gitignore for prettier as well
Change-Id: Ibfe73812a5375cc251b859b8aa441583c0cdcd41
2021-08-25 10:42:34 +02:00
Manuel Stahl 2886203594 Add github action that builds docker images and pushes them to docker hub
Change-Id: I60d39cc266c2e8b905e2ac4a367bd5a22b79b57b
2021-08-25 10:42:34 +02:00
csett86 865fc98336 Add github action that packages a release tarball (#148)
Change-Id: I368a834a27f69550596a041c1e6b84afd40011b7
2021-08-24 09:58:35 +02:00
Dirk Klimpel e6f01f035b Add buttons to protect and unprotect users' media from quarantine (#150) 2021-08-19 11:51:07 +02:00
Manuel Stahl 32e088ac5a yarn: Upgrade packages
- babel 7.15
- eslint 7.32
- react-admin 3.17

Change-Id: Ief4ad5870ae721fb432e19686607376b83eff12a
2021-08-18 09:48:41 +02:00
Dirk Klimpel bf3d13916f Add SSO external_ids to user (#168) 2021-08-18 09:33:22 +02:00
Manuel Stahl 07b1df5855 Add Github action badge for build checks 2021-08-18 09:27:25 +02:00
Dirk Klimpel 634341ea07 Add GitHub Action to run CI tests (#162) 2021-08-18 09:18:09 +02:00
Dirk Klimpel 361643c3da Fix link in README.md (#175) 2021-08-17 08:22:36 +02:00
Dirk Klimpel 5262518699 Update links to new Synapse documentation (#164) 2021-07-06 09:57:44 +02:00
Dirk Klimpel 78a282863a Fix broken CI with language files (#165) 2021-07-06 09:56:09 +02:00
22 changed files with 1017 additions and 1566 deletions
+21
View File
@@ -0,0 +1,21 @@
name: build-test
on:
push:
branches: ["master"]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run tests
run: yarn test
+36
View File
@@ -0,0 +1,36 @@
name: Create docker image(s) and push to docker hub
on:
push:
tags:
- '[0-9]+\.[0-9]+\.[0-9]+'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
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
+28
View File
@@ -0,0 +1,28 @@
name: Create release tarball and attach to tag
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "14"
- run: yarn install
- run: yarn build
- run: |
version=`git describe --dirty --tags || echo unknown`
mkdir -p dist
cp -r build synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
- uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with:
files: dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+19 -8
View File
@@ -1,13 +1,14 @@
[![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin)
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
# Synapse admin ui
This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.34.0 for all functions to work as expected!
It needs at least Synapse v1.38.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/version_api.rst).
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
After entering the URL on the login page of synapse-admin the server version appears below the input field.
@@ -16,17 +17,27 @@ You need access to the following endpoints:
- `/_matrix`
- `/_synapse/admin`
See also [Synapse administration endpoints](https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md#synapse-administration-endpoints)
See also [Synapse administration endpoints](https://matrix-org.github.io/synapse/develop/reverse_proxy.html#synapse-administration-endpoints)
## Step-By-Step install:
You have two options:
You have three options:
1. Download the source code from github and run using nodejs
2. Run the Docker container
1. Download the tarball and serve with any webserver
2. Download the source code from github and run using nodejs
3. Run the Docker container
Steps for 1):
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
- configure a vhost for synapse admin on your webserver
- download the .tar.gz from the latest release: https://github.com/Awesome-Technologies/synapse-admin/releases/latest
- unpack the .tar.gz
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
- open the url of the vhost in your browser
Steps for 2):
- 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`
@@ -38,9 +49,9 @@ Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver
or by editing it in the [.env](.env) file. See also the
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
Steps for 2):
Steps for 3):
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the (docker-compose.yml)[docker-compose.yml]: `docker-compose up -d`
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
+2 -6
View File
@@ -1,6 +1,6 @@
{
"name": "synapse-admin",
"version": "AMP/2021.07",
"version": "0.8.3",
"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",
@@ -40,7 +36,7 @@
"fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other",
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
"prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
"test:code": "react-scripts test",
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
"test:style": "yarn prettier --list-different",
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;
-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;
-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;
+184 -2
View File
@@ -2,6 +2,7 @@ import React, { Fragment, useState } from "react";
import classnames from "classnames";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { makeStyles } from "@material-ui/core/styles";
import { Tooltip } from "@material-ui/core";
import {
BooleanInput,
Button,
@@ -10,16 +11,22 @@ import {
SaveButton,
SimpleForm,
Toolbar,
useCreate,
useDelete,
useNotify,
useRefresh,
useTranslate,
} from "react-admin";
import IconCancel from "@material-ui/icons/Cancel";
import BlockIcon from "@material-ui/icons/Block";
import ClearIcon from "@material-ui/icons/Clear";
import DeleteSweepIcon from "@material-ui/icons/DeleteSweep";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import DeleteSweepIcon from "@material-ui/icons/DeleteSweep";
import IconCancel from "@material-ui/icons/Cancel";
import LockIcon from "@material-ui/icons/Lock";
import LockOpenIcon from "@material-ui/icons/LockOpen";
const useStyles = makeStyles(
theme => ({
@@ -143,3 +150,178 @@ export const DeleteMediaButton = props => {
</Fragment>
);
};
export const ProtectMediaButton = props => {
const { record } = props;
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("protect_media");
const [deleteOne] = useDelete("protect_media");
if (!record) return null;
const handleProtect = () => {
create(
{ payload: { data: record } },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
}
);
};
const handleUnprotect = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.protect_media.action.send_failure", "error"),
}
);
};
return (
/*
Wrapping Tooltip with <div>
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
*/
<Fragment>
{record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.none", {
_: "resources.protect_media.action.none",
})}
>
<div>
{/*
Button instead BooleanField for
consistent appearance and position in the column
*/}
<Button disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.protect_media.action.delete", {
_: "resources.protect_media.action.delete",
})}
arrow
>
<div>
<Button onClick={handleUnprotect} disabled={loading}>
<LockIcon />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.create", {
_: "resources.protect_media.action.create",
})}
>
<div>
<Button onClick={handleProtect} disabled={loading}>
<LockOpenIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
);
};
export const QuarantineMediaButton = props => {
const { record } = props;
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { loading }] = useCreate("quarantine_media");
const [deleteOne] = useDelete("quarantine_media");
if (!record) return null;
const handleQuarantaine = () => {
create(
{ payload: { data: record } },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
}
);
};
const handleRemoveQuarantaine = () => {
deleteOne(
{ payload: { ...record } },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onFailure: () =>
notify("resources.quarantine_media.action.send_failure", "error"),
}
);
};
return (
<Fragment>
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.quarantine_media.action.none", {
_: "resources.quarantine_media.action.none",
})}
>
<div>
<Button disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.delete", {
_: "resources.quarantine_media.action.delete",
})}
>
<div>
<Button onClick={handleRemoveQuarantaine} disabled={loading}>
<BlockIcon color="error" />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.create", {
_: "resources.quarantine_media.action.create",
})}
>
<div>
<Button onClick={handleQuarantaine} disabled={loading}>
<BlockIcon />
</Button>
</div>
</Tooltip>
)}
</Fragment>
);
};
+8 -382
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) {
+31 -77
View File
@@ -1,23 +1,22 @@
import React, { cloneElement, Fragment } from "react";
import Avatar from "@material-ui/core/Avatar";
import PersonPinIcon from "@material-ui/icons/PersonPin";
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
import ContactMailIcon from "@material-ui/icons/ContactMail";
import DevicesIcon from "@material-ui/icons/Devices";
import GetAppIcon from "@material-ui/icons/GetApp";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import NotificationsIcon from "@material-ui/icons/Notifications";
import PermMediaIcon from "@material-ui/icons/PermMedia";
import PersonPinIcon from "@material-ui/icons/PersonPin";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import ViewListIcon from "@material-ui/icons/ViewList";
import {
ArrayInput,
ArrayField,
Button,
CreateButton,
Datagrid,
DateField,
Create,
Edit,
ExportButton,
List,
Filter,
Toolbar,
@@ -30,10 +29,9 @@ import {
PasswordInput,
TextField,
TextInput,
SearchInput,
ReferenceField,
ReferenceManyField,
SelectField,
SearchInput,
SelectInput,
BulkDeleteButton,
DeleteButton,
@@ -41,19 +39,21 @@ import {
regex,
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",
};
};
@@ -173,48 +173,12 @@ 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" />
</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._=\-/]+:.*/,
@@ -266,17 +230,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"
confirmTitle={translate("resources.users.helper.erase", {
@@ -290,20 +244,12 @@ 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" },
]}
/>
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
@@ -331,7 +277,6 @@ const UserTitle = ({ record }) => {
</span>
);
};
export const UserEdit = props => {
const classes = useStyles();
const translate = useTranslate();
@@ -350,15 +295,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"
@@ -398,6 +334,23 @@ export const UserEdit = props => {
</ArrayInput>
</FormTab>
<FormTab
label="synapseadmin.users.tabs.sso"
icon={<AssignmentIndIcon />}
path="sso"
>
<ArrayField source="external_ids" label={false}>
<Datagrid style={{ width: "100%" }}>
<TextField source="auth_provider" sortable={false} />
<TextField
source="external_id"
label="resources.users.fields.id"
sortable={false}
/>
</Datagrid>
</ArrayField>
</FormTab>
<FormTab
label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />}
@@ -513,7 +466,8 @@ export const UserEdit = props => {
<TextField source="media_type" />
<TextField source="upload_name" />
<TextField source="quarantined_by" />
<BooleanField source="safe_from_quarantine" />
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
<DeleteButton mutationMode="pessimistic" redirect={false} />
</Datagrid>
</ReferenceManyField>
+22 -35
View File
@@ -11,25 +11,13 @@ 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",
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",
@@ -104,8 +92,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",
},
},
},
@@ -135,18 +121,7 @@ const de = {
address: "Adresse",
creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen",
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",
auth_provider: "Provider",
},
helper: {
deactivate:
@@ -156,11 +131,6 @@ const de = {
action: {
erase: "Lösche Benutzerdaten",
},
roles: {
user: "Nutzer",
mod: "Moderator",
admin: "Administrator",
},
},
rooms: {
name: "Raum |||| Räume",
@@ -169,8 +139,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",
@@ -274,7 +242,7 @@ const de = {
media_type: "Typ",
upload_name: "Dateiname",
quarantined_by: "Zur Quarantäne hinzugefügt",
safe_from_quarantine: "Geschützt vor Quarantäne",
safe_from_quarantine: "Schutz vor Quarantäne",
created_ts: "Erstellt",
last_access_ts: "Letzter Zugriff",
},
@@ -295,6 +263,25 @@ const de = {
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
},
},
protect_media: {
action: {
create: "Ungeschützt, Schutz erstellen",
delete: "Geschützt, Schutz aufheben",
none: "In Quarantäne",
send_success: "Erfolgreich den Schutz-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
},
quarantine_media: {
action: {
name: "Quarantäne",
create: "Zur Quarantäne hinzufügen",
delete: "In Quarantäne, Quarantäne aufheben",
none: "Geschützt vor Quarantäne",
send_success: "Erfolgreich den Quarantäne-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
},
pushers: {
name: "Pusher |||| Pushers",
fields: {
+21 -34
View File
@@ -11,25 +11,12 @@ 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",
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",
@@ -104,8 +91,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",
},
},
},
@@ -135,17 +120,7 @@ const en = {
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",
role: "Role",
},
type: {
default: "Standard",
free: "Basic",
limited: "Limited",
auth_provider: "Provider",
},
helper: {
deactivate: "You must provide a password to re-activate an account.",
@@ -154,11 +129,6 @@ const en = {
action: {
erase: "Erase user data",
},
roles: {
user: "User",
mod: "Moderator",
admin: "Administrator",
},
},
rooms: {
name: "Room |||| Rooms",
@@ -167,8 +137,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",
@@ -291,6 +259,25 @@ const en = {
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
},
},
protect_media: {
action: {
create: "Unprotected, create protection",
delete: "Protected, remove protection",
none: "In quarantine",
send_success: "Successfully changed the protection status.",
send_failure: "An error has occurred.",
},
},
quarantine_media: {
action: {
name: "Quarantine",
create: "Add to quarantine",
delete: "In quarantine, unquarantine",
none: "Protected from quarantine",
send_success: "Successfully changed the quarantine status.",
send_failure: "An error has occurred.",
},
},
pushers: {
name: "Pusher |||| Pushers",
fields: {
+30 -47
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,7 +35,6 @@ 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,
}),
@@ -71,40 +63,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}`,
@@ -222,6 +182,32 @@ const resourceMap = {
method: "POST",
}),
},
protect_media: {
map: pm => ({ id: pm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unprotect/${params.media_id}`,
method: "POST",
}),
},
quarantine_media: {
map: qm => ({ id: qm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
method: "POST",
}),
},
servernotices: {
map: n => ({ id: n.event_id }),
create: data => ({
@@ -338,7 +324,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();
@@ -399,13 +385,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),
}));
+596 -568
View File
File diff suppressed because it is too large Load Diff