Compare commits

53 Commits

Author SHA1 Message Date
Aine
f639a1c9ff rename release artifact file 2024-09-14 21:36:03 +03:00
Aine
8282bb0926 add github release CI step 2024-09-14 21:32:36 +03:00
Aine
264d2ff59d add on-tags CI build 2024-09-14 21:14:22 +03:00
Aine
90a6c7d8c2 switch to tag-based build 2024-09-14 21:09:58 +03:00
Borislav Pantaleev
7de9166648 Add UI option to block deleted rooms from being rejoined (#26)
Add UI option to block deleted rooms from being rejoined
This is almost a copy of https://github.com/Awesome-Technologies/synapse-admin/pull/166 PR,
authored by @jkanefendt
2024-09-14 11:03:51 +03:00
Borislav Pantaleev
0bf3440fc8 Federation page improvements for dark mode and last_entry without a date (#19)
Federation page improvements
This is almost a copy of https://github.com/Awesome-Technologies/synapse-admin/pull/583 PR,
authored by @rkfg
2024-09-06 00:23:23 +03:00
Aine
cceae77529 fix ci 2024-09-06 00:20:37 +03:00
Aine
1474b46ff0 Put version into the manifest.json on CI builds (Docker, CDN, etc.)
This commit fixes the https://github.com/Awesome-Technologies/synapse-admin/issues/507 issue.
While it's a mere workaround, it may help until the
https://github.com/Awesome-Technologies/synapse-admin/issues/507#issuecomment-2063845882 is implemented
2024-09-06 00:15:00 +03:00
Aine
01e3947b22 add missing translations 2024-09-05 23:52:27 +03:00
Borislav Pantaleev
e093bd8625 Fix base_url being undefined on unsuccessful login (#18)
* Fix base_url being undefined on unsuccessful login

* update readme
2024-09-05 23:23:17 +03:00
Borislav Pantaleev
390aab5ce7 Display actual synapse errors (#17)
* Display actual synapse errors

* Show actual errors from dataProvider requests

* update README
2024-09-05 21:39:39 +03:00
Aine
fb1a04971b [skip ci] add justfile 2024-09-05 09:50:06 +03:00
Borislav Pantaleev
eff17a0929 Fix redirect URL after user creation (#16)
* Fix redirect URL after user creation

* increment version; update readme
2024-09-05 09:02:46 +03:00
Aine
8eaaaa50ec cleanup .dockerignore 2024-09-03 16:07:43 +03:00
Aine
85b305d117 ensure the latest version is checked out 2024-09-03 16:01:41 +03:00
Aine
4cbea6ffb6 enable cdn 2024-09-03 15:55:29 +03:00
Aine
1967546ae4 enable docker action to see how it goes 2024-09-03 15:50:11 +03:00
Aine
c9a3294852 use different path 2024-09-03 15:44:50 +03:00
Aine
f1839387e2 fix ci 2024-09-03 15:41:38 +03:00
Aine
c8246a1d19 try ci artifacts 2024-09-03 15:40:23 +03:00
Aine
3c6259cc88 Revert "switch from nginx to SWS for serving the app (docker image)", as it causes issues with CDN deploy - workflow
should be adjusted first

This reverts commit 317df5af0f.
2024-09-03 14:04:32 +03:00
Aine
317df5af0f switch from nginx to SWS for serving the app (docker image) 2024-09-03 14:02:35 +03:00
Aine
2142770a5b Fix footer overlapping content
This is almost a copy of https://github.com/Awesome-Technologies/synapse-admin/issues/574#issue-2406841809 suggestion,
authored by @ll-SKY-ll
2024-09-03 13:46:31 +03:00
Aine
51297b49fc v0.10.3-etke5 2024-09-03 11:50:14 +03:00
Aine
311cc2a1f4 Merge pull request #9 from etkecc/fix-user-names-in-header
Fix user's display name in header on user's page
2024-09-03 11:48:43 +03:00
Aine
6bc760a6fa Fix user's display name in header on user's page
For example, erased users don't have display name, so header was `User "null"`.
With this change, if there is no display name, the MXID will be show, e.g. `User "@user:example.com"`
2024-09-03 11:45:47 +03:00
Aine
50c96cfd77 Add ability to toggle whether to show locked users
This is almost copy of https://github.com/Awesome-Technologies/synapse-admin/pull/573 PR,
authored by @huw1990
2024-09-03 11:09:46 +03:00
Aine
7747dc7f28 Add identifier when authorizing with password
This PR is almost copy of https://github.com/Awesome-Technologies/synapse-admin/pull/601 PR,
authored by @dklimpel
2024-09-03 10:42:45 +03:00
Aine
678867e981 v0.10.3-etke2 2024-09-03 07:12:36 +03:00
Aine
4a4fae104e Merge pull request #8 from etkecc/fix-users-edit-tab
Fix user's Edit action defualt Tab not being shown
2024-09-03 07:09:35 +03:00
Borislav Pantaleev
265b5157af Fix user's Edit action defualt Tab not being shown 2024-09-03 00:53:29 +03:00
Aine
b5b593945d remove configure steps 2024-08-31 21:12:06 +03:00
Aine
15c8a30c92 fix ci 2024-08-31 21:04:39 +03:00
Aine
40c6eb0b95 fix ci 2024-08-31 20:58:07 +03:00
Aine
4c4e3a07f6 fix ci 2024-08-31 20:51:47 +03:00
Aine
35322fa01d fix ci 2024-08-31 20:35:02 +03:00
Aine
88e88fb6b9 add CDN 2024-08-31 20:28:14 +03:00
Aine
7016e5a349 Update CONTRIBUTING.md 2024-08-31 16:17:28 +03:00
Aine
c7e275d4ec Create CONTRIBUTING.md 2024-08-31 16:16:22 +03:00
Aine
b1d3340fce Merge pull request #7 from etkecc/dependabot/npm_and_yarn/micromatch-4.0.8
Bump micromatch from 4.0.5 to 4.0.8
2024-08-31 15:27:02 +03:00
dependabot[bot]
2ae5930578 Bump micromatch from 4.0.5 to 4.0.8
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-31 12:25:16 +00:00
Aine
9dd6940383 Merge pull request #6 from etkecc/fix-ci
update links, allow working without tags
2024-08-31 15:15:27 +03:00
Aine
e2194f5c49 update links, allow working without tags 2024-08-31 15:14:44 +03:00
Aine
e6dcbcb052 Merge pull request #5 from etkecc/fix-ci
set fetch-depth
2024-08-31 15:00:40 +03:00
Aine
5acf3042c3 set fetch-depth 2024-08-31 15:00:10 +03:00
Aine
7286abaaae Merge pull request #4 from etkecc/fix-ci
add checkout
2024-08-31 14:57:40 +03:00
Aine
0d5e95fa7c add checkout 2024-08-31 14:57:07 +03:00
Aine
c4b54c40fb Merge pull request #3 from etkecc/fix-ci
fix ci
2024-08-31 14:55:58 +03:00
Aine
ec0b980e06 fix ci 2024-08-31 14:54:52 +03:00
Aine
94ccd3ad36 Merge pull request #2 from etkecc/add-ci
update CI and readme
2024-08-31 14:49:17 +03:00
Aine
49d67f9130 update CI and readme 2024-08-31 14:47:33 +03:00
Aine
2bb846734e Merge pull request #1 from etkecc/prevent-self-delete
Prevent self user delete
2024-08-31 09:33:00 +03:00
Borislav Pantaleev
056d9c6b4c Prevent self user delete 2024-08-31 01:00:29 +03:00
34 changed files with 547 additions and 263 deletions

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
liberapay: etkecc

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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

View File

@@ -1,26 +1,5 @@
# Builder FROM ghcr.io/static-web-server/static-web-server:2
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=./
WORKDIR /src ENV SERVER_ROOT=/app
# Copy .yarn directory to the working directory (must be on a separate line!) COPY ./dist /app
# 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

View File

@@ -9,6 +9,36 @@
This project is built using [react-admin](https://marmelab.com/react-admin/). 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 ## Usage
### Supported Synapse ### Supported Synapse

View File

@@ -2,13 +2,13 @@ services:
synapse-admin: synapse-admin:
container_name: synapse-admin container_name: synapse-admin
hostname: synapse-admin hostname: synapse-admin
image: awesometechnologies/synapse-admin:latest image: ghcr.io/etkecc/synapse-admin:latest
# build: # build:
# context: . # context: .
# to use the docker-compose as standalone without a local repo clone, # to use the docker-compose as standalone without a local repo clone,
# replace the context definition with this: # replace the context definition with this:
# context: https://github.com/Awesome-Technologies/synapse-admin.git # context: https://github.com/etkecc/synapse-admin.git
# args: # args:
# - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 # - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1

View File

@@ -121,8 +121,8 @@
</div> </div>
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
<footer <footer
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd"> 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/Awesome-Technologies/synapse-admin" <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;"> 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 Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
</a> </a>

15
justfile Normal file
View 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

View File

@@ -8,7 +8,7 @@
"homepage": ".", "homepage": ".",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Awesome-Technologies/synapse-admin" "url": "https://github.com/etkecc/synapse-admin"
}, },
"packageManager": "yarn@4.1.1", "packageManager": "yarn@4.1.1",
"devDependencies": { "devDependencies": {

View 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
View File

@@ -0,0 +1,6 @@
export type MatrixError = {
errcode: string;
error: string;
}
export const displayError = (errcode: string, status: number, message: string) => `${errcode} (${status}): ${message}`;

View File

@@ -125,6 +125,7 @@ const de: SynapseTranslationMessages = {
erased: "Gelöscht", erased: "Gelöscht",
guests: "Zeige Gäste", guests: "Zeige Gäste",
show_deactivated: "Zeige deaktivierte Benutzer", show_deactivated: "Zeige deaktivierte Benutzer",
show_locked: "Zeige gesperrte Benutzer",
user_id: "Suche Benutzer", user_id: "Suche Benutzer",
displayname: "Anzeigename", displayname: "Anzeigename",
password: "Passwort", password: "Passwort",
@@ -142,6 +143,7 @@ const de: SynapseTranslationMessages = {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.", 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.", deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten", erase: "DSGVO konformes Löschen der Benutzerdaten",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
}, },
action: { action: {
erase: "Lösche Benutzerdaten", erase: "Lösche Benutzerdaten",
@@ -197,6 +199,11 @@ const de: SynapseTranslationMessages = {
title: "Raum löschen", title: "Raum löschen",
content: 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!", "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.",
}, },
}, },
}, },

View File

@@ -124,6 +124,7 @@ const en: SynapseTranslationMessages = {
erased: "Erased", erased: "Erased",
guests: "Show guests", guests: "Show guests",
show_deactivated: "Show deactivated users", show_deactivated: "Show deactivated users",
show_locked: "Show locked users",
user_id: "Search user", user_id: "Search user",
displayname: "Displayname", displayname: "Displayname",
password: "Password", password: "Password",
@@ -141,6 +142,7 @@ const en: SynapseTranslationMessages = {
password: "Changing password will log user out of all sessions.", password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased", erase: "Mark the user as GDPR-erased",
erase_admin_error: "Deleting own user is not allowed.",
}, },
action: { action: {
erase: "Erase user data", erase: "Erase user data",
@@ -196,6 +198,11 @@ const en: SynapseTranslationMessages = {
title: "Delete room", title: "Delete room",
content: 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!", "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.",
}, },
}, },
}, },

View File

@@ -120,6 +120,7 @@ const fa: SynapseTranslationMessages = {
deactivated: "غیرفعال", deactivated: "غیرفعال",
guests: "نمایش مهمانان", guests: "نمایش مهمانان",
show_deactivated: "نمایش کاربران غیرفعال شده", show_deactivated: "نمایش کاربران غیرفعال شده",
show_locked: "نمایش کاربران قفل شده",
user_id: "جستجوی کاربر", user_id: "جستجوی کاربر",
displayname: "نام نمایشی", displayname: "نام نمایشی",
password: "رمز عبور", password: "رمز عبور",

View File

@@ -124,6 +124,7 @@ const fr: SynapseTranslationMessages = {
deactivated: "Désactivé", deactivated: "Désactivé",
guests: "Afficher les visiteurs", guests: "Afficher les visiteurs",
show_deactivated: "Afficher les utilisateurs désactivés", show_deactivated: "Afficher les utilisateurs désactivés",
show_locked: "Afficher les utilisateurs verrouillés",
user_id: "Rechercher un utilisateur", user_id: "Rechercher un utilisateur",
displayname: "Nom d'affichage", displayname: "Nom d'affichage",
password: "Mot de passe", password: "Mot de passe",
@@ -139,6 +140,7 @@ const fr: SynapseTranslationMessages = {
helper: { helper: {
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.", deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD", 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: { action: {
erase: "Effacer les données de l'utilisateur", erase: "Effacer les données de l'utilisateur",
@@ -194,6 +196,11 @@ const fr: SynapseTranslationMessages = {
title: "Supprimer le salon", title: "Supprimer le salon",
content: 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 !", "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
View File

@@ -120,6 +120,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
erased?: string; // TODO: fa, fr, it, zh erased?: string; // TODO: fa, fr, it, zh
guests: string; guests: string;
show_deactivated: string; show_deactivated: string;
show_locked?: string; // TODO: de, fa, fr, it, zh
user_id: string; user_id: string;
displayname: string; displayname: string;
password: string; password: string;
@@ -137,6 +138,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
password?: string; password?: string;
deactivate: string; deactivate: string;
erase: string; erase: string;
erase_admin_error: string;
}; };
action: { action: {
erase: string; erase: string;
@@ -190,6 +192,11 @@ interface SynapseTranslationMessages extends TranslationMessages {
erase: { erase: {
title: string; title: string;
content: string; content: string;
fields: {
block: string;
},
success: string;
failure: string;
}; };
}; };
}; };

View File

@@ -121,6 +121,7 @@ const it: SynapseTranslationMessages = {
deactivated: "Disattivato", deactivated: "Disattivato",
guests: "Mostra gli ospiti", guests: "Mostra gli ospiti",
show_deactivated: "Mostra gli utenti disattivati", show_deactivated: "Mostra gli utenti disattivati",
show_locked: "Mostra gli utenti bloccati",
user_id: "Cerca utente", user_id: "Cerca utente",
displayname: "Nickname", displayname: "Nickname",
password: "Password", password: "Password",
@@ -141,6 +142,7 @@ const it: SynapseTranslationMessages = {
}, },
action: { action: {
erase: "Cancella i dati dell'utente", erase: "Cancella i dati dell'utente",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
}, },
}, },
rooms: { rooms: {

View File

@@ -133,6 +133,7 @@ const ru: SynapseTranslationMessages = {
erased: "Удалён", erased: "Удалён",
guests: "Показывать гостей", guests: "Показывать гостей",
show_deactivated: "Показывать деактивированных", show_deactivated: "Показывать деактивированных",
show_locked: "Показывать заблокированных",
user_id: "Поиск пользователя", user_id: "Поиск пользователя",
displayname: "Отображаемое имя", displayname: "Отображаемое имя",
password: "Пароль", password: "Пароль",
@@ -150,6 +151,7 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.", password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.", deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR", erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_admin_error: "Удаление собственного пользователя запрещено.",
}, },
action: { action: {
erase: "Удалить данные пользователя", erase: "Удалить данные пользователя",
@@ -208,6 +210,11 @@ const ru: SynapseTranslationMessages = {
title: "Удалить комнату", title: "Удалить комнату",
content: content:
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!", "Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
fields: {
block: "Заблокировать и запретить пользователям присоединяться к комнате",
},
success: "Комната/ы успешно удалены",
failure: "Комната/ы не могут быть удалены.",
}, },
}, },
}, },

View File

@@ -120,6 +120,7 @@ const zh: SynapseTranslationMessages = {
deactivated: "被禁用", deactivated: "被禁用",
guests: "显示访客", guests: "显示访客",
show_deactivated: "显示被禁用的账户", show_deactivated: "显示被禁用的账户",
show_locked: "显示被锁定的账户",
user_id: "搜索用户", user_id: "搜索用户",
displayname: "显示名字", displayname: "显示名字",
password: "密码", password: "密码",
@@ -134,6 +135,7 @@ const zh: SynapseTranslationMessages = {
helper: { helper: {
deactivate: "您必须提供一串密码来激活账户。", deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了", erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_admin_error: "不允许删除自己的用户",
}, },
action: { action: {
erase: "抹除用户信息", erase: "抹除用户信息",

View File

@@ -168,7 +168,9 @@ const LoginPage = () => {
const [matrixVersions, setMatrixVersions] = useState(""); const [matrixVersions, setMatrixVersions] = useState("");
const handleUsernameChange = () => { 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 // check if username is a full qualified userId then set base_url accordingly
const domain = splitMxid(formData.username)?.domain; const domain = splitMxid(formData.username)?.domain;
if (domain) { if (domain) {
@@ -180,6 +182,9 @@ const LoginPage = () => {
}; };
useEffect(() => { useEffect(() => {
if (!formData.base_url) {
form.setValue("base_url", "");
}
if (formData.base_url === "" && allowMultipleBaseUrls) { if (formData.base_url === "" && allowMultipleBaseUrls) {
form.setValue("base_url", restrictBaseUrl[0]); form.setValue("base_url", restrictBaseUrl[0]);
} }

View File

@@ -27,14 +27,19 @@ import {
useNotify, useNotify,
useRefresh, useRefresh,
useTranslate, useTranslate,
DateFieldProps,
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../components/date"; import { DATE_FORMAT } from "../components/date";
import { get } from "lodash";
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const destinationRowSx = (record: RaRecord) => ({ 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 />]; 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) => { export const DestinationList = (props: ListProps) => {
return ( return (
<List <List
@@ -103,7 +116,7 @@ export const DestinationList = (props: ListProps) => {
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}> <Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<TextField source="destination" /> <TextField source="destination" />
<DateField source="failure_ts" showTime options={DATE_FORMAT} /> <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="retry_interval" />
<TextField source="last_successful_stream_ordering" /> <TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton /> <DestinationReconnectButton />

View File

@@ -36,6 +36,7 @@ import {
TopToolbar, TopToolbar,
useRecordContext, useRecordContext,
useTranslate, useTranslate,
useListContext,
} from "react-admin"; } from "react-admin";
import { import {
@@ -45,6 +46,7 @@ import {
RoomDirectoryPublishButton, RoomDirectoryPublishButton,
} from "./room_directory"; } from "./room_directory";
import { DATE_FORMAT } from "../components/date"; import { DATE_FORMAT } from "../components/date";
import DeleteRoomButton from "../components/DeleteRoomButton";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -70,8 +72,8 @@ const RoomShowActions = () => {
return ( return (
<TopToolbar> <TopToolbar>
{publishButton} {publishButton}
<DeleteButton <DeleteRoomButton
mutationMode="pessimistic" selectedIds={[record.id]}
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
/> />
@@ -207,17 +209,20 @@ export const RoomShow = (props: ShowProps) => {
); );
}; };
const RoomBulkActionButtons = () => ( const RoomBulkActionButtons = () => {
<> const record = useListContext();
<RoomDirectoryBulkPublishButton /> return (
<RoomDirectoryBulkUnpublishButton /> <>
<BulkDeleteButton <RoomDirectoryBulkPublishButton />
confirmTitle="resources.rooms.action.erase.title" <RoomDirectoryBulkUnpublishButton />
confirmContent="resources.rooms.action.erase.content" <DeleteRoomButton
mutationMode="pessimistic" selectedIds={record.selectedIds}
/> confirmTitle="resources.rooms.action.erase.title"
</> confirmContent="resources.rooms.action.erase.content"
); />
</>
);
};
const roomFilters = [<SearchInput source="search_term" alwaysOn />]; const roomFilters = [<SearchInput source="search_term" alwaysOn />];

View File

@@ -8,6 +8,8 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin"; import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import { useEffect, useState } from "react";
import { Alert, ownerDocument } from "@mui/material";
import { import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
@@ -42,11 +44,17 @@ import {
useRecordContext, useRecordContext,
useTranslate, useTranslate,
Pagination, Pagination,
SaveButton,
CreateButton, CreateButton,
ExportButton, ExportButton,
TopToolbar, TopToolbar,
Toolbar,
NumberField, NumberField,
useListContext, useListContext,
useNotify,
ToolbarClasses,
Identifier,
RaRecord,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -90,29 +98,65 @@ const userFilters = [
<SearchInput source="name" alwaysOn />, <SearchInput source="name" alwaysOn />,
<BooleanInput source="guests" alwaysOn />, <BooleanInput source="guests" alwaysOn />,
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" 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 /> <ServerNoticeBulkButton />
<BulkDeleteButton <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
label="resources.users.action.erase" <BulkDeleteButton
confirmTitle="resources.users.helper.erase" label="resources.users.action.erase"
mutationMode="pessimistic" 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) => ( export const UserList = (props: ListProps) => (
<List <List
{...props} {...props}
filters={userFilters} filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false }} filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
actions={<UserListActions />} actions={<UserListActions />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}> <Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" /> <AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
<TextField source="id" sortBy="name" /> <TextField source="id" sortBy="name" />
<TextField source="displayname" /> <TextField source="displayname" />
@@ -137,23 +181,32 @@ const validateAddress = [required(), maxLength(255)];
const UserEditActions = () => { const UserEditActions = () => {
const record = useRecordContext(); const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
const ownUserId = localStorage.getItem("user_id");
let ownUserIsSelected = false;
if (record && record.id) {
ownUserIsSelected = record.id === ownUserId;
}
return ( return (
<TopToolbar> <TopToolbar>
{!record?.deactivated && <ServerNoticeButton />} {!record?.deactivated && <ServerNoticeButton />}
<DeleteButton <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
label="resources.users.action.erase" <DeleteButton
confirmTitle={translate("resources.users.helper.erase", { label="resources.users.action.erase"
smart_count: 1, confirmTitle={translate("resources.users.helper.erase", {
})} smart_count: 1,
mutationMode="pessimistic" })}
/> mutationMode="pessimistic"
/>
</UserPreventSelfDelete>
</TopToolbar> </TopToolbar>
); );
}; };
export const UserCreate = (props: CreateProps) => ( export const UserCreate = (props: CreateProps) => (
<Create {...props}> <Create { ...props} redirect={(resource, id, data) => {
return `users/${id}`;
}}>
<SimpleForm> <SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} /> <TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} /> <TextInput source="displayname" validate={maxLength(256)} />
@@ -184,16 +237,49 @@ const UserTitle = () => {
{translate("resources.users.name", { {translate("resources.users.name", {
smart_count: 1, smart_count: 1,
})}{" "} })}{" "}
{record ? `"${record.displayname}"` : ""} {record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
</span> </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) => { export const UserEdit = (props: EditProps) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}> <FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} /> <AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
<TextInput source="id" disabled /> <TextInput source="id" disabled />
@@ -202,7 +288,7 @@ export const UserEdit = (props: EditProps) => {
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable /> <SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
<BooleanInput source="locked" /> <BooleanInput source="locked" />
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" /> <UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
<BooleanInput source="erased" disabled /> <BooleanInput source="erased" disabled />
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} /> <DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
<TextField source="consent_version" /> <TextField source="consent_version" />

View File

@@ -2,6 +2,7 @@ import fetchMock from "jest-fetch-mock";
import authProvider from "./authProvider"; import authProvider from "./authProvider";
import storage from "../storage"; import storage from "../storage";
import { HttpError } from "ra-core";
fetchMock.enableMocks(); fetchMock.enableMocks();
@@ -30,7 +31,7 @@ describe("authProvider", () => {
expect(ret).toBe(undefined); expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", { 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({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -104,7 +105,7 @@ describe("authProvider", () => {
}); });
it("should reject if error.status is 403", async () => { 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();
}); });
}); });

View File

@@ -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 storage from "../storage";
import { MatrixError, displayError } from "../components/error";
const authProvider: AuthProvider = { const authProvider: AuthProvider = {
// called when the user attempts to log in // called when the user attempts to log in
@@ -31,7 +32,10 @@ const authProvider: AuthProvider = {
} }
: { : {
type: "m.login.password", type: "m.login.password",
user: username, identifier: {
type: "m.id.user",
user: username,
},
password: password, password: password,
} }
) )
@@ -41,13 +45,36 @@ const authProvider: AuthProvider = {
// use the base_url from login instead of the well_known entry from the // 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 // server, since the admin might want to access the admin API via some
// private address // 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, ""); base_url = base_url.replace(/\/+$/g, "");
storage.setItem("base_url", base_url); storage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url); const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login"; 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("home_server", json.home_server);
storage.setItem("user_id", json.user_id); storage.setItem("user_id", json.user_id);
storage.setItem("access_token", json.access_token); storage.setItem("access_token", json.access_token);
@@ -74,10 +101,12 @@ const authProvider: AuthProvider = {
} }
}, },
// called when the API returns an error // called when the API returns an error
checkError: ({ status }: { status: number }) => { checkError: (err: HttpError) => {
console.log("checkError " + status); const errorBody = err.body as MatrixError;
const status = err.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
return Promise.reject(); return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)});
} }
return Promise.resolve(); return Promise.resolve();
}, },

View File

@@ -1,11 +1,12 @@
import { stringify } from "query-string"; 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 storage from "../storage";
import { MatrixError, displayError } from "../components/error";
// Adds the access token to all requests // 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"); const token = storage.getItem("access_token");
console.log("httpClient " + url); console.log("httpClient " + url);
if (token != null) { if (token != null) {
@@ -14,7 +15,17 @@ const jsonClient = (url: string, options: Options = {}) => {
token: `Bearer ${token}`, 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) => { const mxcUrlToHttp = (mxcUrl: string) => {
@@ -259,7 +270,7 @@ const resourceMap = {
total: json => json.total_rooms, total: json => json.total_rooms,
delete: (params: DeleteParams) => ({ delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v2/rooms/${params.id}`, endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
body: { block: false }, body: { block: params.meta?.block ?? false },
}), }),
}, },
reports: { reports: {
@@ -491,7 +502,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
const dataProvider: SynapseDataProvider = { const dataProvider: SynapseDataProvider = {
getList: async (resource, params) => { getList: async (resource, params) => {
console.log("getList " + resource); 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 { page, perPage } = params.pagination;
const { field, order } = params.sort; const { field, order } = params.sort;
const from = (page - 1) * perPage; const from = (page - 1) * perPage;
@@ -504,6 +515,7 @@ const dataProvider: SynapseDataProvider = {
destination: destination, destination: destination,
guests: guests, guests: guests,
deactivated: deactivated, deactivated: deactivated,
locked: locked,
valid: valid, valid: valid,
order_by: field, order_by: field,
dir: getSearchOrder(order), dir: getSearchOrder(order),

View File

@@ -8,7 +8,7 @@ export default defineConfig({
plugins: [ plugins: [
react(), react(),
vitePluginVersionMark({ vitePluginVersionMark({
command: "git describe --tags", command: "git describe --tags || git rev-parse --short HEAD",
ifMeta: true, ifMeta: true,
ifLog: true, ifLog: true,
ifGlobal: true, ifGlobal: true,

View File

@@ -2484,7 +2484,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"braces@npm:^3.0.2": "braces@npm:^3.0.3":
version: 3.0.3 version: 3.0.3
resolution: "braces@npm:3.0.3" resolution: "braces@npm:3.0.3"
dependencies: dependencies:
@@ -5867,12 +5867,12 @@ __metadata:
linkType: hard linkType: hard
"micromatch@npm:^4.0.4": "micromatch@npm:^4.0.4":
version: 4.0.5 version: 4.0.8
resolution: "micromatch@npm:4.0.5" resolution: "micromatch@npm:4.0.8"
dependencies: dependencies:
braces: "npm:^3.0.2" braces: "npm:^3.0.3"
picomatch: "npm:^2.3.1" picomatch: "npm:^2.3.1"
checksum: 10c0/3d6505b20f9fa804af5d8c596cb1c5e475b9b0cd05f652c5b56141cf941bd72adaeb7a436fda344235cef93a7f29b7472efc779fcdb83b478eab0867b95cdeff checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8
languageName: node languageName: node
linkType: hard linkType: hard