Compare commits

21 Commits

Author SHA1 Message Date
dependabot[bot]
fa3f2437a3 Bump softprops/action-gh-release from 2.0.8 to 2.2.2 (#660)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.2.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](c062e08bd5...da05d55257)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 17:35:55 +02:00
dependabot[bot]
8dc5238fcb Bump JamesIves/github-pages-deploy-action from 4.6.8 to 4.7.3 (#659)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.8 to 4.7.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.8...v4.7.3)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-version: 4.7.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 17:35:45 +02:00
dependabot[bot]
238350b940 Bump typescript-eslint from 8.32.0 to 8.32.1 (#661)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.32.0 to 8.32.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.32.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.32.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 17:35:26 +02:00
dependabot[bot]
99bf7b1889 Bump vite from 5.4.19 to 6.3.5 (#664)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 6.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 17:30:15 +02:00
dependabot[bot]
d72c91644d Bump ra-language-english from 5.2.3 to 5.8.2 (#667)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.2.3 to 5.8.2.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.3...v5.8.2)

---
updated-dependencies:
- dependency-name: ra-language-english
  dependency-version: 5.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 17:09:09 +02:00
Dirk Klimpel
e8e28b5df1 Upgrade to react-admin v5 (#633)
* bump dependencies
* replace react-query with @tanstack/react-query
* remove darkTheme
* remove obsolete fullWidth
* fix pagination
* fix language for ra.navigation
* fix 'record' is possibly 'undefined'
* add missing @mui/utils
* bump yarn to 4.4.x
2025-05-09 20:53:41 +02:00
rkfg
d5c10b6e02 Federation page improvements (#583)
* Hide zero last retry timestamps in federation list
* Federation dark theme improvements
2025-05-09 17:54:46 +02:00
Huw Carpenter
3085b9ffa0 Add ability to toggle whether to show locked users (#573) 2025-05-09 17:52:55 +02:00
Patrick Kranz
b2a3fb0f87 Update fetch URL to use BASE_URL for config.json (#652)
Fixes #644.
2025-05-09 17:44:41 +02:00
milkomeda
1e8b4cc885 Fixed detection of missingFields in verifyCsv function of CSV import (#650)
Fixes #600, #552, #188.
2025-05-09 17:43:13 +02:00
Manuel Stahl
4d1a9cc147 Add content read permission to build-test workflow
Change-Id: I5dc911e79e7d0d8fbd88d6a9a686fc10f11a97cd
2025-05-09 17:36:42 +02:00
Manuel Stahl
1b8b702270 Add content write permission to ghpage workflow
Change-Id: I13a10316400dd80a8149e6e8a265c13a51aee652
2025-05-09 17:27:55 +02:00
Dirk Klimpel
61c32fb473 Add identifier when login with password (#601)
* Add identifier when login with password
* add identifier to unit tests
2025-05-09 17:24:23 +02:00
dependabot[bot]
ad876bb790 Bump the npm_and_yarn group with 3 updates (#656)
Bumps the npm_and_yarn group with 3 updates: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) and [dompurify](https://github.com/cure53/DOMPurify).


Updates `vite` from 5.4.9 to 5.4.19
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

Updates `@babel/runtime` from 7.24.4 to 7.27.1
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.1/packages/babel-runtime)

Updates `dompurify` from 2.5.0 to 2.5.8
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.5.0...2.5.8)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-version: 2.5.8
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 17:04:48 +02:00
dependabot[bot]
2524848dae Bump typescript-eslint from 7.16.1 to 8.11.0 (#638)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 7.16.1 to 8.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 12:03:03 +02:00
dependabot[bot]
669c1f3079 Bump JamesIves/github-pages-deploy-action from 4.6.3 to 4.6.8 (#626)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.6.3 to 4.6.8.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.6.3...v4.6.8)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 11:17:06 +02:00
dependabot[bot]
590f673167 Bump eslint-plugin-unused-imports from 3.2.0 to 4.1.4 (#620)
Bumps [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) from 3.2.0 to 4.1.4.
- [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/commits/v4.1.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-unused-imports
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 11:16:23 +02:00
dependabot[bot]
307793f000 Bump @testing-library/dom from 10.0.0 to 10.4.0 (#585)
Bumps [@testing-library/dom](https://github.com/testing-library/dom-testing-library) from 10.0.0 to 10.4.0.
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.0.0...v10.4.0)

---
updated-dependencies:
- dependency-name: "@testing-library/dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 11:14:41 +02:00
dependabot[bot]
96f549fe42 Bump micromatch from 4.0.5 to 4.0.8 in the npm_and_yarn group (#610)
Bumps the npm_and_yarn group with 1 update: [micromatch](https://github.com/micromatch/micromatch).


Updates `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
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 11:14:29 +02:00
dependabot[bot]
3de4332477 Bump vite from 5.3.4 to 5.4.9 (#634)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.4 to 5.4.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.9/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 11:13:22 +02:00
Dirk Klimpel
9fc005032c Fix for empty user default tab after creation (#628) 2024-10-08 09:20:55 +02:00
51 changed files with 10127 additions and 7657 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
# 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*

1
.github/FUNDING.yml vendored
View File

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

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

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

26
.github/workflows/build-test.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: build-test
on:
push:
branches: ["master"]
pull_request:
permissions:
contents: read
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 Normal file
View File

@@ -0,0 +1,63 @@
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

31
.github/workflows/edge_ghpage.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Build and Deploy Edge version to GH Pages
on:
workflow_dispatch:
push:
branches:
- main
- master
permissions:
contents: write
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.7.3
with:
branch: gh-pages
folder: dist

30
.github/workflows/github-release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
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@da05d552573ad5aba039eaac05058a918a7bf631
with:
files: dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,119 +0,0 @@
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

3
.gitignore vendored
View File

@@ -191,6 +191,3 @@ sketch
# .pnp.* # .pnp.*
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
/testdata/synapse.data
/testdata/postgres.data

View File

@@ -5,6 +5,6 @@
}, },
"eslint.nodePath": ".yarn/sdks", "eslint.nodePath": ".yarn/sdks",
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true "typescript.enablePromptUseWorkspaceTsdk": true
} }

File diff suppressed because one or more lines are too long

925
.yarn/releases/yarn-4.4.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-4.4.1.cjs

View File

@@ -1,5 +1,26 @@
FROM ghcr.io/static-web-server/static-web-server:2 # 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=./
ENV SERVER_ROOT=/app WORKDIR /src
COPY ./dist /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

View File

@@ -1,52 +1,14 @@
# Synapse Admin UI [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)
# Synapse admin ui
This project is built using [react-admin](https://marmelab.com/react-admin/). This project is built using [react-admin](https://marmelab.com/react-admin/).
## Fork differences
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.
### Available via CDN
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
### Changes
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)
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
_the list will be updated as new changes are added_
### Development
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
user creation fails)
After that open `http://localhost:5173` in your browser, login using the following credentials:
* Login: admin
* Password: admin
* Homeserver URL: http://localhost:8008
## Usage ## Usage
### Supported Synapse ### Supported Synapse

View File

@@ -1,20 +0,0 @@
services:
synapse:
image: ghcr.io/element-hq/synapse:latest
entrypoint: python
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
ports:
- "8008:8008"
volumes:
- ./testdata/synapse:/config
- ./testdata/synapse.data:/media-store
postgres:
image: postgres:alpine
volumes:
- ./testdata/postgres.data:/var/lib/postgresql/data
environment:
POSTGRES_USER: synapse
POSTGRES_PASSWORD: synapse
POSTGRES_DB: synapse
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"

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: ghcr.io/etkecc/synapse-admin:latest image: awesometechnologies/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/etkecc/synapse-admin.git # context: https://github.com/Awesome-Technologies/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: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd"> 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/etkecc/synapse-admin" <a id="copyright" href="https://github.com/Awesome-Technologies/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>

View File

@@ -1,40 +0,0 @@
# Shows help
default:
@just --list --justfile {{ justfile() }}
# build the app
build: __install
@yarn run build --base=./
# run the app in a development mode
run:
@yarn start --host 0.0.0.0
# run dev stack and start the app in a development mode
run-dev:
@echo "Starting the database..."
@docker-compose -f docker-compose-dev.yml up -d postgres
@echo "Starting Synapse..."
@docker-compose -f docker-compose-dev.yml up -d synapse
@echo "Ensure admin user is registered..."
@docker-compose -f docker-compose-dev.yml exec synapse register_new_matrix_user --admin -u admin -p admin -c /config/homeserver.yaml http://localhost:8008 || true
@echo "Starting the app..."
@yarn start --host 0.0.0.0
# stop the dev stack
stop-dev:
@docker-compose -f docker-compose-dev.yml stop
register-user localpart password *admin:
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
# run the app in a production mode
run-prod: build
@python -m http.server -d dist 1313
# install the project
__install:
@yarn install --immutable --network-timeout=300000

View File

@@ -8,39 +8,41 @@
"homepage": ".", "homepage": ".",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/etkecc/synapse-admin" "url": "https://github.com/Awesome-Technologies/synapse-admin"
}, },
"packageManager": "yarn@4.4.1",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.7.0", "@eslint/js": "^9.7.0",
"@testing-library/dom": "^10.0.0", "@mui/utils": "^6.1.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.0.0", "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.13", "@types/jest": "^29.5.12",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@types/node": "^20.14.12", "@types/node": "^20.14.12",
"@types/papaparse": "^5.3.14", "@types/papaparse": "^5.3.14",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1", "@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-yaml": "^1.0.3", "eslint-plugin-yaml": "^1.0.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^7.16.1", "typescript-eslint": "^8.32.1",
"vite": "^5.4.6", "vite": "^6.3.5",
"vite-plugin-version-mark": "^0.1.0" "vite-plugin-version-mark": "^0.1.0"
}, },
"dependencies": { "dependencies": {
@@ -48,26 +50,27 @@
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@haleos/ra-language-german": "^1.0.0", "@haleos/ra-language-german": "^1.0.0",
"@haxqer/ra-language-chinese": "^4.16.2", "@haxqer/ra-language-chinese": "^4.16.2",
"@mui/icons-material": "^6.1.1", "@mui/icons-material": "^5.16.4",
"@mui/material": "^6.1.1", "@mui/material": "^5.16.4",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.59.12",
"history": "^5.3.0", "history": "^5.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"ra-core": "^5.2.0", "query-string": "^7.1.3",
"ra-i18n-polyglot": "^5.2.0", "ra-core": "^5.2.3",
"ra-language-english": "^5.2.0", "ra-i18n-polyglot": "^5.2.3",
"ra-language-english": "^5.8.2",
"ra-language-farsi": "^5.0.0", "ra-language-farsi": "^5.0.0",
"ra-language-french": "^5.2.0", "ra-language-french": "^5.2.3",
"ra-language-italian": "^3.13.1", "ra-language-italian": "^3.13.1",
"ra-language-russian": "^4.14.2", "ra-language-russian": "^4.14.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-admin": "^5.2.0", "react-admin": "^5.2.3",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.52.1",
"react-is": "^18.3.1", "react-is": "^18.3.1",
"react-router": "^6.26.2", "react-router": "^6.28.1",
"react-router-dom": "^6.26.2" "react-router-dom": "^6.28.1"
}, },
"scripts": { "scripts": {
"start": "vite serve", "start": "vite serve",

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import fetchMock from "jest-fetch-mock"; import fetchMock from "jest-fetch-mock";
fetchMock.enableMocks(); fetchMock.enableMocks();

View File

@@ -1,104 +0,0 @@
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
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;

View File

@@ -15,7 +15,7 @@ import {
import { DataProvider, useTranslate } from "ra-core"; import { DataProvider, useTranslate } from "ra-core";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin"; import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
import { generateRandomMxId, generateRandomPassword, returnMXID } from "../synapse/synapse"; import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
const LOGGING = true; const LOGGING = true;
@@ -74,7 +74,7 @@ const FilePicker = () => {
const [conflictMode, setConflictMode] = useState("stop"); const [conflictMode, setConflictMode] = useState("stop");
const [passwordMode, setPasswordMode] = useState(true); const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("update"); const [useridMode, setUseridMode] = useState("ignore");
const translate = useTranslate(); const translate = useTranslate();
const notify = useNotify(); const notify = useNotify();
@@ -121,11 +121,7 @@ const FilePicker = () => {
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => { const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
/* First, verify the presence of required fields */ /* First, verify the presence of required fields */
const missingFields = expectedFields.filter(eF => { const missingFields = expectedFields.filter(eF => !meta.fields?.includes(eF));
const result = meta.fields?.find(mF => eF === mF);
if (result === undefined) { return eF; } // missing field
return undefined; // field found
});
if (missingFields.length > 0) { if (missingFields.length > 0) {
setError(translate("import_users.error.required_field", { field: missingFields[0] })); setError(translate("import_users.error.required_field", { field: missingFields[0] }));
@@ -266,15 +262,12 @@ const FilePicker = () => {
const userRecord = { ...entry }; const userRecord = { ...entry };
// No need to do a bunch of cryptographic random number getting if // No need to do a bunch of cryptographic random number getting if
// we are using neither a generated password nor a generated user id. // we are using neither a generated password nor a generated user id.
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") { if (useridMode === "ignore" || userRecord.id === undefined) {
userRecord.id = generateRandomMxId(); userRecord.id = generateRandomMxId();
} }
if (passwordMode === false || entry.password === undefined || entry.password === "") { if (passwordMode === false || entry.password === undefined) {
userRecord.password = generateRandomPassword(); userRecord.password = generateRandomPassword();
} }
// we want to ensure that the ID is always full MXID, otherwise randomly-generated MXIDs will be in the full
// form, but the ones from the CSV will be localpart-only.
userRecord.id = returnMXID(userRecord.id);
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */ /* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
/* For these modes we will consider the ID that's in the record. /* For these modes we will consider the ID that's in the record.

View File

@@ -64,7 +64,7 @@ export const ServerNoticeButton = () => {
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
if (!record) { if (!record) {
return null; return;
} }
const handleSend = (values: Partial<RaRecord>) => { const handleSend = (values: Partial<RaRecord>) => {

View File

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

View File

@@ -133,7 +133,6 @@ 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",
@@ -151,11 +150,9 @@ 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",
erase_avatar: "Avatar löschen"
}, },
}, },
rooms: { rooms: {
@@ -208,11 +205,6 @@ 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

@@ -142,11 +142,9 @@ 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",
erase_avatar: "Erase avatar"
}, },
}, },
rooms: { rooms: {
@@ -199,11 +197,6 @@ 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,7 +120,6 @@ const fa: SynapseTranslationMessages = {
deactivated: "غیرفعال", deactivated: "غیرفعال",
guests: "نمایش مهمانان", guests: "نمایش مهمانان",
show_deactivated: "نمایش کاربران غیرفعال شده", show_deactivated: "نمایش کاربران غیرفعال شده",
show_locked: "نمایش کاربران قفل شده",
user_id: "جستجوی کاربر", user_id: "جستجوی کاربر",
displayname: "نام نمایشی", displayname: "نام نمایشی",
password: "رمز عبور", password: "رمز عبور",

View File

@@ -124,7 +124,6 @@ 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",
@@ -140,11 +139,9 @@ 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",
erase_avatar: "Effacer l'avatar",
}, },
}, },
rooms: { rooms: {
@@ -197,11 +194,6 @@ 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

@@ -138,11 +138,9 @@ 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;
erase_avatar: string;
}; };
}; };
rooms: { rooms: {
@@ -193,11 +191,6 @@ interface SynapseTranslationMessages extends TranslationMessages {
erase: { erase: {
title: string; title: string;
content: string; content: string;
fields: {
block: string;
},
success: string;
failure: string;
}; };
}; };
}; };

View File

@@ -121,7 +121,6 @@ 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",
@@ -142,7 +141,6 @@ 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

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

View File

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

View File

@@ -5,7 +5,7 @@ import { createRoot } from "react-dom/client";
import App from "./App"; import App from "./App";
import { AppContext } from "./AppContext"; import { AppContext } from "./AppContext";
fetch("config.json") fetch(`${import.meta.env.BASE_URL}/config.json`)
.then(res => res.json()) .then(res => res.json())
.then(props => .then(props =>
createRoot(document.getElementById("root")).render( createRoot(document.getElementById("root")).render(

View File

@@ -8,17 +8,14 @@ import { AppContext } from "../AppContext";
import englishMessages from "../i18n/en"; import englishMessages from "../i18n/en";
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]); const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
import { act } from "@testing-library/react";
describe("LoginForm", () => { describe("LoginForm", () => {
it("renders with no restriction to homeserver", async () => { it("renders with no restriction to homeserver", () => {
await act(async () => {
render( render(
<AdminContext i18nProvider={i18nProvider}> <AdminContext i18nProvider={i18nProvider}>
<LoginPage /> <LoginPage />
</AdminContext> </AdminContext>
); );
});
screen.getByText(englishMessages.synapseadmin.auth.welcome); screen.getByText(englishMessages.synapseadmin.auth.welcome);
screen.getByRole("combobox", { name: "" }); screen.getByRole("combobox", { name: "" });

View File

@@ -168,9 +168,7 @@ const LoginPage = () => {
const [matrixVersions, setMatrixVersions] = useState(""); const [matrixVersions, setMatrixVersions] = useState("");
const handleUsernameChange = () => { const handleUsernameChange = () => {
if (formData.base_url || allowSingleBaseUrl) { if (formData.base_url || allowSingleBaseUrl) return;
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) {
@@ -182,9 +180,6 @@ 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]);
} }
@@ -277,9 +272,9 @@ const LoginPage = () => {
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box> <Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
<Box className="form"> <Box className="form">
<Select <Select
fullWidth
value={locale} value={locale}
onChange={e => setLocale(e.target.value)} onChange={e => setLocale(e.target.value)}
fullWidth
disabled={loading} disabled={loading}
className="select" className="select"
> >

View File

@@ -1,9 +1,11 @@
import { get } from "lodash";
import { MouseEvent } from "react"; import { MouseEvent } from "react";
import AutorenewIcon from "@mui/icons-material/Autorenew"; import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue"; import DestinationsIcon from "@mui/icons-material/CloudQueue";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import { blue } from "@mui/material/colors";
import { import {
Button, Button,
Datagrid, Datagrid,
@@ -31,17 +33,10 @@ import {
} from "react-admin"; } from "react-admin";
import { DATE_FORMAT } from "../components/date"; import { DATE_FORMAT } from "../components/date";
import { get } from "lodash"; import { lighten, useTheme } from '@mui/material';
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />; const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const destinationRowSx = (record: RaRecord) => ({
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 />];
export const DestinationReconnectButton = () => { export const DestinationReconnectButton = () => {
@@ -106,6 +101,16 @@ const RetryDateField = (props: DateFieldProps) => {
}; };
export const DestinationList = (props: ListProps) => { export const DestinationList = (props: ListProps) => {
const { palette: { error, mode }, } = useTheme();
const destinationRowSx = (record: RaRecord) => ({
backgroundColor: record.retry_last_ts > 0 ? lighten(error[mode], 0.5) : undefined,
"& > td": mode === 'dark' ? {
color: record.retry_last_ts > 0 ? "black" : "white",
"& > button": {
color: blue[700],
},
} : undefined,
});
return ( return (
<List <List
{...props} {...props}

View File

@@ -101,7 +101,7 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
const [create, { isLoading }] = useCreate(); const [create, { isLoading }] = useCreate();
if (!record) { if (!record) {
return null; return;
} }
const handleSend = () => { const handleSend = () => {

View File

@@ -36,7 +36,6 @@ import {
TopToolbar, TopToolbar,
useRecordContext, useRecordContext,
useTranslate, useTranslate,
useListContext,
} from "react-admin"; } from "react-admin";
import { import {
@@ -46,7 +45,6 @@ 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]} />;
@@ -67,16 +65,13 @@ const RoomTitle = () => {
const RoomShowActions = () => { const RoomShowActions = () => {
const record = useRecordContext(); const record = useRecordContext();
if (!record) {
return null;
}
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />; const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
// FIXME: refresh after (un)publish // FIXME: refresh after (un)publish
return ( return (
<TopToolbar> <TopToolbar>
{publishButton} {publishButton}
<DeleteRoomButton <DeleteButton
selectedIds={[record.id]} mutationMode="pessimistic"
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
/> />
@@ -212,20 +207,17 @@ export const RoomShow = (props: ShowProps) => {
); );
}; };
const RoomBulkActionButtons = () => { const RoomBulkActionButtons = () => (
const record = useListContext();
return (
<> <>
<RoomDirectoryBulkPublishButton /> <RoomDirectoryBulkPublishButton />
<RoomDirectoryBulkUnpublishButton /> <RoomDirectoryBulkUnpublishButton />
<DeleteRoomButton <BulkDeleteButton
selectedIds={record.selectedIds}
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic"
/> />
</> </>
); );
};
const roomFilters = [<SearchInput source="search_term" alwaysOn />]; const roomFilters = [<SearchInput source="search_term" alwaysOn />];

View File

@@ -8,8 +8,6 @@ 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,
@@ -44,19 +42,12 @@ import {
useRecordContext, useRecordContext,
useTranslate, useTranslate,
Pagination, Pagination,
SaveButton,
CreateButton, CreateButton,
ExportButton, ExportButton,
TopToolbar, TopToolbar,
Toolbar,
NumberField, NumberField,
useListContext, useListContext,
useNotify,
ToolbarClasses,
Identifier, Identifier,
RaRecord,
ImageInput,
ImageField,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -103,50 +94,16 @@ const userFilters = [
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />, <BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
]; ];
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean }> = props => { const UserBulkActionButtons = () => (
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 />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BulkDeleteButton <BulkDeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic" 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
@@ -157,7 +114,10 @@ export const UserList = (props: ListProps) => (
actions={<UserListActions />} actions={<UserListActions />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}> <Datagrid
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
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" />
@@ -182,16 +142,10 @@ 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 />}
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<DeleteButton <DeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", { confirmTitle={translate("resources.users.helper.erase", {
@@ -199,7 +153,6 @@ const UserEditActions = () => {
})} })}
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</UserPreventSelfDelete>
</TopToolbar> </TopToolbar>
); );
}; };
@@ -207,8 +160,8 @@ const UserEditActions = () => {
export const UserCreate = (props: CreateProps) => ( export const UserCreate = (props: CreateProps) => (
<Create <Create
{...props} {...props}
redirect={(resource, id, data) => { redirect={(resource: string | undefined, id: Identifier | undefined) => {
return `users/${id}`; return `${resource}/${id}`;
}} }}
> >
<SimpleForm> <SimpleForm>
@@ -241,71 +194,25 @@ const UserTitle = () => {
{translate("resources.users.name", { {translate("resources.users.name", {
smart_count: 1, smart_count: 1,
})}{" "} })}{" "}
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""} {record ? `"${record.displayname}"` : ""}
</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");
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 />} mutationMode="pessimistic"> <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm toolbar={<UserEditToolbar />}> <TabbedForm>
<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" }} /> <AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" /> <TextInput source="id" disabled />
<ImageInput
source="avatar_file"
label="resources.users.fields.avatar"
accept={{ "image/*": [".png", ".jpg"] }}
>
<ImageField source="src" title="Avatar" />
</ImageInput>
<TextInput source="id" readOnly />
<TextInput source="displayname" /> <TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" /> <PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
<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" />
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" /> <BooleanInput 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,7 +2,6 @@ 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();
@@ -31,7 +30,17 @@ 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","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}', body: JSON.stringify({
device_id: null,
initial_device_display_name: "Synapse Admin",
type: "m.login.password",
user: "@user:example.com",
password: "secret",
identifier: {
type: "m.id.user",
user: "@user:example.com",
}
}),
headers: new Headers({ headers: new Headers({
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -101,11 +110,11 @@ describe("authProvider", () => {
}); });
it("should reject if error.status is 401", async () => { it("should reject if error.status is 401", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
}); });
it("should reject if error.status is 403", async () => { it("should reject if error.status is 403", async () => {
await expect(authProvider.checkError(new HttpError("test-error", 403, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined(); await expect(authProvider.checkError({ status: 403 })).rejects.toBeUndefined();
}); });
}); });

View File

@@ -1,7 +1,6 @@
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin"; import { AuthProvider, Options, fetchUtils } 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
@@ -32,11 +31,12 @@ const authProvider: AuthProvider = {
} }
: { : {
type: "m.login.password", type: "m.login.password",
user: username,
password: password,
identifier: { identifier: {
type: "m.id.user", type: "m.id.user",
user: username, user: username,
}, },
password: password,
} }
) )
), ),
@@ -45,36 +45,13 @@ 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";
let response; const { json } = await fetchUtils.fetchJson(login_api_url, options);
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);
@@ -101,12 +78,10 @@ const authProvider: AuthProvider = {
} }
}, },
// called when the API returns an error // called when the API returns an error
checkError: (err: HttpError) => { checkError: ({ status }: { status: number }) => {
const errorBody = err.body as MatrixError; console.log("checkError " + status);
const status = err.status;
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
return Promise.reject({message: displayError(errorBody.errcode, status, errorBody.error)}); return Promise.reject();
} }
return Promise.resolve(); return Promise.resolve();
}, },

View File

@@ -18,7 +18,7 @@ describe("dataProvider", () => {
JSON.stringify({ JSON.stringify({
users: [ users: [
{ {
name: "@user_id1:provider", name: "user_id1",
password_hash: "password_hash1", password_hash: "password_hash1",
is_guest: 0, is_guest: 0,
admin: 0, admin: 0,
@@ -27,7 +27,7 @@ describe("dataProvider", () => {
displayname: "User One", displayname: "User One",
}, },
{ {
name: "@user_id2:provider", name: "user_id2",
password_hash: "password_hash2", password_hash: "password_hash2",
is_guest: 0, is_guest: 0,
admin: 1, admin: 1,
@@ -47,7 +47,7 @@ describe("dataProvider", () => {
filter: { author_id: 12 }, filter: { author_id: 12 },
}); });
expect(users.data[0].id).toEqual("@user_id1:provider"); expect(users.data[0].id).toEqual("user_id1");
expect(users.total).toEqual(200); expect(users.total).toEqual(200);
expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(1);
}); });
@@ -55,7 +55,7 @@ describe("dataProvider", () => {
it("fetches one user", async () => { it("fetches one user", async () => {
fetchMock.mockResponseOnce( fetchMock.mockResponseOnce(
JSON.stringify({ JSON.stringify({
name: "@user_id1:provider", name: "user_id1",
password: "user_password", password: "user_password",
displayname: "User", displayname: "User",
threepids: [ threepids: [
@@ -74,9 +74,9 @@ describe("dataProvider", () => {
}) })
); );
const user = await dataProvider.getOne("users", { id: "@user_id1:provider" }); const user = await dataProvider.getOne("users", { id: "user_id1" });
expect(user.data.id).toEqual("@user_id1:provider"); expect(user.data.id).toEqual("user_id1");
expect(user.data.displayname).toEqual("User"); expect(user.data.displayname).toEqual("User");
expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(1);
}); });

View File

@@ -1,44 +1,27 @@
import { stringify } from "query-string";
import { import {
DataProvider, DataProvider,
DeleteParams, DeleteParams,
HttpError,
Identifier, Identifier,
Options, Options,
PaginationPayload, PaginationPayload,
RaRecord, RaRecord,
SortPayload, SortPayload,
UpdateParams, fetchUtils
fetchUtils,
withLifecycleCallbacks,
} from "react-admin"; } from "react-admin";
import storage from "../storage"; import storage from "../storage";
import { returnMXID } from "./synapse";
import { MatrixError, displayError } from "../components/error";
// Adds the access token to all requests // Adds the access token to all requests
const jsonClient = async (url: string, options: Options = {}) => { const jsonClient = (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) {
options.user = { options.user = {
authenticated: true, authenticated: true,
token: `Bearer ${token}`, token: `Bearer ${token}`,
}; };
} }
try { return fetchUtils.fetchJson(url, options);
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) => {
@@ -52,10 +35,6 @@ const mxcUrlToHttp = (mxcUrl: string) => {
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
}; };
const filterUndefined = (obj: Record<string, any>) => {
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
};
interface Room { interface Room {
room_id: string; room_id: string;
name?: string; name?: string;
@@ -242,19 +221,8 @@ export interface DeleteMediaResult {
total: number; total: number;
} }
export interface UploadMediaParams {
file: File;
filename: string;
content_type: string;
}
export interface UploadMediaResult {
content_uri: string;
}
export interface SynapseDataProvider extends DataProvider { export interface SynapseDataProvider extends DataProvider {
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>; deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
} }
const resourceMap = { const resourceMap = {
@@ -262,7 +230,7 @@ const resourceMap = {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
map: (u: User) => ({ map: (u: User) => ({
...u, ...u,
id: returnMXID(u.name), id: u.name,
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined, avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
is_guest: !!u.is_guest, is_guest: !!u.is_guest,
admin: !!u.admin, admin: !!u.admin,
@@ -273,12 +241,12 @@ const resourceMap = {
data: "users", data: "users",
total: json => json.total, total: json => json.total,
create: (data: RaRecord) => ({ create: (data: RaRecord) => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(returnMXID(data.id))}`, endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${storage.getItem("home_server")}`,
body: data, body: data,
method: "PUT", method: "PUT",
}), }),
delete: (params: DeleteParams) => ({ delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(returnMXID(params.id))}`, endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
body: { erase: true }, body: { erase: true },
method: "POST", method: "POST",
}), }),
@@ -298,7 +266,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: params.meta?.block ?? false }, body: { block: false },
}), }),
}, },
reports: { reports: {
@@ -377,7 +345,7 @@ const resourceMap = {
id: um.media_id, id: um.media_id,
}), }),
reference: (id: Identifier) => ({ reference: (id: Identifier) => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`, endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
}), }),
data: "media", data: "media",
total: json => json.total, total: json => json.total,
@@ -412,7 +380,7 @@ const resourceMap = {
create: (data: RaServerNotice) => ({ create: (data: RaServerNotice) => ({
endpoint: "/_synapse/admin/v1/send_server_notice", endpoint: "/_synapse/admin/v1/send_server_notice",
body: { body: {
user_id: returnMXID(data.id), user_id: data.id,
content: { content: {
msgtype: "m.text", msgtype: "m.text",
body: data.body, body: data.body,
@@ -425,7 +393,7 @@ const resourceMap = {
path: "/_synapse/admin/v1/statistics/users/media", path: "/_synapse/admin/v1/statistics/users/media",
map: (usms: UserMediaStatistic) => ({ map: (usms: UserMediaStatistic) => ({
...usms, ...usms,
id: returnMXID(usms.user_id), id: usms.user_id,
}), }),
data: "users", data: "users",
total: json => json.total, total: json => json.total,
@@ -527,7 +495,7 @@ function getSearchOrder(order: "ASC" | "DESC") {
} }
} }
const baseDataProvider: 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, locked, search_term, destination, valid } = params.filter; const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
@@ -554,7 +522,7 @@ const baseDataProvider: SynapseDataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`; const url = `${endpoint_url}?${stringify(query)}`;
const { json } = await jsonClient(url); const { json } = await jsonClient(url);
return { return {
@@ -608,7 +576,7 @@ const baseDataProvider: SynapseDataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const ref = res.reference(params.id); const ref = res.reference(params.id);
const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`; const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
const { json } = await jsonClient(endpoint_url); const { json } = await jsonClient(endpoint_url);
return { return {
@@ -769,46 +737,6 @@ const baseDataProvider: SynapseDataProvider = {
const { json } = await jsonClient(endpoint_url, { method: "POST" }); const { json } = await jsonClient(endpoint_url, { method: "POST" });
return json as DeleteMediaResult; return json as DeleteMediaResult;
}, },
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
const base_url = storage.getItem("base_url");
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
const { json } = await jsonClient(`${uploadMediaURL}?filename=${filename}`, {
method: "POST",
body: file,
headers: new Headers({
Accept: "application/json",
"Content-Type": content_type,
}) as Headers,
});
return json as UploadMediaResult;
},
}; };
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
{
resource: "users",
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
const avatarFile = params.data.avatar_file?.rawFile;
const avatarErase = params.data.avatar_erase;
if (avatarErase) {
params.data.avatar_url = "";
return params;
}
if (avatarFile instanceof File) {
const reponse = await dataProvider.uploadMedia({
file: avatarFile,
filename: params.data.avatar_file.title,
content_type: params.data.avatar_file.rawFile.type,
});
params.data.avatar_url = reponse.content_uri;
}
return params;
},
},
]);
export default dataProvider; export default dataProvider;

View File

@@ -1,4 +1,4 @@
import { Identifier, fetchUtils } from "react-admin"; import { fetchUtils } from "react-admin";
import storage from "../storage"; import storage from "../storage";
@@ -72,26 +72,6 @@ export function generateRandomMxId(): string {
return `@${localpart}:${homeserver}`; return `@${localpart}:${homeserver}`;
} }
/**
* Return the full MXID from an arbitrary input
* @param input the input string
* @returns full MXID as string
*/
export function returnMXID(input: string | Identifier): string {
const homeserver = storage.getItem("home_server");
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
const mxidPattern = /^@[^@:]+:[^@:]+$/;
if (typeof input === 'string' && mxidPattern.test(input)) {
return input; // Already a valid MXID
}
// If input is not a valid MXID, assume it's a localpart and construct the MXID
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
return `@${localpart}:${homeserver}`;
}
/** /**
* Generate a random user password * Generate a random user password
* @returns a new random password as string * @returns a new random password as string

View File

@@ -1,191 +0,0 @@
account_threepid_delegates:
msisdn: ''
alias_creation_rules:
- action: allow
alias: '*'
room_id: '*'
user_id: '*'
allow_guest_access: false
allow_public_rooms_over_federation: true
allow_public_rooms_without_auth: true
app_service_config_files: []
autocreate_auto_join_rooms: true
background_updates: null
caches:
global_factor: 0.5
per_cache_factors: null
cas_config: null
database:
args:
cp_max: 10
cp_min: 5
database: synapse
host: postgres
password: synapse
port: 5432
user: synapse
name: psycopg2
txn_limit: 0
default_room_version: '10'
disable_msisdn_registration: true
email:
enable_media_repo: true
enable_metrics: false
enable_registration: false
enable_registration_captcha: false
enable_registration_without_verification: false
enable_room_list_search: true
encryption_enabled_by_default_for_room_type: 'off'
event_cache_size: 100K
federation_rr_transactions_per_room_per_second: 50
form_secret: sLKKoFMsQUZgLAW0vU1PQQ8ca1POGMDheurGtKW0uJ20iGqtxR9O7JQ6Knvs44Wi
include_profile_data_on_invite: true
instance_map: {}
limit_profile_requests_to_users_who_share_rooms: false
limit_remote_rooms: null
listeners:
- bind_addresses:
- '::'
port: 8008
resources:
- compress: false
names:
- client
tls: false
type: http
x_forwarded: true
log_config: /config/synapse.log.config
macaroon_secret_key: Lg8DxGGfy95J367eVJZHLxmqP9XtN4FKdKxWpPvBS3mhviq9at8sw7KHRPkGmyqE
manhole_settings: null
max_spider_size: 10M
max_upload_size: 1024M
media_retention:
local_media_lifetime: 30d
remote_media_lifetime: 7d
media_storage_providers: []
media_store_path: /media-store
metrics_flags: null
modules: []
oembed: null
oidc_providers: null
old_signing_keys: null
opentracing: null
password_config:
enabled: true
localdb_enabled: true
pepper: zfvnYqxe3GTkdJ9BlfZiAqy2zMsjOg02uBTEiWLp2hjQGqlDw33pTSTplE6HoWlF
policy: null
pid_file: /homeserver.pid
presence:
enabled: true
public_baseurl: http://synapse:8008/
push:
include_content: true
rc_admin_redaction:
burst_count: 50
per_second: 1
rc_federation:
concurrent: 3
reject_limit: 50
sleep_delay: 500
sleep_limit: 10
window_size: 1000
rc_invites:
per_issuer:
burst_count: 10
per_second: 0.3
per_room:
burst_count: 10
per_second: 0.3
per_user:
burst_count: 5
per_second: 0.003
rc_joins:
local:
burst_count: 10
per_second: 0.1
remote:
burst_count: 10
per_second: 0.01
rc_login:
account:
burst_count: 3
per_second: 0.17
address:
burst_count: 3
per_second: 0.17
failed_attempts:
burst_count: 3
per_second: 0.17
rc_message:
burst_count: 10
per_second: 0.2
rc_registration:
burst_count: 3
per_second: 0.17
recaptcha_private_key: ''
recaptcha_public_key: ''
redaction_retention_period: 5m
redis:
enabled: false
host: null
password: null
port: 6379
registration_requires_token: false
registration_shared_secret: jBUKJozByo8s3bvKtYFpB350ZAnxGlzXsDpAZkgOFJuQfKAFHhqbc2dw8D54u4T9
report_stats: false
require_auth_for_profile_requests: false
retention:
enabled: true
purge_jobs:
- interval: 12h
room_list_publication_rules:
- action: allow
alias: '*'
room_id: '*'
user_id: '*'
room_prejoin_state: null
saml2_config:
sp_config: null
user_mapping_provider:
config: null
server_name: synapse
signing_key_path: /config/synapse.signing.key
spam_checker: []
sso: null
stats: null
stream_writers: {}
templates: null
tls_certificate_path: null
tls_private_key_path: null
trusted_key_servers:
- server_name: matrix.org
turn_allow_guests: false
ui_auth: null
url_preview_accept_language:
- en-US
- en
url_preview_enabled: true
url_preview_ip_range_blacklist:
- 127.0.0.0/8
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 100.64.0.0/10
- 192.0.0.0/24
- 169.254.0.0/16
- 192.88.99.0/24
- 198.18.0.0/15
- 192.0.2.0/24
- 198.51.100.0/24
- 203.0.113.0/24
- 224.0.0.0/4
- ::1/128
- fe80::/10
- fc00::/7
- 2001:db8::/32
- ff00::/8
- fec0::/10
user_directory: null
user_ips_max_age: 5m

View File

@@ -1,28 +0,0 @@
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
filters:
context:
(): synapse.util.logcontext.LoggingContextFilter
request: ""
handlers:
console:
class: logging.StreamHandler
formatter: precise
filters: [context]
loggers:
synapse:
level: INFO
shared_secret_authenticator:
level: INFO
rest_auth_provider:
level: INFO
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
root:
level: INFO
handlers: [console]

View File

@@ -1 +0,0 @@
ed25519 a_FswB rsh+VxdR4YUv6rFM6393VmSEJJxzaDrdwlVwLe2rcRo

View File

@@ -24,7 +24,7 @@
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */

View File

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

14640
yarn.lock

File diff suppressed because it is too large Load Diff