Compare commits
142 Commits
AMP/2021.0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d6749b413a | |||
| d8df2fa2e8 | |||
| cef7e31cb2 | |||
| b2d1dc0be1 | |||
| 72bc40f322 | |||
| 44b9aca138 | |||
| ab673a91a1 | |||
| e2ecf90f66 | |||
| 1ac7d8d87b | |||
| 7914caa70e | |||
| 24952cba56 | |||
| 6f26a31604 | |||
| dbb9a7f547 | |||
| 705244b4fc | |||
| 46259d3740 | |||
| 4772083040 | |||
| 61e4444727 | |||
| df57f5f908 | |||
| 94d8bb2772 | |||
| e6f7f1d591 | |||
| de7e8d1ff3 | |||
| 317de6ce80 | |||
| f74249073a | |||
| 33dcd46f74 | |||
| 2ab4ab66e3 | |||
| 3410deb510 | |||
| b1c35b670f | |||
| 39dea09895 | |||
| 8804bdc94d | |||
| 36bf2dff3e | |||
| 0958f376a9 | |||
| f287555d53 | |||
| b66a4596e4 | |||
| 4ff83bf261 | |||
| c79453f4f4 | |||
| 149c8590c2 | |||
| eefd5796d0 | |||
| 2b712e5bd9 | |||
| 26014820dc | |||
| fbb82c99a5 | |||
| 27655602da | |||
| dcf6dbe8ae | |||
| 216a23cbb2 | |||
| e83087444c | |||
| 07bb80a1e6 | |||
| fdb43c9193 | |||
| 2c002afe8e | |||
| 29aa686e70 | |||
| edd7f12e5d | |||
| c5b73a222c | |||
| 5b6edfbf11 | |||
| 573857ed4a | |||
| 9b1e3dbfd2 | |||
| e78c2bedaa | |||
| 0872e31411 | |||
| 84a253f50b | |||
| 0726dc788d | |||
| b9501c77f0 | |||
| 558ed60bb4 | |||
| 2197356a1e | |||
| 96c718e45f | |||
| 339848e117 | |||
| 7faf321346 | |||
| 5cdd110bf1 | |||
| 9a05dfee7a | |||
| 9110a8ac99 | |||
|
|
0b153ddcbb | ||
|
|
784c284723 | ||
|
|
e5f73ea8b4 | ||
|
|
96551259c5 | ||
|
|
b2fa533ef0 | ||
|
|
7aa0f9f50d | ||
|
|
5aa90c25f7 | ||
|
|
38d58db08d | ||
|
|
e637df232a | ||
|
|
d97c633cd0 | ||
|
|
b02396c61f | ||
|
|
95de50b925 | ||
|
|
1a150a10fd | ||
|
|
158e7dbe98 | ||
|
|
a642f11503 | ||
|
|
888a3f001b | ||
|
|
4b0845bee8 | ||
|
|
f449e3277a | ||
|
|
3303f253b4 | ||
|
|
0250954ee7 | ||
|
|
efed8b2774 | ||
|
|
c891afa611 | ||
|
|
38541b8f02 | ||
|
|
0f4c382c18 | ||
|
|
b90d4ef00f | ||
|
|
3fb33facc5 | ||
|
|
c4a68ff1d5 | ||
|
|
c4f0fa48ec | ||
|
|
26ed63d65e | ||
|
|
b9e81b2278 | ||
|
|
f6f437b17a | ||
|
|
91af8f1c04 | ||
|
|
abc9d5154e | ||
|
|
8228d7d2c2 | ||
|
|
4adc20f80d | ||
|
|
a5c7d7dd22 | ||
|
|
dc5c2c1d68 | ||
|
|
42b3252353 | ||
|
|
1a17d3e69b | ||
|
|
79ef38ee6b | ||
|
|
0ff4b30d71 | ||
|
|
6c4ff6c791 | ||
|
|
9c36ed6566 | ||
|
|
8536f552d4 | ||
|
|
aaf782d24f | ||
|
|
2886203594 | ||
|
|
865fc98336 | ||
|
|
e6f01f035b | ||
|
|
32e088ac5a | ||
|
|
bf3d13916f | ||
|
|
07b1df5855 | ||
|
|
634341ea07 | ||
|
|
361643c3da | ||
|
|
5262518699 | ||
|
|
78a282863a | ||
|
|
ff0201273a | ||
|
|
e50c95b4be | ||
|
|
9f16e5c6ba | ||
|
|
509a45cba4 | ||
|
|
5c6e5a9641 | ||
|
|
e3d5d51342 | ||
|
|
985673b161 | ||
|
|
d72357f64f | ||
|
|
e19c34324b | ||
|
|
3ea1f51eb5 | ||
|
|
229518e456 | ||
|
|
5a5a7143af | ||
|
|
dda8ba5e85 | ||
|
|
5208198b76 | ||
|
|
c8082a7198 | ||
|
|
10831796e3 | ||
|
|
5ee5288edf | ||
|
|
a5528d9fe7 | ||
|
|
e2fd934851 | ||
|
|
0bc1ce3226 | ||
|
|
41ce58bac8 |
5
.env
Normal file
5
.env
Normal file
@@ -0,0 +1,5 @@
|
||||
# This setting allows to fix the homeserver.
|
||||
# If you set this setting, the user will not be able to select
|
||||
# the server and have to use synapse-admin with this server.
|
||||
|
||||
#REACT_APP_SERVER=https://yourmatrixserver.example.com
|
||||
21
.github/workflows/build-test.yml
vendored
Normal file
21
.github/workflows/build-test.yml
vendored
Normal 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
|
||||
51
.github/workflows/docker-release.yml
vendored
Normal file
51
.github/workflows/docker-release.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Create docker image(s) and push to docker hub
|
||||
|
||||
on:
|
||||
push:
|
||||
# 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:
|
||||
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: 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: .
|
||||
push: true
|
||||
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
26
.github/workflows/edge_ghpage.yml
vendored
Normal file
26
.github/workflows/edge_ghpage.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Build and Deploy Edge version to GH Pages
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2.3.1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "14"
|
||||
- name: Install and Build 🔧
|
||||
run: |
|
||||
yarn install
|
||||
yarn build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.5
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: build
|
||||
31
.github/workflows/github-release.yml
vendored
Normal file
31
.github/workflows/github-release.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Create release tarball and attach to tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
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 }}
|
||||
@@ -7,5 +7,5 @@
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 13
|
||||
- lts/*
|
||||
|
||||
cache: yarn
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# Builder
|
||||
FROM node:current as builder
|
||||
FROM node:lts as builder
|
||||
|
||||
ARG PUBLIC_URL=/
|
||||
ARG REACT_APP_SERVER
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src
|
||||
RUN yarn --network-timeout=100000 install
|
||||
RUN yarn build
|
||||
RUN PUBLIC_URL=$PUBLIC_URL REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||
|
||||
|
||||
# App
|
||||
|
||||
69
README.md
69
README.md
@@ -1,31 +1,72 @@
|
||||
[](https://travis-ci.org/Awesome-Technologies/synapse-admin)
|
||||
[](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.27.0 for all functions to work as expected!
|
||||
```
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
|
||||
## Run `sudo apt-get install -y nodejs` to install Node.js 14.x and npm
|
||||
## You may also need development tools to build native addons:
|
||||
sudo apt-get install gcc g++ make
|
||||
## To install the Yarn package manager, run:
|
||||
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get install yarn
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Supported Synapse
|
||||
|
||||
It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.42.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.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
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:
|
||||
### Use without install
|
||||
|
||||
You have two options:
|
||||
You can use the current version of Synapse Admin without own installation direct
|
||||
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
|
||||
|
||||
1. Download the source code from github and run using nodejs
|
||||
2. Run the Docker container
|
||||
**Note:**
|
||||
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
|
||||
**Remember: You have no need to expose these endpoints to the internet but to your network.**
|
||||
If you want your own deployment, follow the [Step-By-Step Install Guide](#step-by-step-install) below.
|
||||
|
||||
Steps for 1):
|
||||
### Step-By-Step install
|
||||
|
||||
You have three options:
|
||||
|
||||
1. [Download the tarball and serve with any webserver](#steps-for-1)
|
||||
2. [Download the source code from github and run using nodejs](#steps-for-2)
|
||||
3. [Run the Docker container](#steps-for-3)
|
||||
|
||||
#### 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`
|
||||
@@ -33,9 +74,14 @@ Steps for 1):
|
||||
- download dependencies: `yarn install`
|
||||
- start web server: `yarn start`
|
||||
|
||||
Steps for 2):
|
||||
You can fix the homeserver, so that the user can no longer define it himself.
|
||||
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
|
||||
or by editing it in the [.env](.env) file. See also the
|
||||
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
||||
|
||||
- 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`
|
||||
#### 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`
|
||||
|
||||
> 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.
|
||||
|
||||
@@ -50,6 +96,9 @@ Steps for 2):
|
||||
context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||
# args:
|
||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# # see #266
|
||||
# - PUBLIC_URL="/synapse-admin"
|
||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "synapse-admin",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.5",
|
||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||
"license": "Apache-2.0",
|
||||
@@ -11,26 +11,24 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.1.1",
|
||||
"@testing-library/react": "^10.0.2",
|
||||
"@testing-library/user-event": "^12.0.11",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"prettier": "^2.0.0",
|
||||
"ra-test": "^3.14.0"
|
||||
"prettier": "^2.2.0",
|
||||
"ra-test": "^3.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"papaparse": "^5.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-language-chinese": "^2.0.10",
|
||||
"ra-language-german": "^2.1.2",
|
||||
"ra-language-german": "^3.13.4",
|
||||
"react": "^17.0.0",
|
||||
"react-admin": "^3.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-scripts": "^3.4.4"
|
||||
"react-admin": "^3.19.7",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
|
||||
@@ -38,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",
|
||||
|
||||
15
src/App.js
15
src/App.js
@@ -8,12 +8,18 @@ import { RoomList, RoomShow } from "./components/rooms";
|
||||
import { ReportList, ReportShow } from "./components/EventReports";
|
||||
import LoginPage from "./components/LoginPage";
|
||||
import UserIcon from "@material-ui/icons/Group";
|
||||
import ConfirmationNumberIcon from "@material-ui/icons/ConfirmationNumber";
|
||||
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
||||
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 {
|
||||
RegistrationTokenCreate,
|
||||
RegistrationTokenEdit,
|
||||
RegistrationTokenList,
|
||||
} from "./components/RegistrationTokens";
|
||||
import { RoomDirectoryList } from "./components/RoomDirectory";
|
||||
import { Route } from "react-router-dom";
|
||||
import germanMessages from "./i18n/de";
|
||||
@@ -66,6 +72,13 @@ const App = () => (
|
||||
list={RoomDirectoryList}
|
||||
icon={FolderSharedIcon}
|
||||
/>
|
||||
<Resource
|
||||
name="registration_tokens"
|
||||
list={RegistrationTokenList}
|
||||
create={RegistrationTokenCreate}
|
||||
edit={RegistrationTokenEdit}
|
||||
icon={ConfirmationNumberIcon}
|
||||
/>
|
||||
<Resource name="connections" />
|
||||
<Resource name="devices" />
|
||||
<Resource name="room_members" />
|
||||
@@ -73,7 +86,9 @@ const App = () => (
|
||||
<Resource name="joined_rooms" />
|
||||
<Resource name="pushers" />
|
||||
<Resource name="servernotices" />
|
||||
<Resource name="forward_extremities" />
|
||||
<Resource name="room_state" />
|
||||
<Resource name="user_urlpreview" />
|
||||
</Admin>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React from "react";
|
||||
import { TestContext } from "ra-test";
|
||||
import { shallow } from "enzyme";
|
||||
import { render } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
describe("App", () => {
|
||||
it("renders", () => {
|
||||
shallow(
|
||||
<TestContext>
|
||||
<App />
|
||||
</TestContext>
|
||||
);
|
||||
render(<App />);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,15 @@ import {
|
||||
import PageviewIcon from "@material-ui/icons/Pageview";
|
||||
import ViewListIcon from "@material-ui/icons/ViewList";
|
||||
|
||||
const date_format = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const ReportPagination = props => (
|
||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||
);
|
||||
@@ -33,14 +42,7 @@ export const ReportShow = props => {
|
||||
<DateField
|
||||
source="received_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={true}
|
||||
/>
|
||||
<ReferenceField source="user_id" reference="users">
|
||||
@@ -68,18 +70,10 @@ export const ReportShow = props => {
|
||||
icon={<PageviewIcon />}
|
||||
path="detail"
|
||||
>
|
||||
{" "}
|
||||
<DateField
|
||||
source="event_json.origin_server_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={true}
|
||||
/>
|
||||
<ReferenceField source="sender" reference="users">
|
||||
@@ -116,14 +110,7 @@ export const ReportList = ({ ...props }) => {
|
||||
<DateField
|
||||
source="received_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={true}
|
||||
/>
|
||||
<TextField sortable={false} source="user_id" />
|
||||
|
||||
@@ -78,10 +78,48 @@ const LoginPage = ({ theme }) => {
|
||||
const login = useLogin();
|
||||
const notify = useNotify();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||
var locale = useLocale();
|
||||
const setLocale = useSetLocale();
|
||||
const translate = useTranslate();
|
||||
const base_url = localStorage.getItem("base_url");
|
||||
const cfg_base_url = process.env.REACT_APP_SERVER;
|
||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||
|
||||
if (loginToken) {
|
||||
const ssoToken = loginToken[1];
|
||||
console.log("SSO token is", ssoToken);
|
||||
// Prevent further requests
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"",
|
||||
window.location.href.replace(loginToken[0], "#").split("#")[0]
|
||||
);
|
||||
const baseUrl = localStorage.getItem("sso_base_url");
|
||||
localStorage.removeItem("sso_base_url");
|
||||
if (baseUrl) {
|
||||
const auth = {
|
||||
base_url: baseUrl,
|
||||
username: null,
|
||||
password: null,
|
||||
loginToken: ssoToken,
|
||||
};
|
||||
console.log("Base URL is:", baseUrl);
|
||||
console.log("SSO Token is:", ssoToken);
|
||||
console.log("Let's try token login...");
|
||||
login(auth).catch(error => {
|
||||
alert(
|
||||
typeof error === "string"
|
||||
? error
|
||||
: typeof error === "undefined" || !error.message
|
||||
? "ra.auth.sign_in_error"
|
||||
: error.message
|
||||
);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const renderInput = ({
|
||||
meta: { touched, error } = {},
|
||||
@@ -111,7 +149,9 @@ const LoginPage = ({ theme }) => {
|
||||
if (!values.base_url.match(/^(http|https):\/\//)) {
|
||||
errors.base_url = translate("synapseadmin.auth.protocol_error");
|
||||
} else if (
|
||||
!values.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/)
|
||||
!values.base_url.match(
|
||||
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/
|
||||
)
|
||||
) {
|
||||
errors.base_url = translate("synapseadmin.auth.url_error");
|
||||
}
|
||||
@@ -134,6 +174,14 @@ const LoginPage = ({ theme }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSSO = () => {
|
||||
localStorage.setItem("sso_base_url", ssoBaseUrl);
|
||||
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`;
|
||||
window.location.href = ssoFullUrl;
|
||||
};
|
||||
|
||||
const extractHomeServer = username => {
|
||||
const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
|
||||
if (!username) return null;
|
||||
@@ -147,7 +195,7 @@ const LoginPage = ({ theme }) => {
|
||||
const [serverVersion, setServerVersion] = useState("");
|
||||
|
||||
const handleUsernameChange = _ => {
|
||||
if (formData.base_url) return;
|
||||
if (formData.base_url || cfg_base_url) return;
|
||||
// check if username is a full qualified userId then set base_url accordially
|
||||
const home_server = extractHomeServer(formData.username);
|
||||
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
||||
@@ -185,6 +233,31 @@ const LoginPage = ({ theme }) => {
|
||||
.catch(_ => {
|
||||
setServerVersion("");
|
||||
});
|
||||
|
||||
// Set SSO Url
|
||||
const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
|
||||
let supportPass = false,
|
||||
supportSSO = false;
|
||||
fetchUtils
|
||||
.fetchJson(authMethodUrl, { method: "GET" })
|
||||
.then(({ json }) => {
|
||||
json.flows.forEach(f => {
|
||||
if (f.type === "m.login.password") {
|
||||
supportPass = true;
|
||||
} else if (f.type === "m.login.sso") {
|
||||
supportSSO = true;
|
||||
}
|
||||
});
|
||||
setSupportPassAuth(supportPass);
|
||||
if (supportSSO) {
|
||||
setSSOBaseUrl(formData.base_url);
|
||||
} else {
|
||||
setSSOBaseUrl("");
|
||||
}
|
||||
})
|
||||
.catch(_ => {
|
||||
setSSOBaseUrl("");
|
||||
});
|
||||
},
|
||||
[formData.base_url]
|
||||
);
|
||||
@@ -197,8 +270,9 @@ const LoginPage = ({ theme }) => {
|
||||
name="username"
|
||||
component={renderInput}
|
||||
label={translate("ra.auth.username")}
|
||||
disabled={loading}
|
||||
disabled={loading || !supportPassAuth}
|
||||
onBlur={handleUsernameChange}
|
||||
resettable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
@@ -208,7 +282,8 @@ const LoginPage = ({ theme }) => {
|
||||
component={renderInput}
|
||||
label={translate("ra.auth.password")}
|
||||
type="password"
|
||||
disabled={loading}
|
||||
disabled={loading || !supportPassAuth}
|
||||
resettable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
@@ -217,7 +292,8 @@ const LoginPage = ({ theme }) => {
|
||||
name="base_url"
|
||||
component={renderInput}
|
||||
label={translate("synapseadmin.auth.base_url")}
|
||||
disabled={loading}
|
||||
disabled={cfg_base_url || loading}
|
||||
resettable
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
@@ -228,7 +304,7 @@ const LoginPage = ({ theme }) => {
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={{ base_url: base_url }}
|
||||
initialValues={{ base_url: cfg_base_url || base_url }}
|
||||
onSubmit={handleSubmit}
|
||||
validate={validate}
|
||||
render={({ handleSubmit }) => (
|
||||
@@ -267,13 +343,24 @@ const LoginPage = ({ theme }) => {
|
||||
variant="contained"
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
disabled={loading || !supportPassAuth}
|
||||
className={classes.button}
|
||||
fullWidth
|
||||
>
|
||||
{loading && <CircularProgress size={25} thickness={2} />}
|
||||
{translate("ra.auth.sign_in")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleSSO}
|
||||
disabled={loading || ssoBaseUrl === ""}
|
||||
className={classes.button}
|
||||
fullWidth
|
||||
>
|
||||
{loading && <CircularProgress size={25} thickness={2} />}
|
||||
{translate("synapseadmin.auth.sso_sign_in")}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
<Notification />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { TestContext } from "ra-test";
|
||||
import { shallow } from "enzyme";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
describe("LoginForm", () => {
|
||||
it("renders", () => {
|
||||
shallow(
|
||||
render(
|
||||
<TestContext>
|
||||
<LoginPage />
|
||||
</TestContext>
|
||||
|
||||
132
src/components/RegistrationTokens.js
Normal file
132
src/components/RegistrationTokens.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from "react";
|
||||
import {
|
||||
BooleanInput,
|
||||
Create,
|
||||
Datagrid,
|
||||
DateField,
|
||||
DateTimeInput,
|
||||
Edit,
|
||||
Filter,
|
||||
List,
|
||||
maxValue,
|
||||
number,
|
||||
NumberField,
|
||||
NumberInput,
|
||||
regex,
|
||||
SimpleForm,
|
||||
TextInput,
|
||||
TextField,
|
||||
Toolbar,
|
||||
} from "react-admin";
|
||||
|
||||
const date_format = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
|
||||
const validateUsesAllowed = [number()];
|
||||
const validateLength = [number(), maxValue(64)];
|
||||
|
||||
const dateParser = v => {
|
||||
const d = new Date(v);
|
||||
if (isNaN(d)) return 0;
|
||||
return d.getTime();
|
||||
};
|
||||
|
||||
const dateFormatter = v => {
|
||||
if (v === undefined || v === null) return;
|
||||
const d = new Date(v);
|
||||
|
||||
const pad = "00";
|
||||
const year = d.getFullYear().toString();
|
||||
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
|
||||
const day = (pad + d.getDate().toString()).slice(-2);
|
||||
const hour = (pad + d.getHours().toString()).slice(-2);
|
||||
const minute = (pad + d.getMinutes().toString()).slice(-2);
|
||||
|
||||
// target format yyyy-MM-ddThh:mm
|
||||
return `${year}-${month}-${day}T${hour}:${minute}`;
|
||||
};
|
||||
|
||||
const RegistrationTokenFilter = props => (
|
||||
<Filter {...props}>
|
||||
<BooleanInput source="valid" alwaysOn />
|
||||
</Filter>
|
||||
);
|
||||
|
||||
export const RegistrationTokenList = props => {
|
||||
return (
|
||||
<List
|
||||
{...props}
|
||||
filters={<RegistrationTokenFilter />}
|
||||
filterDefaultValues={{ valid: true }}
|
||||
pagination={false}
|
||||
perPage={500}
|
||||
>
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="token" sortable={false} />
|
||||
<NumberField source="uses_allowed" sortable={false} />
|
||||
<NumberField source="pending" sortable={false} />
|
||||
<NumberField source="completed" sortable={false} />
|
||||
<DateField
|
||||
source="expiry_time"
|
||||
showTime
|
||||
options={date_format}
|
||||
sortable={false}
|
||||
/>
|
||||
</Datagrid>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegistrationTokenCreate = props => (
|
||||
<Create {...props}>
|
||||
<SimpleForm redirect="list" toolbar={<Toolbar alwaysEnableSaveButton />}>
|
||||
<TextInput
|
||||
source="token"
|
||||
autoComplete="off"
|
||||
validate={validateToken}
|
||||
resettable
|
||||
/>
|
||||
<NumberInput
|
||||
source="length"
|
||||
validate={validateLength}
|
||||
helperText="resources.registration_tokens.helper.length"
|
||||
step={1}
|
||||
/>
|
||||
<NumberInput
|
||||
source="uses_allowed"
|
||||
validate={validateUsesAllowed}
|
||||
step={1}
|
||||
/>
|
||||
<DateTimeInput source="expiry_time" parse={dateParser} />
|
||||
</SimpleForm>
|
||||
</Create>
|
||||
);
|
||||
|
||||
export const RegistrationTokenEdit = props => {
|
||||
return (
|
||||
<Edit {...props}>
|
||||
<SimpleForm>
|
||||
<TextInput source="token" disabled />
|
||||
<NumberInput source="pending" disabled />
|
||||
<NumberInput source="completed" disabled />
|
||||
<NumberInput
|
||||
source="uses_allowed"
|
||||
validate={validateUsesAllowed}
|
||||
step={1}
|
||||
/>
|
||||
<DateTimeInput
|
||||
source="expiry_time"
|
||||
parse={dateParser}
|
||||
format={dateFormatter}
|
||||
/>
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
@@ -171,10 +171,14 @@ const RoomDirectoryFilter = ({ ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterableRoomDirectoryList = ({ ...props }) => {
|
||||
export const FilterableRoomDirectoryList = ({
|
||||
roomDirectoryFilters,
|
||||
dispatch,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const translate = useTranslate();
|
||||
const filter = props.roomDirectoryFilters;
|
||||
const filter = roomDirectoryFilters;
|
||||
const roomIdFilter = filter && filter.room_id ? true : false;
|
||||
const topicFilter = filter && filter.topic ? true : false;
|
||||
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
|
||||
@@ -187,7 +191,7 @@ export const FilterableRoomDirectoryList = ({ ...props }) => {
|
||||
filters={<RoomDirectoryFilter />}
|
||||
perPage={100}
|
||||
>
|
||||
<Datagrid>
|
||||
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}>
|
||||
<AvatarField
|
||||
source="avatar_src"
|
||||
sortable={false}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "react-admin";
|
||||
import ActionDelete from "@material-ui/icons/Delete";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import { alpha } from "@material-ui/core/styles/colorManipulator";
|
||||
import classnames from "classnames";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
@@ -16,7 +16,7 @@ const useStyles = makeStyles(
|
||||
deleteButton: {
|
||||
color: theme.palette.error.main,
|
||||
"&:hover": {
|
||||
backgroundColor: fade(theme.palette.error.main, 0.12),
|
||||
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||
// Reset on mouse devices
|
||||
"@media (hover: none)": {
|
||||
backgroundColor: "transparent",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import { alpha } from "@material-ui/core/styles/colorManipulator";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { Tooltip } from "@material-ui/core";
|
||||
import {
|
||||
BooleanInput,
|
||||
Button,
|
||||
@@ -10,23 +11,29 @@ 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 => ({
|
||||
deleteButton: {
|
||||
color: theme.palette.error.main,
|
||||
"&:hover": {
|
||||
backgroundColor: fade(theme.palette.error.main, 0.12),
|
||||
backgroundColor: alpha(theme.palette.error.main, 0.12),
|
||||
// Reset on mouse devices
|
||||
"@media (hover: none)": {
|
||||
backgroundColor: "transparent",
|
||||
@@ -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,6 +8,7 @@ import {
|
||||
DeleteButton,
|
||||
Filter,
|
||||
List,
|
||||
NumberField,
|
||||
Pagination,
|
||||
ReferenceField,
|
||||
ReferenceManyField,
|
||||
@@ -18,10 +19,14 @@ import {
|
||||
TabbedShowLayout,
|
||||
TextField,
|
||||
TopToolbar,
|
||||
useRecordContext,
|
||||
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 } 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";
|
||||
import PageviewIcon from "@material-ui/icons/Pageview";
|
||||
@@ -29,6 +34,7 @@ import UserIcon from "@material-ui/icons/Group";
|
||||
import ViewListIcon from "@material-ui/icons/ViewList";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import EventIcon from "@material-ui/icons/Event";
|
||||
|
||||
import {
|
||||
RoomDirectoryBulkDeleteButton,
|
||||
RoomDirectoryBulkSaveButton,
|
||||
@@ -36,6 +42,22 @@ import {
|
||||
RoomDirectorySaveButton,
|
||||
} from "./RoomDirectory";
|
||||
|
||||
const date_format = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
helper_forward_extremities: {
|
||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||
margin: "0.5em",
|
||||
},
|
||||
}));
|
||||
|
||||
const RoomPagination = props => (
|
||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||
);
|
||||
@@ -107,6 +129,7 @@ const RoomShowActions = ({ basePath, data, resource }) => {
|
||||
};
|
||||
|
||||
export const RoomShow = props => {
|
||||
const classes = useStyles({ props });
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
|
||||
@@ -136,7 +159,11 @@ export const RoomShow = props => {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
|
||||
<Tab
|
||||
label="synapseadmin.rooms.tabs.members"
|
||||
icon={<UserIcon />}
|
||||
path="members"
|
||||
>
|
||||
<ReferenceManyField
|
||||
reference="room_members"
|
||||
target="room_id"
|
||||
@@ -218,6 +245,7 @@ export const RoomShow = props => {
|
||||
]}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
label={translate("resources.room_state.name", { smart_count: 2 })}
|
||||
icon={<EventIcon />}
|
||||
@@ -233,14 +261,7 @@ export const RoomShow = props => {
|
||||
<DateField
|
||||
source="origin_server_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={false}
|
||||
/>
|
||||
<TextField source="content" sortable={false} />
|
||||
@@ -254,6 +275,35 @@ export const RoomShow = props => {
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
label="resources.forward_extremities.name"
|
||||
icon={<FastForwardIcon />}
|
||||
path="forward_extremities"
|
||||
>
|
||||
<div className={classes.helper_forward_extremities}>
|
||||
{translate("resources.rooms.helper.forward_extremities")}
|
||||
</div>
|
||||
<ReferenceManyField
|
||||
reference="forward_extremities"
|
||||
target="room_id"
|
||||
addLabel={false}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<TextField source="id" sortable={false} />
|
||||
<DateField
|
||||
source="received_ts"
|
||||
showTime
|
||||
options={date_format}
|
||||
sortable={false}
|
||||
/>
|
||||
<NumberField source="depth" sortable={false} />
|
||||
<TextField source="state_group" sortable={false} />
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</Tab>
|
||||
|
||||
|
||||
</TabbedShowLayout>
|
||||
</Show>
|
||||
);
|
||||
@@ -267,7 +317,7 @@ const RoomBulkActionButtons = props => (
|
||||
{...props}
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
confirmContent="resources.rooms.action.erase.content"
|
||||
undoable={false}
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
@@ -305,8 +355,22 @@ const RoomFilter = ({ ...props }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const FilterableRoomList = ({ ...props }) => {
|
||||
const filter = props.roomFilters;
|
||||
const RoomNameField = props => {
|
||||
const { source } = props;
|
||||
const record = useRecordContext(props);
|
||||
return (
|
||||
<span>{record[source] || record["canonical_alias"] || record["id"]}</span>
|
||||
);
|
||||
};
|
||||
|
||||
RoomNameField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
source: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => {
|
||||
const filter = roomFilters;
|
||||
const localMembersFilter =
|
||||
filter && filter.joined_local_members ? true : false;
|
||||
const stateEventsFilter = filter && filter.state_events ? true : false;
|
||||
@@ -327,7 +391,7 @@ const FilterableRoomList = ({ ...props }) => {
|
||||
sortBy="encryption"
|
||||
label={<HttpsIcon />}
|
||||
/>
|
||||
<TextField source="name" />
|
||||
<RoomNameField source="name" />
|
||||
<TextField source="joined_members" />
|
||||
{localMembersFilter && <TextField source="joined_local_members" />}
|
||||
{stateEventsFilter && <TextField source="state_events" />}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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,
|
||||
@@ -35,8 +37,9 @@ import {
|
||||
BulkDeleteButton,
|
||||
DeleteButton,
|
||||
SaveButton,
|
||||
maxLength,
|
||||
regex,
|
||||
useRedirect,
|
||||
required,
|
||||
useTranslate,
|
||||
Pagination,
|
||||
CreateButton,
|
||||
@@ -45,11 +48,13 @@ import {
|
||||
sanitizeListRestProps,
|
||||
NumberField,
|
||||
} from "react-admin";
|
||||
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";
|
||||
|
||||
const redirect = (basePath, id, data) => {
|
||||
const redirect = () => {
|
||||
return {
|
||||
pathname: "/import_users",
|
||||
};
|
||||
@@ -67,6 +72,15 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
const date_format = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
const UserListActions = ({
|
||||
currentSort,
|
||||
className,
|
||||
@@ -85,7 +99,6 @@ const UserListActions = ({
|
||||
total,
|
||||
...rest
|
||||
}) => {
|
||||
const redirectTo = useRedirect();
|
||||
return (
|
||||
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
|
||||
{filters &&
|
||||
@@ -106,12 +119,7 @@ const UserListActions = ({
|
||||
maxResults={maxResults}
|
||||
/>
|
||||
{/* Add your custom actions */}
|
||||
<Button
|
||||
onClick={() => {
|
||||
redirectTo(redirect);
|
||||
}}
|
||||
label="CSV Import"
|
||||
>
|
||||
<Button component={Link} to={redirect} label="CSV Import">
|
||||
<GetAppIcon style={{ transform: "rotate(180deg)", fontSize: "20" }} />
|
||||
</Button>
|
||||
</TopToolbar>
|
||||
@@ -146,7 +154,7 @@ const UserBulkActionButtons = props => (
|
||||
{...props}
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle="resources.users.helper.erase"
|
||||
undoable={false}
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
@@ -162,6 +170,7 @@ export const UserList = props => {
|
||||
{...props}
|
||||
filters={<UserFilter />}
|
||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||
sort={{ field: "name", order: "ASC" }}
|
||||
actions={<UserListActions maxResults={10000} />}
|
||||
bulkActionButtons={<UserBulkActionButtons />}
|
||||
pagination={<UserPagination />}
|
||||
@@ -169,24 +178,36 @@ export const UserList = props => {
|
||||
<Datagrid rowClick="edit">
|
||||
<AvatarField
|
||||
source="avatar_src"
|
||||
sortable={false}
|
||||
className={classes.small}
|
||||
sortBy="avatar_url"
|
||||
/>
|
||||
<TextField source="id" sortBy="name" />
|
||||
<TextField source="displayname" />
|
||||
<BooleanField source="is_guest" />
|
||||
<BooleanField source="admin" />
|
||||
<BooleanField source="deactivated" />
|
||||
<DateField
|
||||
source="creation_ts"
|
||||
label="resources.users.fields.creation_ts_ms"
|
||||
showTime
|
||||
options={date_format}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
// 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");
|
||||
@@ -233,7 +254,7 @@ const UserEditToolbar = props => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<Toolbar {...props}>
|
||||
<SaveButton submitOnEnter={true} />
|
||||
<SaveButton submitOnEnter={true} disabled={props.pristine} />
|
||||
<DeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle={translate("resources.users.helper.erase", {
|
||||
@@ -250,19 +271,34 @@ export const UserCreate = props => (
|
||||
<Create {...props}>
|
||||
<SimpleForm>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
<TextInput source="displayname" />
|
||||
<PasswordInput source="password" autoComplete="new-password" />
|
||||
<TextInput source="displayname" validate={maxLength(256)} />
|
||||
<PasswordInput
|
||||
source="password"
|
||||
autoComplete="new-password"
|
||||
validate={maxLength(512)}
|
||||
/>
|
||||
<BooleanInput source="admin" />
|
||||
<ArrayInput source="threepids">
|
||||
<SimpleFormIterator>
|
||||
<SimpleFormIterator disableReordering>
|
||||
<SelectInput
|
||||
source="medium"
|
||||
choices={[
|
||||
{ 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 disableReordering>
|
||||
<TextInput source="auth_provider" validate={required()} />
|
||||
<TextInput
|
||||
source="external_id"
|
||||
label="resources.users.fields.id"
|
||||
validate={required()}
|
||||
/>
|
||||
<TextInput source="address" />
|
||||
</SimpleFormIterator>
|
||||
</ArrayInput>
|
||||
</SimpleForm>
|
||||
@@ -303,18 +339,7 @@ export const UserEdit = props => {
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<DateField source="creation_ts_ms" showTime options={date_format} />
|
||||
<TextField source="consent_version" />
|
||||
</FormTab>
|
||||
|
||||
@@ -324,7 +349,7 @@ export const UserEdit = props => {
|
||||
path="threepid"
|
||||
>
|
||||
<ArrayInput source="threepids">
|
||||
<SimpleFormIterator>
|
||||
<SimpleFormIterator disableReordering>
|
||||
<SelectInput
|
||||
source="medium"
|
||||
choices={[
|
||||
@@ -337,6 +362,23 @@ export const UserEdit = props => {
|
||||
</ArrayInput>
|
||||
</FormTab>
|
||||
|
||||
<FormTab
|
||||
label="synapseadmin.users.tabs.sso"
|
||||
icon={<AssignmentIndIcon />}
|
||||
path="sso"
|
||||
>
|
||||
<ArrayInput source="external_ids" label={false}>
|
||||
<SimpleFormIterator disableReordering>
|
||||
<TextInput source="auth_provider" validate={required()} />
|
||||
<TextInput
|
||||
source="external_id"
|
||||
label="resources.users.fields.id"
|
||||
validate={required()}
|
||||
/>
|
||||
</SimpleFormIterator>
|
||||
</ArrayInput>
|
||||
</FormTab>
|
||||
|
||||
<FormTab
|
||||
label={translate("resources.devices.name", { smart_count: 2 })}
|
||||
icon={<DevicesIcon />}
|
||||
@@ -354,14 +396,7 @@ export const UserEdit = props => {
|
||||
<DateField
|
||||
source="last_seen_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={false}
|
||||
/>
|
||||
<DeviceRemoveButton />
|
||||
@@ -386,23 +421,18 @@ export const UserEdit = props => {
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<TextField source="ip" sortable={false} />
|
||||
<TextField source="port" sortable={false} emptyText="13001" />
|
||||
|
||||
<DateField
|
||||
source="last_seen"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
options={date_format}
|
||||
sortable={false}
|
||||
/>
|
||||
<TextField
|
||||
source="user_agent"
|
||||
sortable={false}
|
||||
style={{ width: "100%" }}
|
||||
style={{ width: "90%" }}
|
||||
/>
|
||||
</Datagrid>
|
||||
</ArrayField>
|
||||
@@ -420,45 +450,48 @@ export const UserEdit = props => {
|
||||
addLabel={false}
|
||||
pagination={<UserPagination />}
|
||||
perPage={50}
|
||||
sort={{ field: "created_ts", order: "DESC" }}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<DateField
|
||||
source="created_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
sortable={false}
|
||||
/>
|
||||
<DateField source="created_ts" showTime options={date_format} />
|
||||
<DateField
|
||||
source="last_access_ts"
|
||||
showTime
|
||||
options={{
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}}
|
||||
sortable={false}
|
||||
options={date_format}
|
||||
/>
|
||||
<TextField source="media_id" sortable={false} />
|
||||
<NumberField source="media_length" sortable={false} />
|
||||
<TextField source="media_type" sortable={false} />
|
||||
<TextField source="upload_name" sortable={false} />
|
||||
<TextField source="quarantined_by" sortable={false} />
|
||||
<BooleanField source="safe_from_quarantine" sortable={false} />
|
||||
<TextField source="media_id" />
|
||||
<NumberField source="media_length" />
|
||||
<TextField source="media_type" />
|
||||
<TextField source="upload_name" />
|
||||
<TextField source="quarantined_by" />
|
||||
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
||||
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
||||
<DeleteButton mutationMode="pessimistic" redirect={false} />
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</FormTab>
|
||||
|
||||
<FormTab
|
||||
label={translate("resources.urlpreview.name", { smart_count: 2 })}
|
||||
icon={<PermMediaIcon />}
|
||||
path="user_urlpreview"
|
||||
>
|
||||
<ReferenceManyField
|
||||
reference="user_urlpreview"
|
||||
target="user_id"
|
||||
addLabel={false}
|
||||
pagination={<UserPagination />}
|
||||
perPage={50}
|
||||
sort={{ field: "created_ts", order: "DESC" }}
|
||||
>
|
||||
<Datagrid style={{ width: "100%" }}>
|
||||
<DateField source="created_ts" showTime options={date_format} sortable={false} />
|
||||
<TextField source="media_id" sortable={false} />
|
||||
<TextField source="url_cache" sortable={false} />
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</FormTab>
|
||||
|
||||
<FormTab
|
||||
label={translate("resources.rooms.name", { smart_count: 2 })}
|
||||
icon={<ViewListIcon />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import germanMessages from "ra-language-german";
|
||||
|
||||
export default {
|
||||
const de = {
|
||||
...germanMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
@@ -10,10 +10,11 @@ export default {
|
||||
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",
|
||||
sso_sign_in: "Anmeldung mit SSO",
|
||||
},
|
||||
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",
|
||||
@@ -96,7 +97,6 @@ export default {
|
||||
},
|
||||
resources: {
|
||||
users: {
|
||||
backtolist: "Zurück zur Liste",
|
||||
name: "Benutzer",
|
||||
email: "E-Mail",
|
||||
msisdn: "Telefon",
|
||||
@@ -120,6 +120,7 @@ export default {
|
||||
address: "Adresse",
|
||||
creation_ts_ms: "Zeitpunkt der Erstellung",
|
||||
consent_version: "Zugestimmte Geschäftsbedingungen",
|
||||
auth_provider: "Provider",
|
||||
},
|
||||
helper: {
|
||||
deactivate:
|
||||
@@ -152,6 +153,10 @@ export default {
|
||||
topic: "Thema",
|
||||
avatar: "Avatar",
|
||||
},
|
||||
helper: {
|
||||
forward_extremities:
|
||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
|
||||
},
|
||||
enums: {
|
||||
join_rules: {
|
||||
public: "Öffentlich",
|
||||
@@ -236,7 +241,7 @@ export default {
|
||||
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",
|
||||
},
|
||||
@@ -254,8 +259,26 @@ export default {
|
||||
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||
},
|
||||
helper: {
|
||||
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.",
|
||||
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: {
|
||||
@@ -284,8 +307,7 @@ export default {
|
||||
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
||||
send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
@@ -295,6 +317,15 @@ export default {
|
||||
media_length: "Größe der Dateien",
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "Vorderextremitäten",
|
||||
fields: {
|
||||
id: "Event-ID",
|
||||
received_ts: "Zeitstempel",
|
||||
depth: "Tiefe",
|
||||
state_group: "Zustandsgruppe",
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "Zustandsereignisse",
|
||||
fields: {
|
||||
@@ -321,6 +352,19 @@ export default {
|
||||
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Registrierungstoken",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Gültige Token",
|
||||
uses_allowed: "Anzahl",
|
||||
pending: "Ausstehend",
|
||||
completed: "Abgeschlossen",
|
||||
expiry_time: "Ablaufzeit",
|
||||
length: "Länge",
|
||||
},
|
||||
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
...germanMessages.ra,
|
||||
@@ -341,7 +385,7 @@ export default {
|
||||
},
|
||||
},
|
||||
notification: {
|
||||
...germanMessages.ra.notifiaction,
|
||||
...germanMessages.ra.notification,
|
||||
logged_out: "Abgemeldet",
|
||||
},
|
||||
page: {
|
||||
@@ -349,5 +393,10 @@ export default {
|
||||
empty: "Keine Einträge vorhanden",
|
||||
invite: "",
|
||||
},
|
||||
navigation: {
|
||||
...germanMessages.ra.navigation,
|
||||
skip_nav: "Zum Inhalt springen",
|
||||
},
|
||||
},
|
||||
};
|
||||
export default de;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import englishMessages from "ra-language-english";
|
||||
|
||||
export default {
|
||||
const en = {
|
||||
...englishMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
@@ -10,10 +10,11 @@ export default {
|
||||
username_error: "Please enter fully qualified user ID: '@user:domain'",
|
||||
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||
url_error: "Not a valid Matrix server URL",
|
||||
sso_sign_in: "Sign in with SSO",
|
||||
},
|
||||
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: {
|
||||
tabs: {
|
||||
@@ -95,7 +96,6 @@ export default {
|
||||
},
|
||||
resources: {
|
||||
users: {
|
||||
backtolist: "Back to list",
|
||||
name: "User |||| Users",
|
||||
email: "Email",
|
||||
msisdn: "Phone",
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
address: "Address",
|
||||
creation_ts_ms: "Creation timestamp",
|
||||
consent_version: "Consent version",
|
||||
auth_provider: "Provider",
|
||||
},
|
||||
helper: {
|
||||
deactivate: "You must provide a password to re-activate an account.",
|
||||
@@ -150,6 +151,10 @@ export default {
|
||||
topic: "Topic",
|
||||
avatar: "Avatar",
|
||||
},
|
||||
helper: {
|
||||
forward_extremities:
|
||||
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.",
|
||||
},
|
||||
enums: {
|
||||
join_rules: {
|
||||
public: "Public",
|
||||
@@ -169,10 +174,12 @@ export default {
|
||||
},
|
||||
unencrypted: "Unencrypted",
|
||||
},
|
||||
erase: {
|
||||
title: "Delete room",
|
||||
content:
|
||||
"Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
|
||||
action: {
|
||||
erase: {
|
||||
title: "Delete room",
|
||||
content:
|
||||
"Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
|
||||
},
|
||||
},
|
||||
},
|
||||
reports: {
|
||||
@@ -228,7 +235,7 @@ export default {
|
||||
name: "Media",
|
||||
fields: {
|
||||
media_id: "Media ID",
|
||||
media_length: "Lenght",
|
||||
media_length: "File Size (in Bytes)",
|
||||
media_type: "Type",
|
||||
upload_name: "File name",
|
||||
quarantined_by: "Quarantined by",
|
||||
@@ -250,8 +257,26 @@ export default {
|
||||
send_failure: "An error has occurred.",
|
||||
},
|
||||
helper: {
|
||||
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.",
|
||||
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: {
|
||||
@@ -280,8 +305,7 @@ export default {
|
||||
send_failure: "An error has occurred.",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
||||
send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
@@ -291,6 +315,15 @@ export default {
|
||||
media_length: "Media length",
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "Forward Extremities",
|
||||
fields: {
|
||||
id: "Event ID",
|
||||
received_ts: "Timestamp",
|
||||
depth: "Depth",
|
||||
state_group: "State group",
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "State events",
|
||||
fields: {
|
||||
@@ -318,4 +351,18 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "Registration tokens",
|
||||
fields: {
|
||||
token: "Token",
|
||||
valid: "Valid token",
|
||||
uses_allowed: "Uses allowed",
|
||||
pending: "Pending",
|
||||
completed: "Completed",
|
||||
expiry_time: "Expiry time",
|
||||
length: "Length",
|
||||
},
|
||||
helper: { length: "Length of the token if no token is given." },
|
||||
},
|
||||
};
|
||||
export default en;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chineseMessages from "ra-language-chinese";
|
||||
|
||||
export default {
|
||||
const zh = {
|
||||
...chineseMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
@@ -10,10 +10,12 @@ export default {
|
||||
username_error: "请输入完整有效的用户 ID: '@user:domain'",
|
||||
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
||||
url_error: "不是一个有效的 Matrix 服务器地址",
|
||||
sso_sign_in: "使用 SSO 登录",
|
||||
},
|
||||
users: {
|
||||
invalid_user_id:
|
||||
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||
tabs: { sso: "SSO" },
|
||||
},
|
||||
rooms: {
|
||||
tabs: {
|
||||
@@ -98,7 +100,6 @@ export default {
|
||||
},
|
||||
resources: {
|
||||
users: {
|
||||
backtolist: "回到列表",
|
||||
name: "用户",
|
||||
email: "邮箱",
|
||||
msisdn: "电话",
|
||||
@@ -122,6 +123,7 @@ export default {
|
||||
address: "地址",
|
||||
creation_ts_ms: "创建时间戳",
|
||||
consent_version: "协议版本",
|
||||
device_id: "设备ID"
|
||||
},
|
||||
helper: {
|
||||
deactivate: "您必须提供一串密码来激活账户。",
|
||||
@@ -169,13 +171,16 @@ export default {
|
||||
},
|
||||
unencrypted: "未加密",
|
||||
},
|
||||
helper: {
|
||||
forward_extremities: "转发"
|
||||
}
|
||||
},
|
||||
reports: {
|
||||
name: "报告事件",
|
||||
name: "举报事件",
|
||||
fields: {
|
||||
id: "ID",
|
||||
received_ts: "报告时间",
|
||||
user_id: "报告者",
|
||||
received_ts: "举报时间",
|
||||
user_id: "举报者",
|
||||
name: "房间名",
|
||||
score: "分数",
|
||||
reason: "原因",
|
||||
@@ -198,7 +203,8 @@ export default {
|
||||
name: "连接",
|
||||
fields: {
|
||||
last_seen: "日期",
|
||||
ip: "IP 地址",
|
||||
ip: "源IP地址",
|
||||
port: "源端口",
|
||||
user_agent: "用户代理 (UA)",
|
||||
},
|
||||
},
|
||||
@@ -245,8 +251,7 @@ export default {
|
||||
send_failure: "出现了一个错误。",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
"这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
@@ -275,8 +280,7 @@ export default {
|
||||
send_failure: "出现了一个错误。",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
'向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
||||
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
@@ -286,5 +290,29 @@ export default {
|
||||
media_length: "媒体文件长度",
|
||||
},
|
||||
},
|
||||
room_state: {
|
||||
name: "状态"
|
||||
},
|
||||
room_directory:{
|
||||
name: "目录",
|
||||
action: {
|
||||
create: "新建"
|
||||
}
|
||||
},
|
||||
forward_extremities: {
|
||||
name: "转发"
|
||||
},
|
||||
registration_tokens: {
|
||||
name: "注册"
|
||||
},
|
||||
urlpreview: {
|
||||
name: "第三方链接"
|
||||
},
|
||||
quarantine_media: {
|
||||
action: {
|
||||
name: "隔离"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
export default zh;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
fetchMock.enableMocks();
|
||||
|
||||
@@ -2,21 +2,37 @@ import { fetchUtils } from "react-admin";
|
||||
|
||||
const authProvider = {
|
||||
// called when the user attempts to log in
|
||||
login: ({ base_url, username, password }) => {
|
||||
login: ({ base_url, username, password, loginToken }) => {
|
||||
// force homeserver for protection in case the form is manipulated
|
||||
base_url = process.env.REACT_APP_SERVER || base_url;
|
||||
|
||||
console.log("login ");
|
||||
const options = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
type: "m.login.password",
|
||||
user: username,
|
||||
password: password,
|
||||
initial_device_display_name: "Synapse Admin",
|
||||
}),
|
||||
body: JSON.stringify(
|
||||
Object.assign(
|
||||
{
|
||||
device_id: localStorage.getItem("device_id"),
|
||||
initial_device_display_name: "Synapse Admin",
|
||||
},
|
||||
loginToken
|
||||
? {
|
||||
type: "m.login.token",
|
||||
token: loginToken,
|
||||
}
|
||||
: {
|
||||
type: "m.login.password",
|
||||
user: username,
|
||||
password: password,
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
// use the base_url from login instead of the well_known entry from the
|
||||
// server, since the admin might want to access the admin API via some
|
||||
// private address
|
||||
base_url = base_url.replace(/\/+$/g, "");
|
||||
localStorage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
@@ -48,7 +64,6 @@ const authProvider = {
|
||||
if (typeof access_token === "string") {
|
||||
fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("device_id");
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -41,7 +41,9 @@ const resourceMap = {
|
||||
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",
|
||||
}),
|
||||
@@ -67,9 +69,8 @@ const resourceMap = {
|
||||
return json.total_rooms;
|
||||
},
|
||||
delete: params => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}/delete`,
|
||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
|
||||
body: { block: false },
|
||||
method: "POST",
|
||||
}),
|
||||
},
|
||||
reports: {
|
||||
@@ -101,7 +102,7 @@ const resourceMap = {
|
||||
path: "/_synapse/admin/v1/whois",
|
||||
map: c => ({
|
||||
...c,
|
||||
id: c.user_id,
|
||||
id: c.user_id
|
||||
}),
|
||||
data: "connections",
|
||||
},
|
||||
@@ -155,6 +156,19 @@ const resourceMap = {
|
||||
return json.total;
|
||||
},
|
||||
},
|
||||
user_urlpreview: {
|
||||
map: uu => ({
|
||||
...uu,
|
||||
id: uu.media_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
endpoint: `/_synapse/admin/v1/users/${id}/urlpreview`,
|
||||
}),
|
||||
data: "urlpreview",
|
||||
total: json => {
|
||||
return json.total;
|
||||
},
|
||||
},
|
||||
users_media: {
|
||||
map: um => ({
|
||||
...um,
|
||||
@@ -183,6 +197,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 => ({
|
||||
@@ -208,6 +248,22 @@ const resourceMap = {
|
||||
return json.total;
|
||||
},
|
||||
},
|
||||
forward_extremities: {
|
||||
map: fe => ({
|
||||
...fe,
|
||||
id: fe.event_id,
|
||||
}),
|
||||
reference: id => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
|
||||
}),
|
||||
data: "results",
|
||||
total: json => {
|
||||
return json.count;
|
||||
},
|
||||
delete: params => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
|
||||
}),
|
||||
},
|
||||
room_directory: {
|
||||
path: "/_matrix/client/r0/publicRooms",
|
||||
map: rd => ({
|
||||
@@ -232,6 +288,25 @@ const resourceMap = {
|
||||
method: "PUT",
|
||||
}),
|
||||
},
|
||||
registration_tokens: {
|
||||
path: "/_synapse/admin/v1/registration_tokens",
|
||||
map: rt => ({
|
||||
...rt,
|
||||
id: rt.token,
|
||||
}),
|
||||
data: "registration_tokens",
|
||||
total: json => {
|
||||
return json.registration_tokens.length;
|
||||
},
|
||||
create: params => ({
|
||||
endpoint: "/_synapse/admin/v1/registration_tokens/new",
|
||||
body: params,
|
||||
method: "POST",
|
||||
}),
|
||||
delete: params => ({
|
||||
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
function filterNullValues(key, value) {
|
||||
@@ -253,7 +328,8 @@ function getSearchOrder(order) {
|
||||
const dataProvider = {
|
||||
getList: (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const { user_id, name, guests, deactivated, search_term } = params.filter;
|
||||
const { user_id, name, guests, deactivated, search_term, valid } =
|
||||
params.filter;
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
const from = (page - 1) * perPage;
|
||||
@@ -265,6 +341,7 @@ const dataProvider = {
|
||||
name: name,
|
||||
guests: guests,
|
||||
deactivated: deactivated,
|
||||
valid: valid,
|
||||
order_by: field,
|
||||
dir: getSearchOrder(order),
|
||||
};
|
||||
@@ -314,10 +391,13 @@ const dataProvider = {
|
||||
getManyReference: (resource, params) => {
|
||||
console.log("getManyReference " + resource);
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
const from = (page - 1) * perPage;
|
||||
const query = {
|
||||
from: from,
|
||||
limit: perPage,
|
||||
order_by: field,
|
||||
dir: getSearchOrder(order),
|
||||
};
|
||||
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
|
||||
Reference in New Issue
Block a user