Compare commits
53 Commits
master
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f639a1c9ff | ||
![]() |
8282bb0926 | ||
![]() |
264d2ff59d | ||
![]() |
90a6c7d8c2 | ||
![]() |
7de9166648 | ||
![]() |
0bf3440fc8 | ||
![]() |
cceae77529 | ||
![]() |
1474b46ff0 | ||
![]() |
01e3947b22 | ||
![]() |
e093bd8625 | ||
![]() |
390aab5ce7 | ||
![]() |
fb1a04971b | ||
![]() |
eff17a0929 | ||
![]() |
8eaaaa50ec | ||
![]() |
85b305d117 | ||
![]() |
4cbea6ffb6 | ||
![]() |
1967546ae4 | ||
![]() |
c9a3294852 | ||
![]() |
f1839387e2 | ||
![]() |
c8246a1d19 | ||
![]() |
3c6259cc88 | ||
![]() |
317df5af0f | ||
![]() |
2142770a5b | ||
![]() |
51297b49fc | ||
![]() |
311cc2a1f4 | ||
![]() |
6bc760a6fa | ||
![]() |
50c96cfd77 | ||
![]() |
7747dc7f28 | ||
![]() |
678867e981 | ||
![]() |
4a4fae104e | ||
![]() |
265b5157af | ||
![]() |
b5b593945d | ||
![]() |
15c8a30c92 | ||
![]() |
40c6eb0b95 | ||
![]() |
4c4e3a07f6 | ||
![]() |
35322fa01d | ||
![]() |
88e88fb6b9 | ||
![]() |
7016e5a349 | ||
![]() |
c7e275d4ec | ||
![]() |
b1d3340fce | ||
![]() |
2ae5930578 | ||
![]() |
9dd6940383 | ||
![]() |
e2194f5c49 | ||
![]() |
e6dcbcb052 | ||
![]() |
5acf3042c3 | ||
![]() |
7286abaaae | ||
![]() |
0d5e95fa7c | ||
![]() |
c4b54c40fb | ||
![]() |
ec0b980e06 | ||
![]() |
94ccd3ad36 | ||
![]() |
49d67f9130 | ||
![]() |
2bb846734e | ||
![]() |
056d9c6b4c |
@@ -1,13 +0,0 @@
|
||||
# Exclude a bunch of stuff which can make the build context a larger than it needs to be
|
||||
tests/
|
||||
build/
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
electron_app/
|
||||
karma-reports/
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.idea/
|
||||
.tmp/
|
||||
config.json*
|
16
.github/CONTRIBUTING.md
vendored
Normal file
16
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Contributing to [etkecc/synapse-admin](https://github.com/etkecc/synapse-admin)
|
||||
|
||||
While etke.cc fork is intended to accept more QoL changes and features,
|
||||
it's good idea to open PR into the upstream repo: [Awesome-Technologies/Synapse-Admin](https://github.com/Awesome-Technologies/synapse-admin).
|
||||
|
||||
1. Use the etkecc/synapse-admin **master** branch as your branch upstream: `git checkout master; git pull; git checkout -b my-new-feature`
|
||||
2. Once your changes are ready, please, open **2** PRs: one from your branch to `Awesome-Technologies/Synapse-Admin` **master**, and another one to `etkecc/synapse-admin` **main**
|
||||
3. Once PR is accepted in the `etkecc/synapse-admin`, update `README.md` file (either directly in the `main` branch, or via another PR) to add link to the merged PR in the [Fork differences](https://github.com/etkecc/synapse-admin#fork-differences) section
|
||||
|
||||
### Why?
|
||||
|
||||
The upstream project may not want to accept all the changes, so to ensure they are not lost, we will gladly add them to the etke.cc fork.
|
||||
Unfortunately, it's challenging to keep changes separated, so to avoid messing upstream and fork changes (e.g., CI changes that should not be pushed to the upstream, as they intended for this fork specifically), there are 2 branches:
|
||||
|
||||
* `master` - read-only copy of upstream's master branch to easily sync changes, and use it as base for new PRs
|
||||
* `main` - fork-own branch with all changes
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: etkecc
|
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
@@ -1,20 +0,0 @@
|
||||
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"
|
23
.github/workflows/build-test.yml
vendored
23
.github/workflows/build-test.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: build-test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install dependencies
|
||||
run: yarn --immutable
|
||||
- name: Run checks
|
||||
run: yarn lint
|
||||
- name: Run tests
|
||||
run: yarn test
|
63
.github/workflows/docker-release.yml
vendored
63
.github/workflows/docker-release.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Create docker image(s) and push to docker hub and ghcr.io
|
||||
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
|
||||
|
||||
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:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
awesometechnologies/synapse-admin
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and Push Tag
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
29
.github/workflows/edge_ghpage.yml
vendored
29
.github/workflows/edge_ghpage.yml
vendored
@@ -1,29 +0,0 @@
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
fetch-tags: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Install and Build 🔧
|
||||
run: |
|
||||
yarn install --immutable
|
||||
yarn build --base=/synapse-admin
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.6.3
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: dist
|
30
.github/workflows/github-release.yml
vendored
30
.github/workflows/github-release.yml
vendored
@@ -1,30 +0,0 @@
|
||||
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@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
- run: yarn install --immutable
|
||||
- run: yarn build
|
||||
- run: |
|
||||
version=`git describe --dirty --tags || echo unknown`
|
||||
cp -r dist synapse-admin-$version
|
||||
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
|
||||
- uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
|
||||
with:
|
||||
files: dist/*.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
119
.github/workflows/workflow.yml
vendored
Normal file
119
.github/workflows/workflow.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*" ]
|
||||
env:
|
||||
bunny_version: v0.1.0
|
||||
base_path: ./
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: read
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --network-timeout=300000
|
||||
- name: Set version into manifest.json
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0 || echo "latest")
|
||||
sed -i "s|\"icons\"|\"version\": \"$TAG\",\\n \"icons\"|g" public/manifest.json
|
||||
- name: Build
|
||||
run: yarn build --base=${{ env.base_path }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: dist/
|
||||
name: dist
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
overwrite: true
|
||||
include-hidden-files: true
|
||||
|
||||
docker:
|
||||
name: Docker
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
registry.etke.cc/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
|
||||
type=semver,pattern={{raw}}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
cdn:
|
||||
name: CDN
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload
|
||||
run: |
|
||||
wget -O bunny-upload.tar.gz https://github.com/etkecc/bunny-upload/releases/download/${{ env.bunny_version }}/bunny-upload_Linux_x86_64.tar.gz
|
||||
tar -xzf bunny-upload.tar.gz
|
||||
echo "${{ secrets.BUNNY_CONFIG }}" > bunny-config.yaml
|
||||
./bunny-upload -c bunny-config.yaml
|
||||
|
||||
github-release:
|
||||
name: Github Release
|
||||
needs: build
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Prepare release
|
||||
run: |
|
||||
mv dist synapse-admin
|
||||
tar chvzf synapse-admin.tar.gz synapse-admin
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: synapse-admin.tar.gz
|
||||
generate_release_notes: true
|
||||
make_latest: "true"
|
||||
draft: false
|
||||
prerelease: false
|
27
Dockerfile
27
Dockerfile
@@ -1,26 +1,5 @@
|
||||
# Builder
|
||||
FROM node:lts as builder
|
||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||
# Base path for synapse admin
|
||||
ARG BASE_PATH=./
|
||||
FROM ghcr.io/static-web-server/static-web-server:2
|
||||
|
||||
WORKDIR /src
|
||||
ENV SERVER_ROOT=/app
|
||||
|
||||
# Copy .yarn directory to the working directory (must be on a separate line!)
|
||||
# Use https://docs.docker.com/engine/reference/builder/#copy---parents when available
|
||||
COPY .yarn .yarn
|
||||
COPY package.json .yarnrc.yml yarn.lock ./
|
||||
|
||||
# Disable telemetry and install packages
|
||||
RUN yarn config set enableTelemetry 0 && yarn install --immutable --network-timeout=300000
|
||||
|
||||
COPY . /src
|
||||
RUN yarn build --base=$BASE_PATH
|
||||
|
||||
# App
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY --from=builder /src/dist /app
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
COPY ./dist /app
|
||||
|
30
README.md
30
README.md
@@ -9,6 +9,36 @@
|
||||
|
||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||
|
||||
## Fork differences
|
||||
|
||||
### Available via CDN
|
||||
|
||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
||||
|
||||
### Changes
|
||||
|
||||
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
||||
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||
user-friendly interface for managing Synapse homeservers.
|
||||
|
||||
The following changes are already implemented:
|
||||
|
||||
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||
* [Fix user's default tab not being shown](https://github.com/etkecc/synapse-admin/pull/8)
|
||||
* [Add identifier when authorizing with password](https://github.com/Awesome-Technologies/synapse-admin/pull/601)
|
||||
* [Add ability to toggle whether to show locked users](https://github.com/Awesome-Technologies/synapse-admin/pull/573)
|
||||
* [Fix user's display name in header on user's page](https://github.com/etkecc/synapse-admin/pull/9)
|
||||
* [Fix footer overlapping content](https://github.com/Awesome-Technologies/synapse-admin/issues/574)
|
||||
* Switch from nginx to [SWS](https://static-web-server.net/) for serving the app, reducing the size of the Docker image
|
||||
* [Fix redirect URL after user creation](https://github.com/etkecc/synapse-admin/pull/16)
|
||||
* [Display actual Synapse errors](https://github.com/etkecc/synapse-admin/pull/17)
|
||||
* [Fix base_url being undefined on unsuccessful login](https://github.com/etkecc/synapse-admin/pull/18)
|
||||
* [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (CI only)
|
||||
* [Federation page improvements](https://github.com/Awesome-Technologies/synapse-admin/pull/583) (using theme colors)
|
||||
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
## Usage
|
||||
|
||||
### Supported Synapse
|
||||
|
@@ -2,13 +2,13 @@ services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
hostname: synapse-admin
|
||||
image: awesometechnologies/synapse-admin:latest
|
||||
image: ghcr.io/etkecc/synapse-admin:latest
|
||||
# build:
|
||||
# context: .
|
||||
|
||||
# to use the docker-compose as standalone without a local repo clone,
|
||||
# replace the context definition with this:
|
||||
# context: https://github.com/Awesome-Technologies/synapse-admin.git
|
||||
# context: https://github.com/etkecc/synapse-admin.git
|
||||
|
||||
# args:
|
||||
# - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
|
@@ -121,8 +121,8 @@
|
||||
</div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<footer
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/etkecc/synapse-admin"
|
||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
|
||||
</a>
|
||||
|
15
justfile
Normal file
15
justfile
Normal file
@@ -0,0 +1,15 @@
|
||||
# Shows help
|
||||
default:
|
||||
@just --list --justfile {{ justfile() }}
|
||||
|
||||
# build the app
|
||||
build: __install
|
||||
@yarn run build --base=./
|
||||
|
||||
# run the app in a production mode
|
||||
run: build
|
||||
@python -m http.server -d dist 1313
|
||||
|
||||
# install the project
|
||||
__install:
|
||||
@yarn install --immutable --network-timeout=300000
|
@@ -8,7 +8,7 @@
|
||||
"homepage": ".",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"devDependencies": {
|
||||
|
105
src/components/DeleteRoomButton.tsx
Normal file
105
src/components/DeleteRoomButton.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||
import { Fragment, useState } from "react";
|
||||
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
|
||||
import ActionDelete from "@mui/icons-material/Delete";
|
||||
import ActionCheck from "@mui/icons-material/CheckCircle";
|
||||
import AlertError from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
interface DeleteRoomButtonProps {
|
||||
selectedIds: Identifier[];
|
||||
confirmTitle: string;
|
||||
confirmContent: string;
|
||||
}
|
||||
|
||||
const resourceName = "rooms";
|
||||
|
||||
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
||||
const translate = useTranslate();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [block, setBlock] = useState(true);
|
||||
|
||||
const notify = useNotify();
|
||||
const redirect = useRedirect();
|
||||
|
||||
const [deleteMany, { isLoading }] = useDeleteMany();
|
||||
const unselectAll = useUnselectAll(resourceName);
|
||||
const recordIds = props.selectedIds;
|
||||
|
||||
const handleDialogOpen = () => setOpen(true);
|
||||
const handleDialogClose = () => setOpen(false);
|
||||
|
||||
const handleDelete = (values: {block: boolean}) => {
|
||||
deleteMany(
|
||||
resourceName,
|
||||
{ ids: recordIds, meta: values },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.rooms.action.erase.success");
|
||||
handleDialogClose();
|
||||
unselectAll();
|
||||
redirect("/rooms");
|
||||
},
|
||||
onError: (error) =>
|
||||
notify("resources.rooms.action.erase.failure", { type: 'error' as NotificationType }),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setOpen(false);
|
||||
handleDelete({ block: block });
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button
|
||||
onClick={handleDialogOpen}
|
||||
disabled={isLoading}
|
||||
className={"ra-delete-button"}
|
||||
key="button"
|
||||
size="small"
|
||||
sx={{
|
||||
"&.MuiButton-sizeSmall": {
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
}}
|
||||
color={"error"}
|
||||
startIcon={<ActionDelete />}
|
||||
>
|
||||
{translate("ra.action.delete")}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleDialogClose}>
|
||||
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
||||
<SimpleForm toolbar={false}>
|
||||
<BooleanInput
|
||||
fullWidth
|
||||
source="block"
|
||||
value={block}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
||||
label="resources.rooms.action.erase.fields.block"
|
||||
defaultValue={true}
|
||||
/>
|
||||
</SimpleForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={false} onClick={handleDialogClose} startIcon={<AlertError />}>
|
||||
{translate("ra.action.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={handleConfirm}
|
||||
className={"ra-confirm RaConfirm-confirmPrimary"}
|
||||
autoFocus
|
||||
startIcon={<ActionCheck />}
|
||||
>
|
||||
{translate("ra.action.confirm")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteRoomButton;
|
6
src/components/error.ts
Normal file
6
src/components/error.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type MatrixError = {
|
||||
errcode: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const displayError = (errcode: string, status: number, message: string) => `${errcode} (${status}): ${message}`;
|
@@ -125,6 +125,7 @@ const de: SynapseTranslationMessages = {
|
||||
erased: "Gelöscht",
|
||||
guests: "Zeige Gäste",
|
||||
show_deactivated: "Zeige deaktivierte Benutzer",
|
||||
show_locked: "Zeige gesperrte Benutzer",
|
||||
user_id: "Suche Benutzer",
|
||||
displayname: "Anzeigename",
|
||||
password: "Passwort",
|
||||
@@ -142,6 +143,7 @@ const de: SynapseTranslationMessages = {
|
||||
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",
|
||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
|
||||
},
|
||||
action: {
|
||||
erase: "Lösche Benutzerdaten",
|
||||
@@ -197,6 +199,11 @@ const de: SynapseTranslationMessages = {
|
||||
title: "Raum löschen",
|
||||
content:
|
||||
"Sind Sie sicher dass Sie den Raum löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten und Medien, die der Raum beinhaltet werden vom Server gelöscht!",
|
||||
fields: {
|
||||
block: "Benutzer blockieren und daran hindern, dem Raum beizutreten",
|
||||
},
|
||||
success: "Raum/Räume erfolgreich gelöscht.",
|
||||
failure: "Der/die Raum/Räume konnten nicht gelöscht werden.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -124,6 +124,7 @@ const en: SynapseTranslationMessages = {
|
||||
erased: "Erased",
|
||||
guests: "Show guests",
|
||||
show_deactivated: "Show deactivated users",
|
||||
show_locked: "Show locked users",
|
||||
user_id: "Search user",
|
||||
displayname: "Displayname",
|
||||
password: "Password",
|
||||
@@ -141,6 +142,7 @@ const en: SynapseTranslationMessages = {
|
||||
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",
|
||||
erase_admin_error: "Deleting own user is not allowed.",
|
||||
},
|
||||
action: {
|
||||
erase: "Erase user data",
|
||||
@@ -196,6 +198,11 @@ const en: SynapseTranslationMessages = {
|
||||
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!",
|
||||
fields: {
|
||||
block: "Block and prevent users from joining the room",
|
||||
},
|
||||
success: "Room/s successfully deleted.",
|
||||
failure: "The room/s could not be deleted.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -120,6 +120,7 @@ const fa: SynapseTranslationMessages = {
|
||||
deactivated: "غیرفعال",
|
||||
guests: "نمایش مهمانان",
|
||||
show_deactivated: "نمایش کاربران غیرفعال شده",
|
||||
show_locked: "نمایش کاربران قفل شده",
|
||||
user_id: "جستجوی کاربر",
|
||||
displayname: "نام نمایشی",
|
||||
password: "رمز عبور",
|
||||
|
@@ -124,6 +124,7 @@ const fr: SynapseTranslationMessages = {
|
||||
deactivated: "Désactivé",
|
||||
guests: "Afficher les visiteurs",
|
||||
show_deactivated: "Afficher les utilisateurs désactivés",
|
||||
show_locked: "Afficher les utilisateurs verrouillés",
|
||||
user_id: "Rechercher un utilisateur",
|
||||
displayname: "Nom d'affichage",
|
||||
password: "Mot de passe",
|
||||
@@ -139,6 +140,7 @@ const fr: SynapseTranslationMessages = {
|
||||
helper: {
|
||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||
},
|
||||
action: {
|
||||
erase: "Effacer les données de l'utilisateur",
|
||||
@@ -194,6 +196,11 @@ const fr: SynapseTranslationMessages = {
|
||||
title: "Supprimer le salon",
|
||||
content:
|
||||
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
|
||||
fields: {
|
||||
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
|
||||
},
|
||||
success: "Salle/s supprimées avec succès.",
|
||||
failure: "La/les salle/s n'ont pas pu être supprimées.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
7
src/i18n/index.d.ts
vendored
7
src/i18n/index.d.ts
vendored
@@ -120,6 +120,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
erased?: string; // TODO: fa, fr, it, zh
|
||||
guests: string;
|
||||
show_deactivated: string;
|
||||
show_locked?: string; // TODO: de, fa, fr, it, zh
|
||||
user_id: string;
|
||||
displayname: string;
|
||||
password: string;
|
||||
@@ -137,6 +138,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
password?: string;
|
||||
deactivate: string;
|
||||
erase: string;
|
||||
erase_admin_error: string;
|
||||
};
|
||||
action: {
|
||||
erase: string;
|
||||
@@ -190,6 +192,11 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
erase: {
|
||||
title: string;
|
||||
content: string;
|
||||
fields: {
|
||||
block: string;
|
||||
},
|
||||
success: string;
|
||||
failure: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -121,6 +121,7 @@ const it: SynapseTranslationMessages = {
|
||||
deactivated: "Disattivato",
|
||||
guests: "Mostra gli ospiti",
|
||||
show_deactivated: "Mostra gli utenti disattivati",
|
||||
show_locked: "Mostra gli utenti bloccati",
|
||||
user_id: "Cerca utente",
|
||||
displayname: "Nickname",
|
||||
password: "Password",
|
||||
@@ -141,6 +142,7 @@ const it: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Cancella i dati dell'utente",
|
||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -133,6 +133,7 @@ const ru: SynapseTranslationMessages = {
|
||||
erased: "Удалён",
|
||||
guests: "Показывать гостей",
|
||||
show_deactivated: "Показывать деактивированных",
|
||||
show_locked: "Показывать заблокированных",
|
||||
user_id: "Поиск пользователя",
|
||||
displayname: "Отображаемое имя",
|
||||
password: "Пароль",
|
||||
@@ -150,6 +151,7 @@ const ru: SynapseTranslationMessages = {
|
||||
password: "Смена пароля завершит все сессии пользователя.",
|
||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||
},
|
||||
action: {
|
||||
erase: "Удалить данные пользователя",
|
||||
@@ -208,6 +210,11 @@ const ru: SynapseTranslationMessages = {
|
||||
title: "Удалить комнату",
|
||||
content:
|
||||
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
|
||||
fields: {
|
||||
block: "Заблокировать и запретить пользователям присоединяться к комнате",
|
||||
},
|
||||
success: "Комната/ы успешно удалены",
|
||||
failure: "Комната/ы не могут быть удалены.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -120,6 +120,7 @@ const zh: SynapseTranslationMessages = {
|
||||
deactivated: "被禁用",
|
||||
guests: "显示访客",
|
||||
show_deactivated: "显示被禁用的账户",
|
||||
show_locked: "显示被锁定的账户",
|
||||
user_id: "搜索用户",
|
||||
displayname: "显示名字",
|
||||
password: "密码",
|
||||
@@ -134,6 +135,7 @@ const zh: SynapseTranslationMessages = {
|
||||
helper: {
|
||||
deactivate: "您必须提供一串密码来激活账户。",
|
||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||
erase_admin_error: "不允许删除自己的用户",
|
||||
},
|
||||
action: {
|
||||
erase: "抹除用户信息",
|
||||
|
@@ -168,7 +168,9 @@ const LoginPage = () => {
|
||||
const [matrixVersions, setMatrixVersions] = useState("");
|
||||
|
||||
const handleUsernameChange = () => {
|
||||
if (formData.base_url || allowSingleBaseUrl) return;
|
||||
if (formData.base_url || allowSingleBaseUrl) {
|
||||
return;
|
||||
}
|
||||
// check if username is a full qualified userId then set base_url accordingly
|
||||
const domain = splitMxid(formData.username)?.domain;
|
||||
if (domain) {
|
||||
@@ -180,6 +182,9 @@ const LoginPage = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData.base_url) {
|
||||
form.setValue("base_url", "");
|
||||
}
|
||||
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
||||
form.setValue("base_url", restrictBaseUrl[0]);
|
||||
}
|
||||
|
@@ -27,14 +27,19 @@ import {
|
||||
useNotify,
|
||||
useRefresh,
|
||||
useTranslate,
|
||||
DateFieldProps,
|
||||
} from "react-admin";
|
||||
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import { get } from "lodash";
|
||||
|
||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
const destinationRowSx = (record: RaRecord) => ({
|
||||
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
|
||||
backgroundColor: record.retry_last_ts > 0 ? "warning.light" : "primary.contrastText",
|
||||
"& .MuiButtonBase-root": {
|
||||
color: "primary.dark",
|
||||
},
|
||||
});
|
||||
|
||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||
@@ -92,6 +97,14 @@ const DestinationTitle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const RetryDateField = (props: DateFieldProps) => {
|
||||
const record = useRecordContext(props);
|
||||
if (props.source && get(record, props.source) === 0) {
|
||||
return <DateField {...props} record={{ ...record, [props.source]: null }} />;
|
||||
}
|
||||
return <DateField {...props} />;
|
||||
};
|
||||
|
||||
export const DestinationList = (props: ListProps) => {
|
||||
return (
|
||||
<List
|
||||
@@ -103,7 +116,7 @@ export const DestinationList = (props: ListProps) => {
|
||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||
<TextField source="destination" />
|
||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||
<DateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||
<TextField source="retry_interval" />
|
||||
<TextField source="last_successful_stream_ordering" />
|
||||
<DestinationReconnectButton />
|
||||
|
@@ -36,6 +36,7 @@ import {
|
||||
TopToolbar,
|
||||
useRecordContext,
|
||||
useTranslate,
|
||||
useListContext,
|
||||
} from "react-admin";
|
||||
|
||||
import {
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
RoomDirectoryPublishButton,
|
||||
} from "./room_directory";
|
||||
import { DATE_FORMAT } from "../components/date";
|
||||
import DeleteRoomButton from "../components/DeleteRoomButton";
|
||||
|
||||
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||
|
||||
@@ -70,8 +72,8 @@ const RoomShowActions = () => {
|
||||
return (
|
||||
<TopToolbar>
|
||||
{publishButton}
|
||||
<DeleteButton
|
||||
mutationMode="pessimistic"
|
||||
<DeleteRoomButton
|
||||
selectedIds={[record.id]}
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
confirmContent="resources.rooms.action.erase.content"
|
||||
/>
|
||||
@@ -207,17 +209,20 @@ export const RoomShow = (props: ShowProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RoomBulkActionButtons = () => (
|
||||
const RoomBulkActionButtons = () => {
|
||||
const record = useListContext();
|
||||
return (
|
||||
<>
|
||||
<RoomDirectoryBulkPublishButton />
|
||||
<RoomDirectoryBulkUnpublishButton />
|
||||
<BulkDeleteButton
|
||||
<DeleteRoomButton
|
||||
selectedIds={record.selectedIds}
|
||||
confirmTitle="resources.rooms.action.erase.title"
|
||||
confirmContent="resources.rooms.action.erase.content"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import PersonPinIcon from "@mui/icons-material/PersonPin";
|
||||
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
|
||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, ownerDocument } from "@mui/material";
|
||||
import {
|
||||
ArrayInput,
|
||||
ArrayField,
|
||||
@@ -42,11 +44,17 @@ import {
|
||||
useRecordContext,
|
||||
useTranslate,
|
||||
Pagination,
|
||||
SaveButton,
|
||||
CreateButton,
|
||||
ExportButton,
|
||||
TopToolbar,
|
||||
Toolbar,
|
||||
NumberField,
|
||||
useListContext,
|
||||
useNotify,
|
||||
ToolbarClasses,
|
||||
Identifier,
|
||||
RaRecord,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -90,29 +98,65 @@ const userFilters = [
|
||||
<SearchInput source="name" alwaysOn />,
|
||||
<BooleanInput source="guests" alwaysOn />,
|
||||
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
|
||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||
];
|
||||
|
||||
const UserBulkActionButtons = () => (
|
||||
<>
|
||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode, ownUserIsSelected: boolean }> = (props) => {
|
||||
const ownUserIsSelected = props.ownUserIsSelected;
|
||||
const notify = useNotify();
|
||||
const translate = useTranslate();
|
||||
|
||||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (ownUserIsSelected) {
|
||||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>)
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return <div onClickCapture={handleDeleteClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
};
|
||||
|
||||
const UserBulkActionButtons = () => {
|
||||
const record = useListContext();
|
||||
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
|
||||
const selectedIds = record.selectedIds;
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const notify = useNotify();
|
||||
const translate = useTranslate();
|
||||
|
||||
useEffect(() => {
|
||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||
}, [ selectedIds ]);
|
||||
|
||||
|
||||
return <>
|
||||
<ServerNoticeBulkButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BulkDeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle="resources.users.helper.erase"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
||||
return `/users/${id}`;
|
||||
};
|
||||
|
||||
export const UserList = (props: ListProps) => (
|
||||
<List
|
||||
{...props}
|
||||
filters={userFilters}
|
||||
filterDefaultValues={{ guests: true, deactivated: false }}
|
||||
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
|
||||
sort={{ field: "name", order: "ASC" }}
|
||||
actions={<UserListActions />}
|
||||
pagination={<UserPagination />}
|
||||
>
|
||||
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
|
||||
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
|
||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||
<TextField source="id" sortBy="name" />
|
||||
<TextField source="displayname" />
|
||||
@@ -137,10 +181,16 @@ const validateAddress = [required(), maxLength(255)];
|
||||
const UserEditActions = () => {
|
||||
const record = useRecordContext();
|
||||
const translate = useTranslate();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
let ownUserIsSelected = false;
|
||||
if (record && record.id) {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopToolbar>
|
||||
{!record?.deactivated && <ServerNoticeButton />}
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<DeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle={translate("resources.users.helper.erase", {
|
||||
@@ -148,12 +198,15 @@ const UserEditActions = () => {
|
||||
})}
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</TopToolbar>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserCreate = (props: CreateProps) => (
|
||||
<Create {...props}>
|
||||
<Create { ...props} redirect={(resource, id, data) => {
|
||||
return `users/${id}`;
|
||||
}}>
|
||||
<SimpleForm>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
<TextInput source="displayname" validate={maxLength(256)} />
|
||||
@@ -184,16 +237,49 @@ const UserTitle = () => {
|
||||
{translate("resources.users.name", {
|
||||
smart_count: 1,
|
||||
})}{" "}
|
||||
{record ? `"${record.displayname}"` : ""}
|
||||
{record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const UserEditToolbar = () => {
|
||||
const record = useRecordContext();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
let ownUserIsSelected = false;
|
||||
if (record && record.id) {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={ToolbarClasses.defaultToolbar}>
|
||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||
<SaveButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<DeleteButton />
|
||||
</UserPreventSelfDelete>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
|
||||
const UserBooleanInput = (props) => {
|
||||
const record = useRecordContext();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const isOwnUser = false;
|
||||
let ownUserIsSelected = false;
|
||||
if (record && (record.id === ownUserId)) {
|
||||
ownUserIsSelected = true;
|
||||
}
|
||||
|
||||
return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
|
||||
}
|
||||
|
||||
export const UserEdit = (props: EditProps) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||
<TabbedForm>
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
||||
<TextInput source="id" disabled />
|
||||
@@ -202,7 +288,7 @@ export const UserEdit = (props: EditProps) => {
|
||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||
<BooleanInput source="admin" />
|
||||
<BooleanInput source="locked" />
|
||||
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||
<BooleanInput source="erased" disabled />
|
||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||
<TextField source="consent_version" />
|
||||
|
@@ -2,6 +2,7 @@ import fetchMock from "jest-fetch-mock";
|
||||
|
||||
import authProvider from "./authProvider";
|
||||
import storage from "../storage";
|
||||
import { HttpError } from "ra-core";
|
||||
|
||||
fetchMock.enableMocks();
|
||||
|
||||
@@ -30,7 +31,7 @@ describe("authProvider", () => {
|
||||
|
||||
expect(ret).toBe(undefined);
|
||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
@@ -104,7 +105,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
it("should reject if error.status is 403", async () => {
|
||||
await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
|
||||
await expect(authProvider.checkError(new HttpError("test-error", 403, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { AuthProvider, Options, fetchUtils } from "react-admin";
|
||||
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
// called when the user attempts to log in
|
||||
@@ -31,7 +32,10 @@ const authProvider: AuthProvider = {
|
||||
}
|
||||
: {
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password: password,
|
||||
}
|
||||
)
|
||||
@@ -41,13 +45,36 @@ const authProvider: AuthProvider = {
|
||||
// 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
|
||||
if (!base_url) {
|
||||
// there is some kind of bug with base_url being present in the form, but not submitted
|
||||
// ref: https://github.com/etkecc/synapse-admin/issues/14
|
||||
storage.removeItem("base_url")
|
||||
throw new Error("Homeserver URL is required.");
|
||||
}
|
||||
base_url = base_url.replace(/\/+$/g, "");
|
||||
storage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||
|
||||
const { json } = await fetchUtils.fetchJson(login_api_url, options);
|
||||
let response;
|
||||
try {
|
||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||
} catch(err) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
const errorBody = error.body as MatrixError;
|
||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
||||
|
||||
return Promise.reject(
|
||||
new HttpError(
|
||||
errMsg,
|
||||
errorStatus,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const json = response.json;
|
||||
storage.setItem("home_server", json.home_server);
|
||||
storage.setItem("user_id", json.user_id);
|
||||
storage.setItem("access_token", json.access_token);
|
||||
@@ -74,10 +101,12 @@ const authProvider: AuthProvider = {
|
||||
}
|
||||
},
|
||||
// called when the API returns an error
|
||||
checkError: ({ status }: { status: number }) => {
|
||||
console.log("checkError " + status);
|
||||
checkError: (err: HttpError) => {
|
||||
const errorBody = err.body as MatrixError;
|
||||
const status = err.status;
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
return Promise.reject();
|
||||
return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { stringify } from "query-string";
|
||||
|
||||
import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
||||
import { DataProvider, DeleteParams, HttpError, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
|
||||
// Adds the access token to all requests
|
||||
const jsonClient = (url: string, options: Options = {}) => {
|
||||
const jsonClient = async (url: string, options: Options = {}) => {
|
||||
const token = storage.getItem("access_token");
|
||||
console.log("httpClient " + url);
|
||||
if (token != null) {
|
||||
@@ -14,7 +15,17 @@ const jsonClient = (url: string, options: Options = {}) => {
|
||||
token: `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
return fetchUtils.fetchJson(url, options);
|
||||
try {
|
||||
const response = await fetchUtils.fetchJson(url, options);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
const errorBody = error.body as MatrixError;
|
||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
||||
|
||||
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
||||
}
|
||||
};
|
||||
|
||||
const mxcUrlToHttp = (mxcUrl: string) => {
|
||||
@@ -259,7 +270,7 @@ const resourceMap = {
|
||||
total: json => json.total_rooms,
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
|
||||
body: { block: false },
|
||||
body: { block: params.meta?.block ?? false },
|
||||
}),
|
||||
},
|
||||
reports: {
|
||||
@@ -491,7 +502,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
||||
const dataProvider: SynapseDataProvider = {
|
||||
getList: async (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter;
|
||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
const from = (page - 1) * perPage;
|
||||
@@ -504,6 +515,7 @@ const dataProvider: SynapseDataProvider = {
|
||||
destination: destination,
|
||||
guests: guests,
|
||||
deactivated: deactivated,
|
||||
locked: locked,
|
||||
valid: valid,
|
||||
order_by: field,
|
||||
dir: getSearchOrder(order),
|
||||
|
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
vitePluginVersionMark({
|
||||
command: "git describe --tags",
|
||||
command: "git describe --tags || git rev-parse --short HEAD",
|
||||
ifMeta: true,
|
||||
ifLog: true,
|
||||
ifGlobal: true,
|
||||
|
10
yarn.lock
10
yarn.lock
@@ -2484,7 +2484,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"braces@npm:^3.0.2":
|
||||
"braces@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "braces@npm:3.0.3"
|
||||
dependencies:
|
||||
@@ -5867,12 +5867,12 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"micromatch@npm:^4.0.4":
|
||||
version: 4.0.5
|
||||
resolution: "micromatch@npm:4.0.5"
|
||||
version: 4.0.8
|
||||
resolution: "micromatch@npm:4.0.8"
|
||||
dependencies:
|
||||
braces: "npm:^3.0.2"
|
||||
braces: "npm:^3.0.3"
|
||||
picomatch: "npm:^2.3.1"
|
||||
checksum: 10c0/3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff
|
||||
checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user