Merge branch 'master' into useRecordContext

This commit is contained in:
Michael Albert 2023-01-24 15:27:52 +01:00 committed by GitHub
commit 5e7ef1775f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 5022 additions and 5915 deletions

20
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
# Major updates for react-admin have breaking changes
- dependency-name: "react-admin"
update-types: ["version-update:semver-major"]
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 14
node-version: 16
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Run tests

View File

@ -17,13 +17,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -43,7 +43,7 @@ jobs:
esac
echo "::set-output name=tag::$tag"
- name: Build and Push Tag
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
context: .
push: true

26
.github/workflows/edge_ghpage.yml vendored Normal file
View 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@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- name: Install and Build 🔧
run: |
yarn install
yarn build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.4.1
with:
branch: gh-pages
folder: build

View File

@ -13,10 +13,10 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "14"
node-version: "16"
- run: yarn install
- run: yarn build
- run: |
@ -24,7 +24,7 @@ jobs:
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
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844
with:
files: dist/*.tar.gz
env:

View File

@ -6,6 +6,6 @@
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"bracketSameLine": false,
"arrowParens": "avoid"
}

View File

@ -1,5 +1,6 @@
dist: focal
language: node_js
node_js:
- lts/*
- 17
cache: yarn

View File

@ -1,11 +1,14 @@
# 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

View File

@ -1,17 +1,27 @@
[![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin)
[![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/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)
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)
# Synapse admin ui
This project is built using [react-admin](https://marmelab.com/react-admin/).
It needs at least Synapse v1.41.0 for all functions to work as expected!
## Usage
### Supported Synapse
It needs at least [Synapse](https://github.com/matrix-org/synapse) v1.48.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
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`
@ -19,15 +29,25 @@ You need access to the following 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 can use the current version of Synapse Admin without own installation direct
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
**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.
### Step-By-Step install
You have three options:
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
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):
#### 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
@ -36,7 +56,7 @@ Steps for 1):
- 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):
#### 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`
@ -49,7 +69,7 @@ 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 3):
#### 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`
@ -66,6 +86,9 @@ Steps for 3):
context: https://github.com/Awesome-Technologies/synapse-admin.git
# args:
# - NODE_OPTIONS="--max_old_space_size=1024"
# # see #266, PUBLIC_URL must be without surrounding quotation marks
# - PUBLIC_URL=/synapse-admin
# - REACT_APP_SERVER="https://matrix.example.com"
ports:
- "8080:80"
restart: unless-stopped

View File

@ -12,10 +12,15 @@ services:
# replace the context definition with this:
# context: https://github.com/Awesome-Technologies/synapse-admin.git
# args:
# if you're building on an architecture other than amd64, make sure
# to define a maximum ram for node. otherwise the build will fail.
# args:
# - NODE_OPTIONS="--max_old_space_size=1024"
# default is /
# - PUBLIC_URL=/synapse-admin
# You can use a fixed homeserver, so that the user can no longer
# define it himself
# - REACT_APP_SERVER="https://matrix.example.com"
ports:
- "8080:80"
restart: unless-stopped

View File

@ -1,6 +1,6 @@
{
"name": "synapse-admin",
"version": "0.8.4",
"version": "0.8.5",
"description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0",
@ -10,12 +10,13 @@
"url": "https://github.com/Awesome-Technologies/synapse-admin"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.8",
"eslint": "^7.25.0",
"@testing-library/user-event": "^14.4.3",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0",
"ra-test": "^3.15.0"
@ -26,9 +27,9 @@
"ra-language-chinese": "^2.0.10",
"ra-language-german": "^3.13.4",
"react": "^17.0.0",
"react-admin": "^3.15.0",
"react-admin": "^3.19.7",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.0"
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",

View File

@ -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" />

View File

@ -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" />

View File

@ -78,11 +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 } = {},
@ -137,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;
@ -188,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]
);
@ -200,7 +270,7 @@ const LoginPage = ({ theme }) => {
name="username"
component={renderInput}
label={translate("ra.auth.username")}
disabled={loading}
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
fullWidth
@ -212,7 +282,7 @@ const LoginPage = ({ theme }) => {
component={renderInput}
label={translate("ra.auth.password")}
type="password"
disabled={loading}
disabled={loading || !supportPassAuth}
resettable
fullWidth
/>
@ -273,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 />

View 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>
);
};

View File

@ -1,15 +1,8 @@
import React, { Fragment, useState } from "react";
import {
Button,
useMutation,
useNotify,
Confirm,
useRecordContext,
useRefresh,
} from "react-admin";
import { Button, useDelete, useNotify, Confirm, useRecordContext, useRefresh } from "react-admin";
import ActionDelete from "@material-ui/icons/Delete";
import { makeStyles } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import { alpha } from "@material-ui/core/styles/colorManipulator";
import classnames from "classnames";
const useStyles = makeStyles(
@ -17,7 +10,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",
@ -35,7 +28,7 @@ export const DeviceRemoveButton = props => {
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice, { loading }] = useMutation();
const [removeDevice, { isLoading }] = useDelete("devices");
if (!record) return null;
@ -44,21 +37,15 @@ export const DeviceRemoveButton = props => {
const handleConfirm = () => {
removeDevice(
{
type: "delete",
resource: "devices",
payload: {
id: record.id,
user_id: record.user_id,
},
},
{ payload: { id: record.id, user_id: record.user_id } },
{
onSuccess: () => {
notify("resources.devices.action.erase.success");
refresh();
},
onFailure: () =>
notify("resources.devices.action.erase.failure", "error"),
onFailure: () => {
notify("resources.devices.action.erase.failure", { type: "error" });
},
}
);
setOpen(false);
@ -75,7 +62,7 @@ export const DeviceRemoveButton = props => {
</Button>
<Confirm
isOpen={open}
loading={loading}
loading={isLoading}
onConfirm={handleConfirm}
onClose={handleDialogClose}
title="resources.devices.action.erase.title"

View File

@ -1,6 +1,6 @@
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 {
@ -34,7 +34,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",

View File

@ -41,6 +41,15 @@ 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",
@ -150,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"
@ -248,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} />
@ -288,14 +294,7 @@ export const RoomShow = 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={false}
/>
<NumberField source="depth" sortable={false} />

View File

@ -72,6 +72,25 @@ const useStyles = makeStyles({
},
});
const choices_medium = [
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
];
const choices_type = [
{ id: "bot", name: "bot" },
{ id: "support", name: "support" },
];
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const UserListActions = ({
currentSort,
className,
@ -181,14 +200,7 @@ export const UserList = props => {
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
options={date_format}
/>
</Datagrid>
</List>
@ -248,20 +260,31 @@ export function generateRandomUser() {
};
}
const UserEditToolbar = props => {
const UserEditToolbar = props => (
<Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} />
</Toolbar>
);
const UserEditActions = ({ data }) => {
const translate = useTranslate();
var userStatus = "";
if (data) {
userStatus = data.deactivated;
}
return (
<Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} />
<TopToolbar>
{!userStatus && <ServerNoticeButton record={data} />}
<DeleteButton
record={data}
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
/>
<ServerNoticeButton />
</Toolbar>
</TopToolbar>
);
};
@ -275,22 +298,26 @@ export const UserCreate = props => (
autoComplete="new-password"
validate={maxLength(512)}
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
allowEmpty={true}
resettable
/>
<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" },
]}
choices={choices_medium}
validate={required()}
/>
<TextInput source="address" validate={validateAddress} />
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator>
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
@ -315,11 +342,12 @@ const UserTitle = props => {
</span>
);
};
export const UserEdit = props => {
const classes = useStyles();
const translate = useTranslate();
return (
<Edit {...props} title={<UserTitle />}>
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab
label={translate("resources.users.name", { smart_count: 1 })}
@ -332,24 +360,24 @@ export const UserEdit = props => {
/>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<PasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
allowEmpty={true}
resettable
/>
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<DateField
source="creation_ts_ms"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
/>
<DateField source="creation_ts_ms" showTime options={date_format} />
<TextField source="consent_version" />
</FormTab>
@ -359,14 +387,8 @@ export const UserEdit = props => {
path="threepid"
>
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<SimpleFormIterator disableReordering>
<SelectInput source="medium" choices={choices_medium} />
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
@ -378,7 +400,7 @@ export const UserEdit = props => {
path="sso"
>
<ArrayInput source="external_ids" label={false}>
<SimpleFormIterator>
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
@ -406,14 +428,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 />
@ -441,14 +456,7 @@ export const UserEdit = props => {
<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
@ -475,29 +483,11 @@ export const UserEdit = props => {
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",
}}
/>
<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",
}}
options={date_format}
/>
<TextField source="media_id" />
<NumberField source="media_length" />

View File

@ -10,6 +10,7 @@ const de = {
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: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
@ -96,7 +97,6 @@ const de = {
},
resources: {
users: {
backtolist: "Zurück zur Liste",
name: "Benutzer",
email: "E-Mail",
msisdn: "Telefon",
@ -121,8 +121,11 @@ const de = {
creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen",
auth_provider: "Provider",
user_type: "Benutzertyp",
},
helper: {
password:
"Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
deactivate:
"Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten",
@ -352,6 +355,19 @@ const de = {
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,
@ -372,7 +388,7 @@ const de = {
},
},
notification: {
...germanMessages.ra.notifiaction,
...germanMessages.ra.notification,
logged_out: "Abgemeldet",
},
page: {

View File

@ -10,6 +10,7 @@ const en = {
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: "Localpart of a Matrix user-id without homeserver.",
@ -95,7 +96,6 @@ const en = {
},
resources: {
users: {
backtolist: "Back to list",
name: "User |||| Users",
email: "Email",
msisdn: "Phone",
@ -120,8 +120,10 @@ const en = {
creation_ts_ms: "Creation timestamp",
consent_version: "Consent version",
auth_provider: "Provider",
user_type: "User type",
},
helper: {
password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased",
},
@ -174,10 +176,12 @@ const en = {
},
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: {
@ -349,5 +353,18 @@ const en = {
},
},
},
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;

View File

@ -10,10 +10,12 @@ const zh = {
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 @@ const zh = {
},
resources: {
users: {
backtolist: "回到列表",
name: "用户",
email: "邮箱",
msisdn: "电话",

View File

@ -2,20 +2,31 @@ 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,
device_id: localStorage.getItem("device_id"),
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

View File

@ -41,14 +41,16 @@ const resourceMap = {
data: "users",
total: json => json.total,
create: data => ({
endpoint: `/_synapse/admin/v2/users/@${data.id}:${localStorage.getItem(
"home_server"
)}`,
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
data.id
)}:${localStorage.getItem("home_server")}`,
body: data,
method: "PUT",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/deactivate/${params.id}`,
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
params.id
)}`,
body: { erase: true },
method: "POST",
}),
@ -69,7 +71,7 @@ const resourceMap = {
return json.total_rooms;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
body: { block: false },
}),
},
@ -92,10 +94,12 @@ const resourceMap = {
return json.total;
},
reference: id => ({
endpoint: `/_synapse/admin/v2/users/${id}/devices`,
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
}),
delete: params => ({
endpoint: `/_synapse/admin/v2/users/${params.user_id}/devices/${params.id}`,
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
params.user_id
)}/devices/${params.id}`,
}),
},
connections: {
@ -137,7 +141,7 @@ const resourceMap = {
id: p.pushkey,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/pushers`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
}),
data: "pushers",
total: json => {
@ -149,7 +153,9 @@ const resourceMap = {
id: jr,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/joined_rooms`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
id
)}/joined_rooms`,
}),
data: "joined_rooms",
total: json => {
@ -162,7 +168,7 @@ const resourceMap = {
id: um.media_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${id}/media`,
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
}),
data: "media",
total: json => {
@ -275,11 +281,31 @@ 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) {
// Filtering out null properties
if (value === null) {
// to reset user_type from user, it must be null
if (value === null && key !== "user_type") {
return undefined;
}
return value;
@ -296,7 +322,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;
@ -308,6 +335,7 @@ const dataProvider = {
name: name,
guests: guests,
deactivated: deactivated,
valid: valid,
order_by: field,
dir: getSearchOrder(order),
};
@ -333,9 +361,11 @@ const dataProvider = {
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.id}`).then(({ json }) => ({
data: res.map(json),
}));
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
({ json }) => ({
data: res.map(json),
})
);
},
getMany: (resource, params) => {
@ -347,7 +377,9 @@ const dataProvider = {
const endpoint_url = homeserver + res.path;
return Promise.all(
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`))
params.ids.map(id =>
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
)
).then(responses => ({
data: responses.map(({ json }) => res.map(json)),
total: responses.length,
@ -388,7 +420,7 @@ const dataProvider = {
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.data.id}`, {
return jsonClient(`${endpoint_url}/${encodeURIComponent(params.data.id)}`, {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}).then(({ json }) => ({
@ -405,10 +437,13 @@ const dataProvider = {
const endpoint_url = homeserver + res.path;
return Promise.all(
params.ids.map(id => jsonClient(`${endpoint_url}/${id}`), {
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
})
params.ids.map(
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
{
method: "PUT",
body: JSON.stringify(params.data, filterNullValues),
}
)
).then(responses => ({
data: responses.map(({ json }) => json),
}));

10155
yarn.lock

File diff suppressed because it is too large Load Diff