Compare commits

46 Commits

Author SHA1 Message Date
Aine
ca71038874 Custom Menu Items (#79)
* Custom Menu Items

* update readme

* fix indentation
2024-10-21 23:33:51 +03:00
Borislav Pantaleev
be867b6b0d Fix types and missing translations (#78) 2024-10-21 19:37:29 +03:00
Aine
f2f540b429 fix footer content overlapping and scrollbar 2024-10-21 13:27:32 +03:00
Aine
7feec4ba07 update eslint typescript 2024-10-21 12:01:12 +03:00
Aine
1d5fef1e53 add missing package 2024-10-21 11:43:26 +03:00
dependabot[bot]
9c40efde17 Bump typescript-eslint from 7.18.0 to 8.10.0 (#76)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 7.18.0 to 8.10.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.10.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>
2024-10-21 11:34:26 +03:00
dependabot[bot]
53dff66978 Bump react-admin from 5.2.0 to 5.3.0 (#77)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [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.0...v5.3.0)

---
updated-dependencies:
- dependency-name: react-admin
  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>
2024-10-21 11:31:09 +03:00
dependabot[bot]
3a595247e8 Bump vite-plugin-version-mark from 0.1.1 to 0.1.2 (#75)
Bumps [vite-plugin-version-mark](https://github.com/ZhongxuYang/vite-plugin-version-mark) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/ZhongxuYang/vite-plugin-version-mark/releases)
- [Changelog](https://github.com/ZhongxuYang/vite-plugin-version-mark/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ZhongxuYang/vite-plugin-version-mark/compare/v0.1.1...v0.1.2)

---
updated-dependencies:
- dependency-name: vite-plugin-version-mark
  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>
2024-10-21 11:29:57 +03:00
dependabot[bot]
33f5f60e31 Bump eslint-plugin-import from 2.30.0 to 2.31.0 (#74)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.30.0 to 2.31.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.30.0...v2.31.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  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>
2024-10-21 11:29:35 +03:00
dependabot[bot]
9dd2ea57c9 Bump @types/papaparse from 5.3.14 to 5.3.15 (#73)
Bumps [@types/papaparse](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/papaparse) from 5.3.14 to 5.3.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/papaparse)

---
updated-dependencies:
- dependency-name: "@types/papaparse"
  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>
2024-10-21 11:29:16 +03:00
dependabot[bot]
fae7a696de Bump ra-i18n-polyglot from 5.2.0 to 5.3.0 (#68)
Bumps [ra-i18n-polyglot](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [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.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-i18n-polyglot
  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>
2024-10-19 16:23:56 +03:00
dependabot[bot]
49e8b2d0f5 Bump vite-plugin-version-mark from 0.1.0 to 0.1.1 (#72)
Bumps [vite-plugin-version-mark](https://github.com/ZhongxuYang/vite-plugin-version-mark) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/ZhongxuYang/vite-plugin-version-mark/releases)
- [Changelog](https://github.com/ZhongxuYang/vite-plugin-version-mark/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ZhongxuYang/vite-plugin-version-mark/compare/v0.1.0...v0.1.1)

---
updated-dependencies:
- dependency-name: vite-plugin-version-mark
  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>
2024-10-19 16:22:15 +03:00
dependabot[bot]
281d908d3f Bump ra-language-english from 5.2.0 to 5.3.0 (#71)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [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.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-language-english
  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>
2024-10-19 16:21:57 +03:00
dependabot[bot]
bacc42fe9c Bump react-router-dom from 6.26.2 to 6.27.0 (#70)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.26.2 to 6.27.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@6.27.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.27.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  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>
2024-10-19 16:21:44 +03:00
dependabot[bot]
1c26a28ca9 Bump react-hook-form from 7.53.0 to 7.53.1 (#69)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.53.0 to 7.53.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.53.0...v7.53.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  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>
2024-10-19 16:21:33 +03:00
dependabot[bot]
d3a04cd132 Bump @vitejs/plugin-react from 4.3.1 to 4.3.2 (#67)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.2/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  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>
2024-10-19 16:20:58 +03:00
dependabot[bot]
e6060a23ac Bump @types/node from 20.16.5 to 22.7.7 (#66)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.16.5 to 22.7.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  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>
2024-10-19 16:20:43 +03:00
dependabot[bot]
4b7fbf483a Bump typescript from 5.6.2 to 5.6.3 (#65)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.2 to 5.6.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.2...v5.6.3)

---
updated-dependencies:
- dependency-name: typescript
  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>
2024-10-19 16:20:31 +03:00
dependabot[bot]
bc3c30da92 Bump @testing-library/jest-dom from 6.5.0 to 6.6.2 (#64)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.5.0 to 6.6.2.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.5.0...v6.6.2)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-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>
2024-10-19 16:20:02 +03:00
dependabot[bot]
1896f770d1 Bump ra-core from 5.2.0 to 5.3.0 (#63)
Bumps [ra-core](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [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.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-core
  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>
2024-10-19 16:19:34 +03:00
dependabot[bot]
99d0b9ad72 Bump docker/setup-buildx-action from 1 to 3 (#62)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  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>
2024-10-19 16:19:06 +03:00
Aine
944afb9056 bring back dependabot config 2024-10-19 16:15:23 +03:00
Aine
23f5a24803 update README.md 2024-10-19 15:57:12 +03:00
Borislav Pantaleev
60ae00ac14 Add seprate login button for access token (#61) 2024-10-19 12:03:46 +00:00
Borislav Pantaleev
26862fa708 Add etke.cc to footer and package.json (#60)
* Add etke.cc to footer and package.json

* more links
2024-10-19 11:24:45 +03:00
Aine
853d14c1ce fix 'Sign in' button disabled on SSO-only servers when attempting access token login 2024-10-18 10:21:48 +03:00
jamazi
11a5cac709 Refactor required fields check on Bulk CSV upload (#59)
Related to https://github.com/etkecc/synapse-admin/pull/32
2024-10-17 22:56:19 +03:00
Borislav Pantaleev
0d021021df Add option for access token login (#58)
* Fix SSO login flow, redirect is done after auth

* Add accessToken login

* Add confirmation for session destroy on accessToken logout

* add translations, fix tests, minor renaming

* update readme
2024-10-17 18:34:20 +03:00
Aine
19302466ef update README.md 2024-10-10 22:13:50 +03:00
Aine
0594259ae4 make avatar field sortable in the users list 2024-10-08 12:15:03 +03:00
Aine
ba485bbb18 Merge branch 'master' 2024-10-08 10:51:16 +03:00
Dirk Klimpel
9fc005032c Fix for empty user default tab after creation (#628) 2024-10-08 09:20:55 +02:00
Aine
f5d6f24b30 correctly treat empty or "almost empty" restrictBaseUrl config option, fixes #56 2024-10-07 12:00:33 +03:00
Aine
a42efe7eda do not color failed federation destinations, use an icon instead 2024-10-04 00:16:11 +03:00
Borislav Pantaleev
31d3712dbb Better media preview/download implementation (#53)
* Add download option for non image media

* make view media button change the icon during the loading; update readme
2024-10-03 11:23:51 +03:00
Aine
a15dad4a31 ensure preview works for images only (for now) 2024-10-03 01:18:45 +03:00
Borislav Pantaleev
9062a6afb1 Remove Dialog window for image preview, open all media in new tab 2024-10-03 00:52:07 +03:00
Borislav Pantaleev
a79c3597d6 Authenticated Media Support (#51)
* Fix AvatarField to work with authenticated media

* Fix ViewMediaButton to work for user's media tab and reported events

* remove console.log

* cleanup AvatarField

* use correct thumbnail size

* fix AvatarField.test

* ignore postgres data for watchman

* fix new avatar preview

* watchman should ignore testdata completely, instead of specific subdirs

* update README

* change user's media icon in sidebar - use the same icon as the media tab

* Add preview for user media files if mimeType is image/*

* Add new line in user media Dialog
2024-10-03 00:38:35 +03:00
Aine
470f1b5455 adjust 'redact all user events on remove' to the new API 2024-10-02 23:34:07 +03:00
Aine
46cc0e2fda refactor users' after delete to react-admin lifecycle hooks, fixes #50 2024-09-29 19:46:00 +03:00
Aine
f3080e9468 Provide options to delete media and redact events on user erase (#49)
* Provide options to delete media and redact events on user erase

* update readme
2024-09-29 00:16:44 +03:00
Aine
f8fe1166e2 Do not block deleted rooms from being rejoined by default (UI option), fixes #48 2024-09-27 19:25:15 +03:00
Borislav Pantaleev
02bee92806 Fix missing QueryClient (#47) 2024-09-26 21:42:09 +00:00
Aine
b7f8e03894 update README 2024-09-25 19:26:26 +03:00
Aine
59bb4b4183 update README 2024-09-25 19:10:43 +03:00
Aine
bb53d53692 Add Contact support button (#45) 2024-09-25 19:09:58 +03:00
43 changed files with 1953 additions and 1091 deletions

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

@@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -52,7 +52,7 @@ jobs:
name: dist
path: dist/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:

5
.watchmanconfig Normal file
View File

@@ -0,0 +1,5 @@
{
"ignore_dirs": [
"testdata"
]
}

301
README.md
View File

@@ -1,16 +1,44 @@
# 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)
![Screenshots](./screenshots.jpg)
This project is built using [react-admin](https://marmelab.com/react-admin/).
<!-- vim-markdown-toc GFM -->
* [Fork differences](#fork-differences)
* [Availability](#availability)
* [Changes](#changes)
* [Development](#development)
* [Configuration](#configuration)
* [Restricting available homeserver](#restricting-available-homeserver)
* [Protecting appservice managed users](#protecting-appservice-managed-users)
* [Adding custom menu items](#adding-custom-menu-items)
* [Providing support URL](#providing-support-url)
* [Usage](#usage)
* [Supported Synapse](#supported-synapse)
* [Prerequisites](#prerequisites)
* [Use without install](#use-without-install)
* [Step-By-Step install](#step-by-step-install)
* [Steps for 1)](#steps-for-1)
* [Steps for 2)](#steps-for-2)
* [Steps for 3)](#steps-for-3)
* [Serving Synapse-Admin on a different path](#serving-synapse-admin-on-a-different-path)
* [Development](#development-1)
<!-- vim-markdown-toc -->
## 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
### Availability
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
* As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin)
* Via CDN on [admin.etke.cc](https://admin.etke.cc)
* As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md)
### Changes
@@ -34,6 +62,13 @@ The following changes are already implemented:
* [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)
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
* [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
* [Fix footer causing vertical scrollbar](https://github.com/etkecc/synapse-admin/pull/60)
* [Custom Menu Items](https://github.com/etkecc/synapse-admin/pull/79)
_the list will be updated as new changes are added_
@@ -48,122 +83,6 @@ After that open `http://localhost:5173` in your browser, login using the followi
* Password: admin
* Homeserver URL: http://localhost:8008
## Usage
### Supported Synapse
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
After entering the URL on the login page of synapse-admin the server version appears below the input field.
### Prerequisites
You need access to the following endpoints:
- `/_matrix`
- `/_synapse/admin`
See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)
### Use without install
You can use the current version of Synapse Admin without own installation direct
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
**Note:**
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
**Remember: You have no need to expose these endpoints to the internet but to your network.**
If you want your own deployment, follow the [Step-By-Step Install Guide](#step-by-step-install) below.
### Step-By-Step install
You have three options:
1. [Download the tarball and serve with any webserver](#steps-for-1)
2. [Download the source code from github and run using nodejs](#steps-for-2)
3. [Run the Docker container](#steps-for-3)
#### Steps for 1)
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
- configure a vhost for synapse admin on your webserver
- download the .tar.gz from the latest release: https://github.com/Awesome-Technologies/synapse-admin/releases/latest
- unpack the .tar.gz
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
- open the url of the vhost in your browser
#### Steps for 2)
- make sure you have installed the following: git, yarn, nodejs
- download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git`
- change into downloaded directory: `cd synapse-admin`
- download dependencies: `yarn install`
- start web server: `yarn start`
#### Steps for 3)
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
```yml
services:
synapse-admin:
container_name: synapse-admin
hostname: synapse-admin
build:
context: https://github.com/Awesome-Technologies/synapse-admin.git
args:
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
# - NODE_OPTIONS="--max_old_space_size=1024"
# - BASE_PATH="/synapse-admin"
ports:
- "8080:80"
restart: unless-stopped
```
- browse to http://localhost:8080
### Serving Synapse-Admin on a different path
The path prefix where synapse-admin is served can only be changed during the build step.
If you downloaded the source code, use `yarn build --base=/my-prefix` to set a path prefix.
If you want to build your own Docker container, use the `BASE_PATH` argument.
We do not support directly changing the path where Synapse-Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse-Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
Example for Traefik:
`docker-compose.yml`
```yml
services:
traefik:
image: traefik:mimolette
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
synapse-admin:
image: awesometechnologies/synapse-admin:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
```
## Configuration
You can use `config.json` file to configure synapse-admin
@@ -212,9 +131,151 @@ Example for [mautrix-telegram](https://github.com/mautrix/telegram)
}
```
## Screenshots
### Adding custom menu items
![Screenshots](./screenshots.jpg)
You can add custom menu items to the main menu by providing a `menu` array in the `config.json`.
```json
{
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
```
Where `icon` is one of the [preloaded icons](./src/components/icons.ts)
### Providing support URL
**Deprecated**: use `menu` config option described above. Automatically migrated to the `menu` if the `supportURL` is present.
~~Synapse-Admin provides a support link in the main menu - `Contact support`. By default, the link points to the GitHub issues page of the project. You can change this link by providing a `supportURL` in the `config.json`.~~
```json
{
"supportURL": "https://example.com/support"
}
```
## Usage
### Supported Synapse
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.116.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
After entering the URL on the login page of synapse-admin the server version appears below the input field.
### Prerequisites
You need access to the following endpoints:
- `/_matrix`
- `/_synapse/admin`
See also [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)
### Use without install
You can use the current version of Synapse Admin without own installation direct
via [admin.etke.cc](https://admin.etke.cc).
**Note:**
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
**Remember: You have no need to expose these endpoints to the internet but to your network.**
If you want your own deployment, follow the [Step-By-Step Install Guide](#step-by-step-install) below.
### Step-By-Step install
You have three options:
1. [Download the tarball and serve with any webserver](#steps-for-1)
2. [Download the source code from github and run using nodejs](#steps-for-2)
3. [Run the Docker container](#steps-for-3)
#### Steps for 1)
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
- configure a vhost for synapse admin on your webserver
- download the .tar.gz [from the latest release](https://github.com/etkecc/synapse-admin/releases/latest)
- unpack the .tar.gz
- move or symlink the `synapse-admin` into your vhosts root dir
- open the url of the vhost in your browser
#### Steps for 2)
- make sure you have installed the following: git, yarn, nodejs
- download the source code: `git clone https://github.com/etkecc/synapse-admin.git`
- change into downloaded directory: `cd synapse-admin`
- download dependencies: `yarn install`
- start web server: `yarn start`
#### Steps for 3)
- run the Docker container from the public docker registry: `docker run -p 8080:80 ghcr.io/etkecc/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
```yml
services:
synapse-admin:
container_name: synapse-admin
hostname: synapse-admin
build:
context: https://github.com/etkecc/synapse-admin.git
args:
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
# - NODE_OPTIONS="--max_old_space_size=1024"
# - BASE_PATH="/synapse-admin"
ports:
- "8080:80"
restart: unless-stopped
```
- browse to http://localhost:8080
### Serving Synapse-Admin on a different path
The path prefix where synapse-admin is served can only be changed during the build step.
If you downloaded the source code, use `yarn build --base=/my-prefix` to set a path prefix.
If you want to build your own Docker container, use the `BASE_PATH` argument.
We do not support directly changing the path where Synapse-Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse-Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
Example for Traefik:
`docker-compose.yml`
```yml
services:
traefik:
image: traefik:mimolette
restart: unless-stopped
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
synapse-admin:
image: etkecc/synapse-admin:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
```
## Development

View File

@@ -22,6 +22,11 @@
font-family: sans-serif;
}
.layout {
min-height: 90vh !important;
height: 90vh;
}
.loader-container {
display: flex;
align-items: center;
@@ -120,13 +125,7 @@
</div>
</div>
<script type="module" src="/src/index.tsx"></script>
<footer
style="position: relative; z-index: 2; height: 2em; margin-top: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
<a id="copyright" href="https://github.com/etkecc/synapse-admin"
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
</a>
</footer>
<span id="js-version" style="display: none;"></span>
</body>
<script>document.getElementById("version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
<script>document.getElementById("js-version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
</html>

View File

@@ -25,11 +25,15 @@ run-dev:
stop-dev:
@docker-compose -f docker-compose-dev.yml stop
# register a user in the dev stack
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 yarn {fix,lint,test} commands
test:
@-yarn run fix
@-yarn run lint
@-yarn run test
# run the app in a production mode
run-prod: build

View File

@@ -3,7 +3,7 @@
"version": "0.10.3",
"description": "Admin GUI for the Matrix.org server Synapse",
"type": "module",
"author": "Awesome Technologies Innovationslabor GmbH",
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
"license": "Apache-2.0",
"homepage": ".",
"repository": {
@@ -13,20 +13,20 @@
"devDependencies": {
"@eslint/js": "^9.7.0",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.7",
"@types/node": "^20.14.12",
"@types/papaparse": "^5.3.14",
"@types/node": "^22.7.7",
"@types/papaparse": "^5.3.15",
"@types/react": "^18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react": "^4.3.1",
"@typescript-eslint/eslint-plugin": "^8.10.0",
"@typescript-eslint/parser": "^8.10.0",
"@vitejs/plugin-react": "^4.3.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^3.2.0",
@@ -38,10 +38,10 @@
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.16.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.10.0",
"vite": "^5.4.6",
"vite-plugin-version-mark": "^0.1.0"
"vite-plugin-version-mark": "^0.1.2"
},
"dependencies": {
"@emotion/react": "^11.13.0",
@@ -50,24 +50,25 @@
"@haxqer/ra-language-chinese": "^4.16.2",
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@mui/utils": "^5.16.6",
"@tanstack/react-query": "^5.56.2",
"history": "^5.3.0",
"lodash": "^4.17.21",
"papaparse": "^5.4.1",
"ra-core": "^5.2.0",
"ra-i18n-polyglot": "^5.2.0",
"ra-language-english": "^5.2.0",
"ra-core": "^5.3.0",
"ra-i18n-polyglot": "^5.3.0",
"ra-language-english": "^5.3.0",
"ra-language-farsi": "^5.0.0",
"ra-language-french": "^5.2.0",
"ra-language-italian": "^3.13.1",
"ra-language-russian": "^4.14.2",
"react": "^18.3.1",
"react-admin": "^5.2.0",
"react-admin": "^5.3.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.53.1",
"react-is": "^18.3.1",
"react-router": "^6.26.2",
"react-router-dom": "^6.26.2"
"react-router-dom": "^6.27.0"
},
"scripts": {
"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";
fetchMock.enableMocks();
@@ -9,4 +9,4 @@ describe("App", () => {
render(<App />);
await screen.findAllByText("Welcome to Synapse-admin");
});
});
});

View File

@@ -4,6 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom";
import { AdminLayout } from "./components/AdminLayout";
import { ImportFeature } from "./components/ImportFeature";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
@@ -21,6 +22,8 @@ import userMediaStats from "./resources/user_media_statistics";
import users from "./resources/users";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Footer from "./components/Footer";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
@@ -45,36 +48,42 @@ const i18nProvider = polyglotI18nProvider(
]
);
const queryClient = new QueryClient();
const App = () => (
<Admin
disableTelemetry
requireAuth
loginPage={LoginPage}
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
</CustomRoutes>
<Resource {...users} />
<Resource {...rooms} />
<Resource {...userMediaStats} />
<Resource {...reports} />
<Resource {...roomDirectory} />
<Resource {...destinations} />
<Resource {...registrationToken} />
<Resource name="connections" />
<Resource name="devices" />
<Resource name="room_members" />
<Resource name="users_media" />
<Resource name="joined_rooms" />
<Resource name="pushers" />
<Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
<Resource name="destination_rooms" />
</Admin>
<QueryClientProvider client={queryClient}>
<Admin
disableTelemetry
requireAuth
layout={AdminLayout}
loginPage={LoginPage}
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
</CustomRoutes>
<Resource {...users} />
<Resource {...rooms} />
<Resource {...userMediaStats} />
<Resource {...reports} />
<Resource {...roomDirectory} />
<Resource {...destinations} />
<Resource {...registrationToken} />
<Resource name="connections" />
<Resource name="devices" />
<Resource name="room_members" />
<Resource name="users_media" />
<Resource name="joined_rooms" />
<Resource name="pushers" />
<Resource name="servernotices" />
<Resource name="forward_extremities" />
<Resource name="room_state" />
<Resource name="destination_rooms" />
</Admin>
<Footer />
</QueryClientProvider>
);
export default App;

View File

@@ -3,6 +3,14 @@ import { createContext, useContext } from "react";
interface AppContextType {
restrictBaseUrl: string | string[];
asManagedUsers: string[];
supportURL: string;
menu: MenuItem[];
}
interface MenuItem {
label: string;
icon: string;
url: string;
}
export const AppContext = createContext({});

View File

@@ -0,0 +1,94 @@
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "./icons";
const AdminUserMenu = () => {
const [open, setOpen] = useState(false);
const logout = useLogout();
const checkLoginType = (ev: React.MouseEvent<HTMLDivElement>) => {
const loginType: LoginMethod = (localStorage.getItem("login_type") || "credentials") as LoginMethod;
if (loginType === "accessToken") {
ev.stopPropagation();
setOpen(true);
}
};
const handleConfirm = () => {
setOpen(false);
logout();
};
const handleDialogClose = () => {
setOpen(false);
localStorage.removeItem("access_token");
localStorage.removeItem("login_type");
window.location.reload();
};
return (
<UserMenu>
<div onClickCapture={checkLoginType}>
<Logout />
</div>
<Confirm
isOpen={open}
title="synapseadmin.auth.logout_acces_token_dialog.title"
content="synapseadmin.auth.logout_acces_token_dialog.content"
onConfirm={handleConfirm}
onClose={handleDialogClose}
confirm="synapseadmin.auth.logout_acces_token_dialog.confirm"
cancel="synapseadmin.auth.logout_acces_token_dialog.cancel"
/>
</UserMenu>
);
};
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />;
const AdminMenu = (props) => {
const [menu, setMenu] = useState([]);
useEffect(() => {
const menuConfig = localStorage.getItem('menu');
if (menuConfig) {
setMenu(JSON.parse(menuConfig));
}
}, []);
return (
<Menu {...props}>
<Menu.ResourceItems />
{menu.map((item, index) => {
const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return (
<Suspense key={index}>
<Menu.Item
to={url}
target="_blank"
primaryText={label}
leftIcon={IconComponent ? <IconComponent /> : <DefaultIcon />}
onClick={props.onMenuClick}
/>
</Suspense>
);
})}
</Menu>
);
};
export const AdminLayout = ({ children }) => (
<Layout appBar={AdminAppBar} menu={AdminMenu} sx={{
['& .RaLayout-appFrame']: {
minHeight: '90vh',
height: '90vh',
},
['& .RaLayout-content']: {
marginBottom: '3rem',
},
}}>
{children}
</Layout>
);

View File

@@ -1,18 +1,43 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { RecordContextProvider } from "react-admin";
import { act } from "react";
import AvatarField from "./AvatarField";
describe("AvatarField", () => {
it("shows image", () => {
beforeEach(() => {
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.resolve(new Blob(["mock image data"], { type: 'image/jpeg' })),
})
) as jest.Mock;
// Mock URL.createObjectURL
global.URL.createObjectURL = jest.fn(() => "mock-object-url");
});
afterEach(() => {
jest.restoreAllMocks();
});
it.only("shows image", async () => {
const value = {
avatar: "foo",
avatar: "mxc://serverName/mediaId",
};
render(
<RecordContextProvider value={value}>
<AvatarField source="avatar" />
</RecordContextProvider>
);
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
await act(async () => {
render(
<RecordContextProvider value={value}>
<AvatarField source="avatar" />
</RecordContextProvider>
);
});
await waitFor(() => {
const img = screen.getByRole("img");
expect(img.getAttribute("src")).toBe("mock-object-url");
});
expect(global.fetch).toHaveBeenCalled();
});
});

View File

@@ -1,12 +1,36 @@
import { get } from "lodash";
import { Avatar, AvatarProps } from "@mui/material";
import { FieldProps, useRecordContext } from "react-admin";
import { useState, useEffect, useCallback } from "react";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { Avatar } from "@mui/material";
import { useRecordContext } from "react-admin";
const AvatarField = ({ source, ...rest }) => {
const record = useRecordContext(rest);
const src = get(record, source)?.toString();
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
const { alt, classes, sizes, sx, variant } = rest;
const record = useRecordContext(rest);
const mxcURL = get(record, source)?.toString();
const [src, setSrc] = useState<string>("");
const fetchAvatar = useCallback(async (mxcURL: string) => {
const response = await fetchAuthenticatedMedia(mxcURL, "thumbnail");
const blob = await response.blob();
const blobURL = URL.createObjectURL(blob);
setSrc(blobURL);
}, []);
useEffect(() => {
if (mxcURL) {
fetchAvatar(mxcURL);
}
// Cleanup function to revoke the object URL
return () => {
if (src) {
URL.revokeObjectURL(src);
}
};
}, [mxcURL, fetchAvatar]);
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
};

View File

@@ -16,7 +16,7 @@ const resourceName = "rooms";
const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
const translate = useTranslate();
const [open, setOpen] = useState(false);
const [block, setBlock] = useState(true);
const [block, setBlock] = useState(false);
const notify = useNotify();
const redirect = useRedirect();
@@ -78,7 +78,7 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
value={block}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
label="resources.rooms.action.erase.fields.block"
defaultValue={true}
defaultValue={false}
/>
</SimpleForm>
</DialogContent>

View File

@@ -0,0 +1,117 @@
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 DeleteUserButtonProps {
selectedIds: Identifier[];
confirmTitle: string;
confirmContent: string;
}
const resourceName = "users";
const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const translate = useTranslate();
const [open, setOpen] = useState(false);
const [deleteMedia, setDeleteMedia] = useState(false);
const [redactEvents, setRedactEvents] = useState(false);
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: {deleteMedia: boolean, redactEvents: boolean}) => {
deleteMany(
resourceName,
{ ids: recordIds, meta: values },
{
onSuccess: () => {
notify("ra.notification.deleted", {
messageArgs: {
smart_count: recordIds.length,
},
type: 'info' as NotificationType,
});
handleDialogClose();
unselectAll();
redirect("/users");
},
onError: (error) =>
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
}
);
};
const handleConfirm = () => {
setOpen(false);
handleDelete({ deleteMedia: deleteMedia, redactEvents: redactEvents });
};
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="deleteMedia"
value={deleteMedia}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDeleteMedia(event.target.checked)}
label="resources.users.action.delete_media"
defaultValue={false}
/>
<BooleanInput
source="redactEvents"
value={redactEvents}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setRedactEvents(event.target.checked)}
label="resources.users.action.redact_events"
defaultValue={false}
/>
</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 DeleteUserButton;

40
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { Box, Link, Typography } from "@mui/material";
import { useEffect, useState } from "react";
const Footer = () => {
const [version, setVersion] = useState<string | null>(null);
useEffect(() => {
const version = document.getElementById("js-version")?.textContent;
if (version) {
setVersion(version);
}
}, []);
return (<Box
component="footer"
sx={{
position: 'fixed',
zIndex: 100,
bottom: 0,
width: '100%',
bgcolor: "#eee",
borderTop: '1px solid',
borderColor: '#ddd',
p: 1,
}}>
<Typography variant="body2">
<Link sx={{ color: "#888", textDecoration: 'none' }} href="https://github.com/etkecc/synapse-admin" target="_blank">
Synapse-Admin
</Link> <Link href={`https://github.com/etkecc/synapse-admin/releases/tag/`+version} target="_blank">
<span style={{ fontWeight: 'bold', color: "#000" }}>{version}</span>
</Link> <Link sx={{ color: "#888", textDecoration: 'none' }} href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin" target="_blank">
by etke.cc
</Link> <Link sx={{ color: "#888", textDecoration: 'none' }} href="https://github.com/awesome-technologies/synapse-admin" target="_blank">
(originally developed by Awesome Technologies Innovationslabor GmbH)
</Link>
</Typography>
</Box>
);
};
export default Footer;

View File

@@ -121,11 +121,7 @@ const FilePicker = () => {
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
/* First, verify the presence of required fields */
const missingFields = expectedFields.filter(eF => {
const result = meta.fields?.find(mF => eF === mF);
if (result === undefined) { return eF; } // missing field
return undefined; // field found
});
const missingFields = expectedFields.filter(eF => !meta.fields?.find(mF => eF === mF));
if (missingFields.length > 0) {
setError(translate("import_users.error.required_field", { field: missingFields[0] }));

View File

@@ -0,0 +1,58 @@
import { styled } from "@mui/material/styles";
import { Box } from "@mui/material";
const LoginFormBox = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1rem)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
[`& .card`]: {
width: "30rem",
marginTop: "6rem",
marginBottom: "6rem",
},
[`& .avatar`]: {
margin: "1rem",
display: "flex",
justifyContent: "center",
},
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
[`& .hint`]: {
marginTop: "1em",
marginBottom: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[600],
},
[`& .form`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .select`]: {
marginBottom: "2rem",
},
[`& .actions`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem",
},
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
},
}));
export default LoginFormBox;

12
src/components/icons.ts Normal file
View File

@@ -0,0 +1,12 @@
import { lazy } from "react";
export const Icons = {
Announcement: lazy(() => import('@mui/icons-material/Announcement')),
Engineering: lazy(() => import('@mui/icons-material/Engineering')),
HelpCenter: lazy(() => import('@mui/icons-material/HelpCenter')),
SupportAgent: lazy(() => import('@mui/icons-material/SupportAgent')),
Default: lazy(() => import('@mui/icons-material/OpenInNew')),
// Add more icons as needed
};
export const DefaultIcon = Icons.Default;

View File

@@ -8,7 +8,9 @@ import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import FileOpenIcon from "@mui/icons-material/FileOpen";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip } from "@mui/material";
import DownloadIcon from '@mui/icons-material/Download';
import DownloadingIcon from '@mui/icons-material/Downloading';
import { Grid2 as Grid, Box, Dialog, DialogContent, DialogContentText, DialogTitle, Tooltip, Link } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import {
BooleanInput,
@@ -29,12 +31,11 @@ import {
useTranslate,
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { dateParser } from "./date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import { getMediaUrl } from "../synapse/synapse";
import storage from "../storage";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
const translate = useTranslate();
@@ -311,48 +312,104 @@ export const QuarantineMediaButton = (props: ButtonProps) => {
);
};
export const ViewMediaButton = ({ media_id, label }) => {
export const ViewMediaButton = ({ mxcURL, label, uploadName, mimetype }) => {
const translate = useTranslate();
const url = getMediaUrl(media_id);
const [loading, setLoading] = useState(false);
const isImage = mimetype && mimetype.startsWith("image/");
const openFileInNewTab = (blobURL: string) => {
const anchorElement = document.createElement("a");
anchorElement.href = blobURL;
anchorElement.target = "_blank";
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
setTimeout(() => URL.revokeObjectURL(blobURL), 10);
};
const downloadFile = async (blobURL: string) => {
console.log("downloadFile", blobURL, uploadName);
const anchorElement = document.createElement("a");
anchorElement.href = blobURL;
anchorElement.download = uploadName;
document.body.appendChild(anchorElement);
anchorElement.click();
document.body.removeChild(anchorElement);
setTimeout(() => URL.revokeObjectURL(blobURL), 10);;
};
const handleFile = async (preview: boolean) => {
setLoading(true);
const response = await fetchAuthenticatedMedia(mxcURL, "original");
const blob = await response.blob();
const blobURL = URL.createObjectURL(blob);
if (preview) {
openFileInNewTab(blobURL);
} else {
downloadFile(blobURL);
}
setLoading(false);
};
return (
<Box style={{ whiteSpace: "pre" }}>
<Tooltip title={translate("resources.users_media.action.open")}>
<span>
<Button
component={Link}
to={url}
target="_blank"
rel="noopener"
style={{ minWidth: 0, paddingLeft: 0, paddingRight: 0 }}
>
<FileOpenIcon />
</Button>
</span>
</Tooltip>
{label}
</Box>
<>
<Box display="flex" alignItems="center">
<Tooltip title={translate("resources.users_media.action.open")}>
<span>
{isImage && (
<Button
disabled={loading}
onClick={() => handleFile(true)}
style={{ minWidth: 0, padding: 0, marginRight: 8 }}
>
{loading ? <DownloadingIcon /> : <FileOpenIcon />}
</Button>
)}
</span>
</Tooltip>
<Button
disabled={loading}
onClick={() => handleFile(false)}
style={{ minWidth: 0, padding: 0, marginRight: 8 }}
>
{loading ? <DownloadingIcon /> : <DownloadIcon />}
</Button>
<span>{label}</span>
</Box>
</>
);
};
export const MediaIDField = ({ source }) => {
const record = useRecordContext();
if (!record) {
return null;
}
const homeserver = storage.getItem("home_server");
const record = useRecordContext();
if (!record) return null;
const src = get(record, source)?.toString();
if (!src) return null;
const mediaID = get(record, source)?.toString();
if (!mediaID) {
return null;
}
return <ViewMediaButton media_id={`${homeserver}/${src}`} label={src} />;
const uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
const mxcURL = `mxc://${homeserver}/${mediaID}`;
return <ViewMediaButton mxcURL={mxcURL} label={mediaID} uploadName={uploadName} mimetype={record.media_type}/>;
};
export const MXCField = ({ source }) => {
export const ReportMediaContent = ({ source }) => {
const record = useRecordContext();
if (!record) return null;
if (!record) {
return null;
}
const src = get(record, source)?.toString();
if (!src) return null;
const mxcURL = get(record, source)?.toString();
if (!mxcURL) {
return null;
}
const media_id = src.replace("mxc://", "");
const uploadName = decodeURIComponent(get(record, "event_json.content.body")?.toString());
return <ViewMediaButton media_id={media_id} label={src} />;
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>;
};

View File

@@ -1,11 +1,12 @@
import { Identifier } from "ra-core";
/**
* Check if a user is managed by an application service
* @param id The user ID to check
* @returns Whether the user is managed by an application service
*/
export const isASManaged = (id: string) => {
const managedUsersString = localStorage.getItem("as_managed_users");
export const isASManaged = (id: string | Identifier): boolean => {
const managedUsersString = localStorage.getItem("as_managed_users") || '';
try {
const asManagedUsers = JSON.parse(managedUsersString).map(regex => new RegExp(regex));
return asManagedUsers.some(regex => regex.test(id));

View File

@@ -2,7 +2,7 @@ import { formalGermanMessages } from "@haleos/ra-language-german";
import { SynapseTranslationMessages } from ".";
const de: SynapseTranslationMessages = {
const fixedGermanMessages = {
...formalGermanMessages,
ra: {
...formalGermanMessages.ra,
@@ -10,8 +10,30 @@ const de: SynapseTranslationMessages = {
...formalGermanMessages.ra.navigation,
no_filtered_results: "Keine Ergebnisse",
clear_filters: "Alle Filter entfernen",
add_filter: "Filter hinzufügen",
},
action: {
...formalGermanMessages.ra.action,
update_application: "Anwendung aktualisieren",
},
page: {
...formalGermanMessages.ra.page,
empty: "Leer",
access_denied: "Zugriff verweigert",
authentication_error: "Authentifizierungsfehler",
},
message: {
...formalGermanMessages.ra.message,
access_denied:
"Sie haben nicht die richtigen Berechtigungen um auf diese Seite zuzugreifen.",
authentication_error:
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
},
},
}
const de: SynapseTranslationMessages = {
...fixedGermanMessages,
synapseadmin: {
auth: {
base_url: "Heimserver URL",
@@ -22,6 +44,14 @@ const de: SynapseTranslationMessages = {
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL",
sso_sign_in: "Anmeldung mit SSO",
credentials: "Anmeldedaten",
access_token: "Zugriffstoken",
logout_acces_token_dialog: {
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
confirm: "Sitzung beenden",
cancel: "Nur vom Admin-Panel abmelden",
},
},
users: {
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
@@ -151,12 +181,15 @@ const de: SynapseTranslationMessages = {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten.",
erase_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
},
action: {
erase: "Lösche Benutzerdaten",
erase_avatar: "Avatar löschen"
erase_avatar: "Avatar löschen",
delete_media: "Alle von dem/den Benutzer(n) hochgeladenen Medien löschen",
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
},
},
rooms: {

View File

@@ -14,6 +14,14 @@ const en: SynapseTranslationMessages = {
protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL",
sso_sign_in: "Sign in with SSO",
credentials: "Credentials",
access_token: "Access token",
logout_acces_token_dialog: {
title: "You are using an existing Matrix access token.",
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
confirm: "Destroy session",
cancel: "Just logout from admin panel",
},
},
users: {
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
@@ -142,12 +150,15 @@ const en: SynapseTranslationMessages = {
password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased",
erase_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
erase_admin_error: "Deleting own user is not allowed.",
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
},
action: {
erase: "Erase user data",
erase_avatar: "Erase avatar"
erase_avatar: "Erase avatar",
delete_media: "Delete all media uploaded by the user(-s)",
redact_events: "Redact all events sent by the user(-s)",
},
},
rooms: {

View File

@@ -13,6 +13,14 @@ const fa: SynapseTranslationMessages = {
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
url_error: "آدرس وارد شده یک سرور معتبر نیست",
sso_sign_in: "با SSO وارد شوید",
credentials: "اعتبارنامه",
access_token: "توکن دسترسی",
logout_acces_token_dialog: {
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
content: "آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
confirm: "نابودی جلسه",
cancel: "فقط خروج از پنل مدیریت",
},
},
users: {
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
@@ -138,11 +146,15 @@ const fa: SynapseTranslationMessages = {
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
},
action: {
erase: "پاک کردن اطلاعات کاربر",
erase_avatar: "محو الصورة الرمزية",
delete_media: "حذف جميع الوسائط التي تم تحميلها بواسطة المستخدم (المستخدمين)",
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
},
},
rooms: {

View File

@@ -13,6 +13,14 @@ const fr: SynapseTranslationMessages = {
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
url_error: "L'URL du serveur Matrix n'est pas valide",
sso_sign_in: "Se connecter avec lauthentification unique",
credentials: "Identifiants",
access_token: "Jeton d'accès",
logout_acces_token_dialog: {
title: "Vous utilisez un jeton d'accès Matrix existant.",
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
confirm: "Détruire la session",
cancel: "Se déconnecter simplement du panneau d'administration",
},
},
users: {
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
@@ -140,12 +148,15 @@ const fr: SynapseTranslationMessages = {
helper: {
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
erase_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
},
action: {
erase: "Effacer les données de l'utilisateur",
erase_avatar: "Effacer l'avatar",
delete_media: "Supprimer tous les médias téléchargés par le(s) utilisateur(s)",
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
},
},
rooms: {
@@ -198,9 +209,9 @@ const fr: SynapseTranslationMessages = {
title: "Supprimer le salon",
content:
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
fields: {
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
},
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.",
},

12
src/i18n/index.d.ts vendored
View File

@@ -11,6 +11,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
protocol_error: string;
url_error: string;
sso_sign_in: string;
credentials: string;
access_token: string;
logout_acces_token_dialog: {
title: string;
content: string;
confirm: string;
cancel: string;
};
};
users: {
invalid_user_id: string;
@@ -138,11 +146,15 @@ interface SynapseTranslationMessages extends TranslationMessages {
password?: string;
deactivate: string;
erase: string;
erase_text: string;
erase_admin_error: string;
modify_managed_user_error: string;
};
action: {
erase: string;
erase_avatar: string;
delete_media: string;
redact_events: string;
};
};
rooms: {

View File

@@ -13,6 +13,14 @@ const it: SynapseTranslationMessages = {
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
url_error: "URL del server Matrix non valido",
sso_sign_in: "Accedi con SSO",
credentials: "Credenziali",
access_token: "Token di accesso",
logout_acces_token_dialog: {
title: "Stai utilizzando un token di accesso Matrix esistente.",
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
confirm: "Distruggi sessione",
cancel: "Disconnetti solo dal pannello di amministrazione",
},
},
users: {
invalid_user_id: "ID utente non valido su questo homeserver.",
@@ -139,11 +147,15 @@ const it: SynapseTranslationMessages = {
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
deactivate: "Devi fornire una password per riattivare l'account.",
erase: "Constrassegna l'utente come cancellato dal GDPR",
erase_text: "Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
},
action: {
erase: "Cancella i dati dell'utente",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
erase_avatar: "Cancella l'avatar dell'utente",
delete_media: "Elimina tutti i media caricati dall'utente(-s)",
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
},
},
rooms: {
@@ -170,211 +182,211 @@ const it: SynapseTranslationMessages = {
},
helper: {
/* forward_extremities:
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
},
enums: {
"Forward extremities are the leaf events at the end of a Directed acyclic graph (DAG) in a room, aka events that have no children. The more exist in a room, the more state resolution that Synapse needs to perform (hint: it's an expensive operation). While Synapse has code to prevent too many of these existing at one time in a room, bugs can sometimes make them crop up again. If a room has >10 forward extremities, it's worth checking which room is the culprit and potentially removing them using the SQL queries mentioned in #1760.", */
},
enums: {
join_rules: {
public: "Pubblica",
knock: "Bussa",
invite: "Invita",
private: "Privata",
},
guest_access: {
can_join: "Gli utenti ospiti possono entrare",
forbidden: "Gli utenti ospiti non possono entrare",
},
history_visibility: {
invited: "Dall'invito",
joined: "Dall'entrata",
shared: "Dalla condivisione",
world_readable: "Chiunque",
},
unencrypted: "Non criptata",
public: "Pubblica",
knock: "Bussa",
invite: "Invita",
private: "Privata",
},
action: {
erase: {
title: "Cancella stanza",
content:
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
},
guest_access: {
can_join: "Gli utenti ospiti possono entrare",
forbidden: "Gli utenti ospiti non possono entrare",
},
history_visibility: {
invited: "Dall'invito",
joined: "Dall'entrata",
shared: "Dalla condivisione",
world_readable: "Chiunque",
},
unencrypted: "Non criptata",
},
reports: {
name: "Evento segnalato |||| Eventi segnalati",
fields: {
id: "ID",
received_ts: "Orario del report",
user_id: "richiedente",
name: "nome della stanza",
score: "punteggio",
reason: "ragione",
event_id: "ID dell'evento",
event_json: {
origin: "server di origine",
origin_server_ts: "ora dell'invio",
type: "tipo di evento",
content: {
msgtype: "tipo di contenuto",
body: "contenuto",
format: "formato",
formatted_body: "contenuto formattato",
algorithm: "algoritmo",
},
},
},
},
connections: {
name: "Connessioni",
fields: {
last_seen: "Data",
ip: "Indirizzo IP",
user_agent: "agente utente",
},
},
devices: {
name: "Dispositivo |||| Dispositivi",
fields: {
device_id: "ID del dispositivo",
display_name: "Nome del dispositivo",
last_seen_ts: "Timestamp",
last_seen_ip: "Indirizzo IP",
},
action: {
erase: {
title: "Rimozione del dispositivo %{id}",
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
success: "Dispositivo rimosso con successo.",
failure: "C'è stato un errore.",
},
},
},
users_media: {
name: "Media",
fields: {
media_id: "ID del media",
media_length: "Peso del file (in Byte)",
media_type: "Tipo",
upload_name: "Nome del file",
quarantined_by: "In quarantena da",
safe_from_quarantine: "Protetto dalla quarantena",
created_ts: "Creato",
last_access_ts: "Ultimo accesso",
},
},
protect_media: {
action: {
create: "Non protetto, proteggi",
delete: "Protetto, rimuovi protezione",
none: "In quarantena",
send_success: "Stato della protezione cambiato con successo.",
send_failure: "C'è stato un errore.",
},
},
quarantine_media: {
action: {
name: "Quarantina",
create: "Aggiungi alla quarantena",
delete: "In quarantena, rimuovi dalla quarantena",
none: "Protetto dalla quarantena",
send_success: "Stato della quarantena cambiato con successo.",
send_failure: "C'è stato un errore.",
},
},
pushers: {
name: "Pusher |||| Pusher",
fields: {
app: "App",
app_display_name: "Nome dell'app",
app_id: "ID dell'app",
device_display_name: "Nome del dispositivo",
kind: "Tipo",
lang: "Lingua",
profile_tag: "Tag del profilo",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "Avvisi del server",
send: "Invia avvisi",
fields: {
body: "Messaggio",
},
action: {
send: "Invia nota",
send_success: "Avviso inviato con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
},
},
user_media_statistics: {
name: "Media degli utenti",
fields: {
media_count: "Numero media",
media_length: "Lunghezza media",
},
},
forward_extremities: {
name: "Invia estremità",
fields: {
id: "Event ID",
received_ts: "Timestamp",
depth: "Profondità",
state_group: "State group",
},
},
room_state: {
name: "Eventi di stato",
fields: {
type: "Tipo",
content: "Contenuto",
origin_server_ts: "Ora dell'invio",
sender: "Mittente",
},
},
room_directory: {
name: "Elenco delle stanze",
fields: {
world_readable: "gli utenti ospite possono vedere senza entrare",
guest_can_join: "gli utenti ospite possono entrare",
},
action: {
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
action: {
erase: {
title: "Cancella stanza",
content:
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
erase: "Rimuovi dall'elenco",
create: "Crea",
send_success: "Stanza creata con successo.",
send_failure: "C'è stato un errore.",
"Sei sicuro di voler eliminare questa stanza? Questa azione è definitiva. Tutti i messaggi e i media condivisi in questa stanza verranno eliminati dal server!",
},
},
destinations: {
name: "Federazione",
fields: {
destination: "Destinazione",
failure_ts: "Timestamp dell'errore",
retry_last_ts: "Tentativo ultimo timestamp",
retry_interval: "Intervallo dei tentativi",
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
stream_ordering: "Flusso",
},
reports: {
name: "Evento segnalato |||| Eventi segnalati",
fields: {
id: "ID",
received_ts: "Orario del report",
user_id: "richiedente",
name: "nome della stanza",
score: "punteggio",
reason: "ragione",
event_id: "ID dell'evento",
event_json: {
origin: "server di origine",
origin_server_ts: "ora dell'invio",
type: "tipo di evento",
content: {
msgtype: "tipo di contenuto",
body: "contenuto",
format: "formato",
formatted_body: "contenuto formattato",
algorithm: "algoritmo",
},
},
action: { reconnect: "Riconnetti" },
},
registration_tokens: {
name: "Token di registrazione",
fields: {
token: "Token",
valid: "Token valido",
uses_allowed: "Usi permessi",
pending: "In attesa",
completed: "Completato",
expiry_time: "Data della scadenza",
length: "Lunghezza",
},
connections: {
name: "Connessioni",
fields: {
last_seen: "Data",
ip: "Indirizzo IP",
user_agent: "agente utente",
},
},
devices: {
name: "Dispositivo |||| Dispositivi",
fields: {
device_id: "ID del dispositivo",
display_name: "Nome del dispositivo",
last_seen_ts: "Timestamp",
last_seen_ip: "Indirizzo IP",
},
action: {
erase: {
title: "Rimozione del dispositivo %{id}",
content: 'Sei sicuro di voler rimuovere il dispositivo "%{name}"?',
success: "Dispositivo rimosso con successo.",
failure: "C'è stato un errore.",
},
helper: { length: "Lunghezza del token se non viene dato alcun token." },
},
},
users_media: {
name: "Media",
fields: {
media_id: "ID del media",
media_length: "Peso del file (in Byte)",
media_type: "Tipo",
upload_name: "Nome del file",
quarantined_by: "In quarantena da",
safe_from_quarantine: "Protetto dalla quarantena",
created_ts: "Creato",
last_access_ts: "Ultimo accesso",
},
},
protect_media: {
action: {
create: "Non protetto, proteggi",
delete: "Protetto, rimuovi protezione",
none: "In quarantena",
send_success: "Stato della protezione cambiato con successo.",
send_failure: "C'è stato un errore.",
},
},
quarantine_media: {
action: {
name: "Quarantina",
create: "Aggiungi alla quarantena",
delete: "In quarantena, rimuovi dalla quarantena",
none: "Protetto dalla quarantena",
send_success: "Stato della quarantena cambiato con successo.",
send_failure: "C'è stato un errore.",
},
},
pushers: {
name: "Pusher |||| Pusher",
fields: {
app: "App",
app_display_name: "Nome dell'app",
app_id: "ID dell'app",
device_display_name: "Nome del dispositivo",
kind: "Tipo",
lang: "Lingua",
profile_tag: "Tag del profilo",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "Avvisi del server",
send: "Invia avvisi",
fields: {
body: "Messaggio",
},
action: {
send: "Invia nota",
send_success: "Avviso inviato con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: 'Invia un avviso dal server agli utenti selezionati. La feature "Avvisi del server" è stata attivata sul server.',
},
},
user_media_statistics: {
name: "Media degli utenti",
fields: {
media_count: "Numero media",
media_length: "Lunghezza media",
},
},
forward_extremities: {
name: "Invia estremità",
fields: {
id: "Event ID",
received_ts: "Timestamp",
depth: "Profondità",
state_group: "State group",
},
},
room_state: {
name: "Eventi di stato",
fields: {
type: "Tipo",
content: "Contenuto",
origin_server_ts: "Ora dell'invio",
sender: "Mittente",
},
},
room_directory: {
name: "Elenco delle stanze",
fields: {
world_readable: "gli utenti ospite possono vedere senza entrare",
guest_can_join: "gli utenti ospite possono entrare",
},
action: {
title: "Cancella stanza dall'elenco |||| Cancella %{smart_count} stanze dall'elenco",
content:
"Sei sicuro di voler rimuovere questa stanza dall'elenco? |||| Sei sicuro di voler rimuovere %{smart_count} stanze dall'elenco?",
erase: "Rimuovi dall'elenco",
create: "Crea",
send_success: "Stanza creata con successo.",
send_failure: "C'è stato un errore.",
},
},
destinations: {
name: "Federazione",
fields: {
destination: "Destinazione",
failure_ts: "Timestamp dell'errore",
retry_last_ts: "Tentativo ultimo timestamp",
retry_interval: "Intervallo dei tentativi",
last_successful_stream_ordering: "Ultimo flusso riuscito con successo",
stream_ordering: "Flusso",
},
action: { reconnect: "Riconnetti" },
},
registration_tokens: {
name: "Token di registrazione",
fields: {
token: "Token",
valid: "Token valido",
uses_allowed: "Usi permessi",
pending: "In attesa",
completed: "Completato",
expiry_time: "Data della scadenza",
length: "Lunghezza",
},
helper: { length: "Lunghezza del token se non viene dato alcun token." },
},
},
};
export default it;

View File

@@ -2,7 +2,7 @@ import russianMessages from "ra-language-russian";
import { SynapseTranslationMessages } from ".";
const ru: SynapseTranslationMessages = {
const fixedRussianMessages = {
...russianMessages,
ra: {
...russianMessages.ra,
@@ -11,7 +11,24 @@ const ru: SynapseTranslationMessages = {
no_filtered_results: "Нет результатов",
clear_filters: "Все фильтры сбросить",
},
page: {
...russianMessages.ra.page,
empty: "Пусто",
access_denied: "Доступ запрещен",
authentication_error: "Ошибка аутентификации",
},
message: {
...russianMessages.ra.message,
access_denied:
"У вас нет прав доступа к этой странице.",
authentication_error:
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
},
},
}
const ru: SynapseTranslationMessages = {
...fixedRussianMessages,
synapseadmin: {
auth: {
base_url: "Адрес домашнего сервера",
@@ -22,6 +39,14 @@ const ru: SynapseTranslationMessages = {
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Неверный адрес сервера Matrix",
sso_sign_in: "Вход через SSO",
credentials: "Учетные данные",
access_token: "Токен доступа",
logout_acces_token_dialog: {
title: "Вы используете существующий токен доступа Matrix.",
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
confirm: "Завершить сессию",
cancel: "Просто выйти из панели администрирования",
},
},
users: {
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
@@ -87,7 +112,7 @@ const ru: SynapseTranslationMessages = {
header: "Загрузить CSV файл",
explanation:
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
},
startImport: {
simulate_only: "Только симулировать",
@@ -122,10 +147,10 @@ const ru: SynapseTranslationMessages = {
helper: {
send: "Это API удаляет локальные файлы с вашего собственного сервера, включая локальные миниатюры и копии скачанных файлов. \
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
},
},
resources: {
users: {
},
},
resources: {
users: {
name: "Пользователь |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
@@ -159,265 +184,268 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
erase_admin_error: "Удаление собственного пользователя запрещено.",
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
},
action: {
erase: "Удалить данные пользователя",
erase_avatar: "Удалить аватар",
delete_media: "Удаление всех медиафайлов, загруженных пользователем (-ами)",
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
},
},
rooms: {
name: "Комната |||| Комнаты",
fields: {
room_id: "ID комнаты",
name: "Название",
canonical_alias: "Псевдоним",
joined_members: "Участники",
joined_local_members: "Локальные участники",
joined_local_devices: "Локальные устройства",
state_events: "События состояния / Сложность",
version: "Версия",
is_encrypted: "Зашифровано",
encryption: "Шифрование",
federatable: "Федерация",
public: "Отображается в каталоге комнат",
creator: "Создатель",
join_rules: "Правила входа",
guest_access: "Гостевой доступ",
history_visibility: "Видимость истории",
topic: "Тема",
avatar: "Аватар",
},
helper: {
forward_extremities:
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
rooms: {
name: "Комната |||| Комнаты",
fields: {
room_id: "ID комнаты",
name: "Название",
canonical_alias: "Псевдоним",
joined_members: "Участники",
joined_local_members: "Локальные участники",
joined_local_devices: "Локальные устройства",
state_events: "События состояния / Сложность",
version: "Версия",
is_encrypted: "Зашифровано",
encryption: "Шифрование",
federatable: "Федерация",
public: "Отображается в каталоге комнат",
creator: "Создатель",
join_rules: "Правила входа",
guest_access: "Гостевой доступ",
history_visibility: "Видимость истории",
topic: "Тема",
avatar: "Аватар",
},
helper: {
forward_extremities:
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
},
enums: {
join_rules: {
public: "Для всех",
knock: "Надо постучать",
invite: "По приглашению",
private: "Приватная",
},
guest_access: {
can_join: "Гости могут войти",
forbidden: "Гости не могут войти",
},
history_visibility: {
invited: "С момента приглашения",
joined: "С момента входа",
shared: "С момента открытия доступа",
world_readable: "Для всех",
},
unencrypted: "Без шифрования",
},
action: {
erase: {
title: "Удалить комнату",
content:
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
fields: {
block: "Заблокировать и запретить пользователям присоединяться к комнате",
enums: {
join_rules: {
public: "Для всех",
knock: "Надо постучать",
invite: "По приглашению",
private: "Приватная",
},
guest_access: {
can_join: "Гости могут войти",
forbidden: "Гости не могут войти",
},
history_visibility: {
invited: "С момента приглашения",
joined: "С момента входа",
shared: "С момента открытия доступа",
world_readable: "Для всех",
},
unencrypted: "Без шифрования",
},
action: {
erase: {
title: "Удалить комнату",
content:
"Действительно удалить эту комнату? Это действие будет невозможно отменить. Все сообщения и файлы в комнате будут удалены с сервера!",
fields: {
block: "Заблокировать и запретить пользователям присоединяться к комнате",
},
success: "Комната/ы успешно удалены",
failure: "Комната/ы не могут быть удалены.",
},
success: "Комната/ы успешно удалены",
failure: "Комната/ы не могут быть удалены.",
},
},
},
reports: {
name: "Жалоба |||| Жалобы",
fields: {
id: "ID",
received_ts: "Дата и время жалобы",
user_id: "Автор жалобы",
name: "Название комнаты",
score: "Баллы",
reason: "Причина",
event_id: "ID события",
event_json: {
origin: "Исходнный сервер",
origin_server_ts: "Дата и время отправки",
type: "Тип события",
content: {
msgtype: "Тип содержимого",
body: "Содержимое",
format: "Формат",
formatted_body: "Форматированное содержимое",
algorithm: "Алгоритм",
url: "Ссылка",
info: {
mimetype: "Тип",
reports: {
name: "Жалоба |||| Жалобы",
fields: {
id: "ID",
received_ts: "Дата и время жалобы",
user_id: "Автор жалобы",
name: "Название комнаты",
score: "Баллы",
reason: "Причина",
event_id: "ID события",
event_json: {
origin: "Исходнный сервер",
origin_server_ts: "Дата и время отправки",
type: "Тип события",
content: {
msgtype: "Тип содержимого",
body: "Содержимое",
format: "Формат",
formatted_body: "Форматированное содержимое",
algorithm: "Алгоритм",
url: "Ссылка",
info: {
mimetype: "Тип",
},
},
},
},
},
action: {
erase: {
title: "Удалить жалобу",
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
action: {
erase: {
title: "Удалить жалобу",
content: "Действительно удалить жалобу? Это действие будет невозможно отменить.",
},
},
},
},
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "Юзер-агент",
},
},
devices: {
name: "Устройство |||| Устройства",
fields: {
device_id: "ID устройства",
display_name: "Название",
last_seen_ts: "Дата и время",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Действительно удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "Юзер-агент",
},
},
},
users_media: {
name: "Файлы",
fields: {
media_id: "ID файла",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "На карантине",
safe_from_quarantine: "Защитить от карантина",
created_ts: "Создано",
last_access_ts: "Последний доступ",
devices: {
name: "Устройство |||| Устройства",
fields: {
device_id: "ID устройства",
display_name: "Название",
last_seen_ts: "Дата и время",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Действительно удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
},
},
},
action: {
open: "Открыть файл в новом окне",
users_media: {
name: "Файлы",
fields: {
media_id: "ID файла",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "На карантине",
safe_from_quarantine: "Защитить от карантина",
created_ts: "Создано",
last_access_ts: "Последний доступ",
},
action: {
open: "Открыть файл в новом окне",
},
},
},
protect_media: {
action: {
create: "Не защищён, установить защиту",
delete: "Защищён, снять защиту",
none: "На карантине",
send_success: "Статус защиты успешно изменён.",
send_failure: "Произошла ошибка.",
protect_media: {
action: {
create: "Не защищён, установить защиту",
delete: "Защищён, снять защиту",
none: "На карантине",
send_success: "Статус защиты успешно изменён.",
send_failure: "Произошла ошибка.",
},
},
},
quarantine_media: {
action: {
name: "Карантин",
create: "Поместить на карантин",
delete: "На карантине, снять карантин",
none: "Защищено от карантина",
send_success: "Статус карантина успешно изменён.",
send_failure: "Произошла ошибка.",
quarantine_media: {
action: {
name: "Карантин",
create: "Поместить на карантин",
delete: "На карантине, снять карантин",
none: "Защищено от карантина",
send_success: "Статус карантина успешно изменён.",
send_failure: "Произошла ошибка.",
},
},
},
pushers: {
name: "Пушер |||| Пушеры",
fields: {
app: риложение",
app_display_name: "Название приложения",
app_id: "ID приложения",
device_display_name: "Название устройства",
kind: "Вид",
lang: "Язык",
profile_tag: "Тег профиля",
pushkey: "Ключ",
data: { url: "URL" },
pushers: {
name: "Пушер |||| Пушеры",
fields: {
app: "Приложение",
app_display_name: "Название приложения",
app_id: "ID приложения",
device_display_name: "Название устройства",
kind: "Вид",
lang: "Язык",
profile_tag: "Тег профиля",
pushkey: "Ключ",
data: { url: "URL" },
},
},
},
servernotices: {
name: "Серверные уведомления",
send: "Отправить серверные уведомления",
fields: {
body: "Сообщение",
servernotices: {
name: "Серверные уведомления",
send: "Отправить серверные уведомления",
fields: {
body: "Сообщение",
},
action: {
send: "Отправить",
send_success: "Серверное уведомление успешно отправлено.",
send_failure: "Произошла ошибка.",
},
helper: {
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
},
},
action: {
send: "Отправить",
send_success: "Серверное уведомление успешно отправлено.",
send_failure: "Произошла ошибка.",
user_media_statistics: {
name: "Файлы пользователей",
fields: {
media_count: "Количество файлов",
media_length: "Размер файлов",
},
},
helper: {
send: 'Отправить серверное уведомление выбранным пользователям. На сервере должна быть активна функция "Server Notices".',
forward_extremities: {
name: "Оконечности",
fields: {
id: "ID события",
received_ts: "Дата и время",
depth: "Глубина",
state_group: "Группа состояния",
},
},
},
user_media_statistics: {
name: "Файлы пользователей",
fields: {
media_count: "Количество файлов",
media_length: "Размер файлов",
room_state: {
name: "События состояния",
fields: {
type: "Тип",
content: "Содержимое",
origin_server_ts: "Дата отправки",
sender: "Отправитель",
},
},
},
forward_extremities: {
name: "Оконечности",
fields: {
id: "ID события",
received_ts: "Дата и время",
depth: "Глубина",
state_group: "Группа состояния",
room_directory: {
name: "Каталог комнат",
fields: {
world_readable: "Гости могут просматривать без входа",
guest_can_join: "Гости могут войти",
},
action: {
title:
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
content:
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
erase: "Удалить из каталога комнат",
create: "Опубликовать в каталоге комнат",
send_success: "Комната успешно опубликована.",
send_failure: "Произошла ошибка.",
},
},
},
room_state: {
name: "События состояния",
fields: {
type: "Тип",
content: "Содержимое",
origin_server_ts: "Дата отправки",
sender: "Отправитель",
destinations: {
name: "Федерация",
fields: {
destination: "Назначение",
failure_ts: "Дата и время ошибки",
retry_last_ts: "Дата и время последней попытки",
retry_interval: "Интервал между попытками",
last_successful_stream_ordering: "Последний успешный поток",
stream_ordering: "Поток",
},
action: { reconnect: "Переподключиться" },
},
},
room_directory: {
name: "Каталог комнат",
fields: {
world_readable: "Гости могут просматривать без входа",
guest_can_join: "Гости могут войти",
registration_tokens: {
name: "Токены регистрации",
fields: {
token: "Токен",
valid: "Рабочий токен",
uses_allowed: "Количество использований",
pending: "Ожидает",
completed: "Завершено",
expiry_time: "Дата окончания",
length: "Длина",
},
helper: { length: "Длина токена, если токен не задан." },
},
action: {
title:
"Удалить комнату из каталога |||| Удалить %{smart_count} комнаты из каталога |||| Удалить %{smart_count} комнат из каталога",
content:
"Действительно удалить комнату из каталога? |||| Действительно удалить %{smart_count} комнаты из каталога? |||| Действительно удалить %{smart_count} комнат из каталога?",
erase: "Удалить из каталога комнат",
create: "Опубликовать в каталоге комнат",
send_success: "Комната успешно опубликована.",
send_failure: "Произошла ошибка.",
},
},
destinations: {
name: "Федерация",
fields: {
destination: "Назначение",
failure_ts: "Дата и время ошибки",
retry_last_ts: "Дата и время последней попытки",
retry_interval: "Интервал между попытками",
last_successful_stream_ordering: "Последний успешный поток",
stream_ordering: "Поток",
},
action: { reconnect: "Переподключиться" },
},
registration_tokens: {
name: "Токены регистрации",
fields: {
token: "Токен",
valid: "Рабочий токен",
uses_allowed: "Количество использований",
pending: "Ожидает",
completed: "Завершено",
expiry_time: "Дата окончания",
length: "Длина",
},
helper: { length: "Длина токена, если токен не задан." },
},
},
},
};
export default ru;

View File

@@ -2,7 +2,7 @@ import chineseMessages from "@haxqer/ra-language-chinese";
import { SynapseTranslationMessages } from ".";
const zh: SynapseTranslationMessages = {
const fixedChineseMessages = {
...chineseMessages,
ra: {
...chineseMessages.ra,
@@ -11,7 +11,27 @@ const zh: SynapseTranslationMessages = {
no_filtered_results: "没有结果",
clear_filters: "清除所有过滤器",
},
action: {
...chineseMessages.ra.action,
update_application: "Anwendung aktualisieren",
},
page: {
...chineseMessages.ra.page,
access_denied: "拒绝访问",
authentication_error: "认证错误",
},
message: {
...chineseMessages.ra.message,
access_denied:
"您没有访问此页面的权限。",
authentication_error:
"身份验证服务器返回错误,无法验证您的凭据。",
},
},
}
const zh: SynapseTranslationMessages = {
...fixedChineseMessages,
synapseadmin: {
auth: {
base_url: "服务器 URL",
@@ -21,6 +41,14 @@ const zh: SynapseTranslationMessages = {
protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址",
sso_sign_in: "使用 SSO 登录",
credentials: "凭证",
access_token: "访问令牌",
logout_acces_token_dialog: {
title: "您正在使用现有的 Matrix 访问令牌。",
content: "您想销毁此会话(可能在其他地方使用,例如在 Matrix 客户端中)还是仅从管理面板退出?",
confirm: "销毁会话",
cancel: "仅从管理面板退出",
},
},
users: {
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
@@ -143,12 +171,15 @@ const zh: SynapseTranslationMessages = {
helper: {
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
erase_admin_error: "不允许删除自己的用户",
modify_managed_user_error: "不允许修改系统管理的用户。",
},
action: {
erase: "抹除用户信息",
erase_avatar: "抹掉头像",
delete_media: "删除用户上传的所有媒体",
redact_events: "重新编辑用户(-s发送的所有事件",
},
},
rooms: {

View File

@@ -3,13 +3,33 @@ import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { AppContext } from "./AppContext";
import { AppContext, MenuItem } from "./AppContext";
import storage from "./storage";
fetch("config.json")
.then(res => res.json())
.then(props => {
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
if (props.asManagedUsers) {
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
}
let menu: MenuItem[] = [];
if (props.menu) {
menu = props.menu;
}
if (props.supportURL) {
const migratedSupportURL = {
label: "Contact support",
icon: "SupportAgent",
url: props.supportURL,
};
console.warn("supportURL config option is deprecated. Please, use the menu option instead. Automatically migrated to the new menu option:", migratedSupportURL);
menu.push(migratedSupportURL as MenuItem);
}
if (menu.length > 0) {
storage.setItem("menu", JSON.stringify(menu));
}
return createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AppContext.Provider value={props}>

View File

@@ -1,8 +1,7 @@
import { useState, useEffect } from "react";
import LockIcon from "@mui/icons-material/Lock";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
import {
Form,
FormDataConsumer,
@@ -17,7 +16,7 @@ import {
useLocales,
} from "react-admin";
import { useFormContext } from "react-hook-form";
import LoginFormBox from "../components/LoginFormBox";
import { useAppContext } from "../AppContext";
import {
getServerVersion,
@@ -29,66 +28,18 @@ import {
} from "../synapse/synapse";
import storage from "../storage";
const FormBox = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1rem)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
[`& .card`]: {
width: "30rem",
marginTop: "6rem",
marginBottom: "6rem",
},
[`& .avatar`]: {
margin: "1rem",
display: "flex",
justifyContent: "center",
},
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
[`& .hint`]: {
marginTop: "1em",
marginBottom: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[600],
},
[`& .form`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .select`]: {
marginBottom: "2rem",
},
[`& .actions`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem",
},
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
},
}));
export type LoginMethod = "credentials" | "accessToken";
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const { restrictBaseUrl } = useAppContext();
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
const allowMultipleBaseUrls =
Array.isArray(restrictBaseUrl) &&
restrictBaseUrl.length > 0 &&
restrictBaseUrl[0] !== "" &&
restrictBaseUrl[0] !== null;
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
@@ -98,8 +49,13 @@ const LoginPage = () => {
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
useEffect(() => {
if (!loginToken) {
return;
}
if (loginToken) {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
// Prevent further requests
@@ -127,7 +83,7 @@ const LoginPage = () => {
console.error(error);
});
}
}
}, [loginToken]);
const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) {
@@ -213,29 +169,53 @@ const LoginPage = () => {
return (
<>
<Box>
<TextInput
autoFocus
source="username"
label="ra.auth.username"
autoComplete="username"
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
validate={required()}
/>
</Box>
<Box>
<PasswordInput
source="password"
label="ra.auth.password"
type="password"
autoComplete="current-password"
disabled={loading || !supportPassAuth}
resettable
validate={required()}
/>
</Box>
<Tabs
value={loginMethod}
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
indicatorColor="primary"
textColor="primary"
centered
>
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
</Tabs>
{loginMethod === "credentials" ? (
<>
<Box>
<TextInput
autoFocus
source="username"
label="ra.auth.username"
autoComplete="username"
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
validate={required()}
/>
</Box>
<Box>
<PasswordInput
source="password"
label="ra.auth.password"
type="password"
autoComplete="current-password"
disabled={loading || !supportPassAuth}
resettable
validate={required()}
/>
</Box>
</>
) : (
<Box>
<TextInput
source="accessToken"
label="synapseadmin.auth.access_token"
disabled={loading}
resettable
validate={required()}
/>
</Box>
)}
<Box>
<TextInput
source="base_url"
@@ -263,7 +243,7 @@ const LoginPage = () => {
return (
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
<FormBox>
<LoginFormBox>
<Card className="card">
<Box className="avatar">
{loading ? (
@@ -290,7 +270,7 @@ const LoginPage = () => {
))}
</Select>
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
<CardActions className="actions">
{loginMethod === "credentials" && <CardActions className="actions">
<Button
variant="contained"
type="submit"
@@ -309,10 +289,21 @@ const LoginPage = () => {
>
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
</CardActions>}
{loginMethod === "accessToken" && <CardActions className="actions">
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
fullWidth
>
{translate("ra.auth.sign_in")}
</Button>
</CardActions>}
</Box>
</Card>
</FormBox>
</LoginFormBox>
<Notification />
</Form>
);

View File

@@ -4,6 +4,7 @@ import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList";
import ErrorIcon from '@mui/icons-material/Error';
import {
Button,
Datagrid,
@@ -21,6 +22,7 @@ import {
Tab,
TabbedShowLayout,
TextField,
FunctionField,
TopToolbar,
useRecordContext,
useDelete,
@@ -35,13 +37,6 @@ import { get } from "lodash";
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 />];
export const DestinationReconnectButton = () => {
@@ -105,7 +100,22 @@ const RetryDateField = (props: DateFieldProps) => {
return <DateField {...props} />;
};
const destinationFieldRender = (record: RaRecord) => {
if (record.retry_last_ts > 0) {
return (
<>
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/>
{record.destination}
</>
);
}
return <> {record.destination} </>;
}
export const DestinationList = (props: ListProps) => {
const record = useRecordContext(props);
return (
<List
{...props}
@@ -113,8 +123,8 @@ export const DestinationList = (props: ListProps) => {
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
>
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<TextField source="destination" />
<Datagrid rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<FunctionField source="destination" render={destinationFieldRender} />
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
<TextField source="retry_interval" />

View File

@@ -22,7 +22,7 @@ import {
} from "react-admin";
import { DATE_FORMAT } from "../components/date";
import { MXCField } from "../components/media";
import { ReportMediaContent } from "../components/media";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -62,7 +62,7 @@ export const ReportShow = (props: ShowProps) => {
<TextField source="event_json.content.msgtype" />
<TextField source="event_json.content.body" />
<TextField source="event_json.content.info.mimetype" />
<MXCField source="event_json.content.url" />
<ReportMediaContent source="event_json.content.url" />
<TextField source="event_json.content.format" />
<TextField source="event_json.content.formatted_body" />
<TextField source="event_json.content.algorithm" />

View File

@@ -26,9 +26,9 @@ import {
useUnselectAll,
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import AvatarField from "../components/AvatarField";
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
@@ -144,7 +144,6 @@ export const RoomDirectoryList = () => (
>
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>

View File

@@ -47,6 +47,7 @@ import {
} from "./room_directory";
import { DATE_FORMAT } from "../components/date";
import DeleteRoomButton from "../components/DeleteRoomButton";
import AvatarField from "../components/AvatarField";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -90,6 +91,11 @@ export const RoomShow = (props: ShowProps) => {
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
<TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<AvatarField
source="avatar"
sx={{ height: "120px", width: "120px" }}
label="resources.rooms.fields.avatar"
/>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="topic" />

View File

@@ -1,4 +1,4 @@
import EqualizerIcon from "@mui/icons-material/Equalizer";
import PermMediaIcon from "@mui/icons-material/PermMedia";
import {
Datagrid,
ExportButton,
@@ -48,7 +48,7 @@ export const UserMediaStatsList = (props: ListProps) => (
const resource: ResourceProps = {
name: "user_media_statistics",
icon: EqualizerIcon,
icon: PermMediaIcon,
list: UserMediaStatsList,
};

View File

@@ -36,7 +36,6 @@ import {
ResourceProps,
SearchInput,
SelectInput,
BulkDeleteButton,
DeleteButton,
maxLength,
regex,
@@ -52,15 +51,17 @@ import {
NumberField,
useListContext,
useNotify,
ToolbarClasses,
Identifier,
ToolbarClasses,
RaRecord,
ImageInput,
ImageField,
FunctionField,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton";
import { isASManaged } from "../components/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../components/date";
@@ -90,11 +91,6 @@ const UserListActions = () => {
);
};
UserListActions.defaultProps = {
selectedIds: [],
onUnselectItems: () => null,
};
const UserPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const userFilters = [
@@ -141,20 +137,16 @@ const UserBulkActionButtons = () => {
<>
<ServerNoticeBulkButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
<BulkDeleteButton
label="resources.users.action.erase"
<DeleteUserButton
selectedIds={selectedIds}
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
confirmContent="resources.users.helper.erase_text"
/>
</UserPreventSelfDelete>
</>
);
};
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
return `/users/${id}`;
};
export const UserList = (props: ListProps) => (
<List
{...props}
@@ -164,7 +156,10 @@ export const UserList = (props: ListProps) => (
actions={<UserListActions />}
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" />
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
@@ -200,15 +195,15 @@ const UserEditActions = () => {
return (
<TopToolbar>
{!record?.deactivated && <ServerNoticeButton />}
{record && record.id && (
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
<DeleteUserButton
selectedIds={[record?.id]}
confirmTitle="resources.users.helper.erase"
confirmContent="resources.users.helper.erase_text"
/>
</UserPreventSelfDelete>
)}
</TopToolbar>
);
};
@@ -216,9 +211,7 @@ const UserEditActions = () => {
export const UserCreate = (props: CreateProps) => (
<Create
{...props}
redirect={(resource, id, data) => {
return `users/${id}`;
}}
redirect={(resource: string | undefined, id: Identifier | undefined) => `${resource}/${id}`}
>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
@@ -244,6 +237,10 @@ export const UserCreate = (props: CreateProps) => (
const UserTitle = () => {
const record = useRecordContext();
if (!record) {
return null;
}
const translate = useTranslate();
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""
if (isASManaged(record?.id)) {
@@ -319,14 +316,19 @@ export const UserEdit = (props: EditProps) => {
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
<ImageInput
source="avatar_file"
label="resources.users.fields.avatar"
accept={{ "image/*": [".png", ".jpg"] }}
>
<ImageField source="src" title="Avatar" />
<ImageField source="src" title="Avatar" sx={{ '& img': {
width: "120px !important",
height: "120px !important",
objectFit: "cover !important",
borderRadius: '50% !important',
}}} />
</ImageInput>
<TextInput source="id" readOnly />
<TextInput source="displayname" />
@@ -360,7 +362,7 @@ export const UserEdit = (props: EditProps) => {
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
<ReferenceManyField reference="devices" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons="">
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="device_id" sortable={false} />
<TextField source="display_name" sortable={false} />
<TextField source="last_seen_ip" sortable={false} />
@@ -400,8 +402,8 @@ export const UserEdit = (props: EditProps) => {
<DateField source="created_ts" showTime options={DATE_FORMAT} />
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
<NumberField source="media_length" />
<TextField source="media_type" />
<TextField source="upload_name" />
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
<FunctionField source="upload_name" render={record => decodeURIComponent(record.upload_name)} />
<TextField source="quarantined_by" />
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />

View File

@@ -23,14 +23,14 @@ describe("authProvider", () => {
})
);
const ret: undefined = await authProvider.login({
const ret = await authProvider.login({
base_url: "http://example.com",
username: "@user:example.com",
password: "secret",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("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"}',
headers: new Headers({
Accept: "application/json",
@@ -55,12 +55,12 @@ describe("authProvider", () => {
})
);
const ret: undefined = await authProvider.login({
const ret = await authProvider.login({
base_url: "https://example.com/",
loginToken: "login_token",
});
expect(ret).toBe(undefined);
expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
@@ -83,7 +83,7 @@ describe("authProvider", () => {
await authProvider.logout(null);
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/r0/logout", {
headers: new Headers({
Accept: "application/json",
Authorization: "Bearer foo",
@@ -123,7 +123,9 @@ describe("authProvider", () => {
describe("getPermissions", () => {
it("should do nothing", async () => {
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
if (authProvider.getPermissions) {
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
}
});
});
});

View File

@@ -1,4 +1,4 @@
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin";
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
import storage from "../storage";
import { MatrixError, displayError } from "../components/error";
@@ -10,14 +10,16 @@ const authProvider: AuthProvider = {
username,
password,
loginToken,
accessToken,
}: {
base_url: string;
username: string;
password: string;
loginToken: string;
accessToken: string;
}) => {
console.log("login ");
const options: Options = {
let options: Options = {
method: "POST",
body: JSON.stringify(
Object.assign(
@@ -55,11 +57,30 @@ const authProvider: AuthProvider = {
storage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login");
let response;
try {
if (accessToken) {
// this a login with an already obtained access token, let's just validate it
options = {
headers: new Headers({
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
}),
};
}
response = await fetchUtils.fetchJson(login_api_url, options);
const json = response.json;
storage.setItem("home_server", accessToken ? base_url : json.home_server);
storage.setItem("user_id", json.user_id);
storage.setItem("access_token", accessToken ? accessToken : json.access_token);
storage.setItem("device_id", json.device_id);
storage.setItem("login_type", accessToken ? "accessToken" : "credentials");
return Promise.resolve({redirectTo: "/"});
} catch(err) {
const error = err as HttpError;
const errorStatus = error.status;
@@ -71,14 +92,8 @@ const authProvider: AuthProvider = {
errMsg,
errorStatus,
)
);
);
}
const json = response.json;
storage.setItem("home_server", json.home_server);
storage.setItem("user_id", json.user_id);
storage.setItem("access_token", json.access_token);
storage.setItem("device_id", json.device_id);
},
// called when the user clicks on the logout button
logout: async () => {
@@ -96,8 +111,14 @@ const authProvider: AuthProvider = {
};
if (typeof access_token === "string") {
await fetchUtils.fetchJson(logout_api_url, options);
storage.removeItem("access_token");
try {
await fetchUtils.fetchJson(logout_api_url, options);
} catch (err) {
console.log("Error logging out", err);
} finally {
storage.removeItem("access_token");
storage.removeItem("login_type");
}
}
},
// called when the API returns an error

View File

@@ -1,6 +1,7 @@
import {
DataProvider,
DeleteParams,
DeleteManyParams,
HttpError,
Identifier,
Options,
@@ -41,21 +42,16 @@ const jsonClient = async (url: string, options: Options = {}) => {
}
};
const mxcUrlToHttp = (mxcUrl: string) => {
const homeserver = storage.getItem("base_url");
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl);
console.log("mxcClient " + ret);
if (ret == null) return null;
const serverName = ret[1];
const mediaId = ret[2];
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 Action {
endpoint: string;
method?: string;
body?: Record<string, any>;
}
interface Room {
room_id: string;
name?: string;
@@ -260,10 +256,10 @@ export interface SynapseDataProvider extends DataProvider {
const resourceMap = {
users: {
path: "/_synapse/admin/v2/users",
map: (u: User) => ({
map: async (u: User) => ({
...u,
id: returnMXID(u.name),
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
avatar_src: u.avatar_url ? u.avatar_url : undefined,
is_guest: !!u.is_guest,
admin: !!u.admin,
deactivated: !!u.deactivated,
@@ -451,7 +447,7 @@ const resourceMap = {
id: rd.room_id,
public: !!rd.public,
guest_access: !!rd.guest_access,
avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined,
avatar_src: rd.avatar_url ? rd.avatar_url : undefined,
}),
data: "chunk",
total: json => json.total_room_count_estimate,
@@ -557,8 +553,10 @@ const baseDataProvider: SynapseDataProvider = {
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`;
const { json } = await jsonClient(url);
const formattedData = await json[res.data].map(res.map);
return {
data: json[res.data].map(res.map),
data: formattedData,
total: res.total(json, from, perPage),
};
},
@@ -714,7 +712,7 @@ const baseDataProvider: SynapseDataProvider = {
},
deleteMany: async (resource, params) => {
console.log("deleteMany " + resource);
console.log("deleteMany " + resource, "params", params);
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
@@ -731,6 +729,7 @@ const baseDataProvider: SynapseDataProvider = {
});
})
);
return {
data: responses.map(({ json }) => json),
};
@@ -808,6 +807,39 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
}
return params;
},
beforeDelete: async (params: DeleteParams<any>, dataProvider: DataProvider) => {
if (params.meta?.deleteMedia) {
const base_url = storage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
const base_url = storage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(params.id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}
return params;
},
beforeDeleteMany: async (params: DeleteManyParams<any>, dataProvider: DataProvider) => {
await Promise.all(
params.ids.map(async id => {
if (params.meta?.deleteMedia) {
const base_url = storage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
const base_url = storage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}
})
);
return params;
},
},
]);

View File

@@ -54,11 +54,6 @@ export const getSupportedLoginFlows = async baseUrl => {
return response.json.flows;
};
export const getMediaUrl = media_id => {
const baseUrl = storage.getItem("base_url");
return `${baseUrl}/_matrix/media/v1/download/${media_id}?allow_redirect=true`;
};
/**
* Generate a random MXID for current homeserver
* @returns full MXID as string

43
src/utils/fetchMedia.ts Normal file
View File

@@ -0,0 +1,43 @@
import storage from "../storage";
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => {
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl);
console.log("mxcClient " + ret);
if (ret == null) {
throw new Error("Invalid mxcUrl");
}
const serverName = ret[1];
const mediaId = ret[2];
return { serverName, mediaId };
};
export type MediaType = "thumbnail" | "original";
export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType): Promise<Response> => {
const homeserver = storage.getItem("base_url");
const accessToken = storage.getItem("access_token");
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
if (!serverName || !mediaId) {
throw new Error("Invalid mxcUrl");
}
let url = "";
if (type === "thumbnail") {
// ref: https://spec.matrix.org/latest/client-server-api/#thumbnails
url = `${homeserver}/_matrix/client/v1/media/thumbnail/${serverName}/${mediaId}?width=320&height=240&method=scale`;
} else if (type === "original") {
url = `${homeserver}/_matrix/client/v1/media/download/${serverName}/${mediaId}`;
} else {
throw new Error("Invalid authenticated media type");
}
const response = await fetch(`${url}`, {
headers: {
authorization: `Bearer ${accessToken}`,
},
});
return response;
};

560
yarn.lock
View File

@@ -23,33 +23,41 @@
"@babel/highlight" "^7.24.7"
picocolors "^1.0.0"
"@babel/compat-data@^7.25.2":
version "7.25.4"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz"
integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==
"@babel/code-frame@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7"
integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==
dependencies:
"@babel/highlight" "^7.25.7"
picocolors "^1.0.0"
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.24.5":
version "7.25.2"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz"
integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==
"@babel/compat-data@^7.25.7":
version "7.25.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402"
integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.25.2":
version "7.25.8"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6"
integrity sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.24.7"
"@babel/generator" "^7.25.0"
"@babel/helper-compilation-targets" "^7.25.2"
"@babel/helper-module-transforms" "^7.25.2"
"@babel/helpers" "^7.25.0"
"@babel/parser" "^7.25.0"
"@babel/template" "^7.25.0"
"@babel/traverse" "^7.25.2"
"@babel/types" "^7.25.2"
"@babel/code-frame" "^7.25.7"
"@babel/generator" "^7.25.7"
"@babel/helper-compilation-targets" "^7.25.7"
"@babel/helper-module-transforms" "^7.25.7"
"@babel/helpers" "^7.25.7"
"@babel/parser" "^7.25.8"
"@babel/template" "^7.25.7"
"@babel/traverse" "^7.25.7"
"@babel/types" "^7.25.8"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.25.0", "@babel/generator@^7.25.6", "@babel/generator@^7.7.2":
"@babel/generator@^7.25.6", "@babel/generator@^7.7.2":
version "7.25.6"
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz"
integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==
@@ -59,18 +67,28 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
"@babel/helper-compilation-targets@^7.25.2":
version "7.25.2"
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz"
integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==
"@babel/generator@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56"
integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==
dependencies:
"@babel/compat-data" "^7.25.2"
"@babel/helper-validator-option" "^7.24.8"
browserslist "^4.23.1"
"@babel/types" "^7.25.7"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-compilation-targets@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4"
integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==
dependencies:
"@babel/compat-data" "^7.25.7"
"@babel/helper-validator-option" "^7.25.7"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7":
"@babel/helper-module-imports@^7.16.7":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz"
integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==
@@ -78,51 +96,74 @@
"@babel/traverse" "^7.24.7"
"@babel/types" "^7.24.7"
"@babel/helper-module-transforms@^7.25.2":
version "7.25.2"
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz"
integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==
"@babel/helper-module-imports@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472"
integrity sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==
dependencies:
"@babel/helper-module-imports" "^7.24.7"
"@babel/helper-simple-access" "^7.24.7"
"@babel/helper-validator-identifier" "^7.24.7"
"@babel/traverse" "^7.25.2"
"@babel/traverse" "^7.25.7"
"@babel/types" "^7.25.7"
"@babel/helper-module-transforms@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz#2ac9372c5e001b19bc62f1fe7d96a18cb0901d1a"
integrity sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==
dependencies:
"@babel/helper-module-imports" "^7.25.7"
"@babel/helper-simple-access" "^7.25.7"
"@babel/helper-validator-identifier" "^7.25.7"
"@babel/traverse" "^7.25.7"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0":
version "7.24.8"
resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz"
integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==
"@babel/helper-simple-access@^7.24.7":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz"
integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==
"@babel/helper-plugin-utils@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c"
integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==
"@babel/helper-simple-access@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0"
integrity sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==
dependencies:
"@babel/traverse" "^7.24.7"
"@babel/types" "^7.24.7"
"@babel/traverse" "^7.25.7"
"@babel/types" "^7.25.7"
"@babel/helper-string-parser@^7.24.8":
version "7.24.8"
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz"
integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
"@babel/helper-string-parser@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54"
integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==
"@babel/helper-validator-identifier@^7.24.7":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz"
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
"@babel/helper-validator-option@^7.24.8":
version "7.24.8"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz"
integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==
"@babel/helper-validator-identifier@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5"
integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==
"@babel/helpers@^7.25.0":
version "7.25.6"
resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz"
integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==
"@babel/helper-validator-option@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729"
integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==
"@babel/helpers@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2"
integrity sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==
dependencies:
"@babel/template" "^7.25.0"
"@babel/types" "^7.25.6"
"@babel/template" "^7.25.7"
"@babel/types" "^7.25.7"
"@babel/highlight@^7.24.7":
version "7.24.7"
@@ -134,6 +175,16 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/highlight@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.7.tgz#20383b5f442aa606e7b5e3043b0b1aafe9f37de5"
integrity sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==
dependencies:
"@babel/helper-validator-identifier" "^7.25.7"
chalk "^2.4.2"
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.0", "@babel/parser@^7.25.6":
version "7.25.6"
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz"
@@ -141,6 +192,13 @@
dependencies:
"@babel/types" "^7.25.6"
"@babel/parser@^7.25.7", "@babel/parser@^7.25.8":
version "7.25.8"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2"
integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==
dependencies:
"@babel/types" "^7.25.8"
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz"
@@ -260,19 +318,19 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.8"
"@babel/plugin-transform-react-jsx-self@^7.24.5":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz"
integrity sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==
"@babel/plugin-transform-react-jsx-self@^7.24.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.7.tgz#3d11df143131fd8f5486a1f7d3839890f88f8c85"
integrity sha512-JD9MUnLbPL0WdVK8AWC7F7tTG2OS6u/AKKnsK+NdRhUiVdnzyR1S3kKQCaRLOiaULvUiqK6Z4JQE635VgtCFeg==
dependencies:
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/helper-plugin-utils" "^7.25.7"
"@babel/plugin-transform-react-jsx-source@^7.24.1":
version "7.24.7"
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz"
integrity sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==
"@babel/plugin-transform-react-jsx-source@^7.24.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.7.tgz#a0d8372310d5ea5b0447dfa03a8485f960eff7be"
integrity sha512-S/JXG/KrbIY06iyJPKfxr0qRxnhNOdkNXYBl/rmwgDd72cQLH9tEGkDm/yJPGvcSIUoikzfjMios9i+xT/uv9w==
dependencies:
"@babel/helper-plugin-utils" "^7.24.7"
"@babel/helper-plugin-utils" "^7.25.7"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.8", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
version "7.25.6"
@@ -290,7 +348,16 @@
"@babel/parser" "^7.25.0"
"@babel/types" "^7.25.0"
"@babel/traverse@^7.24.7", "@babel/traverse@^7.25.2":
"@babel/template@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769"
integrity sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==
dependencies:
"@babel/code-frame" "^7.25.7"
"@babel/parser" "^7.25.7"
"@babel/types" "^7.25.7"
"@babel/traverse@^7.24.7":
version "7.25.6"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz"
integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==
@@ -303,7 +370,20 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6", "@babel/types@^7.3.3":
"@babel/traverse@^7.25.7":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8"
integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==
dependencies:
"@babel/code-frame" "^7.25.7"
"@babel/generator" "^7.25.7"
"@babel/parser" "^7.25.7"
"@babel/template" "^7.25.7"
"@babel/types" "^7.25.7"
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.6", "@babel/types@^7.3.3":
version "7.25.6"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz"
integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==
@@ -312,6 +392,15 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.25.7", "@babel/types@^7.25.8":
version "7.25.8"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1"
integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==
dependencies:
"@babel/helper-string-parser" "^7.25.7"
"@babel/helper-validator-identifier" "^7.25.7"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
@@ -1053,10 +1142,10 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@remix-run/router@1.19.2":
version "1.19.2"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz"
integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==
"@remix-run/router@1.20.0":
version "1.20.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
"@rollup/rollup-android-arm-eabi@4.22.4":
version "4.22.4"
@@ -1188,10 +1277,10 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^6.0.0":
version "6.5.0"
resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz"
integrity sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==
"@testing-library/jest-dom@^6.6.2":
version "6.6.2"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz#8186aa9a07263adef9cc5a59a4772db8c31f4a5b"
integrity sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==
dependencies:
"@adobe/css-tools" "^4.4.0"
aria-query "^5.0.0"
@@ -1334,17 +1423,17 @@
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz"
integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
"@types/node@*", "@types/node@^20.14.12":
version "20.16.5"
resolved "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz"
integrity sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==
"@types/node@*", "@types/node@^22.7.7":
version "22.7.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.7.tgz#6cd9541c3dccb4f7e8b141b491443f4a1570e307"
integrity sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==
dependencies:
undici-types "~6.19.2"
"@types/papaparse@^5.3.14":
version "5.3.14"
resolved "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz"
integrity sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==
"@types/papaparse@^5.3.15":
version "5.3.15"
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.15.tgz#7cafa16757a1d121422deefbb10b6310b224ecc4"
integrity sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==
dependencies:
"@types/node" "*"
@@ -1395,85 +1484,85 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@7.18.0", "@typescript-eslint/eslint-plugin@^7.16.1":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz"
integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==
"@typescript-eslint/eslint-plugin@8.10.0", "@typescript-eslint/eslint-plugin@^8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.10.0.tgz#9c8218ed62f9a322df10ded7c34990f014df44f2"
integrity sha512-phuB3hoP7FFKbRXxjl+DRlQDuJqhpOnm5MmtROXyWi3uS/Xg2ZXqiQfcG2BJHiN4QKyzdOJi3NEn/qTnjUlkmQ==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/type-utils" "7.18.0"
"@typescript-eslint/utils" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/scope-manager" "8.10.0"
"@typescript-eslint/type-utils" "8.10.0"
"@typescript-eslint/utils" "8.10.0"
"@typescript-eslint/visitor-keys" "8.10.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/parser@7.18.0", "@typescript-eslint/parser@^7.16.1":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz"
integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==
"@typescript-eslint/parser@8.10.0", "@typescript-eslint/parser@^8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.10.0.tgz#3cbe7206f5e42835878a74a76da533549f977662"
integrity sha512-E24l90SxuJhytWJ0pTQydFT46Nk0Z+bsLKo/L8rtQSL93rQ6byd1V/QbDpHUTdLPOMsBCcYXZweADNCfOCmOAg==
dependencies:
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/scope-manager" "8.10.0"
"@typescript-eslint/types" "8.10.0"
"@typescript-eslint/typescript-estree" "8.10.0"
"@typescript-eslint/visitor-keys" "8.10.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz"
integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==
"@typescript-eslint/scope-manager@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.10.0.tgz#606ffe18314d7b5c2f118f2f02aaa2958107a19c"
integrity sha512-AgCaEjhfql9MDKjMUxWvH7HjLeBqMCBfIaBbzzIcBbQPZE7CPh1m6FF+L75NUMJFMLYhCywJXIDEMa3//1A0dw==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/types" "8.10.0"
"@typescript-eslint/visitor-keys" "8.10.0"
"@typescript-eslint/type-utils@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz"
integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==
"@typescript-eslint/type-utils@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.10.0.tgz#99f1d2e21f8c74703e7d9c4a67a87271eaf57597"
integrity sha512-PCpUOpyQSpxBn230yIcK+LeCQaXuxrgCm2Zk1S+PTIRJsEfU6nJ0TtwyH8pIwPK/vJoA+7TZtzyAJSGBz+s/dg==
dependencies:
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/utils" "7.18.0"
"@typescript-eslint/typescript-estree" "8.10.0"
"@typescript-eslint/utils" "8.10.0"
debug "^4.3.4"
ts-api-utils "^1.3.0"
"@typescript-eslint/types@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz"
integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==
"@typescript-eslint/types@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.10.0.tgz#eb29c4bc2ed23489348c297469c76d28c38fb618"
integrity sha512-k/E48uzsfJCRRbGLapdZgrX52csmWJ2rcowwPvOZ8lwPUv3xW6CcFeJAXgx4uJm+Ge4+a4tFOkdYvSpxhRhg1w==
"@typescript-eslint/typescript-estree@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz"
integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==
"@typescript-eslint/typescript-estree@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.10.0.tgz#36cc66e06c5f44d6781f95cb03b132e985273a33"
integrity sha512-3OE0nlcOHaMvQ8Xu5gAfME3/tWVDpb/HxtpUZ1WeOAksZ/h/gwrBzCklaGzwZT97/lBbbxJ16dMA98JMEngW4w==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/visitor-keys" "7.18.0"
"@typescript-eslint/types" "8.10.0"
"@typescript-eslint/visitor-keys" "8.10.0"
debug "^4.3.4"
globby "^11.1.0"
fast-glob "^3.3.2"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/utils@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz"
integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==
"@typescript-eslint/utils@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.10.0.tgz#d78d1ce3ea3d2a88a2593ebfb1c98490131d00bf"
integrity sha512-Oq4uZ7JFr9d1ZunE/QKy5egcDRXT/FrS2z/nlxzPua2VHFtmMvFNDvpq1m/hq0ra+T52aUezfcjGRIB7vNJF9w==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "7.18.0"
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/typescript-estree" "7.18.0"
"@typescript-eslint/scope-manager" "8.10.0"
"@typescript-eslint/types" "8.10.0"
"@typescript-eslint/typescript-estree" "8.10.0"
"@typescript-eslint/visitor-keys@7.18.0":
version "7.18.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz"
integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==
"@typescript-eslint/visitor-keys@8.10.0":
version "8.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.10.0.tgz#7ce4c0c3b82140415c9cd9babe09e0000b4e9979"
integrity sha512-k8nekgqwr7FadWk548Lfph6V3r9OVqjzAIVskE7orMZR23cGJjAOVazsZSJW+ElyjfTM4wx/1g88Mi70DDtG9A==
dependencies:
"@typescript-eslint/types" "7.18.0"
"@typescript-eslint/types" "8.10.0"
eslint-visitor-keys "^3.4.3"
"@ungap/structured-clone@^1.2.0":
@@ -1481,14 +1570,14 @@
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vitejs/plugin-react@^4.3.1":
version "4.3.1"
resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz"
integrity sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==
"@vitejs/plugin-react@^4.3.2":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.2.tgz#1e13f666fe3135b477220d3c13b783704636b6e4"
integrity sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==
dependencies:
"@babel/core" "^7.24.5"
"@babel/plugin-transform-react-jsx-self" "^7.24.5"
"@babel/plugin-transform-react-jsx-source" "^7.24.1"
"@babel/core" "^7.25.2"
"@babel/plugin-transform-react-jsx-self" "^7.24.7"
"@babel/plugin-transform-react-jsx-source" "^7.24.7"
"@types/babel__core" "^7.20.5"
react-refresh "^0.14.2"
@@ -1634,11 +1723,6 @@ array-includes@^3.1.6, array-includes@^3.1.8:
get-intrinsic "^1.2.4"
is-string "^1.0.7"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array.prototype.findlastindex@^1.2.5:
version "1.2.5"
resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz"
@@ -1847,13 +1931,13 @@ broadcast-channel@^3.4.1:
rimraf "3.0.2"
unload "2.2.0"
browserslist@^4.23.1:
version "4.23.3"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz"
integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==
browserslist@^4.24.0:
version "4.24.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4"
integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==
dependencies:
caniuse-lite "^1.0.30001646"
electron-to-chromium "^1.5.4"
caniuse-lite "^1.0.30001663"
electron-to-chromium "^1.5.28"
node-releases "^2.0.18"
update-browserslist-db "^1.1.0"
@@ -1902,10 +1986,10 @@ camelcase@^6.2.0:
resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001646:
version "1.0.30001662"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz"
integrity sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==
caniuse-lite@^1.0.30001663:
version "1.0.30001669"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3"
integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==
chalk@^2.4.2:
version "2.4.2"
@@ -2288,13 +2372,6 @@ diff@^4.0.1:
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz"
@@ -2379,10 +2456,10 @@ ejs@^3.1.10:
dependencies:
jake "^10.8.5"
electron-to-chromium@^1.5.4:
version "1.5.25"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.25.tgz"
integrity sha512-kMb204zvK3PsSlgvvwzI3wBIcAw15tRkYk+NQdsjdDtcQWTp2RABbMQ9rUBy8KNEOM+/E6ep+XC3AykiWZld4g==
electron-to-chromium@^1.5.28:
version "1.5.41"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz#eae1ba6c49a1a61d84cf8263351d3513b2bcc534"
integrity sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==
emittery@^0.13.1:
version "0.13.1"
@@ -2626,17 +2703,17 @@ eslint-import-resolver-node@^0.3.9:
is-core-module "^2.13.0"
resolve "^1.22.4"
eslint-module-utils@^2.9.0:
version "2.11.0"
resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz"
integrity sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==
eslint-module-utils@^2.12.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b"
integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==
dependencies:
debug "^3.2.7"
eslint-plugin-import@^2.29.1:
version "2.30.0"
resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz"
integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==
eslint-plugin-import@^2.31.0:
version "2.31.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7"
integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==
dependencies:
"@rtsao/scc" "^1.1.0"
array-includes "^3.1.8"
@@ -2646,7 +2723,7 @@ eslint-plugin-import@^2.29.1:
debug "^3.2.7"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.9.0"
eslint-module-utils "^2.12.0"
hasown "^2.0.2"
is-core-module "^2.15.1"
is-glob "^4.0.3"
@@ -2655,6 +2732,7 @@ eslint-plugin-import@^2.29.1:
object.groupby "^1.0.3"
object.values "^1.2.0"
semver "^6.3.1"
string.prototype.trimend "^1.0.8"
tsconfig-paths "^3.15.0"
eslint-plugin-jsx-a11y@^6.9.0:
@@ -2853,7 +2931,7 @@ fast-diff@^1.1.2:
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.9:
fast-glob@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -3088,18 +3166,6 @@ globalthis@^1.0.3:
define-properties "^1.2.1"
gopd "^1.0.1"
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
@@ -3177,11 +3243,6 @@ hoist-non-react-statics@^3.3.1:
dependencies:
react-is "^16.7.0"
hotscript@^1.0.12:
version "1.0.13"
resolved "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz"
integrity sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==
html-encoding-sniffer@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz"
@@ -4031,6 +4092,11 @@ jsesc@^2.5.1:
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
jsesc@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==
jshint@^2.13.6:
version "2.13.6"
resolved "https://registry.npmjs.org/jshint/-/jshint-2.13.6.tgz"
@@ -4213,7 +4279,7 @@ merge-stream@^2.0.0:
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.3.0, merge2@^1.4.1:
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
@@ -4683,16 +4749,15 @@ ra-core@^4.11.2, ra-core@^4.16.2:
react-is "^17.0.2"
react-query "^3.32.1"
ra-core@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ra-core/-/ra-core-5.2.0.tgz"
integrity sha512-HAl/rCLeGt55VvfimQd8emuErPIjI5R0f8fEpy7B+jucxGmGTOw2nes+Tf7UIZKjIoN2BHhb/pailjfd2n8Nlg==
ra-core@^5.2.0, ra-core@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ra-core/-/ra-core-5.3.0.tgz#02ce7e7c5d7617dbb2b2805803fde1d0c88dc4bd"
integrity sha512-e5Y0u+P+0Kx2XeYEAUDV63NdlAvMtlrb4ndYLtIbCaIeOx1o6GXlD2BedICaYSb6XXjz+KoNZ1nmWuZ6tS4f7Q==
dependencies:
"@tanstack/react-query" "^5.8.4"
clsx "^2.1.1"
date-fns "^3.6.0"
eventemitter3 "^5.0.1"
hotscript "^1.0.12"
inflection "^3.0.0"
jsonexport "^3.2.0"
lodash "~4.17.5"
@@ -4700,20 +4765,20 @@ ra-core@^5.2.0:
react-error-boundary "^4.0.13"
react-is "^18.2.0"
ra-i18n-polyglot@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-5.2.0.tgz"
integrity sha512-w/3voyhjjlrj+ETeHBUj1Xy5trqmwy9g2qACssC/NJ+fAGtq+QhzZZfO27WKIR5LtM0DIT+lM0hdFxtrR5SnSg==
ra-i18n-polyglot@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ra-i18n-polyglot/-/ra-i18n-polyglot-5.3.0.tgz#ed10be1e3133b3e4a42990d045646ef336faf1ce"
integrity sha512-Dc2m6MlpF1qHeqH5iJukd7t6oVxnqC/k8AY+HEYQCXCTHhVWkzHDjLOilcKHbgiY6st1p6KoCL93CP8lyEjaqg==
dependencies:
node-polyglot "^2.2.2"
ra-core "^5.2.0"
ra-core "^5.3.0"
ra-language-english@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ra-language-english/-/ra-language-english-5.2.0.tgz"
integrity sha512-vu5BjCPtJ6pEcn4/+/SJkqE5RuFU3DymQbnJSon/KoYUD7F9QOIPmRlVWoCdCKAJPBi8fR6Ffs2aGyAgmx90KQ==
ra-language-english@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ra-language-english/-/ra-language-english-5.3.0.tgz#f2811d9c72513c18ff0f48498bfa92f97911b4ad"
integrity sha512-Gp8dt4annXcxFd7BJ/EgngLmI74A25++iwE5lS8WqD6yBL3byU5Xwu9+CQ8YQkU9+cQEFhLhkWIGfXXczxSymw==
dependencies:
ra-core "^5.2.0"
ra-core "^5.3.0"
ra-language-farsi@^5.0.0:
version "5.0.0"
@@ -4739,17 +4804,16 @@ ra-language-russian@^4.14.2:
dependencies:
ra-core "^4.16.2"
ra-ui-materialui@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-5.2.0.tgz"
integrity sha512-OTjj/CJSxDDojH+e8Ba2eN0S2E672JeVxO5h+KpNg/h8jEF/PTb4Vc3uNF1oXxoXakoMNiKF1DhfTNAZgOfsnQ==
ra-ui-materialui@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ra-ui-materialui/-/ra-ui-materialui-5.3.0.tgz#14b1c35f98a4d133dd0ef6be85191de71da88bb2"
integrity sha512-jmEqI77LDcEbhrMC45QM1YmUMSOwzNH8dEjr9A/Xm+UW+YnzOFDkVJJumfP3jpFL4wviX48Qy5mBi54t19CYjw==
dependencies:
"@tanstack/react-query" "^5.8.4"
autosuggest-highlight "^3.1.1"
clsx "^2.1.1"
css-mediaquery "^0.1.2"
dompurify "^2.4.3"
hotscript "^1.0.12"
inflection "^3.0.0"
jsonexport "^3.2.0"
lodash "~4.17.5"
@@ -4758,19 +4822,19 @@ ra-ui-materialui@^5.2.0:
react-error-boundary "^4.0.13"
react-transition-group "^4.4.5"
react-admin@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/react-admin/-/react-admin-5.2.0.tgz"
integrity sha512-EEHz5jhB/OLMN1PPINVY5LF86hOhqMA6n7nTPwK8hJUT56OUfK9MTUdRkn4ucc8Cy+vxT5a3BlmH6BJSnDs5sw==
react-admin@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-admin/-/react-admin-5.3.0.tgz#c94dce0e96b949717e3c79df03b7cba133949ba1"
integrity sha512-86M0c76ClN7mbMsONLY83lV32hAQREH1hnhHXANZPS4BnRSKSyFjI93Wke+TwUkWlYiqXlEwaYzISfDTTU3xqA==
dependencies:
"@emotion/react" "^11.4.1"
"@emotion/styled" "^11.3.0"
"@mui/icons-material" "^5.15.20"
"@mui/material" "^5.15.20"
ra-core "^5.2.0"
ra-i18n-polyglot "^5.2.0"
ra-language-english "^5.2.0"
ra-ui-materialui "^5.2.0"
ra-core "^5.3.0"
ra-i18n-polyglot "^5.3.0"
ra-language-english "^5.3.0"
ra-ui-materialui "^5.3.0"
react-hook-form "^7.53.0"
react-router "^6.22.0"
react-router-dom "^6.22.0"
@@ -4799,10 +4863,10 @@ react-error-boundary@^4.0.13:
dependencies:
"@babel/runtime" "^7.12.5"
react-hook-form@^7.53.0:
version "7.53.0"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz"
integrity sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==
react-hook-form@^7.53.0, react-hook-form@^7.53.1:
version "7.53.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.1.tgz#3f2cd1ed2b3af99416a4ac674da2d526625add67"
integrity sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0, react-is@^18.3.1:
version "18.3.1"
@@ -4833,20 +4897,20 @@ react-refresh@^0.14.2:
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
react-router-dom@^6.22.0, react-router-dom@^6.26.2:
version "6.26.2"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz"
integrity sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==
react-router-dom@^6.22.0, react-router-dom@^6.27.0:
version "6.27.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.27.0.tgz#8d7972a425fd75f91c1e1ff67e47240c5752dc3f"
integrity sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==
dependencies:
"@remix-run/router" "1.19.2"
react-router "6.26.2"
"@remix-run/router" "1.20.0"
react-router "6.27.0"
react-router@6.26.2, react-router@^6.22.0, react-router@^6.26.2:
version "6.26.2"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz"
integrity sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==
react-router@6.27.0, react-router@^6.22.0, react-router@^6.26.2:
version "6.27.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.27.0.tgz#db292474926c814c996c0ff3ef0162d1f9f60ed4"
integrity sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==
dependencies:
"@remix-run/router" "1.19.2"
"@remix-run/router" "1.20.0"
react-shallow-renderer@^16.15.0:
version "16.15.0"
@@ -5501,19 +5565,19 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript-eslint@^7.16.1:
version "7.18.0"
resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.18.0.tgz"
integrity sha512-PonBkP603E3tt05lDkbOMyaxJjvKqQrXsnow72sVeOFINDE/qNmnnd+f9b4N+U7W6MXnnYyrhtmF2t08QWwUbA==
typescript-eslint@^8.10.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.10.0.tgz#7f7d51577e9b93538cc8801f2cbfdd66098a00e7"
integrity sha512-YIu230PeN7z9zpu/EtqCIuRVHPs4iSlqW6TEvjbyDAE3MZsSl2RXBo+5ag+lbABCG8sFM1WVKEXhlQ8Ml8A3Fw==
dependencies:
"@typescript-eslint/eslint-plugin" "7.18.0"
"@typescript-eslint/parser" "7.18.0"
"@typescript-eslint/utils" "7.18.0"
"@typescript-eslint/eslint-plugin" "8.10.0"
"@typescript-eslint/parser" "8.10.0"
"@typescript-eslint/utils" "8.10.0"
typescript@^5.4.5:
version "5.6.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
typescript@^5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
unbox-primitive@^1.0.2:
version "1.0.2"
@@ -5580,10 +5644,10 @@ v8-to-istanbul@^9.0.1:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^2.0.0"
vite-plugin-version-mark@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/vite-plugin-version-mark/-/vite-plugin-version-mark-0.1.0.tgz"
integrity sha512-Na9KG6anEwAQVUKm3DOl4AufzvkgWlOogBnGAMR6YEcMMOYMl45/0KqMuvNxGd0WR8aW8cpUiGHDCFlIZ15YeQ==
vite-plugin-version-mark@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/vite-plugin-version-mark/-/vite-plugin-version-mark-0.1.2.tgz#85b2315cffce7ac2d83b6afceb313d0d1c47c4d4"
integrity sha512-+mPRTwx6SOEfEq3/y1u+O0ju5Lz0Ac27Fx6hE5Xn1fMpIQfo3aERPB2/xxYI8lIa8GPHCX5c8ApqBKWwFNHxwA==
vite@^5.4.6:
version "5.4.6"