Compare commits

..

313 Commits

Author SHA1 Message Date
311db77306
Disable registration tokens 2024-04-20 22:59:35 +03:00
7c08f846a5
Update translate 2024-04-20 22:52:08 +03:00
f2f096d8a5
Update russian language 2024-04-20 22:34:55 +03:00
16191d9cc8
Update russian language 2024-04-20 22:08:36 +03:00
703129f88b
Add russian language 2024-04-19 13:14:04 +03:00
Manuel Stahl
c9364f631b Fetch tags in github workflows
Tags are required to construct the version information.

Change-Id: Ic1af3e8f50eafafcc8a0c3ca37f362d6bd05e116
2024-04-18 21:13:06 +02:00
Manuel Stahl
08dc5f6271 Push docker images also to ghcr.io
Fixes #350.

Change-Id: Ifdb7e4e7fda46efd0ed9e760587033f52ff4a130
2024-04-18 17:44:06 +02:00
Manuel Stahl
c9cb9aa9e0 Show Matrix specs supported by the homeserver
Change-Id: I01c110fb4b3de4de49b34f290c91c8bf424521fe
2024-04-18 10:01:52 +02:00
Manuel Stahl
25020c2d5b Remove unused function "renderInput"
Seems to be obsolete since react-admin v4.

Change-Id: I9f1d528a43510efd61befd23a05d1c8ebf40ddfd
2024-04-18 10:01:52 +02:00
Manuel Stahl
1acffdb618 Make functions in dataProvider async
Change-Id: Iab36ba6379340e47e7d58b1b2d882cd7cc111f41
2024-04-18 10:01:52 +02:00
Manuel Stahl
0b4f3a60c0 Make login and logout in authProvider async
Change-Id: I6bfb1c7a5a3c5a43f9fa622e87d9d487a95a0b6e
2024-04-18 10:01:52 +02:00
Manuel Stahl
33d29e01b1 Add authProvider test
Change-Id: Ia5acce659a386437687e38ae03d578e3bccb9324
2024-04-18 10:01:52 +02:00
Gavin Mogan
a2e47cb793
Add source urls to docker so tools can find sourcecode (#506)
For tools like renovate or dependabot, they like to put changelog notes in PRs updating deps. Having the labels allows the tools to link it back to sourcecode and share commits/release notes
2024-04-17 20:32:41 +02:00
Manuel Stahl
7deb9bcf7e Bump version to 0.9.2
Change-Id: I8d5f98f10fe16189c12b2ce0f0fff073ec81fed5
2024-04-17 09:53:48 +02:00
Manuel Stahl
8185d7f0b0 Remove obsolete .travis.yml
Change-Id: I65f80b5bcf6db3fd85ddbb3ea25f86bb2466bace
2024-04-17 09:53:48 +02:00
Manuel Stahl
37e1fcc96d Fix App test
Change-Id: Iacaa6f5e70925b857f24554e6aba64234b1cae44
2024-04-17 09:53:48 +02:00
Fateme Shamohammadi
f6e193c51c Add farsi translations (#504)
Change-Id: Iee74dbf229197359a148dec7e75ef6f744a1856d
2024-04-16 14:53:05 +02:00
dependabot[bot]
fa1f86491f
Bump ra-language-french from 4.16.12 to 4.16.15 (#503)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.16.12 to 4.16.15.
- [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/v4.16.12...v4.16.15)

---
updated-dependencies:
- dependency-name: ra-language-french
  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-04-16 09:27:50 +02:00
dependabot[bot]
b6546b89ad
Bump the npm_and_yarn group with 3 updates (#496)
Bumps the npm_and_yarn group with 3 updates: [express](https://github.com/expressjs/express), [follow-redirects](https://github.com/follow-redirects/follow-redirects) and [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware).


Updates `express` from 4.18.2 to 4.19.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

Updates `follow-redirects` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

Updates `webpack-dev-middleware` from 5.3.3 to 5.3.4
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-16 09:22:55 +02:00
dependabot[bot]
baa8e4ad95
Bump @testing-library/react from 14.2.1 to 15.0.2 (#505)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.2.1 to 15.0.2.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v14.2.1...v15.0.2)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  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-04-16 09:22:43 +02:00
dependabot[bot]
bd6d5847e1
Bump react-admin from 4.16.11 to 4.16.15 (#502)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 4.16.11 to 4.16.15.
- [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/v4.16.11...v4.16.15)

---
updated-dependencies:
- dependency-name: react-admin
  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-04-16 09:22:03 +02:00
dependabot[bot]
32c5867e2a
Bump @mui/material from 5.15.14 to 5.15.15 (#501)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.15.14 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/next/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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-04-16 09:21:33 +02:00
dependabot[bot]
f68a5de64a
Bump softprops/action-gh-release from 1 to 2 (#482)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](de2c0eb89a...3198ee18f8)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  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-04-16 09:19:14 +02:00
dependabot[bot]
e326599da2
Bump @mui/icons-material from 5.15.10 to 5.15.15 (#499)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.15.10 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v5.15.15/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/packages/mui-icons-material)

---
updated-dependencies:
- dependency-name: "@mui/icons-material"
  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-04-04 12:28:59 +02:00
dependabot[bot]
8d6852ca8c
Bump @mui/styles from 5.15.10 to 5.15.15 (#498)
Bumps [@mui/styles](https://github.com/mui/material-ui/tree/HEAD/packages/mui-styles) from 5.15.10 to 5.15.15.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/v5.15.15/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.15/packages/mui-styles)

---
updated-dependencies:
- dependency-name: "@mui/styles"
  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-04-04 12:28:49 +02:00
dependabot[bot]
ee859a2926
Bump @mui/material from 5.15.10 to 5.15.14 (#492)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.15.10 to 5.15.14.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/next/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.14/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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-04-04 12:22:31 +02:00
dependabot[bot]
0c4ca1459f
Bump ra-language-french from 4.16.11 to 4.16.12 (#481)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.16.11 to 4.16.12.
- [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/v4.16.11...v4.16.12)

---
updated-dependencies:
- dependency-name: ra-language-french
  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-04-04 12:22:19 +02:00
dependabot[bot]
ebba0f66f7
Bump eslint from 8.56.0 to 8.57.0 (#479)
Bumps [eslint](https://github.com/eslint/eslint) from 8.56.0 to 8.57.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.56.0...v8.57.0)

---
updated-dependencies:
- dependency-name: eslint
  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-04-04 12:22:09 +02:00
Manuel Stahl
7d4d765ab4 Bump react-admin from 4.16.9 to 4.16.11
Bumps [react-admin](https://github.com/marmelab/react-admin) from 4.16.9 to 4.16.11.
- [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/v4.16.9...v4.16.11)

Change-Id: I6ae1c3ad892a65b707f9ee6e3a22b6be8f706394
2024-02-19 12:37:26 +01:00
Manuel Stahl
dfee94af96 Bump @mui/* from 5.15.7 to 5.15.10
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.10/packages/mui-material)

Change-Id: I59b486e1b3351f4e685d7bd317ea4d96c3d24209
2024-02-19 12:36:02 +01:00
Manuel Stahl
ae7f6e18e5 Use --immutable flag whenever "yarn install" is called by a tool
Fixes #347

Change-Id: I1b8423f9cef46a425c1ec7665c8285af10c56df6
2024-02-19 11:58:44 +01:00
Manuel Stahl
d1e9f38b14 Fix example.csv
User must be only the name part, not the full MXID as we can only create
local users.

Fixes #406

Change-Id: Ida7b6db28d88417f28b59a02df1f3d7a010aa110
2024-02-19 11:58:44 +01:00
Sebastian
4054249359
Update RegistrationTokens.jsx: Fix resource name (#469)
fixes #468
2024-02-08 17:30:21 +01:00
Manuel Stahl
f240318525 Bump version to 0.9.1
Change-Id: I8436d0ebd48a99f4b2ca2b7b213d94689b440d57
2024-02-08 15:05:26 +01:00
Dirk Klimpel
0852b54a8e
Disable bulkActionButtons for not needed room and user tabs (#466) 2024-02-08 09:31:11 +01:00
Manuel Stahl
8688ab7d0e Fix update in dataProvider
Fixes #461

Change-Id: Icc4b0264cfda04a8a28595d153c43cdf75524673
2024-02-07 16:48:28 +01:00
Manuel Stahl
abc677dc16 Simplify DeviceRemoveButton
Change-Id: I23dcb327d2612db7fc132889d623b709dce34f06
2024-02-07 16:40:42 +01:00
Steffo
9d26a1ce3a
Allow deletion of event reports (#462)
* feat: Allow event reports to get deleted
* chore: Change german translation of reports name to be more fitting
2024-02-07 16:34:50 +01:00
Timo Gurr
3116b4e07a Show topic in room basic view 2024-02-07 16:23:54 +01:00
dependabot[bot]
3a34c03509
Bump @mui/styles from 5.15.7 to 5.15.8 (#463)
Bumps [@mui/styles](https://github.com/mui/material-ui/tree/HEAD/packages/mui-styles) from 5.15.7 to 5.15.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.15.8/packages/mui-styles)

---
updated-dependencies:
- dependency-name: "@mui/styles"
  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-02-07 16:18:50 +01:00
dependabot[bot]
384bc6553c
Bump JamesIves/github-pages-deploy-action from 4.4.3 to 4.5.0 (#457)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.3 to 4.5.0.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.3...v4.5.0)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  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-02-07 16:18:15 +01:00
dependabot[bot]
df87432157
Bump follow-redirects from 1.14.8 to 1.15.5 (#450)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 16:17:49 +01:00
Manuel Stahl
2afc7aeca4 Rename all JSX files to have proper file extension
Change-Id: I4ab382f7673a815164f74154e6b03b370fd76a33
2024-02-07 15:27:34 +01:00
Manuel Stahl
ac843b3244 Upgrade packages to latest version
yarn upgrade --latest

Change-Id: I07c71927ffa6c811fe7cbf8bd2a47503e55499ce
2024-02-07 15:27:34 +01:00
Manuel Stahl
82155c23a1 Upgrade react to v18
https://react.dev/blog/2022/03/08/react-18-upgrade-guide#updates-to-client-rendering-apis

Change-Id: Ibac40eb3d900f54955dfbc8f5e2833a0c47941a6
2024-02-07 15:27:33 +01:00
Manuel Stahl
64a89f6552 Extract helper functions from LoginPage
Change-Id: I507e223d0eff00bac3963d0b71f9bd648b9ab7b1
2024-02-07 15:15:40 +01:00
Manuel Stahl
5b8882bd80 Fix AvatarField
Change-Id: I9614163942fcb8667885b524caf944500605c55d
2024-02-07 15:15:36 +01:00
Manuel Stahl
3fe0e95069 Set "requireAuth" for all pages
Change-Id: I1b68d46f9f7d9a843a5b26f0906d1f71569487cf
2024-02-07 12:03:00 +01:00
Dirk Klimpel
3cd0aa4446
Update links to Synapse in README.md (#458) 2024-02-07 11:14:25 +01:00
dklimpel
3adc6b4663 Use new API of dataProvider
Change-Id: I2789f1f1384b48e876bee5af421ff5db66fa3416
2024-02-07 08:49:26 +01:00
Manuel Stahl
76ef017244 Refactor media
Change-Id: Ic24c53048c35b76532af24d9c5c9bf831688344b
2024-02-07 08:49:11 +01:00
dklimpel
00ecb29d6b Refactor RoomDirectory
Change-Id: Ie3bd606fc91b2673d2a3422f8fd465258d3211b0
2024-02-07 08:28:12 +01:00
Manuel Stahl
4204eb902f Refactor ServerNotices
https://marmelab.com/react-admin/Upgrade.html#usequery-usemutation-and-usequerywithstore-have-been-removed

Change-Id: Id12f727d8813f78c3ae300035aeb1333a1272e02
2024-02-07 08:28:12 +01:00
dklimpel
6430aca02b Rename save to onSubmit in SimpleForm
https: //marmelab.com/react-admin/Upgrade.html#the-form-components-save-prop-has-been-renamed-to-onsubmit

Change-Id: Iaf2c0b665c8058336d4df6326531780a2790e71d
2024-02-07 08:28:12 +01:00
dklimpel
b8a0b4bef5 Move redirect from SimpleForm to Create
Change-Id: I7c5c0043a49bcb16c131e400b2ebe022e233c5ae
2024-02-06 15:11:20 +01:00
dklimpel
2c769c309e Move Toolbar's alwaysEnableSaveButton into SaveButton
https: //marmelab.com/react-admin/Upgrade.html#toolbars-alwaysenablesavebutton-prop-has-been-removed

Change-Id: I6c8693d4f55bfabdeaa677bd294d8663b7f14d69
2024-02-06 15:11:20 +01:00
dklimpel
82578c6570 Change unselectAll syntax
https: //marmelab.com/react-admin/Upgrade.html#useunselectall-syntax-changed

Change-Id: Ie8d261e863fe4726b3a5925ed0446eb824c6e517
2024-02-06 15:11:20 +01:00
dklimpel
005abfb4a2 Update pagination
https: //marmelab.com/react-admin/Upgrade.html#no-more-props-injection-in-custom-pagination-and-empty-components

Change-Id: I6f4d3941dee22cf00da30bada5442f3fdd345127
2024-02-06 15:11:20 +01:00
dklimpel
155e73b9c6 Rename currentSort to sort
https: //marmelab.com/react-admin/Upgrade.html#currentsort-renamed-to-sort

Change-Id: I676adefe0073a9a0343dcd598e9559ecf30c38af
2024-02-06 15:11:20 +01:00
dklimpel
1eb787fd9b Replace "onFailure" with "onError"
https://marmelab.com/react-admin/Upgrade.html#onsuccess-and-onfailure-props-have-moved

Change-Id: I30ae51e06df0293391988a7a84be9c6ef2b158b3
2024-02-06 15:11:12 +01:00
dklimpel
691969e1a1 Fix translation of device_id in EventReport
Change-Id: Ife6cfdae1fce9b477fc12b2e0cdd6bcea4b8b734
2024-02-06 15:11:07 +01:00
Manuel Stahl
d520c6d618 Export resources as objects
Change-Id: I3c501369abf27fa21293c0434c56a00aaf8a64cd
2024-02-05 15:59:43 +01:00
Manuel Stahl
af453eea71 Remove/mark unused parameters
All top level components should pass props to the generic react-admin
component to be more versatile.

Change-Id: I25dd099cde1aefacbc748dc4716a8b0a3db9ab93
2024-02-05 15:59:43 +01:00
Manuel Stahl
78d1d34a84 Simplify filters
Change-Id: I3e4cb7134a92c949bfb62d753c682a6c8fca6736
2024-02-05 15:59:43 +01:00
dklimpel
a222af273f Update dataProvider hooks
Change-Id: Ic19f7a6ad97b1392c96c91a19e76b8983c9d0fd2
2024-02-05 15:59:43 +01:00
Manuel Stahl
51def5775d Replace Fragment with short form
https://legacy.reactjs.org/docs/fragments.html#short-syntax

Change-Id: Ib1af57fc5e87ded8c1fee38dcbd60fae8621cb07
2024-02-05 15:59:43 +01:00
Manuel Stahl
6363e3d32e Use icon as loading spinner in login page
Change-Id: Ie0e8d0a9e1242849fb8b18875d752dd15facaaf9
2024-02-05 15:59:43 +01:00
Manuel Stahl
ba4345be1e Bump version to 0.9.0
Change-Id: I2acd7e62b4a3c7cd664e3e33200cfb4db96d638a
2024-02-05 15:59:40 +01:00
dklimpel
b70ee7c55d Upgrade to React-Admin 4 (#332)
Change-Id: Ia03486edfd934438580e614af754a0966f6fd6e3
2024-02-05 13:44:22 +01:00
Manuel Stahl
9f03ec9b0f Remove unused Menu.js
Change-Id: Idac1d6bcfd703ee499a2522eace6411e48ac176b
2024-02-05 12:47:14 +01:00
Dirk Klimpel
d8d393cdf6
Update deprecated resource definitions (#331) 2024-02-02 17:14:07 +01:00
Dirk Klimpel
58e02d6dff
Migrate makeStyles to MUI v5 (#330) 2024-02-02 16:37:39 +01:00
dependabot[bot]
17379a7325
Bump docker/setup-buildx-action from 2 to 3 (#395)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...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-02-02 15:04:39 +01:00
dependabot[bot]
13fbc419c2
Bump docker/login-action from 2 to 3 (#394)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-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-02-02 15:04:29 +01:00
dependabot[bot]
e757876c35
Bump docker/setup-qemu-action from 2 to 3 (#396)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-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-02-02 15:04:05 +01:00
dependabot[bot]
8b99695e60
Bump docker/build-push-action from 4 to 5 (#397)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-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-02-02 15:03:52 +01:00
dependabot[bot]
4ab5ae2585
Bump @babel/traverse from 7.16.5 to 7.23.2 (#418)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.16.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 15:03:27 +01:00
dependabot[bot]
7579873d87
Bump actions/setup-node from 3 to 4 (#421)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  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-02-02 15:03:17 +01:00
dependabot[bot]
e2ce934f2a
Bump @adobe/css-tools from 4.3.1 to 4.3.2 (#438)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 15:03:02 +01:00
dependabot[bot]
56bc4a56b9
Bump ra-language-french from 4.13.3 to 4.16.2 (#439)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.13.3 to 4.16.2.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v4.13.3...v4.16.2)

---
updated-dependencies:
- dependency-name: ra-language-french
  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-02-02 15:02:53 +01:00
dependabot[bot]
4bae32b57c
Bump @mui/icons-material from 5.14.8 to 5.14.19 (#440)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.14.8 to 5.14.19.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.14.19/packages/mui-icons-material)

---
updated-dependencies:
- dependency-name: "@mui/icons-material"
  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-02-02 15:02:41 +01:00
dependabot[bot]
6e395e3b0f
Bump eslint from 8.48.0 to 8.55.0 (#441)
Bumps [eslint](https://github.com/eslint/eslint) from 8.48.0 to 8.55.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.48.0...v8.55.0)

---
updated-dependencies:
- dependency-name: eslint
  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-02-02 15:02:28 +01:00
Dirk Klimpel
323ad9f9e2
Disallow crawling in robots.txt (#448) 2024-01-31 19:24:46 +01:00
Dirk Klimpel
50af6499b0
Bump NodeJS to v18 (#407) 2023-10-04 09:23:43 +02:00
dependabot[bot]
12c22a170b
Bump papaparse from 5.3.1 to 5.4.1 (#388)
Bumps [papaparse](https://github.com/mholt/PapaParse) from 5.3.1 to 5.4.1.
- [Release notes](https://github.com/mholt/PapaParse/releases)
- [Commits](https://github.com/mholt/PapaParse/compare/5.3.1...5.4.1)

---
updated-dependencies:
- dependency-name: papaparse
  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>
2023-09-12 15:00:40 +02:00
dependabot[bot]
5bbb2fc77b
Bump prop-types from 15.7.2 to 15.8.1 (#390)
Bumps [prop-types](https://github.com/facebook/prop-types) from 15.7.2 to 15.8.1.
- [Changelog](https://github.com/facebook/prop-types/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/prop-types/compare/v15.7.2...v15.8.1)

---
updated-dependencies:
- dependency-name: prop-types
  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>
2023-09-12 13:41:00 +02:00
dependabot[bot]
499d15e667
Bump @mui/material from 5.4.0 to 5.14.8 (#391)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 5.4.0 to 5.14.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.14.8/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  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>
2023-09-12 13:40:40 +02:00
dependabot[bot]
bcf7dec10c
Bump eslint-config-prettier from 8.10.0 to 9.0.0 (#389)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.10.0 to 9.0.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.10.0...v9.0.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  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>
2023-09-12 13:27:26 +02:00
dependabot[bot]
e1c7f44fec
Bump @mui/icons-material from 5.14.7 to 5.14.8 (#392)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.14.7 to 5.14.8.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.14.8/packages/mui-icons-material)

---
updated-dependencies:
- dependency-name: "@mui/icons-material"
  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>
2023-09-12 13:26:27 +02:00
dependabot[bot]
0e25633398
Bump ra-language-french from 4.13.2 to 4.13.3 (#382)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.13.2 to 4.13.3.
- [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/v4.13.2...v4.13.3)

---
updated-dependencies:
- dependency-name: ra-language-french
  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>
2023-09-05 12:54:58 +02:00
Ezwen
8097fa4da4
Add raw sender uid in reports page (#334)
Fixes #333.
2023-09-05 10:50:38 +02:00
Stefan Möhrle
ad9a1c502b
Fix SSO-login for base urls with explicit port (#337) 2023-09-05 10:49:24 +02:00
dependabot[bot]
ec2e7b2ccb
Bump @emotion/react from 11.7.1 to 11.11.1 (#385)
Bumps [@emotion/react](https://github.com/emotion-js/emotion) from 11.7.1 to 11.11.1.
- [Release notes](https://github.com/emotion-js/emotion/releases)
- [Changelog](https://github.com/emotion-js/emotion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emotion-js/emotion/compare/@emotion/react@11.7.1...@emotion/react@11.11.1)

---
updated-dependencies:
- dependency-name: "@emotion/react"
  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>
2023-09-05 10:47:45 +02:00
dependabot[bot]
8bdeddd8f6
Bump ra-test from 3.19.4 to 3.19.12 (#384)
Bumps [ra-test](https://github.com/marmelab/react-admin) from 3.19.4 to 3.19.12.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/v3.19.12/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v3.19.4...v3.19.12)

---
updated-dependencies:
- dependency-name: ra-test
  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>
2023-09-05 10:47:25 +02:00
dependabot[bot]
e253fcb516
Bump @mui/icons-material from 5.3.1 to 5.14.7 (#386)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 5.3.1 to 5.14.7.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v5.14.7/packages/mui-icons-material)

---
updated-dependencies:
- dependency-name: "@mui/icons-material"
  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>
2023-09-05 08:26:09 +02:00
dependabot[bot]
05a4f94d2f
Bump eslint from 8.32.0 to 8.48.0 (#383)
Bumps [eslint](https://github.com/eslint/eslint) from 8.32.0 to 8.48.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.32.0...v8.48.0)

---
updated-dependencies:
- dependency-name: eslint
  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>
2023-09-05 08:25:48 +02:00
dependabot[bot]
d747e24a96
Bump JamesIves/github-pages-deploy-action from 4.4.1 to 4.4.3 (#381)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.1 to 4.4.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.1...v4.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 08:23:21 +02:00
dependabot[bot]
35e3bbeb88
Bump actions/checkout from 3 to 4 (#380)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  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>
2023-09-05 08:23:03 +02:00
Francesco Carmelo Capria
c750f6b8e9
Add italian language option to login page (#379) 2023-08-31 21:57:51 +02:00
Francesco Carmelo Capria
0c5c762224
Italian translations (#374)
* Added italian language

* Bump @testing-library/react from 11.2.7 to 12.1.5 (#327)

Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.7 to 12.1.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.2.7...v12.1.5)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  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>

* Bump docker/build-push-action from 3 to 4 (#328)

Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-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>

* Added italian language

* Fix

* Updated yarn.lock

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-31 14:50:44 +02:00
dependabot[bot]
8fa78356a0
Bump docker/build-push-action from 3 to 4 (#328)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-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>
2023-08-31 09:24:22 +02:00
dependabot[bot]
3fec64021a
Bump @testing-library/react from 11.2.7 to 12.1.5 (#327)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.7 to 12.1.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.2.7...v12.1.5)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  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>
2023-08-31 09:23:51 +02:00
dependabot[bot]
15528e3e9b
Bump tough-cookie from 4.0.0 to 4.1.3 (#378)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 22:15:18 +02:00
dependabot[bot]
39643bd8ab
Bump @adobe/css-tools from 4.0.2 to 4.3.1 (#377)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.2 to 4.3.1.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 22:15:06 +02:00
dependabot[bot]
d592234d4e
Bump word-wrap from 1.2.3 to 1.2.5 (#376)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 22:14:56 +02:00
Dirk Klimpel
67500a6023
Migrate material-ui icons to v5 (#329) 2023-08-30 22:13:14 +02:00
dependabot[bot]
ffce8bd64c
Bump semver from 6.3.0 to 6.3.1 (#375)
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 22:11:22 +02:00
dependabot[bot]
8b00f7d5ac
Bump react-admin from 3.19.7 to 3.19.12 (#339)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 3.19.7 to 3.19.12.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/v3.19.12/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v3.19.7...v3.19.12)

---
updated-dependencies:
- dependency-name: react-admin
  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>
2023-08-30 22:09:57 +02:00
dependabot[bot]
bbd63f4838
Bump @emotion/styled from 11.6.0 to 11.10.6 (#342)
Bumps [@emotion/styled](https://github.com/emotion-js/emotion) from 11.6.0 to 11.10.6.
- [Release notes](https://github.com/emotion-js/emotion/releases)
- [Changelog](https://github.com/emotion-js/emotion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emotion-js/emotion/compare/@emotion/styled@11.6.0...@emotion/styled@11.10.6)

---
updated-dependencies:
- dependency-name: "@emotion/styled"
  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>
2023-08-30 22:09:42 +02:00
dependabot[bot]
bdf29afda0
Bump webpack from 5.74.0 to 5.76.1 (#349)
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 22:09:30 +02:00
dependabot[bot]
46fa936f20
Bump eslint-config-prettier from 8.3.0 to 8.8.0 (#353)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.3.0 to 8.8.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.3.0...v8.8.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  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>
2023-08-30 22:09:16 +02:00
dependabot[bot]
d502696c1f
Bump ra-language-french from 4.2.0 to 4.9.3 (#359)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 4.2.0 to 4.9.3.
- [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/v4.2.0...v4.9.3)

---
updated-dependencies:
- dependency-name: ra-language-french
  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>
2023-08-30 22:08:20 +02:00
Michael Albert
9b43d41040 Remove fixed PUBLIC_URL in Dockerfile
Change-Id: Ifb2e41f02f6568fd80fe9de4c76b633236ceffe4
2023-02-03 22:17:08 +01:00
Michael Albert
3276a9b6ed Raise yarn install timeout limit to avoid build errors
@mui/icons-material needs much time to install, raises the overall yarn install time and causes a timeout error

Change-Id: I0327c6e0523528fb08da7e4b993fb53e9e070305
2023-01-27 08:34:24 +01:00
Michael Albert
b3a611e7ad Create test-docker-image.yml
Change-Id: I2a350b2a33233c9a271739643acc0887558cc52e
2023-01-24 22:16:58 +01:00
Michael Albert
de03e23524 Fix coding style of french translation
Change-Id: Id5e2a5b2fefcbff4cefbd577cc9fae5315be9cb6
2023-01-24 21:50:59 +01:00
Charlie Calendre
a200a8932b
feat: add french translations (#271)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2023-01-24 21:41:56 +01:00
Dirk Klimpel
41656748bb
Add dependencies @mui/icons-material to package (#242)
* Add dependencies `@mui/icons-material` to package

* Replace MUI icons v4 with v5

* Replace more `@material-ui/core` with `@mui/material`

* consolidate `Dialog`, `DialogContent`, `DialogContentText` and
`DialogTitle`

* update `alpha` `Container` and `useMediaQuery`
2023-01-24 21:27:02 +01:00
Dirk Klimpel
42a4decc2a
Remove not needed translate (#240) 2023-01-24 21:25:20 +01:00
Dirk Klimpel
74f77e6988
Replace ({ record }) with useRecordContext() (#236)
* replace `({ record })` with `useRecordContext()`

* code style

Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2023-01-24 16:36:08 +01:00
Dirk Klimpel
8501f19a03
Add admin API for destinations (#213)
* Add admin API for destinations

* Add rooms and connections to federation/destinations
2023-01-24 16:35:42 +01:00
Dirk Klimpel
6d30af9976
Update usage of useNotify hook (#234)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2023-01-24 15:28:01 +01:00
Dirk Klimpel
2e59190bd0
Migrate useMutation to useDelete in devices.js (#225)
* Migrate `useMutation` to `useDelete` in `devices.js`

* Update deprecated `notify` call
2023-01-24 15:24:14 +01:00
Charlie Calendre
a647c91f4f
fix: prevent requests from failing (#272)
Encode userid to handle the case of a localpart with a slash
2023-01-24 15:18:09 +01:00
dependabot[bot]
21ba5c9862
Bump actions/setup-node from 2 to 3 (#316)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 3.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 10:07:29 +01:00
dependabot[bot]
0a701df5d2
Bump json5 from 1.0.1 to 1.0.2 (#302)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 10:04:49 +01:00
dependabot[bot]
347d1c3114
Bump loader-utils from 2.0.2 to 2.0.4 (#297)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 10:02:12 +01:00
dependabot[bot]
8982487eee
Bump decode-uri-component from 0.2.0 to 0.2.2 (#299)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 09:58:44 +01:00
dependabot[bot]
7fa835f973
Bump @testing-library/jest-dom from 5.16.1 to 5.16.5 (#314)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.1 to 5.16.5.
- [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/v5.16.1...v5.16.5)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 09:53:32 +01:00
Dirk Klimpel
5767817733
Set dist for Travis CI (#300) 2023-01-24 09:51:19 +01:00
dependabot[bot]
e43f732bde
Bump docker/setup-buildx-action from 1 to 2 (#318)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 09:50:09 +01:00
dependabot[bot]
c0f4a92f1a
Bump softprops/action-gh-release from 0.1.5 to 0.1.15 (#317)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 0.1.5 to 0.1.15.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](b7e450da2a...de2c0eb89a)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 09:47:19 +01:00
dependabot[bot]
e70c680e8a
Bump eslint from 7.32.0 to 8.32.0 (#315)
Bumps [eslint](https://github.com/eslint/eslint) from 7.32.0 to 8.32.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.32.0...v8.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-24 09:35:54 +01:00
Dirk Klimpel
3b4da1d3c2
Update travis-ci URLs (#311) 2023-01-24 09:29:59 +01:00
Dirk Klimpel
3b2aa776eb
Fix broken CI / lint (#320) 2023-01-24 09:29:14 +01:00
Michael Albert
ee8050f697 Add args to docker-compose as suggested in #285
Change-Id: I67083d86ea45c15b02f8db54a1649dbb013c3d50
2023-01-16 20:27:22 +01:00
Sebastian Wagner
ca5fde9190
doc README: fix PUBLIC_URL in docker-compose example (#289)
PUBLIC_URL must not be enclosed in quotation marks, see Awesome-Technologies/synapse-admin#286
- add a note on this in the example
- fix the example itself

fixes Awesome-Technologies/synapse-admin#286
2023-01-16 20:20:02 +01:00
Przemysław Romanik
561daf7737
Add session logout warning below password input (#212)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2023-01-16 20:18:21 +01:00
dependabot[bot]
f2526dc00e
Bump @testing-library/user-event from 13.5.0 to 14.4.3 (#309)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 13.5.0 to 14.4.3.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v13.5.0...v14.4.3)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:13:59 +01:00
dependabot[bot]
3cce1a61d3
Bump eslint-plugin-prettier from 3.4.1 to 4.2.1 (#310)
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 3.4.1 to 4.2.1.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/commits/v4.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:13:41 +01:00
dependabot[bot]
af3b5439a5
Bump actions/checkout from 2 to 3 (#308)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:13:25 +01:00
dependabot[bot]
59afa49be0
Bump docker/login-action from 1 to 2 (#307)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:13:12 +01:00
dependabot[bot]
bfd38f50ea
Bump JamesIves/github-pages-deploy-action from 4.1.5 to 4.4.1 (#304)
Bumps [JamesIves/github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) from 4.1.5 to 4.4.1.
- [Release notes](https://github.com/JamesIves/github-pages-deploy-action/releases)
- [Commits](https://github.com/JamesIves/github-pages-deploy-action/compare/4.1.5...v4.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:12:59 +01:00
dependabot[bot]
37ed5c4156
Bump docker/setup-qemu-action from 1 to 2 (#306)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 20:06:09 +01:00
dependabot[bot]
2eb16eb747
Bump docker/build-push-action from 2 to 3 (#305)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 2 to 3.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 19:58:38 +01:00
Dirk Klimpel
bd6b6eef85
Use delete room API v2 (delete room in background) (#211)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2023-01-16 19:53:17 +01:00
Dirk Klimpel
43f1b82d33
Add dependabot.yml config (#292) 2023-01-16 19:52:25 +01:00
Michael Albert
24afbd2953 Fix build environment
Change-Id: I1e32a0eeaaa0e6b8c8d4845183e7d37320a4eeae
2023-01-16 13:42:16 +01:00
Dirk Klimpel
a74a6166a3
Add some status badges to README.md (#291) 2023-01-16 12:53:38 +01:00
Dirk Klimpel
e23e9ccd82
Create UserEditActions on user page (#222)
* Create `UserEditActions` on user page

* Fix crash with crashes `undefined`
2023-01-16 12:52:00 +01:00
Dirk Klimpel
e787b0a940
Add edit user_type to user (#209) 2023-01-16 12:37:30 +01:00
Dirk Klimpel
67066a1ba7
Bump react-scripts to ^5.0.1 (#280) 2022-09-07 09:20:25 +02:00
dklimpel
08a7d5c0f6 Bump GHA to NodeJS v16 2022-09-07 08:49:02 +02:00
dependabot[bot]
472b13ec26 Bump terser from 4.8.0 to 4.8.1
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 08:48:17 +02:00
dependabot[bot]
b901c9e0ff Bump eventsource from 1.1.0 to 1.1.1
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-07 08:48:04 +02:00
Dirk Klimpel
0b153ddcbb Update README.md
Co-authored-by: Dominik Fuchß <develop@fuchss.org>
2022-05-23 14:07:49 +02:00
Dirk Klimpel
784c284723 Add note to GH pages 2022-05-23 14:07:49 +02:00
dependabot[bot]
e5f73ea8b4 Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-23 14:07:01 +02:00
dependabot[bot]
96551259c5 Bump cross-fetch from 3.1.4 to 3.1.5
Bumps [cross-fetch](https://github.com/lquixada/cross-fetch) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/lquixada/cross-fetch/releases)
- [Commits](https://github.com/lquixada/cross-fetch/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: cross-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-23 14:06:50 +02:00
dependabot[bot]
b2fa533ef0 Bump async from 2.6.3 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-23 14:06:40 +02:00
Leon Schmidt
7aa0f9f50d
Make PUBLIC_URL and REACT_APP_SERVER configurable (#266)
* Adjust Dockerfile to include args in build process
* Adjust README.md
2022-05-23 14:06:04 +02:00
Dirk Klimpel
5aa90c25f7
Add path to rooms tab "members" (#232) 2022-03-22 19:11:12 +01:00
dependabot[bot]
38d58db08d
Bump url-parse from 1.5.7 to 1.5.10 (#253)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-22 18:17:18 +01:00
Dirk Klimpel
e637df232a
Upgrade react-admin to ^3.19.7 (#241) 2022-03-22 18:13:01 +01:00
Dirk Klimpel
d97c633cd0
Remove not needed {" "} from EventReports (#235) 2022-03-22 17:59:48 +01:00
Dirk Klimpel
b02396c61f
disableReordering in users' SimpleFormIterator (#219) 2022-03-22 17:58:28 +01:00
Michael Albert
95de50b925 Bump version to 0.8.5
Change-Id: I8c8f0af01d693bb76dbf706a92fdd4dfa6ba9a8b
2022-02-17 20:56:27 +01:00
dependabot[bot]
1a150a10fd
Bump url-parse from 1.5.3 to 1.5.7 (#244)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-17 20:53:56 +01:00
Dirk Klimpel
158e7dbe98
Move date format to a constant in EventReports.js (#237) 2022-02-17 20:52:13 +01:00
Dirk Klimpel
a642f11503
Fix typo notification in german language file (#233) 2022-02-17 20:51:42 +01:00
Dirk Klimpel
888a3f001b
Move date format to a constant in rooms.js (#238) 2022-02-17 20:51:11 +01:00
Dirk Klimpel
4b0845bee8
Move date format to a constant in users.js (#223)
* Move date format to a constant in `users.js`

* yarn prettier
2022-02-17 20:50:38 +01:00
Dirk Klimpel
f449e3277a
Replace deprecated fade in devices.js (#220) 2022-02-17 20:49:34 +01:00
Dirk Klimpel
3303f253b4
Replace deprecated fade in media.js (#221) 2022-02-17 20:49:19 +01:00
dependabot[bot]
0250954ee7
Bump follow-redirects from 1.14.7 to 1.14.8 (#243)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-17 20:45:37 +01:00
Dirk Klimpel
efed8b2774
Add token based registration creation and management (#200)
* Add token based registration creation and management

* yarn fix

* Apply suggestions from code review

Remove empty line

* move date to `const date_format`
2022-02-17 20:45:21 +01:00
Nya Candy
c891afa611
feat: support SSO login (#196)
* feat: support SSO login

* fix: lint

* fix: add back homeserver force protection

* fix: add back login notice

* fix: simplify login options
2022-02-17 20:24:46 +01:00
Michael Albert
38541b8f02 Fix english translation for room deletion
Change-Id: I5bb02e64832902a79379d66c77d3128169d3fca8
2022-02-17 20:15:49 +01:00
Dirk Klimpel
0f4c382c18
Remove not needed translation backtolist (#208) 2022-01-31 17:50:14 +01:00
Dirk Klimpel
b90d4ef00f
Add more headlines for installation to README (#216) 2022-01-31 17:39:18 +01:00
Dominik Fuchß
3fb33facc5
GitHub Pages Deploy (#189)
* Create edge_ghpage.yml

Added Build and Deploy Edge version to GH Pages

* Update edge_ghpage.yml

Added missing node setup

* Update edge_ghpage.yml

Restrict building of GH Pages to main / master branch & workflow dispatch

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

* Update .github/workflows/edge_ghpage.yml

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>

Co-authored-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>
2022-01-31 17:38:34 +01:00
Dirk Klimpel
c4a68ff1d5
Fix typo in .prettierrc (#224) 2022-01-31 17:24:37 +01:00
dependabot[bot]
c4f0fa48ec
Bump nanoid from 3.1.30 to 3.2.0 (#229)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 17:20:39 +01:00
dependabot[bot]
26ed63d65e
Bump follow-redirects from 1.14.6 to 1.14.7 (#227)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-31 17:20:29 +01:00
Michael Albert
b9e81b2278 Bump version and update dependencies
Change-Id: I0c0349b4429ce06bea51453c092f0a11156aaa05
2021-12-17 20:38:18 +01:00
sakkiii
f6f437b17a
version tag on docker hub (#187)
* version tag on docker hub

* resolve name collision

* master branch added for latest tag

* prettier-ignore fix
2021-12-08 22:02:43 +01:00
Dirk Klimpel
91af8f1c04
Add sorting users by creation timestamp (#174)
* Add `creation_ts` to list users

* remove filter

* Bring back origin columns sort order
2021-12-08 21:59:09 +01:00
Aaron R
abc9d5154e
Switch Dockerfile to use current LTS version of Node (#205)
Node 17 current fails due to https://github.com/webpack/webpack/issues/14532. It probably makes sense to use the current LTS version of Node instead of the absolute latest version of Node so these kinds of bleeding edge issues are less likely to happen.
2021-11-15 21:35:23 +01:00
dependabot[bot]
8228d7d2c2
Bump tar from 6.1.8 to 6.1.11 (#207)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.8 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.8...v6.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:18:58 +01:00
Dirk Klimpel
4adc20f80d
replace undoable prop with mutationMode prop (#202) 2021-11-15 21:18:29 +01:00
Dirk Klimpel
a5c7d7dd22
Make items in "Room directory" are clickable (#199) 2021-11-15 21:15:11 +01:00
dependabot[bot]
dc5c2c1d68
Bump tmpl from 1.0.4 to 1.0.5 (#193)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 21:11:49 +01:00
Dirk Klimpel
42b3252353
Add pristine to UserEdit and ServerNotice (#185) 2021-11-15 21:02:47 +01:00
Dirk Klimpel
1a17d3e69b
Automatically set the homeserver for a new user (#184)
and enhance form validation
2021-11-15 20:57:38 +01:00
Dirk Klimpel
79ef38ee6b
Enable modify user external_ids (#179)
* Enable modify user `external_ids`

* add input validation
2021-11-15 20:40:05 +01:00
Manuel Stahl
0ff4b30d71 Remove update repo stop from docker release action
See https://github.com/peter-evans/dockerhub-description/issues/10

Change-Id: I42d2d4e1a28117be1419591f7d43653591182f0c
2021-08-26 10:43:11 +02:00
Manuel Stahl
6c4ff6c791 Add write permissions to github release action
Change-Id: Ie7db1e7410bbc1c0fccbc2d00119363629e10f22
2021-08-26 10:40:11 +02:00
Manuel Stahl
9c36ed6566 Increment version
Change-Id: I48fb812632dc2e498ad267477809a16fc882cf4c
2021-08-25 10:42:34 +02:00
dklimpel
8536f552d4 Add button to quarantine media (#180)
Change-Id: I6496826fdf75ab8b7b3ed5a9056abf86a50caea3
2021-08-25 10:42:34 +02:00
Manuel Stahl
aaf782d24f Use .gitignore for prettier as well
Change-Id: Ibfe73812a5375cc251b859b8aa441583c0cdcd41
2021-08-25 10:42:34 +02:00
Manuel Stahl
2886203594 Add github action that builds docker images and pushes them to docker hub
Change-Id: I60d39cc266c2e8b905e2ac4a367bd5a22b79b57b
2021-08-25 10:42:34 +02:00
csett86
865fc98336 Add github action that packages a release tarball (#148)
Change-Id: I368a834a27f69550596a041c1e6b84afd40011b7
2021-08-24 09:58:35 +02:00
Dirk Klimpel
e6f01f035b
Add buttons to protect and unprotect users' media from quarantine (#150) 2021-08-19 11:51:07 +02:00
Manuel Stahl
32e088ac5a yarn: Upgrade packages
- babel 7.15
- eslint 7.32
- react-admin 3.17

Change-Id: Ief4ad5870ae721fb432e19686607376b83eff12a
2021-08-18 09:48:41 +02:00
Dirk Klimpel
bf3d13916f
Add SSO external_ids to user (#168) 2021-08-18 09:33:22 +02:00
Manuel Stahl
07b1df5855
Add Github action badge for build checks 2021-08-18 09:27:25 +02:00
Dirk Klimpel
634341ea07
Add GitHub Action to run CI tests (#162) 2021-08-18 09:18:09 +02:00
Dirk Klimpel
361643c3da
Fix link in README.md (#175) 2021-08-17 08:22:36 +02:00
Dirk Klimpel
5262518699
Update links to new Synapse documentation (#164) 2021-07-06 09:57:44 +02:00
Dirk Klimpel
78a282863a
Fix broken CI with language files (#165) 2021-07-06 09:56:09 +02:00
Michael Albert
ff0201273a Bump version and update packages
Change-Id: If148587c9e5895ab6b8a70ec34c9190f0bb8f2e0
2021-07-05 14:48:25 +02:00
Dirk Klimpel
e50c95b4be
Fix CSV import button (#154) 2021-07-05 14:32:51 +02:00
Dirk Klimpel
9f16e5c6ba
Change delete room API to DELETE (#151) 2021-07-05 14:32:44 +02:00
dependabot[bot]
509a45cba4
Bump dns-packet from 1.3.1 to 1.3.4 (#145)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 11:03:47 +02:00
dependabot[bot]
5c6e5a9641
Bump ws from 6.2.1 to 6.2.2 (#152)
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 11:03:31 +02:00
Dirk Klimpel
e3d5d51342
Fix broken RoomDirectoryFilter (#155) 2021-06-15 11:03:03 +02:00
Michael Albert
985673b161 Increment version
Change-Id: I149896f55be7840b240d92fed5880e3f5624b857
2021-05-25 15:03:15 +02:00
John Francis Sukamto
d72357f64f
Update en.js (#144)
Suggested a UI name change for media size (assuming length is size in bytes)
2021-05-25 15:01:13 +02:00
Dirk Klimpel
e19c34324b
Allow fixed homeserver (#142) 2021-05-18 12:39:53 +02:00
Dirk Klimpel
3ea1f51eb5
Add a new tab to rooms with forward extremities (#107)
Add a new tab to rooms with forward extremities.
2021-05-08 19:10:51 +02:00
Manuel Stahl
229518e456 Show room alias or room id in room list if room has no name
Change-Id: Iad769f31347566ccf0b8a978b31f5123553e9dbc
2021-05-05 20:24:15 +02:00
Dirk Klimpel
5a5a7143af
Enable sorting of user list (#133)
New in Synapse 1.32.0
Fixes: #132, #136
2021-05-05 19:36:47 +02:00
Manuel Stahl
dda8ba5e85 Update nodejs version for travis
Change-Id: I7d44f5df7d4479efcb1d44f5ba23467effad147e
2021-05-05 19:31:50 +02:00
Manuel Stahl
5208198b76 Replace enzyme with testing-library/react
Enzyme is not compatible with react 17.

Change-Id: If9bca2c482bfe10a18d2ee2bc213dab966849b5b
2021-05-05 19:23:01 +02:00
Manuel Stahl
c8082a7198 Remove TestContext from App.test.js
The TestContext is only required for components that depend on react-admin,
but not for the Admin component itself.

Change-Id: I3e07cb6bfa592f1bf59ca282cdf1c2e6c922f619
2021-05-05 19:23:01 +02:00
Michael Albert
10831796e3 Add missing translation
Change-Id: Iab3203742498d2c6768b5885c0522ff8365b58f2
2021-05-05 09:41:05 +00:00
Michael Albert
5ee5288edf Fix some DOM errors
Change-Id: I22a108fd5ce6a344e629e4af0345a0221de44052
2021-05-05 09:40:48 +00:00
Manuel Stahl
a5528d9fe7 yarn: Upgrade packages
- eslint 7.25
- ra-language-german 3.13
- react-admin 3.15
- react-dom 17.0
- react-scripts 4.0

Change-Id: Iad982cf647470bc16194000519a72c401009c9fa
2021-05-04 18:42:05 +02:00
Manuel Stahl
e2fd934851 Allow base URL with path
Ignore and remove trailing slashes.
Fixes #134.

Change-Id: Iedf266e9a93e6939f7f66707fee59a2b56226216
2021-05-04 18:41:54 +02:00
Manuel Stahl
0bc1ce3226 Reuse device_id for synapse-admin on login
Change-Id: I47bbfd1e33ef8bffb618101ae233aeb093cf0ada
2021-05-04 17:10:49 +02:00
Manuel Stahl
41ce58bac8 Enable sorting in tab of users' media (#138) 2021-05-04 16:18:12 +02:00
Manuel Stahl
57c41cc069 Increment version 2021-05-04 15:06:41 +02:00
Nya Candy
0e3375c5ad Add zh-cn support (#131) 2021-05-04 15:06:41 +02:00
Dirk Klimpel
2cdd41b615 Add a new tab to rooms with state events (#108)
Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2021-05-04 14:42:27 +02:00
Dirk Klimpel
2ab4343970
Add the lists of public rooms on the server (#105)
* Add room directory and the switches to rooms settings

* Fix react admin version
2021-05-04 13:52:43 +02:00
Dirk Klimpel
0268cc0e94
Add joined_local_devices to rooms (#96) 2021-05-04 13:49:20 +02:00
Lukas Wolfsteiner
f8331a459d Add notes about the docker-compose usage
Change-Id: I73c8467ab58caf2082740e682c9175f3eb494a14
2021-04-21 10:51:19 +02:00
Lukas Wolfsteiner
a12cf95457 Add docker-compose.yml 2021-04-21 10:51:17 +02:00
dklimpel
60854bcc60 Add button to delete media by size and date 2021-04-21 10:51:14 +02:00
Manuel Stahl
62b3d094b7 yarn: Upgrade packages
- babel: 7.13
- material-ui: 4.11
- prettier: 2.2
- react-admin: 3.14

Change-Id: I26ab6d9d75110f3522282b83cd1131af98b8c43c
2021-04-21 10:50:49 +02:00
dependabot[bot]
81231b5ea6 Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-21 09:35:59 +02:00
dependabot[bot]
c114e58278
Bump ssri from 6.0.1 to 6.0.2
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-19 18:23:21 +00:00
Dirk Klimpel
c6b6e54617
Fix broken redirect to login page (#116) 2021-03-16 12:13:32 +01:00
Michael Albert
8ff0ac913c Add missing translations
Change-Id: Ie46554bcd10dde771c03e7d3fd0c3639f904429d
2021-03-16 12:12:54 +01:00
dependabot[bot]
96d2c96740
Bump elliptic from 6.5.3 to 6.5.4 (#122)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-11 13:29:43 +01:00
Aaron Raimist
8adab0e927
Name device used by Synapse Admin and hook up logout button (#113)
* Name device used by Synapse Admin

* Actually logout when the logout button is pressed so that old sessions get deleted

* Fix lint
2021-03-02 11:45:05 +01:00
Dirk Klimpel
536ffc2fbf
Disable telemetry which was introduced with react-admin v3.11.0 (#109) 2021-03-01 09:26:58 +01:00
Michael Albert
684c44e470 Bump version to 0.7.0
Change-Id: I5b854ae8720c12e0895c2c614f4c02f1257c0d09
2021-02-11 21:26:23 +01:00
Dirk Klimpel
7f92e1a3c0
Add new list with information about users' media (#89) 2021-02-11 21:14:02 +01:00
Dirk Klimpel
ea59d0dd02
Add a new tab to user page with media (#87)
* Add a new tab to user page with media
2021-02-11 21:11:34 +01:00
Dirk Klimpel
706114a382
Add a new tab to user page with room memberships (#73)
The admin API for user's room memberships is new in synapse v1.21.0.

Co-authored-by: Michael Albert <37796947+awesome-michael@users.noreply.github.com>
2021-02-11 20:45:02 +01:00
Dirk Klimpel
f2a1275673
Add a new tab to user page with pushers (#85)
* Add a new tab to user page with pushers

* Update pushers `total` and `id`
2021-02-11 20:37:20 +01:00
Dirk Klimpel
425c210cfc
Add delete button to room detail page (#97)
* Add delete button to room detail page

* Add confirmation dialog for room deletion
2021-02-11 20:24:17 +01:00
Dirk Klimpel
b184954ffa
Update total in dataProvider (#101) 2021-02-11 20:20:42 +01:00
Dirk Klimpel
2f96951c19
Add view of reported events (#84) 2021-01-04 09:14:33 +01:00
dependabot[bot]
1706cd3c9d Bump ini from 1.3.5 to 1.3.7
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-14 13:13:16 +01:00
Dirk Klimpel
eadc04a6a0
Update helper text for activate an user (#95) 2020-12-14 13:12:13 +01:00
Manuel Stahl
1432724a64 yarn: Upgrade packages
- react-admin: 3.10.0
 - react-dom: 16.14.0
 - react-scripts: 3.4.4

Change-Id: I805bcd2f65419747f7e8c5a3e2c6b9896010ca9e
2020-11-16 13:31:39 +01:00
Manuel Stahl
c841720f0c Fix style
Change-Id: I345a942905f15edfa21f51de228efd7671f080c7
2020-11-16 10:55:00 +01:00
Dirk Klimpel
60ecafdf54
Bugfix translation in users' device tab (#86) 2020-11-16 10:54:43 +01:00
Dirk Klimpel
5de5015655
Add SearchInput to RoomFilter (#83) 2020-11-12 14:56:21 +01:00
dklimpel
2bf8a6debb Switch to relative URL for example.csv 2020-11-12 14:48:11 +01:00
dklimpel
c8d9a6db55 Bugfix URL for import users from CSV 2020-11-12 14:48:11 +01:00
Michael Albert
e5d2d895d9 Import users from CSV
Change-Id: I3565dae7531b7de1d68ebcb2392be931083dda9e
2020-11-02 18:29:56 +00:00
Dirk Klimpel
3d4f45d070 Bugfix delete room (#72)
Fixes #70
Bugfix delete a room in room list with `deleteMany`.
2020-10-28 09:25:22 +01:00
Dirk Klimpel
cfe4f4a995 Add more information and requirements to README.md (#79) 2020-10-28 09:25:17 +01:00
Michael Albert
04de65ce51 Add missing translation
Change-Id: I8f827f5e0c3bf6f32f3578a41c73462e70a2cc95
2020-10-13 11:28:20 +00:00
Michael Albert
3d7c0bf67f Add search for users
Change-Id: I4bb4b33819b66846645e2e7a3d38d3e837a5bcd8
2020-10-08 08:07:55 +02:00
Michael Albert
4324ebfdb8 Add confirmation when deleting rooms
Change-Id: I5dce8ebfb7cab988e0e6664c704c04b44a4d2f92
2020-10-07 21:42:19 +02:00
Dirk Klimpel
26b8cea6a5
Add button to purge rooms to room list (#44)
* Add button to purge rooms to room list

Add button (BulkDeleteButton) to delete rooms with no local users:
- purge rooms `POST /_synapse/admin/v1/purge_room`
(https://github.com/matrix-org/synapse/blob/master/docs/admin_api/purge_room.md)

* Bugfix of merge 'master'

* Change from purge room to delete room endpoint
2020-10-07 20:50:10 +02:00
Manuel Stahl
8db881a64d Use current node image as builder
Change-Id: I2bccf5bcd21e37625477695ee927d49dd4333ac4
2020-08-25 17:19:17 +02:00
Manuel Stahl
c09e59d47a yarn: Upgrade packages
- babel: 7.11

Change-Id: I6eebc1afbf54b6ce72e1b534f240e3e0aeb1c30a
2020-08-25 17:19:17 +02:00
Dirk Klimpel
8bdf76f27e
Bugfix plural in user tab (#62) 2020-08-11 10:25:05 +02:00
Michael Albert
7c9a87bc86 Show room members in room detail view
Change-Id: I7d82f728de0e503d4834f6815bde000c7a0204a3
2020-08-07 21:29:58 +02:00
Manuel Stahl
1f56bac356 Add basic tests for dataProvider
Change-Id: Ib399cbb4e927ab18f714371e07606df83170df52
2020-07-30 13:52:17 +02:00
Manuel Stahl
bbbca0c57c Add git context to docker, so we can derive the release version
"git describe --tags" requires the git context.

Change-Id: I2bc5dde056c2ac480513004fb99336397355af30
2020-07-29 17:49:30 +02:00
Manuel Stahl
314906657f Add support to remove user devices (#57)
Change-Id: I19176daa656b9280ccd00f1ca0095e72870ca21e
2020-07-29 17:49:30 +02:00
Manuel Stahl
78e7c5f391 Fix translation of user devices
Fixes #58.

Change-Id: Ic2f91917310fd1ba59636d06c81c338ca9dd297e
2020-07-23 09:24:18 +02:00
Michael Albert
1074178e31 Rename Admin -> Server Administrator
Change-Id: Ic11539252af553dbb7cca37996f2402669a9c0e4
2020-07-21 11:52:13 +02:00
Manuel Stahl
352ab1290a Add devices tab to UserEdit component
Allows to view user devices.
API was added by synapse v1.15.0.

Change-Id: Id0693bf6cd6f6182c657412cf8036537e2db9df7
2020-07-13 10:32:13 +02:00
Manuel Stahl
12447b7708 Increase export of users up to 10000
Change-Id: I54c7a52ae35aeb311074f0f3b103a2fdb92aaedd
2020-07-13 10:20:24 +02:00
Manuel Stahl
cd4efb7c07 yarn: Upgrade packages
- react-admin:  3.7.0

Change-Id: I01e4a2244a6ca0d6fc231fe83a94f73d48618824
2020-07-13 10:20:03 +02:00
Michael Albert
ff59ee4c2e Hide some room list information by default
Change-Id: Ic6fbf0d941d2ffcc87fb5b7793517b96792ce16d
2020-07-08 11:19:09 +02:00
Manuel Stahl
61938405e9 Add room detail view
Needs Synapse v1.14.0 or later

Change-Id: I6e3956a1e02fad5ba2f847458cd184af6aaedef0
2020-07-08 10:46:49 +02:00
Manuel Stahl
ac0657c428 Add hint about required synapse version
Change-Id: I8152681d695ded78d0f9b9d82bc038aa67723fbc
2020-07-08 09:55:52 +02:00
Michael Albert
ab709aee3e Add screenshots and install instructions
Change-Id: Ibfede12d700924b7b3b01af5f3a3624f21a93862
2020-07-08 07:49:42 +00:00
dklimpel
6da3c8b885 Bugfix translation of plural in UserTitle 2020-07-08 08:50:00 +02:00
Manuel Stahl
ab04db5baf Get avatar_url and displayname from v2/users API
API was added by synapse v1.13.0.

Change-Id: I927b81882fa20e5b3de3d9fc216e2136f7036bba
2020-07-07 18:51:35 +02:00
Dirk Klimpel
8282a3caf8
Move threepids in UserEdit to a separate tab (#51)
Separates information into individual tabs for a better overview.
2020-07-06 12:35:26 +02:00
Michael Albert
2fc75cd6fc
Merge pull request #43 from dklimpel/extend_room_list
Extend the room list with further attributes
2020-07-03 19:32:00 +02:00
Dirk Klimpel
3fd615943c
Shows encrypted status with icons 2020-07-01 22:36:15 +02:00
dklimpel
aaf1ebb909 Change field creation_ts * 1000 to creation_ts_ms 2020-06-16 10:15:22 +02:00
dklimpel
627f3d2917 Add creation timestamp and consent version to UserEdit
Add information about the user to UserEdit
- creation timestamp
- consent version
2020-06-16 10:15:22 +02:00
dklimpel
168e249296 Bugfix removes the ability to click on individual connections.
If you click on a connection in UserEdit, you will get an empty page.
This solves the problem.
2020-06-16 09:45:43 +02:00
dklimpel
5bdfb80db7 Bugfix sort users by user_id
Users are not sortable by `user_id`.
Set `sortable={false}`.
2020-06-10 13:11:35 +02:00
dependabot[bot]
b7c3684b80
Bump websocket-extensions from 0.1.3 to 0.1.4 (#49)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-09 14:50:58 +02:00
Manuel Stahl
0ada5287d7 Show Synapse server version on login page
Change-Id: Id9b0d2adf83590524069d308f4fe9e5d14147295
2020-05-25 13:29:15 +02:00
dklimpel
3884c50012 Extend the room list with further attributes
Add further attributes:
- is_encrypted
- federatable
- public
- state_events
- version
- joined_local_members

Also add the ability to sort.

API was added by synapse v1.13.0.
2020-05-23 17:43:33 +02:00
Michael Albert
300e22a537 Show version of Synapse-Admin
Change-Id: I354e3f3b3e4f45e5ca72318ce70b66bee433f3d6
2020-05-15 13:28:51 +02:00
Dirk Klimpel
009ce803e2
Add ServerNoticeButton to UserBulkActionButtons (#41)
This adds the button to send "Server Notices" to many users at once.
2020-05-06 09:03:33 +02:00
Manuel Stahl
c41b8ab846 Add ServerNoticeButton to UserEditToolbar
For this, the feature "Server Notices" must be activated on the server.

Change-Id: If3873dc5548822a06a7be0c55e48835c9fb8f78f
2020-05-05 13:34:16 +02:00
Dirk Klimpel
7f16f784f9
Allow port in homeserver URL (#40)
Allow to use homeserver URL with port (e.g. ':443').
2020-05-04 21:28:04 +02:00
Michael Albert
1002b6464a Build with relative paths
Change-Id: I50e47cf4d5a68fa84972c1f205eb066d5df3f9db
2020-05-04 18:28:06 +02:00
Michael Albert
50b770a312 Extract homeserver URL from fully qualified user id
Also lookup the .well-known entry and use it if available.

Change-Id: I609046f01860fd5e3ba8cb801006e6098a4ad840
2020-05-02 16:24:53 +02:00
Manuel Stahl
2d0ce50444 Save base_url from login input
Change-Id: I58447145dfc2df4ab3544b6a165721f900e29b24
2020-05-02 16:22:53 +02:00
Manuel Stahl
1fb89c9e58 Add missing german translations
Change-Id: I297a730f73a4a4aa47a4ce679bd13ef0af69cc38
2020-05-02 16:22:53 +02:00
Michael Albert
8a4c0fe0fe Use input components for LoginPage
Change-Id: Icaaa579eaeaaafe183fb027e4d3bf206f8f5516a
2020-05-02 16:22:53 +02:00
Michael Albert
dd022eab04 Validate URL on input instead of automatic rewrite of http to https
Change-Id: I3f3a9c5fb408af1f03ef876456133b331dc4cea3
2020-05-02 16:22:40 +02:00
Manuel Stahl
437fd70d6d Make creating users a special case in dataProvider
Since users are created with PUT instead of POST, this is actually a
special case.

Change-Id: Ibe430fcac4d81de9723abd650804ffa93f87bf6d
2020-04-23 16:57:24 +02:00
Manuel Stahl
1e6e526e3c Show displayname as title for user
Change-Id: I0ba8e2265e5b8e1fe392f56052e96e0243cd3eb6
2020-04-23 16:50:58 +02:00
Manuel Stahl
d812cff5fc Rename homeserver_url to endpoint_url in dataProvider
Change-Id: I86441cd90e9b9b6b04ba2d28fee12a1cfa0684a7
2020-04-23 14:29:13 +02:00
Manuel Stahl
a39033e25b yarn: Upgrade packages
Change-Id: Id01070e1e4ca2c2dd42c8ffcd278a57cb31b7b51
2020-04-23 14:29:00 +02:00
Manuel Stahl
b7f009e559 Add buttons to erase users (#32)
Change-Id: I9b5644394d213dc66b30e39f19e9e392b69a0be3
2020-04-09 11:37:02 +02:00
57 changed files with 14688 additions and 9627 deletions

View File

@ -1,5 +1,4 @@
# Exclude a bunch of stuff which can make the build context a larger than it needs to be # Exclude a bunch of stuff which can make the build context a larger than it needs to be
.git/
tests/ tests/
build/ build/
lib/ lib/

5
.env Normal file
View File

@ -0,0 +1,5 @@
# This setting allows to fix the homeserver.
# If you set this setting, the user will not be able to select
# the server and have to use synapse-admin with this server.
#REACT_APP_SERVER=https://yourmatrixserver.example.com

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

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

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

@ -0,0 +1,21 @@
name: build-test
on:
push:
branches: ["master"]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Install dependencies
run: yarn --immutable
- name: Run tests
run: yarn test

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

@ -0,0 +1,63 @@
name: Create docker image(s) and push to docker hub and ghcr.io
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
on:
push:
# Sequence of patterns matched against refs/heads
# prettier-ignore
branches:
# Push events on master branch
- master
# Sequence of patterns matched against refs/tags
tags:
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
jobs:
docker:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-tags: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
awesometechnologies/synapse-admin
ghcr.io/${{ github.repository }}
- name: Build and Push Tag
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64

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

@ -0,0 +1,28 @@
name: Build and Deploy Edge version to GH Pages
on:
workflow_dispatch:
push:
branches:
- main
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: Install and Build 🔧
run: |
yarn install --immutable
yarn build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
branch: gh-pages
folder: build

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

@ -0,0 +1,33 @@
name: Create release tarball and attach to tag
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4
with:
node-version: "18"
- run: yarn install --immutable
- run: yarn build
- run: |
version=`git describe --dirty --tags || echo unknown`
mkdir -p dist
cp -r build synapse-admin-$version
tar chvzf dist/synapse-admin-$version.tar.gz synapse-admin-$version
- uses: softprops/action-gh-release@3198ee18f814cdf787321b4a32a26ddbf37acc52
with:
files: dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

51
.github/workflows/test-docker-image.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Test docker image creation
on:
push:
# Sequence of patterns matched against refs/heads
# prettier-ignore
branches:
# Push events on branch fix_docker_cd
- fix_docker_cd
# Sequence of patterns matched against refs/tags
tags:
- '[0-9]+\.[0-9]+\.[0-9]+' # Push events to 0.X.X tag
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Calculate docker image tag
id: set-tag
run: |
case "${GITHUB_REF}" in
refs/heads/master|refs/heads/main)
tag=latest
;;
refs/tags/*)
tag=${GITHUB_REF#refs/tags/}
;;
*)
tag=${GITHUB_SHA}
;;
esac
echo "::set-output name=tag::$tag"
- name: Build and Push Tag
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
platforms: linux/amd64,linux/arm64

View File

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

View File

@ -1,5 +0,0 @@
language: node_js
node_js:
- 13
cache: yarn

View File

@ -1,11 +1,13 @@
# Builder # Builder
FROM node:10-alpine as builder FROM node:lts as builder
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
ARG REACT_APP_SERVER
WORKDIR /src WORKDIR /src
COPY . /src COPY . /src
RUN yarn --network-timeout=100000 install RUN yarn --network-timeout=300000 install --immutable
RUN yarn build RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
# App # App

103
README.md
View File

@ -1,9 +1,106 @@
[![Build Status](https://travis-ci.org/Awesome-Technologies/synapse-admin.svg?branch=master)](https://travis-ci.org/Awesome-Technologies/synapse-admin) [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)
# Synapse admin ui # Synapse admin ui
This project is built using [react-admin](https://marmelab.com/react-admin/). This project is built using [react-admin](https://marmelab.com/react-admin/).
Use `yarn install` after cloning this repo. ## Usage
Use `yarn start` to launch the webserver. ### Supported Synapse
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.52.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`
You can fix the homeserver, so that the user can no longer define it himself.
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
or by editing it in the [.env](.env) file. See also the
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
#### Steps for 3)
- 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
version: "3"
services:
synapse-admin:
container_name: synapse-admin
hostname: synapse-admin
build:
context: https://github.com/Awesome-Technologies/synapse-admin.git
# args:
# - NODE_OPTIONS="--max_old_space_size=1024"
# # see #266, PUBLIC_URL must be without surrounding quotation marks
# - PUBLIC_URL=/synapse-admin
# - REACT_APP_SERVER="https://matrix.example.com"
ports:
- "8080:80"
restart: unless-stopped
```
- browse to http://localhost:8080
## Screenshots
![Screenshots](./screenshots.jpg)
## Development
- Use `yarn test` to run all style, lint and unit tests
- Use `yarn fix` to fix the coding style

26
docker-compose.yml Normal file
View File

@ -0,0 +1,26 @@
version: "3"
services:
synapse-admin:
container_name: synapse-admin
hostname: synapse-admin
image: awesometechnologies/synapse-admin:latest
# build:
# context: .
# to use the docker-compose as standalone without a local repo clone,
# replace the context definition with this:
# context: https://github.com/Awesome-Technologies/synapse-admin.git
# args:
# if you're building on an architecture other than amd64, make sure
# to define a maximum ram for node. otherwise the build will fail.
# - NODE_OPTIONS="--max_old_space_size=1024"
# default is .
# - PUBLIC_URL=/synapse-admin
# You can use a fixed homeserver, so that the user can no longer
# define it himself
# - REACT_APP_SERVER="https://matrix.example.com"
ports:
- "8080:80"
restart: unless-stopped

View File

@ -1,40 +1,48 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.1.0", "version": "0.9.2",
"description": "Admin GUI for the Matrix.org server Synapse", "description": "Admin GUI for the Matrix.org server Synapse",
"author": "Awesome Technologies Innovationslabor GmbH", "author": "Awesome Technologies Innovationslabor GmbH",
"license": "Apache-2.0", "license": "Apache-2.0",
"homepage": ".",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Awesome-Technologies/synapse-admin" "url": "https://github.com/Awesome-Technologies/synapse-admin"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.1.1", "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^10.0.2", "@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^10.0.1", "@testing-library/user-event": "^14.5.2",
"enzyme": "^3.11.0", "eslint": "^8.57.0",
"enzyme-adapter-react-16": "^1.15.2", "eslint-config-prettier": "^9.1.0",
"eslint": "^6.8.0", "eslint-config-react-app": "^7.0.1",
"eslint-config-prettier": "^6.10.1", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-prettier": "^3.1.2", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.0.0" "prettier": "^3.2.5"
}, },
"dependencies": { "dependencies": {
"prop-types": "^15.7.2", "@mui/icons-material": "^5.15.15",
"ra-language-german": "^2.1.2", "@mui/material": "^5.15.15",
"react": "^16.13.1", "@mui/styles": "^5.15.15",
"react-admin": "^3.4.0", "papaparse": "^5.4.1",
"react-dom": "^16.13.1", "ra-language-chinese": "^2.0.10",
"react-scripts": "^3.4.1", "ra-language-french": "^4.16.15",
"react-admin-import-csv": "^0.2.5" "ra-language-german": "^3.13.4",
"ra-language-italian": "^3.13.1",
"ra-language-farsi": "^4.2.0",
"ra-language-russian": "^4.14.2",
"react": "^18.0.0",
"react-admin": "^4.16.15",
"react-dom": "^18.0.0",
"react-scripts": "^5.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "REACT_APP_VERSION=$(git describe --tags) react-scripts start",
"build": "react-scripts build", "build": "REACT_APP_VERSION=$(git describe --tags) react-scripts build",
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
"fix:code": "yarn test:lint --fix", "fix:code": "yarn test:lint --fix",
"fix": "yarn fix:code && yarn fix:other", "fix": "yarn fix:code && yarn fix:other",
"prettier": "prettier \"**/*.{js,jsx,json,md,scss,yaml,yml}\"", "prettier": "prettier --ignore-path .gitignore \"**/*.{js,jsx,json,md,scss,yaml,yml}\"",
"test:code": "react-scripts test", "test:code": "react-scripts test",
"test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .", "test:lint": "eslint --ignore-path .gitignore --ext .js,.jsx .",
"test:style": "yarn prettier --list-different", "test:style": "yarn prettier --list-different",

3
public/data/example.csv Normal file
View File

@ -0,0 +1,3 @@
id,displayname,password,is_guest,admin,deactivated
testuser22,Jane Doe,secretpassword,false,true,false
,John Doe,,false,false,false
1 id displayname password is_guest admin deactivated
2 testuser22 Jane Doe secretpassword false true false
3 John Doe false false false

View File

@ -38,5 +38,12 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<footer
style="position: relative; z-index: 2; height: 2em; margin-top: -2em; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
<a id="copyright" href="https://github.com/Awesome-Technologies/synapse-admin"
style="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>(%REACT_APP_VERSION%)</b> by Awesome Technologies Innovationslabor GmbH
</a>
</footer>
</body> </body>
</html> </html>

View File

@ -1,2 +1,3 @@
# https://www.robotstxt.org/robotstxt.html # https://www.robotstxt.org/robotstxt.html
User-agent: * User-agent: *
Disallow: /

BIN
screenshots.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@ -1,43 +0,0 @@
import React from "react";
import { Admin, Resource, resolveBrowserLocale } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import { UserList, UserCreate, UserEdit } from "./components/users";
import { RoomList } from "./components/rooms";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
de: germanMessages,
en: englishMessages,
};
const i18nProvider = polyglotI18nProvider(
locale => (messages[locale] ? messages[locale] : messages.en),
resolveBrowserLocale()
);
const App = () => (
<Admin
loginPage={LoginPage}
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<Resource
name="users"
list={UserList}
create={UserCreate}
edit={UserEdit}
icon={UserIcon}
/>
<Resource name="rooms" list={RoomList} icon={RoomIcon} />
<Resource name="connections" />
</Admin>
);
export default App;

72
src/App.jsx Normal file
View File

@ -0,0 +1,72 @@
import React from "react";
import {
Admin,
CustomRoutes,
Resource,
resolveBrowserLocale,
} from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import users from "./components/users";
import rooms from "./components/rooms";
import userMediaStats from "./components/statistics";
import reports from "./components/EventReports";
import roomDirectory from "./components/RoomDirectory";
import destinations from "./components/destinations";
import LoginPage from "./components/LoginPage";
import { ImportFeature } from "./components/ImportFeature";
import { Route } from "react-router-dom";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
import chineseMessages from "./i18n/zh";
import italianMessages from "./i18n/it";
import russianMessages from "./i18n/ru";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
de: germanMessages,
en: englishMessages,
fr: frenchMessages,
it: italianMessages,
zh: chineseMessages,
ru: russianMessages
};
const i18nProvider = polyglotI18nProvider(
locale => (messages[locale] ? messages[locale] : messages.en),
resolveBrowserLocale()
);
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 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>
);
export default App;

View File

@ -1,14 +0,0 @@
import React from "react";
import { TestContext } from "react-admin";
import { shallow } from "enzyme";
import App from "./App";
describe("App", () => {
it("renders", () => {
shallow(
<TestContext>
<App />
</TestContext>
);
});
});

10
src/App.test.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";
describe("App", () => {
it("renders", async () => {
render(<App />);
await screen.findAllByText("Welcome to Synapse-admin");
});
});

View File

@ -0,0 +1,22 @@
import React from "react";
import get from "lodash/get";
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 { alt, classes, sizes, sx, variant } = rest;
return (
<Avatar
alt={alt}
classes={classes}
sizes={sizes}
src={src}
sx={sx}
variant={variant}
/>
);
};
export default AvatarField;

View File

@ -0,0 +1,18 @@
import React from "react";
import { RecordContextProvider } from "react-admin";
import { render, screen } from "@testing-library/react";
import AvatarField from "./AvatarField";
describe("AvatarField", () => {
it("shows image", () => {
const value = {
avatar: "foo",
};
render(
<RecordContextProvider value={value}>
<AvatarField source="avatar" />
</RecordContextProvider>
);
expect(screen.getByRole("img").getAttribute("src")).toBe("foo");
});
});

View File

@ -0,0 +1,148 @@
import React from "react";
import {
Datagrid,
DateField,
DeleteButton,
List,
NumberField,
Pagination,
ReferenceField,
Show,
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useTranslate,
} from "react-admin";
import PageviewIcon from "@mui/icons-material/Pageview";
import ReportIcon from "@mui/icons-material/Warning";
import ViewListIcon from "@mui/icons-material/ViewList";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const ReportPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
export const ReportShow = props => {
const translate = useTranslate();
return (
<Show {...props} actions={<ReportShowActions />}>
<TabbedShowLayout>
<Tab
label={translate("synapseadmin.reports.tabs.basic", {
smart_count: 1,
})}
icon={<ViewListIcon />}
>
<DateField
source="received_ts"
showTime
options={date_format}
sortable={true}
/>
<ReferenceField source="user_id" reference="users">
<TextField source="id" />
</ReferenceField>
<NumberField source="score" />
<TextField source="reason" />
<TextField source="name" />
<TextField
source="canonical_alias"
label="resources.rooms.fields.canonical_alias"
/>
<ReferenceField
source="room_id"
reference="rooms"
link="show"
label="resources.rooms.fields.room_id"
>
<TextField source="id" />
</ReferenceField>
</Tab>
<Tab
label="synapseadmin.reports.tabs.detail"
icon={<PageviewIcon />}
path="detail"
>
<DateField
source="event_json.origin_server_ts"
showTime
options={date_format}
sortable={true}
/>
<ReferenceField source="sender" reference="users">
<TextField source="id" />
</ReferenceField>
<TextField source="sender" label="Sender (raw user ID)" />
<TextField source="event_id" />
<TextField source="event_json.origin" />
<TextField source="event_json.type" />
<TextField source="event_json.content.msgtype" />
<TextField source="event_json.content.body" />
<TextField source="event_json.content.format" />
<TextField source="event_json.content.formatted_body" />
<TextField source="event_json.content.algorithm" />
<TextField
source="event_json.content.device_id"
label="resources.devices.fields.device_id"
/>
</Tab>
</TabbedShowLayout>
</Show>
);
};
const ReportShowActions = () => {
const record = useRecordContext();
return (
<TopToolbar>
<DeleteButton
record={record}
mutationMode="pessimistic"
confirmTitle="resources.reports.action.erase.title"
confirmContent="resources.reports.action.erase.content"
/>
</TopToolbar>
);
};
export const ReportList = props => (
<List
{...props}
pagination={<ReportPagination />}
sort={{ field: "received_ts", order: "DESC" }}
>
<Datagrid rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} />
<DateField
source="received_ts"
showTime
options={date_format}
sortable={true}
/>
<TextField sortable={false} source="user_id" />
<TextField sortable={false} source="name" />
<TextField sortable={false} source="score" />
</Datagrid>
</List>
);
const resource = {
name: "reports",
icon: ReportIcon,
list: ReportList,
show: ReportShow,
};
export default resource;

View File

@ -0,0 +1,655 @@
import React, { useState } from "react";
import { useDataProvider, useNotify, Title } from "react-admin";
import { parse as parseCsv, unparse as unparseCsv } from "papaparse";
import {
Button,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
Container,
FormControlLabel,
NativeSelect,
} from "@mui/material";
import { useTranslate } from "ra-core";
import { generateRandomUser } from "./users";
const LOGGING = true;
const expectedFields = ["id", "displayname"].sort();
const optionalFields = [
"user_type",
"guest",
"admin",
"deactivated",
"avatar_url",
"password",
].sort();
function TranslatableOption({ value, text }) {
const translate = useTranslate();
return <option value={value}>{translate(text)}</option>;
}
const FilePicker = () => {
const [values, setValues] = useState(null);
const [error, setError] = useState(null);
const [stats, setStats] = useState(null);
const [dryRun, setDryRun] = useState(true);
const [progress, setProgress] = useState(null);
const [importResults, setImportResults] = useState(null);
const [skippedRecords, setSkippedRecords] = useState(null);
const [conflictMode, setConflictMode] = useState("stop");
const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("ignore");
const translate = useTranslate();
const notify = useNotify();
const dataProvider = useDataProvider();
const onFileChange = async e => {
if (progress !== null) return;
setValues(null);
setError(null);
setStats(null);
setImportResults(null);
const file = e.target.files ? e.target.files[0] : null;
/* Let's refuse some unreasonably big files instead of freezing
* up the browser */
if (file.size > 100000000) {
const message = translate("import_users.errors.unreasonably_big", {
size: (file.size / (1024 * 1024)).toFixed(2),
});
notify(message);
setError(message);
return;
}
try {
parseCsv(file, {
header: true,
skipEmptyLines: true /* especially for a final EOL in the csv file */,
complete: result => {
if (result.error) {
setError(result.error);
}
/* Papaparse is very lenient, we may be able to salvage
* the data in the file. */
verifyCsv(result, { setValues, setStats, setError });
},
});
} catch {
setError(true);
return null;
}
};
const verifyCsv = (
{ data, meta, errors },
{ setValues, setStats, setError }
) => {
/* First, verify the presence of required fields */
let eF = Array.from(expectedFields);
let oF = Array.from(optionalFields);
meta.fields.forEach(name => {
if (eF.includes(name)) {
eF = eF.filter(v => v !== name);
}
if (oF.includes(name)) {
oF = oF.filter(v => v !== name);
}
});
if (eF.length !== 0) {
setError(
translate("import_users.error.required_field", { field: eF[0] })
);
return false;
}
// XXX after deciding on how "name" and friends should be handled below,
// this place will want changes, too.
/* Collect some stats to prevent sneaky csv files from adding admin
users or something.
*/
let stats = {
user_types: { default: 0 },
is_guest: 0,
admin: 0,
deactivated: 0,
password: 0,
avatar_url: 0,
id: 0,
total: data.length,
};
data.forEach((line, idx) => {
if (line.user_type === undefined || line.user_type === "") {
stats.user_types.default++;
} else {
stats.user_types[line.user_type] += 1;
}
/* XXX correct the csv export that react-admin offers for the users
* resource so it gives sensible field names and doesn't duplicate
* id as "name"?
*/
if (meta.fields.includes("name")) {
delete line.name;
}
if (meta.fields.includes("user_type")) {
delete line.user_type;
}
if (meta.fields.includes("is_admin")) {
line.admin = line.is_admin;
delete line.is_admin;
}
["is_guest", "admin", "deactivated"].forEach(f => {
if (line[f] === "true") {
stats[f]++;
line[f] = true; // we need true booleans instead of strings
} else {
if (line[f] !== "false" && line[f] !== "") {
errors.push(
translate("import_users.error.invalid_value", {
field: f,
row: idx,
})
);
}
line[f] = false; // default values to false
}
});
if (line.password !== undefined && line.password !== "") {
stats.password++;
}
if (line.avatar_url !== undefined && line.avatar_url !== "") {
stats.avatar_url++;
}
if (line.id !== undefined && line.id !== "") {
stats.id++;
}
});
if (errors.length > 0) {
setError(errors);
}
setStats(stats);
setValues(data);
return true;
};
const runImport = async _e => {
if (progress !== null) {
notify("import_users.errors.already_in_progress");
return;
}
const results = await doImport(
dataProvider,
values,
conflictMode,
passwordMode,
useridMode,
dryRun,
setProgress,
setError
);
setImportResults(results);
// offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful
// records manually when fixing stuff in the CSV)
setSkippedRecords(unparseCsv(results.skippedRecords));
if (LOGGING) console.log("Skipped records:");
if (LOGGING) console.log(skippedRecords);
};
// XXX every single one of the requests will restart the activity indicator
// which doesn't look very good.
const doImport = async (
dataProvider,
data,
conflictMode,
passwordMode,
useridMode,
dryRun,
setProgress,
setError
) => {
let skippedRecords = [];
let erroredRecords = [];
let succeededRecords = [];
let changeStats = {
toAdmin: 0,
toGuest: 0,
toRegular: 0,
replacedPassword: 0,
};
let entriesDone = 0;
let entriesCount = data.length;
try {
setProgress({ done: entriesDone, limit: entriesCount });
for (const entry of data) {
let userRecord = {};
let overwriteData = {};
// No need to do a bunch of cryptographic random number getting if
// we are using neither a generated password nor a generated user id.
if (
useridMode === "ignore" ||
entry.id === undefined ||
entry.password === undefined ||
passwordMode === false
) {
overwriteData = generateRandomUser();
// Ignoring IDs or the entry lacking an ID means we keep the
// ID field in the overwrite data.
if (!(useridMode === "ignore" || entry.id === undefined)) {
delete overwriteData.id;
}
// Not using passwords from the csv or this entry lacking a password
// means we keep the password field in the overwrite data.
if (
!(
passwordMode === false ||
entry.password === undefined ||
entry.password === ""
)
) {
delete overwriteData.password;
}
}
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
Object.assign(userRecord, entry);
Object.assign(userRecord, overwriteData);
/* For these modes we will consider the ID that's in the record.
* If the mode is "stop", we will not continue adding more records, and
* we will offer information on what was already added and what was
* skipped.
*
* If the mode is "skip", we record the record for later, but don't
* send it to the server.
*
* If the mode is "update", we change fields that are reasonable to
* update.
* - If the "password mode" is "true" (i.e. "use passwords from csv"):
* - if the record has a password
* - send the password along with the record
* - if the record has no password
* - generate a new password
* - If the "password mode" is "false"
* - never generate a new password to update existing users with
*/
/* We just act as if there are no IDs in the CSV, so every user will be
* created anew.
* We do a simple retry loop so that an accidental hit on an existing ID
* doesn't trip us up.
*/
if (LOGGING)
console.log(
"will check for existence of record " + JSON.stringify(userRecord)
);
let retries = 0;
const submitRecord = recordData => {
return dataProvider.getOne("users", { id: recordData.id }).then(
async _alreadyExists => {
if (LOGGING) console.log("already existed");
if (useridMode === "update" || conflictMode === "skip") {
skippedRecords.push(recordData);
} else if (conflictMode === "stop") {
throw new Error(
translate("import_users.error.id_exits", {
id: recordData.id,
})
);
} else {
const overwriteData = generateRandomUser();
const newRecordData = Object.assign({}, recordData, {
id: overwriteData.id,
});
retries++;
if (retries > 512) {
console.warn("retry loop got stuck? pathological situation?");
skippedRecords.push(recordData);
} else {
await submitRecord(newRecordData);
}
}
},
async _okToSubmit => {
if (LOGGING)
console.log(
"OK to create record " +
recordData.id +
" (" +
recordData.displayname +
")."
);
if (!dryRun) {
await dataProvider.create("users", { data: recordData });
}
succeededRecords.push(recordData);
}
);
};
await submitRecord(userRecord);
entriesDone++;
setProgress({ done: entriesDone, limit: data.length });
}
setProgress(null);
} catch (e) {
setError(
translate("import_users.error.at_entry", {
entry: entriesDone + 1,
message: e.message,
})
);
setProgress(null);
}
return {
skippedRecords,
erroredRecords,
succeededRecords,
totalRecordCount: entriesCount,
changeStats,
wasDryRun: dryRun,
};
};
const downloadSkippedRecords = () => {
const element = document.createElement("a");
console.log(skippedRecords);
const file = new Blob([skippedRecords], {
type: "text/comma-separated-values",
});
element.href = URL.createObjectURL(file);
element.download = "skippedRecords.csv";
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
};
const onConflictModeChanged = async e => {
if (progress !== null) {
return;
}
const value = e.target.value;
setConflictMode(value);
};
const onPasswordModeChange = e => {
if (progress !== null) {
return;
}
setPasswordMode(e.target.checked);
};
const onUseridModeChanged = async e => {
if (progress !== null) {
return;
}
const value = e.target.value;
setUseridMode(value);
};
const onDryRunModeChanged = ev => {
if (progress !== null) {
return;
}
setDryRun(ev.target.checked);
};
// render individual small components
const statsCards = stats &&
!importResults && [
<Container>
<CardHeader
title={translate("import_users.cards.importstats.header")}
/>
<CardContent>
<div>
{translate(
"import_users.cards.importstats.users_total",
stats.total
)}
</div>
<div>
{translate(
"import_users.cards.importstats.guest_count",
stats.is_guest
)}
</div>
<div>
{translate(
"import_users.cards.importstats.admin_count",
stats.admin
)}
</div>
</CardContent>
</Container>,
<Container>
<CardHeader title={translate("import_users.cards.ids.header")} />
<CardContent>
<div>
{stats.id === stats.total
? translate("import_users.cards.ids.all_ids_present")
: translate("import_users.cards.ids.count_ids_present", stats.id)}
</div>
{stats.id > 0 ? (
<div>
<NativeSelect
onChange={onUseridModeChanged}
value={useridMode}
enabled={(progress !== null).toString()}
>
<TranslatableOption
value="ignore"
text="import_users.cards.ids.mode.ignore"
/>
<TranslatableOption
value="update"
text="import_users.cards.ids.mode.update"
/>
</NativeSelect>
</div>
) : (
""
)}
</CardContent>
</Container>,
<Container>
<CardHeader title={translate("import_users.cards.passwords.header")} />
<CardContent>
<div>
{stats.password === stats.total
? translate("import_users.cards.passwords.all_passwords_present")
: translate(
"import_users.cards.passwords.count_passwords_present",
stats.password
)}
</div>
{stats.password > 0 ? (
<div>
<FormControlLabel
control={
<Checkbox
checked={passwordMode}
enabled={(progress !== null).toString()}
onChange={onPasswordModeChange}
/>
}
label={translate("import_users.cards.passwords.use_passwords")}
/>
</div>
) : (
""
)}
</CardContent>
</Container>,
];
let conflictCards = stats && !importResults && (
<Container>
<CardHeader title={translate("import_users.cards.conflicts.header")} />
<CardContent>
<div>
<NativeSelect
onChange={onConflictModeChanged}
value={conflictMode}
enabled={(progress !== null).toString()}
>
<TranslatableOption
value="stop"
text="import_users.cards.conflicts.mode.stop"
/>
<TranslatableOption
value="skip"
text="import_users.cards.conflicts.mode.skip"
/>
</NativeSelect>
</div>
</CardContent>
</Container>
);
let errorCards = error && (
<Container>
<CardHeader title={translate("import_users.error.error")} />
<CardContent>
{(Array.isArray(error) ? error : [error]).map(e => (
<div>{e}</div>
))}
</CardContent>
</Container>
);
let uploadCard = !importResults && (
<Container>
<CardHeader title={translate("import_users.cards.upload.header")} />
<CardContent>
{translate("import_users.cards.upload.explanation")}
<a href="./data/example.csv">example.csv</a>
<br />
<br />
<input
type="file"
onChange={onFileChange}
enabled={(progress !== null).toString()}
/>
</CardContent>
</Container>
);
let resultsCard = importResults && (
<CardContent>
<CardHeader title={translate("import_users.cards.results.header")} />
<div>
{translate(
"import_users.cards.results.total",
importResults.totalRecordCount
)}
<br />
{translate(
"import_users.cards.results.successful",
importResults.succeededRecords.length
)}
<br />
{importResults.skippedRecords.length
? [
translate(
"import_users.cards.results.skipped",
importResults.skippedRecords.length
),
<div>
<button onClick={downloadSkippedRecords}>
{translate("import_users.cards.results.download_skipped")}
</button>
</div>,
<br />,
]
: ""}
{importResults.erroredRecords.length
? [
translate(
"import_users.cards.results.skipped",
importResults.erroredRecords.length
),
<br />,
]
: ""}
<br />
{importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"),
<br />,
]}
</div>
</CardContent>
);
let startImportCard =
!values || values.length === 0 || importResults ? undefined : (
<CardActions>
<FormControlLabel
control={
<Checkbox
checked={dryRun}
onChange={onDryRunModeChanged}
enabled={(progress !== null).toString()}
/>
}
label={translate("import_users.cards.startImport.simulate_only")}
/>
<Button
size="large"
onClick={runImport}
enabled={(progress !== null).toString()}
>
{translate("import_users.cards.startImport.run_import")}
</Button>
{progress !== null ? (
<div>
{progress.done} of {progress.limit} done
</div>
) : null}
</CardActions>
);
let allCards = [];
if (uploadCard) allCards.push(uploadCard);
if (errorCards) allCards.push(errorCards);
if (conflictCards) allCards.push(conflictCards);
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
let cardContainer = <Card>{allCards}</Card>;
return [
<Title defaultTitle={translate("import_users.title")} />,
cardContainer,
];
};
export const ImportFeature = FilePicker;

View File

@ -1,198 +0,0 @@
import React, { useState } from "react";
import {
Notification,
useLogin,
useNotify,
useLocale,
useSetLocale,
useTranslate,
} from "react-admin";
import { Field, Form } from "react-final-form";
import {
Avatar,
Button,
Card,
CardActions,
CircularProgress,
MenuItem,
Select,
TextField,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import LockIcon from "@material-ui/icons/Lock";
const useStyles = makeStyles(theme => ({
main: {
display: "flex",
flexDirection: "column",
minHeight: "100vh",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
},
card: {
minWidth: 300,
marginTop: "6em",
},
avatar: {
margin: "1em",
display: "flex",
justifyContent: "center",
},
icon: {
backgroundColor: theme.palette.secondary.main,
},
hint: {
marginTop: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[500],
},
form: {
padding: "0 1em 1em 1em",
},
input: {
marginTop: "1em",
},
actions: {
padding: "0 1em 1em 1em",
},
}));
const LoginPage = ({ theme }) => {
const classes = useStyles({ theme });
const login = useLogin();
const notify = useNotify();
const [loading, setLoading] = useState(false);
var locale = useLocale();
const setLocale = useSetLocale();
const translate = useTranslate();
const homeserver = localStorage.getItem("base_url");
const renderInput = ({
meta: { touched, error } = {},
input: { ...inputProps },
...props
}) => (
<TextField
error={!!(touched && error)}
helperText={touched && error}
{...inputProps}
{...props}
fullWidth
/>
);
const validate = values => {
const errors = {};
if (!values.homeserver) {
errors.homeserver = translate("ra.validation.required");
}
if (!values.username) {
errors.username = translate("ra.validation.required");
}
if (!values.password) {
errors.password = translate("ra.validation.required");
}
return errors;
};
const handleSubmit = auth => {
setLoading(true);
login(auth).catch(error => {
setLoading(false);
notify(
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message,
"warning"
);
});
};
return (
<Form
initialValues={{ homeserver: homeserver }}
onSubmit={handleSubmit}
validate={validate}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} noValidate>
<div className={classes.main}>
<Card className={classes.card}>
<div className={classes.avatar}>
<Avatar className={classes.icon}>
<LockIcon />
</Avatar>
</div>
<div className={classes.hint}>
{translate("synapseadmin.auth.welcome")}
</div>
<div className={classes.form}>
<div className={classes.input}>
<Select
value={locale}
onChange={e => {
setLocale(e.target.value);
}}
fullWidth
disabled={loading}
>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem>
</Select>
</div>
<div className={classes.input}>
<Field
autoFocus
name="homeserver"
component={renderInput}
label={translate("synapseadmin.auth.homeserver")}
disabled={loading}
/>
</div>
<div className={classes.input}>
<Field
name="username"
component={renderInput}
label={translate("ra.auth.username")}
disabled={loading}
/>
</div>
<div className={classes.input}>
<Field
name="password"
component={renderInput}
label={translate("ra.auth.password")}
type="password"
disabled={loading}
/>
</div>
</div>
<CardActions className={classes.actions}>
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
className={classes.button}
fullWidth
>
{loading && <CircularProgress size={25} thickness={2} />}
{translate("ra.auth.sign_in")}
</Button>
</CardActions>
</Card>
<Notification />
</div>
</form>
)}
/>
);
};
export default LoginPage;

View File

@ -0,0 +1,329 @@
import React, { useState, useEffect } from "react";
import {
Form,
FormDataConsumer,
Notification,
required,
useLogin,
useNotify,
useLocaleState,
useTranslate,
PasswordInput,
TextInput,
} from "react-admin";
import { useFormContext } from "react-hook-form";
import {
Avatar,
Box,
Button,
Card,
CardActions,
CircularProgress,
MenuItem,
Select,
Typography,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock";
import {
getServerVersion,
getSupportedFeatures,
getSupportedLoginFlows,
getWellKnownUrl,
isValidBaseUrl,
splitMxid,
} from "../synapse/synapse";
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",
},
}));
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
const [locale, setLocale] = useLocaleState();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
if (loginToken) {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
// Prevent further requests
window.history.replaceState(
{},
"",
window.location.href.replace(loginToken[0], "#").split("#")[0]
);
const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
username: null,
password: null,
loginToken: ssoToken,
};
console.log("Base URL is:", baseUrl);
console.log("SSO Token is:", ssoToken);
console.log("Let's try token login...");
login(auth).catch(error => {
alert(
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message
);
console.error(error);
});
}
}
const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) {
return translate("synapseadmin.auth.protocol_error");
} else if (
!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)
) {
return translate("synapseadmin.auth.url_error");
} else {
return undefined;
}
};
const handleSubmit = auth => {
setLoading(true);
login(auth).catch(error => {
setLoading(false);
notify(
typeof error === "string"
? error
: typeof error === "undefined" || !error.message
? "ra.auth.sign_in_error"
: error.message,
{ type: "warning" }
);
});
};
const handleSSO = () => {
localStorage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
window.location.href = ssoFullUrl;
};
const UserData = ({ formData }) => {
const form = useFormContext();
const [serverVersion, setServerVersion] = useState("");
const [matrixVersions, setMatrixVersions] = useState("");
const handleUsernameChange = _ => {
if (formData.base_url || cfg_base_url) return;
// check if username is a full qualified userId then set base_url accordingly
const domain = splitMxid(formData.username)?.domain;
if (domain) {
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
}
};
useEffect(() => {
if (!isValidBaseUrl(formData.base_url)) return;
getServerVersion(formData.base_url)
.then(serverVersion =>
setServerVersion(
`${translate("synapseadmin.auth.server_version")} ${serverVersion}`
)
)
.catch(() => setServerVersion(""));
getSupportedFeatures(formData.base_url)
.then(features =>
setMatrixVersions(
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
)
)
.catch(() => setMatrixVersions(""));
// Set SSO Url
getSupportedLoginFlows(formData.base_url)
.then(loginFlows => {
const supportPass =
loginFlows.find(f => f.type === "m.login.password") !== undefined;
const supportSSO =
loginFlows.find(f => f.type === "m.login.sso") !== undefined;
setSupportPassAuth(supportPass);
setSSOBaseUrl(supportSSO ? formData.base_url : "");
})
.catch(() => setSSOBaseUrl(""));
}, [formData.base_url]);
return (
<>
<Box>
<TextInput
autoFocus
name="username"
label="ra.auth.username"
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
fullWidth
className="input"
validate={required()}
/>
</Box>
<Box>
<PasswordInput
name="password"
label="ra.auth.password"
type="password"
disabled={loading || !supportPassAuth}
resettable
fullWidth
className="input"
validate={required()}
/>
</Box>
<Box>
<TextInput
name="base_url"
label="synapseadmin.auth.base_url"
disabled={cfg_base_url || loading}
resettable
fullWidth
className="input"
validate={[required(), validateBaseUrl]}
/>
</Box>
<Typography className="serverVersion">{serverVersion}</Typography>
<Typography className="matrixVersions">{matrixVersions}</Typography>
</>
);
};
return (
<Form
defaultValues={{ base_url: cfg_base_url || base_url }}
onSubmit={handleSubmit}
mode="onTouched"
>
<FormBox>
<Card className="card">
<Box className="avatar">
{loading ? (
<CircularProgress size={25} thickness={2} />
) : (
<Avatar className="icon">
<LockIcon />
</Avatar>
)}
</Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
<Box className="form">
<Select
value={locale}
onChange={e => {
setLocale(e.target.value);
}}
fullWidth
disabled={loading}
className="select"
>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="zh">简体中文</MenuItem>
<MenuItem value="fa">Persian(فارسی)</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
</Select>
<FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />}
</FormDataConsumer>
<CardActions className="actions">
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading || !supportPassAuth}
fullWidth
>
{translate("ra.auth.sign_in")}
</Button>
<Button
variant="contained"
color="secondary"
onClick={handleSSO}
disabled={loading || ssoBaseUrl === ""}
fullWidth
>
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
</Box>
</Card>
</FormBox>
<Notification />
</Form>
);
};
export default LoginPage;

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import { TestContext } from "react-admin"; import { render } from "@testing-library/react";
import { shallow } from "enzyme"; import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage"; import LoginPage from "./LoginPage";
describe("LoginForm", () => { describe("LoginForm", () => {
it("renders", () => { it("renders", () => {
shallow( render(
<TestContext> <AdminContext>
<LoginPage /> <LoginPage />
</TestContext> </AdminContext>
); );
}); });
}); });

View File

@ -0,0 +1,142 @@
import React from "react";
import {
BooleanInput,
Create,
Datagrid,
DateField,
DateTimeInput,
Edit,
List,
maxValue,
number,
NumberField,
NumberInput,
regex,
SaveButton,
SimpleForm,
TextInput,
TextField,
Toolbar,
} from "react-admin";
import RegistrationTokenIcon from "@mui/icons-material/ConfirmationNumber";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];
const validateLength = [number(), maxValue(64)];
const dateParser = v => {
const d = new Date(v);
if (isNaN(d)) return 0;
return d.getTime();
};
const dateFormatter = v => {
if (v === undefined || v === null) return;
const d = new Date(v);
const pad = "00";
const year = d.getFullYear().toString();
const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
const day = (pad + d.getDate().toString()).slice(-2);
const hour = (pad + d.getHours().toString()).slice(-2);
const minute = (pad + d.getMinutes().toString()).slice(-2);
// target format yyyy-MM-ddThh:mm
return `${year}-${month}-${day}T${hour}:${minute}`;
};
const registrationTokenFilters = [<BooleanInput source="valid" alwaysOn />];
export const RegistrationTokenList = props => (
<List
{...props}
filters={registrationTokenFilters}
filterDefaultValues={{ valid: true }}
pagination={false}
perPage={500}
>
<Datagrid rowClick="edit">
<TextField source="token" sortable={false} />
<NumberField source="uses_allowed" sortable={false} />
<NumberField source="pending" sortable={false} />
<NumberField source="completed" sortable={false} />
<DateField
source="expiry_time"
showTime
options={date_format}
sortable={false}
/>
</Datagrid>
</List>
);
export const RegistrationTokenCreate = props => (
<Create {...props} redirect="list">
<SimpleForm
toolbar={
<Toolbar>
{/* It is possible to create tokens per default without input. */}
<SaveButton alwaysEnable />
</Toolbar>
}
>
<TextInput
source="token"
autoComplete="off"
validate={validateToken}
resettable
/>
<NumberInput
source="length"
validate={validateLength}
helperText="resources.registration_tokens.helper.length"
step={1}
/>
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput source="expiry_time" parse={dateParser} />
</SimpleForm>
</Create>
);
export const RegistrationTokenEdit = props => (
<Edit {...props}>
<SimpleForm>
<TextInput source="token" disabled />
<NumberInput source="pending" disabled />
<NumberInput source="completed" disabled />
<NumberInput
source="uses_allowed"
validate={validateUsesAllowed}
step={1}
/>
<DateTimeInput
source="expiry_time"
parse={dateParser}
format={dateFormatter}
/>
</SimpleForm>
</Edit>
);
const resource = {
name: "registration_tokens",
icon: RegistrationTokenIcon,
list: RegistrationTokenList,
edit: RegistrationTokenEdit,
create: RegistrationTokenCreate,
};
export default resource;

View File

@ -0,0 +1,206 @@
import React from "react";
import {
BooleanField,
BulkDeleteButton,
Button,
DatagridConfigurable,
ExportButton,
DeleteButton,
List,
NumberField,
Pagination,
SelectColumnsButton,
TextField,
TopToolbar,
useCreate,
useDataProvider,
useListContext,
useNotify,
useTranslate,
useRecordContext,
useRefresh,
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import RoomDirectoryIcon from "@mui/icons-material/FolderShared";
import AvatarField from "./AvatarField";
const RoomDirectoryPagination = () => (
<Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />
);
export const RoomDirectoryUnpublishButton = props => {
const translate = useTranslate();
return (
<DeleteButton
{...props}
label="resources.room_directory.action.erase"
redirect={false}
mutationMode="pessimistic"
confirmTitle={translate("resources.room_directory.action.title", {
smart_count: 1,
})}
confirmContent={translate("resources.room_directory.action.content", {
smart_count: 1,
})}
resource="room_directory"
icon={<RoomDirectoryIcon />}
/>
);
};
export const RoomDirectoryBulkUnpublishButton = props => (
<BulkDeleteButton
{...props}
label="resources.room_directory.action.erase"
mutationMode="pessimistic"
confirmTitle="resources.room_directory.action.title"
confirmContent="resources.room_directory.action.content"
resource="room_directory"
icon={<RoomDirectoryIcon />}
/>
);
export const RoomDirectoryBulkPublishButton = props => {
const { selectedIds } = useListContext();
const notify = useNotify();
const refresh = useRefresh();
const unselectAllRooms = useUnselectAll("rooms");
const dataProvider = useDataProvider();
const { mutate, isLoading } = useMutation(
() =>
dataProvider.createMany("room_directory", {
ids: selectedIds,
data: {},
}),
{
onSuccess: () => {
notify("resources.room_directory.action.send_success");
unselectAllRooms();
refresh();
},
onError: () =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
return (
<Button
{...props}
label="resources.room_directory.action.create"
onClick={mutate}
disabled={isLoading}
>
<RoomDirectoryIcon />
</Button>
);
};
export const RoomDirectoryPublishButton = props => {
const record = useRecordContext();
const notify = useNotify();
const refresh = useRefresh();
const [create, { isLoading }] = useCreate();
const handleSend = () => {
create(
"room_directory",
{ data: { id: record.id } },
{
onSuccess: () => {
notify("resources.room_directory.action.send_success");
refresh();
},
onError: () =>
notify("resources.room_directory.action.send_failure", {
type: "error",
}),
}
);
};
return (
<Button
{...props}
label="resources.room_directory.action.create"
onClick={handleSend}
disabled={isLoading}
>
<RoomDirectoryIcon />
</Button>
);
};
const RoomDirectoryListActions = () => (
<TopToolbar>
<SelectColumnsButton />
<ExportButton />
</TopToolbar>
);
export const RoomDirectoryList = () => (
<List
pagination={<RoomDirectoryPagination />}
perPage={100}
actions={<RoomDirectoryListActions />}
>
<DatagridConfigurable
rowClick={(id, _resource, _record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkUnpublishButton />}
omit={["room_id", "canonical_alias", "topic"]}
>
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "40px", width: "40px" }}
label="resources.rooms.fields.avatar"
/>
<TextField
source="name"
sortable={false}
label="resources.rooms.fields.name"
/>
<TextField
source="room_id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
<TextField
source="canonical_alias"
sortable={false}
label="resources.rooms.fields.canonical_alias"
/>
<TextField
source="topic"
sortable={false}
label="resources.rooms.fields.topic"
/>
<NumberField
source="num_joined_members"
sortable={false}
label="resources.rooms.fields.joined_members"
/>
<BooleanField
source="world_readable"
sortable={false}
label="resources.room_directory.fields.world_readable"
/>
<BooleanField
source="guest_can_join"
sortable={false}
label="resources.room_directory.fields.guest_can_join"
/>
</DatagridConfigurable>
</List>
);
const resource = {
name: "room_directory",
icon: RoomDirectoryIcon,
list: RoomDirectoryList,
};
export default resource;

View File

@ -0,0 +1,155 @@
import React, { useState } from "react";
import {
Button,
SaveButton,
SimpleForm,
TextInput,
Toolbar,
required,
useCreate,
useDataProvider,
useListContext,
useNotify,
useRecordContext,
useTranslate,
useUnselectAll,
} from "react-admin";
import { useMutation } from "react-query";
import MessageIcon from "@mui/icons-material/Message";
import IconCancel from "@mui/icons-material/Cancel";
import {
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
const ServerNoticeDialog = ({ open, loading, onClose, onSubmit }) => {
const translate = useTranslate();
const ServerNoticeToolbar = props => (
<Toolbar {...props}>
<SaveButton
label="resources.servernotices.action.send"
disabled={props.pristine}
/>
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
</Toolbar>
);
return (
<Dialog open={open} onClose={onClose} loading={loading}>
<DialogTitle>
{translate("resources.servernotices.action.send")}
</DialogTitle>
<DialogContent>
<DialogContentText>
{translate("resources.servernotices.helper.send")}
</DialogContentText>
<SimpleForm toolbar={<ServerNoticeToolbar />} onSubmit={onSubmit}>
<TextInput
source="body"
label="resources.servernotices.fields.body"
fullWidth
multiline
rows="4"
resettable
validate={required()}
/>
</SimpleForm>
</DialogContent>
</Dialog>
);
};
export const ServerNoticeButton = () => {
const record = useRecordContext();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [create, { isloading }] = useCreate();
const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);
const handleSend = values => {
create(
"servernotices",
{ data: { id: record.id, ...values } },
{
onSuccess: () => {
notify("resources.servernotices.action.send_success");
handleDialogClose();
},
onError: () =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
};
return (
<>
<Button
label="resources.servernotices.send"
onClick={handleDialogOpen}
disabled={isloading}
>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={handleDialogClose}
onSubmit={handleSend}
/>
</>
);
};
export const ServerNoticeBulkButton = () => {
const { selectedIds } = useListContext();
const [open, setOpen] = useState(false);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const notify = useNotify();
const unselectAllUsers = useUnselectAll("users");
const dataProvider = useDataProvider();
const { mutate: sendNotices, isLoading } = useMutation(
data =>
dataProvider.createMany("servernotices", {
ids: selectedIds,
data: data,
}),
{
onSuccess: () => {
notify("resources.servernotices.action.send_success");
unselectAllUsers();
closeDialog();
},
onError: () =>
notify("resources.servernotices.action.send_failure", {
type: "error",
}),
}
);
return (
<>
<Button
label="resources.servernotices.send"
onClick={openDialog}
disabled={isLoading}
>
<MessageIcon />
</Button>
<ServerNoticeDialog
open={open}
onClose={closeDialog}
onSubmit={sendNotices}
/>
</>
);
};

View File

@ -0,0 +1,189 @@
import React from "react";
import {
Button,
Datagrid,
DateField,
List,
Pagination,
ReferenceField,
ReferenceManyField,
SearchInput,
Show,
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useDelete,
useNotify,
useRefresh,
useTranslate,
} from "react-admin";
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";
const DestinationPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const destinationRowSx = (record, _index) => ({
backgroundColor: record.retry_last_ts > 0 ? "#ffcccc" : "white",
});
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
export const DestinationReconnectButton = () => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [handleReconnect, { isLoading }] = useDelete();
// Reconnect is not required if no error has occurred. (`failure_ts`)
if (!record || !record.failure_ts) return null;
const handleClick = e => {
// Prevents redirection to the detail page when clicking in the list
e.stopPropagation();
handleReconnect(
"destinations",
{ id: record.id },
{
onSuccess: () => {
notify("ra.notification.updated", {
messageArgs: { smart_count: 1 },
});
refresh();
},
onError: () => {
notify("ra.message.error", { type: "error" });
},
}
);
};
return (
<Button
label="resources.destinations.action.reconnect"
onClick={handleClick}
disabled={isLoading}
>
<AutorenewIcon />
</Button>
);
};
const DestinationShowActions = () => (
<TopToolbar>
<DestinationReconnectButton />
</TopToolbar>
);
const DestinationTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
return (
<span>
{translate("resources.destinations.name", 1)} {record.destination}
</span>
);
};
export const DestinationList = props => {
return (
<List
{...props}
filters={destinationFilters}
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
>
<Datagrid
rowSx={destinationRowSx}
rowClick={(id, _resource, _record) => `${id}/show/rooms`}
bulkActionButtons={false}
>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
<DestinationReconnectButton />
</Datagrid>
</List>
);
};
export const DestinationShow = props => {
const translate = useTranslate();
return (
<Show
actions={<DestinationShowActions />}
title={<DestinationTitle />}
{...props}
>
<TabbedShowLayout>
<Tab label="status" icon={<ViewListIcon />}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} />
<DateField source="retry_last_ts" showTime options={date_format} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
</Tab>
<Tab
label={translate("resources.rooms.name", { smart_count: 2 })}
icon={<FolderSharedIcon />}
path="rooms"
>
<ReferenceManyField
reference="destination_rooms"
target="destination"
addLabel={false}
pagination={<DestinationPagination />}
perPage={50}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, resource, record) => `/rooms/${id}/show`}
>
<TextField
source="room_id"
label="resources.rooms.fields.room_id"
/>
<TextField source="stream_ordering" sortable={false} />
<ReferenceField
label="resources.rooms.fields.name"
source="id"
reference="rooms"
sortable={false}
link=""
>
<TextField source="name" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
);
};
const resource = {
name: "destinations",
icon: DestinationsIcon,
list: DestinationList,
show: DestinationShow,
};
export default resource;

View File

@ -0,0 +1,51 @@
import React from "react";
import {
DeleteButton,
useDelete,
useNotify,
useRecordContext,
useRefresh,
} from "react-admin";
export const DeviceRemoveButton = props => {
const record = useRecordContext();
const refresh = useRefresh();
const notify = useNotify();
const [removeDevice] = useDelete();
if (!record) return null;
const handleConfirm = () => {
removeDevice(
"devices",
// needs previousData for user_id
{ id: record.id, previousData: record },
{
onSuccess: () => {
notify("resources.devices.action.erase.success");
refresh();
},
onError: () => {
notify("resources.devices.action.erase.failure", { type: "error" });
},
}
);
};
return (
<DeleteButton
{...props}
label="ra.action.remove"
confirmTitle="resources.devices.action.erase.title"
confirmContent="resources.devices.action.erase.content"
onConfirm={handleConfirm}
mutationMode="pessimistic"
redirect={false}
translateOptions={{
id: record.id,
name: record.display_name ? record.display_name : record.id,
}}
/>
);
};

335
src/components/media.jsx Normal file
View File

@ -0,0 +1,335 @@
import React, { useState } from "react";
import {
BooleanInput,
Button,
DateTimeInput,
NumberInput,
SaveButton,
SimpleForm,
Toolbar,
useCreate,
useDelete,
useNotify,
useRecordContext,
useRefresh,
useTranslate,
} from "react-admin";
import BlockIcon from "@mui/icons-material/Block";
import ClearIcon from "@mui/icons-material/Clear";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
import {
Dialog,
DialogContent,
DialogContentText,
DialogTitle,
Tooltip,
} from "@mui/material";
import IconCancel from "@mui/icons-material/Cancel";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { alpha, useTheme } from "@mui/material/styles";
const DeleteMediaDialog = ({ open, loading, onClose, onSubmit }) => {
const translate = useTranslate();
const dateParser = v => {
const d = new Date(v);
if (isNaN(d)) return 0;
return d.getTime();
};
const DeleteMediaToolbar = props => (
<Toolbar {...props}>
<SaveButton
label="resources.delete_media.action.send"
icon={<DeleteSweepIcon />}
/>
<Button label="ra.action.cancel" onClick={onClose}>
<IconCancel />
</Button>
</Toolbar>
);
return (
<Dialog open={open} onClose={onClose} loading={loading}>
<DialogTitle>
{translate("resources.delete_media.action.send")}
</DialogTitle>
<DialogContent>
<DialogContentText>
{translate("resources.delete_media.helper.send")}
</DialogContentText>
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
<DateTimeInput
fullWidth
source="before_ts"
label="resources.delete_media.fields.before_ts"
defaultValue={0}
parse={dateParser}
/>
<NumberInput
fullWidth
source="size_gt"
label="resources.delete_media.fields.size_gt"
defaultValue={0}
min={0}
step={1024}
/>
<BooleanInput
fullWidth
source="keep_profiles"
label="resources.delete_media.fields.keep_profiles"
defaultValue={true}
/>
</SimpleForm>
</DialogContent>
</Dialog>
);
};
export const DeleteMediaButton = props => {
const theme = useTheme();
const [open, setOpen] = useState(false);
const notify = useNotify();
const [deleteOne, { isLoading }] = useDelete();
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const deleteMedia = values => {
deleteOne(
"delete_media",
// needs meta.before_ts, meta.size_gt and meta.keep_profiles
{ meta: values },
{
onSuccess: () => {
notify("resources.delete_media.action.send_success");
closeDialog();
},
onError: () =>
notify("resources.delete_media.action.send_failure", {
type: "error",
}),
}
);
};
return (
<>
<Button
{...props}
label="resources.delete_media.action.send"
onClick={openDialog}
disabled={isLoading}
sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
}}
>
<DeleteSweepIcon />
</Button>
<DeleteMediaDialog
open={open}
onClose={closeDialog}
onSubmit={deleteMedia}
/>
</>
);
};
export const ProtectMediaButton = () => {
const record = useRecordContext();
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete();
if (!record) return null;
const handleProtect = () => {
create(
"protect_media",
{ data: record },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onError: () =>
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
}
);
};
const handleUnprotect = () => {
deleteOne(
"protect_media",
{ id: record.id },
{
onSuccess: () => {
notify("resources.protect_media.action.send_success");
refresh();
},
onError: () =>
notify("resources.protect_media.action.send_failure", {
type: "error",
}),
}
);
};
return (
/*
Wrapping Tooltip with <div>
https://github.com/marmelab/react-admin/issues/4349#issuecomment-578594735
*/
<>
{record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.none", {
_: "resources.protect_media.action.none",
})}
>
<div>
{/*
Button instead BooleanField for
consistent appearance and position in the column
*/}
<Button disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.protect_media.action.delete", {
_: "resources.protect_media.action.delete",
})}
arrow
>
<div>
<Button onClick={handleUnprotect} disabled={isLoading}>
<LockIcon />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.protect_media.action.create", {
_: "resources.protect_media.action.create",
})}
>
<div>
<Button onClick={handleProtect} disabled={isLoading}>
<LockOpenIcon />
</Button>
</div>
</Tooltip>
)}
</>
);
};
export const QuarantineMediaButton = props => {
const record = useRecordContext();
const translate = useTranslate();
const refresh = useRefresh();
const notify = useNotify();
const [create, { isLoading }] = useCreate();
const [deleteOne] = useDelete();
if (!record) return null;
const handleQuarantaine = () => {
create(
"quarantine_media",
{ data: record },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onError: () =>
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),
}
);
};
const handleRemoveQuarantaine = () => {
deleteOne(
"quarantine_media",
{ id: record.id, previousData: record },
{
onSuccess: () => {
notify("resources.quarantine_media.action.send_success");
refresh();
},
onError: () =>
notify("resources.quarantine_media.action.send_failure", {
type: "error",
}),
}
);
};
return (
<>
{record.safe_from_quarantine && (
<Tooltip
title={translate("resources.quarantine_media.action.none", {
_: "resources.quarantine_media.action.none",
})}
>
<div>
<Button {...props} disabled={true}>
<ClearIcon />
</Button>
</div>
</Tooltip>
)}
{record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.delete", {
_: "resources.quarantine_media.action.delete",
})}
>
<div>
<Button
{...props}
onClick={handleRemoveQuarantaine}
disabled={isLoading}
>
<BlockIcon color="error" />
</Button>
</div>
</Tooltip>
)}
{!record.safe_from_quarantine && !record.quarantined_by && (
<Tooltip
title={translate("resources.quarantine_media.action.create", {
_: "resources.quarantine_media.action.create",
})}
>
<div>
<Button onClick={handleQuarantaine} disabled={isLoading}>
<BlockIcon />
</Button>
</div>
</Tooltip>
)}
</>
);
};

View File

@ -1,17 +0,0 @@
import React from "react";
import { Datagrid, List, TextField, Pagination } from "react-admin";
const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
export const RoomList = props => (
<List {...props} pagination={<RoomPagination />}>
<Datagrid>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="canonical_alias" />
<TextField source="joined_members" />
</Datagrid>
</List>
);

355
src/components/rooms.jsx Normal file
View File

@ -0,0 +1,355 @@
import React from "react";
import {
BooleanField,
BulkDeleteButton,
DateField,
Datagrid,
DatagridConfigurable,
DeleteButton,
ExportButton,
FunctionField,
List,
NumberField,
Pagination,
ReferenceField,
ReferenceManyField,
SearchInput,
SelectColumnsButton,
SelectField,
Show,
Tab,
TabbedShowLayout,
TextField,
TopToolbar,
useRecordContext,
useTranslate,
} from "react-admin";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import FastForwardIcon from "@mui/icons-material/FastForward";
import HttpsIcon from "@mui/icons-material/Https";
import NoEncryptionIcon from "@mui/icons-material/NoEncryption";
import PageviewIcon from "@mui/icons-material/Pageview";
import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EventIcon from "@mui/icons-material/Event";
import RoomIcon from "@mui/icons-material/ViewList";
import {
RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./RoomDirectory";
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const RoomPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const RoomTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
var name = "";
if (record) {
name = record.name !== "" ? record.name : record.id;
}
return (
<span>
{translate("resources.rooms.name", 1)} {name}
</span>
);
};
const RoomShowActions = () => {
const record = useRecordContext();
var roomDirectoryStatus = "";
if (record) {
roomDirectoryStatus = record.public;
}
return (
<TopToolbar>
{roomDirectoryStatus === false && <RoomDirectoryPublishButton />}
{roomDirectoryStatus === true && <RoomDirectoryUnpublishButton />}
<DeleteButton
mutationMode="pessimistic"
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
/>
</TopToolbar>
);
};
export const RoomShow = props => {
const translate = useTranslate();
return (
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
<TabbedShowLayout>
<Tab label="synapseadmin.rooms.tabs.basic" icon={<ViewListIcon />}>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="topic" />
<TextField source="canonical_alias" />
<ReferenceField source="creator" reference="users">
<TextField source="id" />
</ReferenceField>
</Tab>
<Tab
label="synapseadmin.rooms.tabs.detail"
icon={<PageviewIcon />}
path="detail"
>
<TextField source="joined_members" />
<TextField source="joined_local_members" />
<TextField source="joined_local_devices" />
<TextField source="state_events" />
<TextField source="version" />
<TextField
source="encryption"
emptyText={translate("resources.rooms.enums.unencrypted")}
/>
</Tab>
<Tab
label="synapseadmin.rooms.tabs.members"
icon={<UserIcon />}
path="members"
>
<ReferenceManyField
reference="room_members"
target="room_id"
addLabel={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, resource, record) => "/users/" + id}
bulkActionButtons={false}
>
<TextField
source="id"
sortable={false}
label="resources.users.fields.id"
/>
<ReferenceField
label="resources.users.fields.displayname"
source="id"
reference="users"
sortable={false}
link=""
>
<TextField source="displayname" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab
label="synapseadmin.rooms.tabs.permission"
icon={<VisibilityIcon />}
path="permission"
>
<BooleanField source="federatable" />
<BooleanField source="public" />
<SelectField
source="join_rules"
choices={[
{ id: "public", name: "resources.rooms.enums.join_rules.public" },
{ id: "knock", name: "resources.rooms.enums.join_rules.knock" },
{ id: "invite", name: "resources.rooms.enums.join_rules.invite" },
{
id: "private",
name: "resources.rooms.enums.join_rules.private",
},
]}
/>
<SelectField
source="guest_access"
choices={[
{
id: "can_join",
name: "resources.rooms.enums.guest_access.can_join",
},
{
id: "forbidden",
name: "resources.rooms.enums.guest_access.forbidden",
},
]}
/>
<SelectField
source="history_visibility"
choices={[
{
id: "invited",
name: "resources.rooms.enums.history_visibility.invited",
},
{
id: "joined",
name: "resources.rooms.enums.history_visibility.joined",
},
{
id: "shared",
name: "resources.rooms.enums.history_visibility.shared",
},
{
id: "world_readable",
name: "resources.rooms.enums.history_visibility.world_readable",
},
]}
/>
</Tab>
<Tab
label={translate("resources.room_state.name", { smart_count: 2 })}
icon={<EventIcon />}
path="state"
>
<ReferenceManyField
reference="room_state"
target="room_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="type" sortable={false} />
<DateField
source="origin_server_ts"
showTime
options={date_format}
sortable={false}
/>
<TextField source="content" sortable={false} />
<ReferenceField
source="sender"
reference="users"
sortable={false}
>
<TextField source="id" />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</Tab>
<Tab
label="resources.forward_extremities.name"
icon={<FastForwardIcon />}
path="forward_extremities"
>
<Box
sx={{
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
margin: "0.5em",
}}
>
{translate("resources.rooms.helper.forward_extremities")}
</Box>
<ReferenceManyField
reference="forward_extremities"
target="room_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="id" sortable={false} />
<DateField
source="received_ts"
showTime
options={date_format}
sortable={false}
/>
<NumberField source="depth" sortable={false} />
<TextField source="state_group" sortable={false} />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
);
};
const RoomBulkActionButtons = () => (
<>
<RoomDirectoryBulkPublishButton />
<RoomDirectoryBulkUnpublishButton />
<BulkDeleteButton
confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic"
/>
</>
);
const roomFilters = [<SearchInput source="search_term" alwaysOn />];
const RoomListActions = () => (
<TopToolbar>
<SelectColumnsButton />
<ExportButton />
</TopToolbar>
);
export const RoomList = props => {
const theme = useTheme();
return (
<List
{...props}
pagination={<RoomPagination />}
sort={{ field: "name", order: "ASC" }}
filters={roomFilters}
actions={<RoomListActions />}
>
<DatagridConfigurable
rowClick="show"
bulkActionButtons={<RoomBulkActionButtons />}
omit={[
"joined_local_members",
"state_events",
"version",
"federatable",
]}
>
<BooleanField
source="is_encrypted"
sortBy="encryption"
TrueIcon={HttpsIcon}
FalseIcon={NoEncryptionIcon}
label={<HttpsIcon />}
sx={{
[`& [data-testid="true"]`]: { color: theme.palette.success.main },
[`& [data-testid="false"]`]: { color: theme.palette.error.main },
}}
/>
<FunctionField
source="name"
render={record =>
record["name"] || record["canonical_alias"] || record["id"]
}
/>
<TextField source="joined_members" />
<TextField source="joined_local_members" />
<TextField source="state_events" />
<TextField source="version" />
<BooleanField source="federatable" />
<BooleanField source="public" />
</DatagridConfigurable>
</List>
);
};
const resource = {
name: "rooms",
icon: RoomIcon,
list: RoomList,
show: RoomShow,
};
export default resource;

View File

@ -0,0 +1,79 @@
import React from "react";
import { cloneElement } from "react";
import {
Datagrid,
ExportButton,
List,
NumberField,
Pagination,
sanitizeListRestProps,
SearchInput,
TextField,
TopToolbar,
useListContext,
} from "react-admin";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import { DeleteMediaButton } from "./media";
const ListActions = props => {
const { className, exporter, filters, maxResults, ...rest } = props;
const { sort, resource, displayedFilters, filterValues, showFilter, total } =
useListContext();
return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
cloneElement(filters, {
resource,
showFilter,
displayedFilters,
filterValues,
context: "button",
})}
<DeleteMediaButton />
<ExportButton
disabled={total === 0}
resource={resource}
sort={sort}
filterValues={filterValues}
maxResults={maxResults}
/>
</TopToolbar>
);
};
const UserMediaStatsPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const userMediaStatsFilters = [<SearchInput source="search_term" alwaysOn />];
export const UserMediaStatsList = props => (
<List
{...props}
actions={<ListActions />}
filters={userMediaStatsFilters}
pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }}
>
<Datagrid
rowClick={(id, resource, record) => "/users/" + id + "/media"}
bulkActionButtons={false}
>
<TextField source="user_id" label="resources.users.fields.id" />
<TextField
source="displayname"
label="resources.users.fields.displayname"
/>
<NumberField source="media_count" />
<NumberField source="media_length" />
</Datagrid>
</List>
);
const resource = {
name: "user_media_statistics",
icon: EqualizerIcon,
list: UserMediaStatsList,
};
export default resource;

View File

@ -1,197 +0,0 @@
import React from "react";
import PersonPinIcon from "@material-ui/icons/PersonPin";
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
import {
ArrayInput,
ArrayField,
Datagrid,
DateField,
Create,
Edit,
List,
Filter,
SimpleForm,
SimpleFormIterator,
TabbedForm,
FormTab,
BooleanField,
BooleanInput,
ImageField,
PasswordInput,
TextField,
TextInput,
ReferenceField,
Toolbar,
TopToolbar,
SelectInput,
regex,
Pagination,
} from "react-admin";
import { ImportButton } from "react-admin-import-csv";
import { CreateButton, ExportButton } from "ra-ui-materialui";
const UserPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const UserFilter = props => (
<Filter {...props}>
<BooleanInput source="guests" alwaysOn />
<BooleanInput
label="resources.users.fields.show_deactivated"
source="deactivated"
alwaysOn
/>
</Filter>
);
const ListActions = props => {
const {
className,
basePath,
total,
resource,
currentSort,
filterValues,
exporter
} = props;
return (
<TopToolbar className={className}>
<CreateButton basePath={basePath} />
<ImportButton {...props} />
<ExportButton
disabled={total === 0}
resource={resource}
sort={currentSort}
filter={filterValues}
exporter={exporter}
/>
</TopToolbar>
);
};
export const UserList = props => (
<List
{...props}
filters={<UserFilter />}
filterDefaultValues={{ guests: true, deactivated: false }}
bulkActionButtons={false}
pagination={<UserPagination />}
actions={<ListActions />}
>
<Datagrid rowClick="edit">
<ReferenceField
source="Avatar"
reference="users"
link={false}
sortable={false}
>
<ImageField source="avatar_url" title="displayname" />
</ReferenceField>
<TextField source="id" />
{/* Hack since the users endpoint does not give displaynames in the list*/}
<ReferenceField
source="name"
reference="users"
link={false}
sortable={false}
>
<TextField source="displayname" />
</ReferenceField>
<BooleanField source="is_guest" sortable={false} />
<BooleanField source="admin" sortable={false} />
<BooleanField source="deactivated" sortable={false} />
</Datagrid>
</List>
);
// https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/,
"synapseadmin.users.invalid_user_id"
);
export const UserCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Create>
);
export const UserEdit = props => (
<Edit {...props}>
<TabbedForm>
<FormTab label="resources.users.name" icon={<PersonPinIcon />}>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" />
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<ArrayInput source="threepids">
<SimpleFormIterator>
<SelectInput
source="medium"
choices={[
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
]}
/>
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
>
<ReferenceField reference="connections" source="id" addLabel={false}>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
>
<Datagrid style={{ width: "100%" }}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
</TabbedForm>
</Edit>
);

538
src/components/users.jsx Normal file
View File

@ -0,0 +1,538 @@
import React, { cloneElement } from "react";
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
import ContactMailIcon from "@mui/icons-material/ContactMail";
import DevicesIcon from "@mui/icons-material/Devices";
import GetAppIcon from "@mui/icons-material/GetApp";
import NotificationsIcon from "@mui/icons-material/Notifications";
import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import UserIcon from "@mui/icons-material/Group";
import ViewListIcon from "@mui/icons-material/ViewList";
import {
ArrayInput,
ArrayField,
Button,
Datagrid,
DateField,
Create,
Edit,
List,
Toolbar,
SimpleForm,
SimpleFormIterator,
TabbedForm,
FormTab,
BooleanField,
BooleanInput,
PasswordInput,
TextField,
TextInput,
ReferenceField,
ReferenceManyField,
SearchInput,
SelectInput,
BulkDeleteButton,
DeleteButton,
SaveButton,
maxLength,
regex,
required,
useRecordContext,
useTranslate,
Pagination,
CreateButton,
ExportButton,
TopToolbar,
sanitizeListRestProps,
NumberField,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "./AvatarField";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media";
const choices_medium = [
{ id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" },
];
const choices_type = [
{ id: "bot", name: "bot" },
{ id: "support", name: "support" },
];
const date_format = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
const UserListActions = ({
sort,
className,
resource,
filters,
displayedFilters,
exporter, // you can hide ExportButton if exporter = (null || false)
filterValues,
permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false
selectedIds,
onUnselectItems,
showFilter,
maxResults,
total,
...rest
}) => {
return (
<TopToolbar className={className} {...sanitizeListRestProps(rest)}>
{filters &&
cloneElement(filters, {
resource,
showFilter,
displayedFilters,
filterValues,
context: "button",
})}
<CreateButton />
<ExportButton
disabled={total === 0}
resource={resource}
sort={sort}
filter={{ ...filterValues, ...permanentFilter }}
exporter={exporter}
maxResults={maxResults}
/>
{/* Add your custom actions */}
<Button component={Link} to="/import_users" label="CSV Import">
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
</Button>
</TopToolbar>
);
};
UserListActions.defaultProps = {
selectedIds: [],
onUnselectItems: () => null,
};
const UserPagination = () => (
<Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
);
const userFilters = [
<SearchInput source="name" alwaysOn />,
<BooleanInput source="guests" alwaysOn />,
<BooleanInput
label="resources.users.fields.show_deactivated"
source="deactivated"
alwaysOn
/>,
];
const UserBulkActionButtons = () => (
<>
<ServerNoticeBulkButton />
<BulkDeleteButton
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
</>
);
export const UserList = props => (
<List
{...props}
filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />}
pagination={<UserPagination />}
>
<Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField
source="avatar_src"
sx={{ height: "40px", width: "40px" }}
sortBy="avatar_url"
/>
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<DateField
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
options={date_format}
/>
</Datagrid>
</List>
);
// https://matrix.org/docs/spec/appendices#user-identifiers
// here only local part of user_id
// maxLength = 255 - "@" - ":" - localStorage.getItem("home_server").length
// localStorage.getItem("home_server").length is not valid here
const validateUser = [
required(),
maxLength(253),
regex(/^[a-z0-9._=\-/]+$/, "synapseadmin.users.invalid_user_id"),
];
const validateAddress = [required(), maxLength(255)];
export function generateRandomUser() {
const homeserver = localStorage.getItem("home_server");
const user_id =
"@" +
Array(8)
.fill("0123456789abcdefghijklmnopqrstuvwxyz")
.map(
x =>
x[
Math.floor(
(crypto.getRandomValues(new Uint32Array(1))[0] /
(0xffffffff + 1)) *
x.length
)
]
)
.join("") +
":" +
homeserver;
const password = Array(20)
.fill(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
)
.map(
x =>
x[
Math.floor(
(crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)) *
x.length
)
]
)
.join("");
return {
id: user_id,
password: password,
};
}
const UserEditToolbar = props => (
<Toolbar {...props}>
<SaveButton disabled={props.pristine} />
</Toolbar>
);
const UserEditActions = ({ data }) => {
const translate = useTranslate();
var userStatus = "";
if (data) {
userStatus = data.deactivated;
}
return (
<TopToolbar>
{!userStatus && <ServerNoticeButton record={data} />}
<DeleteButton
record={data}
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
/>
</TopToolbar>
);
};
export const UserCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} />
<PasswordInput
source="password"
autoComplete="new-password"
validate={maxLength(512)}
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
resettable
/>
<BooleanInput source="admin" />
<ArrayInput source="threepids">
<SimpleFormIterator disableReordering>
<SelectInput
source="medium"
choices={choices_medium}
validate={required()}
/>
<TextInput source="address" validate={validateAddress} />
</SimpleFormIterator>
</ArrayInput>
<ArrayInput source="external_ids" label="synapseadmin.users.tabs.sso">
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Create>
);
const UserTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
return (
<span>
{translate("resources.users.name", {
smart_count: 1,
})}{" "}
{record ? `"${record.displayname}"` : ""}
</span>
);
};
export const UserEdit = props => {
const translate = useTranslate();
return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab
label={translate("resources.users.name", { smart_count: 1 })}
icon={<PersonPinIcon />}
>
<AvatarField
source="avatar_src"
sortable={false}
sx={{ height: "120px", width: "120px", float: "right" }}
/>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput
source="password"
autoComplete="new-password"
helperText="resources.users.helper.password"
/>
<SelectInput
source="user_type"
choices={choices_type}
translateChoice={false}
resettable
/>
<BooleanInput source="admin" />
<BooleanInput
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
<DateField source="creation_ts_ms" showTime options={date_format} />
<TextField source="consent_version" />
</FormTab>
<FormTab
label="resources.users.threepid"
icon={<ContactMailIcon />}
path="threepid"
>
<ArrayInput source="threepids">
<SimpleFormIterator disableReordering>
<SelectInput source="medium" choices={choices_medium} />
<TextInput source="address" />
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
label="synapseadmin.users.tabs.sso"
icon={<AssignmentIndIcon />}
path="sso"
>
<ArrayInput source="external_ids" label={false}>
<SimpleFormIterator disableReordering>
<TextInput source="auth_provider" validate={required()} />
<TextInput
source="external_id"
label="resources.users.fields.id"
validate={required()}
/>
</SimpleFormIterator>
</ArrayInput>
</FormTab>
<FormTab
label={translate("resources.devices.name", { smart_count: 2 })}
icon={<DevicesIcon />}
path="devices"
>
<ReferenceManyField
reference="devices"
target="user_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }}>
<TextField source="device_id" sortable={false} />
<TextField source="display_name" sortable={false} />
<TextField source="last_seen_ip" sortable={false} />
<DateField
source="last_seen_ts"
showTime
options={date_format}
sortable={false}
/>
<DeviceRemoveButton />
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab
label="resources.connections.name"
icon={<SettingsInputComponentIcon />}
path="connections"
>
<ReferenceField
reference="connections"
source="id"
addLabel={false}
link={false}
>
<ArrayField
source="devices[].sessions[0].connections"
label="resources.connections.name"
>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="ip" sortable={false} />
<DateField
source="last_seen"
showTime
options={date_format}
sortable={false}
/>
<TextField
source="user_agent"
sortable={false}
style={{ width: "100%" }}
/>
</Datagrid>
</ArrayField>
</ReferenceField>
</FormTab>
<FormTab
label={translate("resources.users_media.name", { smart_count: 2 })}
icon={<PermMediaIcon />}
path="media"
>
<ReferenceManyField
reference="users_media"
target="user_id"
addLabel={false}
pagination={<UserPagination />}
perPage={50}
sort={{ field: "created_ts", order: "DESC" }}
>
<Datagrid style={{ width: "100%" }}>
<DateField source="created_ts" showTime options={date_format} />
<DateField
source="last_access_ts"
showTime
options={date_format}
/>
<TextField source="media_id" />
<NumberField source="media_length" />
<TextField source="media_type" />
<TextField source="upload_name" />
<TextField source="quarantined_by" />
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
<DeleteButton mutationMode="pessimistic" redirect={false} />
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab
label={translate("resources.rooms.name", { smart_count: 2 })}
icon={<ViewListIcon />}
path="rooms"
>
<ReferenceManyField
reference="joined_rooms"
target="user_id"
addLabel={false}
>
<Datagrid
style={{ width: "100%" }}
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
bulkActionButtons={false}
>
<TextField
source="id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
<ReferenceField
label="resources.rooms.fields.name"
source="id"
reference="rooms"
sortable={false}
link=""
>
<TextField source="name" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab
label={translate("resources.pushers.name", { smart_count: 2 })}
icon={<NotificationsIcon />}
path="pushers"
>
<ReferenceManyField
reference="pushers"
target="user_id"
addLabel={false}
>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="kind" sortable={false} />
<TextField source="app_display_name" sortable={false} />
<TextField source="app_id" sortable={false} />
<TextField source="data.url" sortable={false} />
<TextField source="device_display_name" sortable={false} />
<TextField source="lang" sortable={false} />
<TextField source="profile_tag" sortable={false} />
<TextField source="pushkey" sortable={false} />
</Datagrid>
</ReferenceManyField>
</FormTab>
</TabbedForm>
</Edit>
);
};
const resource = {
name: "users",
icon: UserIcon,
list: UserList,
edit: UserEdit,
create: UserCreate,
};
export default resource;

View File

@ -1,29 +1,113 @@
import germanMessages from "ra-language-german"; import germanMessages from "ra-language-german";
export default { const de = {
...germanMessages, ...germanMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
homeserver: "Heimserver", base_url: "Heimserver URL",
welcome: "Willkommen bei Synapse-admin", welcome: "Willkommen bei Synapse-admin",
server_version: "Synapse Version",
supports_specs: "unterstützt Matrix-Specs",
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL",
sso_sign_in: "Anmeldung mit SSO",
}, },
users: { users: {
invalid_user_id: invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver", tabs: { sso: "SSO" },
},
rooms: {
details: "Raumdetails",
tabs: {
basic: "Allgemein",
members: "Mitglieder",
detail: "Details",
permission: "Berechtigungen",
},
},
reports: { tabs: { basic: "Allgemein", detail: "Details" } },
},
import_users: {
error: {
at_entry: "Bei Eintrag %{entry}: %{message}",
error: "Fehler",
required_field: "Pflichtfeld '%{field}' fehlt",
invalid_value:
"Ungültiger Wert in Zeile %{row}. Feld '%{field}' darf nur die Werte 'true' oder 'false' enthalten",
unreasonably_big: "Datei ist zu groß für den Import (%{size} Megabytes)",
already_in_progress: "Es läuft bereits ein Import",
id_exits: "ID %{id} existiert bereits",
},
title: "Benutzer aus CSV importieren",
goToPdf: "Gehe zum PDF",
cards: {
importstats: {
header: "Benutzer importieren",
users_total:
"%{smart_count} Benutzer in der CSV Datei |||| %{smart_count} Benutzer in der CSV Datei",
guest_count: "%{smart_count} Gast |||| %{smart_count} Gäste",
admin_count:
"%{smart_count} Server Administrator |||| %{smart_count} Server Administratoren",
},
conflicts: {
header: "Konfliktstrategie",
mode: {
stop: "Stoppe bei Fehlern",
skip: "Zeige Fehler und überspringe fehlerhafte Einträge",
},
},
ids: {
header: "IDs",
all_ids_present: "IDs in jedem Eintrag vorhanden",
count_ids_present:
"%{smart_count} Eintrag mit ID |||| %{smart_count} Einträge mit IDs",
mode: {
ignore: "Ignoriere IDs der CSV-Datei und erstelle neue",
update: "Aktualisiere existierende Benutzer",
},
},
passwords: {
header: "Passwörter",
all_passwords_present: "Passwörter in jedem Eintrag vorhanden",
count_passwords_present:
"%{smart_count} Eintrag mit Passwort |||| %{smart_count} Einträge mit Passwörtern",
use_passwords: "Verwende Passwörter aus der CSV Datei",
},
upload: {
header: "CSV Datei importieren",
explanation:
"Hier können Sie eine Datei mit kommagetrennten Daten hochladen, die verwendet werden um Benutzer anzulegen oder zu ändern. Die Datei muss mindestens die Felder 'id' und 'displayname' enthalten. Hier können Sie eine Beispieldatei herunterladen und anpassen: ",
},
startImport: {
simulate_only: "Nur simulieren",
run_import: "Importieren",
},
results: {
header: "Ergebnis",
total:
"%{smart_count} Eintrag insgesamt |||| %{smart_count} Einträge insgesamt",
successful: "%{smart_count} Einträge erfolgreich importiert",
skipped: "%{smart_count} Einträge übersprungen",
download_skipped: "Übersprungene Einträge herunterladen",
with_error:
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
simulated_only: "Import-Vorgang war nur simuliert",
},
}, },
}, },
resources: { resources: {
users: { users: {
backtolist: "Zurück zur Liste",
name: "Benutzer", name: "Benutzer",
email: "E-Mail", email: "E-Mail",
msisdn: "Telefon", msisdn: "Telefon",
threepid: "E-Mail / Telefon",
fields: { fields: {
avatar: "Avatar", avatar: "Avatar",
id: "Benutzer-ID", id: "Benutzer-ID",
name: "Name", name: "Name",
is_guest: "Gast", is_guest: "Gast",
admin: "Admin", admin: "Server Administrator",
deactivated: "Deaktiviert", deactivated: "Deaktiviert",
guests: "Zeige Gäste", guests: "Zeige Gäste",
show_deactivated: "Zeige deaktivierte Benutzer", show_deactivated: "Zeige deaktivierte Benutzer",
@ -31,12 +115,24 @@ export default {
displayname: "Anzeigename", displayname: "Anzeigename",
password: "Passwort", password: "Passwort",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Adresse", address: "Adresse",
creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen",
auth_provider: "Provider",
user_type: "Benutzertyp",
}, },
helper: { helper: {
deactivate: "Deaktivierte Nutzer können nicht wieder aktiviert werden.", 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",
},
action: {
erase: "Lösche Benutzerdaten",
}, },
}, },
rooms: { rooms: {
@ -46,6 +142,81 @@ export default {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
joined_local_members: "Lokale Mitglieder",
joined_local_devices: "Lokale Endgeräte",
state_events: "Zustandsereignisse / Komplexität",
version: "Version",
is_encrypted: "Verschlüsselt",
encryption: "Verschlüsselungs-Algorithmus",
federatable: "Fö­de­rierbar",
public: "Sichtbar im Raumverzeichnis",
creator: "Ersteller",
join_rules: "Beitrittsregeln",
guest_access: "Gastzugriff",
history_visibility: "Historie-Sichtbarkeit",
topic: "Thema",
avatar: "Avatar",
},
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: {
join_rules: {
public: "Öffentlich",
knock: "Auf Anfrage",
invite: "Nur auf Einladung",
private: "Privat",
},
guest_access: {
can_join: "Gäste können beitreten",
forbidden: "Gäste können nicht beitreten",
},
history_visibility: {
invited: "Ab Einladung",
joined: "Ab Beitritt",
shared: "Ab Setzen der Einstellung",
world_readable: "Jeder",
},
unencrypted: "Nicht verschlüsselt",
},
action: {
erase: {
title: "Raum löschen",
content:
"Sind Sie sicher dass Sie den Raum löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten und Medien, die der Raum beinhaltet werden vom Server gelöscht!",
},
},
},
reports: {
name: "Gemeldetes Ereignis |||| Gemeldete Ereignisse",
fields: {
id: "ID",
received_ts: "Meldezeit",
user_id: "Meldender",
name: "Raumname",
score: "Wert",
reason: "Grund",
event_id: "Event-ID",
event_json: {
origin: "Ursprungsserver",
origin_server_ts: "Sendezeit",
type: "Eventtyp",
content: {
msgtype: "Inhaltstyp",
body: "Nachrichteninhalt",
format: "Nachrichtenformat",
formatted_body: "Formatierter Nachrichteninhalt",
algorithm: "Verschlüsselungsalgorithmus",
},
},
},
action: {
erase: {
title: "Gemeldetes Event löschen",
content:
"Sind Sie sicher dass Sie das gemeldete Event löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
},
}, },
}, },
connections: { connections: {
@ -56,5 +227,199 @@ export default {
user_agent: "User Agent", user_agent: "User Agent",
}, },
}, },
devices: {
name: "Gerät |||| Geräte",
fields: {
device_id: "Geräte-ID",
display_name: "Gerätename",
last_seen_ts: "Zeitstempel",
last_seen_ip: "IP-Adresse",
},
action: {
erase: {
title: "Entferne %{id}",
content: 'Möchten Sie das Gerät "%{name}" wirklich entfernen?',
success: "Gerät erfolgreich entfernt.",
failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
},
users_media: {
name: "Medien",
fields: {
media_id: "Medien ID",
media_length: "Größe",
media_type: "Typ",
upload_name: "Dateiname",
quarantined_by: "Zur Quarantäne hinzugefügt",
safe_from_quarantine: "Schutz vor Quarantäne",
created_ts: "Erstellt",
last_access_ts: "Letzter Zugriff",
},
},
delete_media: {
name: "Medien",
fields: {
before_ts: "Letzter Zugriff vor",
size_gt: "Größer als (in Bytes)",
keep_profiles: "Behalte Profilbilder",
},
action: {
send: "Medien löschen",
send_success: "Anfrage erfolgreich versendet.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
helper: {
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
},
},
protect_media: {
action: {
create: "Ungeschützt, Schutz erstellen",
delete: "Geschützt, Schutz aufheben",
none: "In Quarantäne",
send_success: "Erfolgreich den Schutz-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
},
quarantine_media: {
action: {
name: "Quarantäne",
create: "Zur Quarantäne hinzufügen",
delete: "In Quarantäne, Quarantäne aufheben",
none: "Geschützt vor Quarantäne",
send_success: "Erfolgreich den Quarantäne-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
},
pushers: {
name: "Pusher |||| Pushers",
fields: {
app: "App",
app_display_name: "App-Anzeigename",
app_id: "App ID",
device_display_name: "Geräte-Anzeigename",
kind: "Art",
lang: "Sprache",
profile_tag: "Profil-Tag",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "Serverbenachrichtigungen",
send: "Servernachricht versenden",
fields: {
body: "Nachricht",
},
action: {
send: "Sende Nachricht",
send_success: "Nachricht erfolgreich versendet.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
},
helper: {
send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
},
},
user_media_statistics: {
name: "Dateien je Benutzer",
fields: {
media_count: "Anzahl der Dateien",
media_length: "Größe der Dateien",
},
},
forward_extremities: {
name: "Vorderextremitäten",
fields: {
id: "Event-ID",
received_ts: "Zeitstempel",
depth: "Tiefe",
state_group: "Zustandsgruppe",
},
},
room_state: {
name: "Zustandsereignisse",
fields: {
type: "Typ",
content: "Inhalt",
origin_server_ts: "Sendezeit",
sender: "Absender",
},
},
room_directory: {
name: "Raumverzeichnis",
fields: {
world_readable: "Gastbenutzer dürfen ohne Beitritt lesen",
guest_can_join: "Gastbenutzer dürfen beitreten",
},
action: {
title:
"Raum aus Verzeichnis löschen |||| %{smart_count} Räume aus Verzeichnis löschen",
content:
"Möchten Sie den Raum wirklich aus dem Raumverzeichnis löschen? |||| Möchten Sie die %{smart_count} Räume wirklich aus dem Raumverzeichnis löschen?",
erase: "Lösche aus Verzeichnis",
create: "Eintragen ins Verzeichnis",
send_success: "Raum erfolgreich eingetragen.",
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
destinations: {
name: "Föderation",
fields: {
destination: "Ziel",
failure_ts: "Fehlerzeitpunkt",
retry_last_ts: "Letzter Wiederholungsversuch",
retry_interval: "Wiederholungsintervall",
last_successful_stream_ordering: "letzte erfogreicher Stream",
stream_ordering: "Stream",
},
action: { reconnect: "Neu verbinden" },
},
registration_tokens: {
name: "Registrierungstoken",
fields: {
token: "Token",
valid: "Gültige Token",
uses_allowed: "Anzahl",
pending: "Ausstehend",
completed: "Abgeschlossen",
expiry_time: "Ablaufzeit",
length: "Länge",
},
helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
},
},
ra: {
...germanMessages.ra,
action: {
...germanMessages.ra.action,
unselect: "Abwählen",
},
auth: {
...germanMessages.ra.auth,
auth_check_error: "Anmeldung fehlgeschlagen",
},
input: {
...germanMessages.ra.input,
password: {
...germanMessages.ra.input.password,
toggle_hidden: "Anzeigen",
toggle_visible: "Verstecken",
},
},
notification: {
...germanMessages.ra.notification,
logged_out: "Abgemeldet",
},
page: {
...germanMessages.ra.page,
empty: "Keine Einträge vorhanden",
invite: "",
},
navigation: {
...germanMessages.ra.navigation,
skip_nav: "Zum Inhalt springen",
},
}, },
}; };
export default de;

View File

@ -1,29 +1,112 @@
import englishMessages from "ra-language-english"; import englishMessages from "ra-language-english";
export default { const en = {
...englishMessages, ...englishMessages,
synapseadmin: { synapseadmin: {
auth: { auth: {
homeserver: "Homeserver", base_url: "Homeserver URL",
welcome: "Welcome to Synapse-admin", welcome: "Welcome to Synapse-admin",
server_version: "Synapse version",
supports_specs: "supports Matrix specs",
username_error: "Please enter fully qualified user ID: '@user:domain'",
protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL",
sso_sign_in: "Sign in with SSO",
}, },
users: { users: {
invalid_user_id: invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver", tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "Basic",
members: "Members",
detail: "Details",
permission: "Permissions",
},
},
reports: { tabs: { basic: "Basic", detail: "Details" } },
},
import_users: {
error: {
at_entry: "At entry %{entry}: %{message}",
error: "Error",
required_field: "Required field '%{field}' is not present",
invalid_value:
"Invalid value on line %{row}. '%{field}' field may only be 'true' or 'false'",
unreasonably_big:
"Refused to load unreasonably big file of %{size} megabytes",
already_in_progress: "An import run is already in progress",
id_exits: "ID %{id} already present",
},
title: "Import users via CSV",
goToPdf: "Go to PDF",
cards: {
importstats: {
header: "Import users",
users_total:
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
},
conflicts: {
header: "Conflict strategy",
mode: {
stop: "Stop on conflict",
skip: "Show error and skip on conflict",
},
},
ids: {
header: "IDs",
all_ids_present: "IDs present on every entry",
count_ids_present:
"%{smart_count} entry with ID |||| %{smart_count} entries with IDs",
mode: {
ignore: "Ignore IDs in CSV and create new ones",
update: "Update existing records",
},
},
passwords: {
header: "Passwords",
all_passwords_present: "Passwords present on every entry",
count_passwords_present:
"%{smart_count} entry with password |||| %{smart_count} entries with passwords",
use_passwords: "Use passwords from CSV",
},
upload: {
header: "Input CSV file",
explanation:
"Here you can upload a file with comma separated values that is processed to create or update users. The file must include the fields 'id' and 'displayname'. You can download and adapt an example file here: ",
},
startImport: {
simulate_only: "Simulate only",
run_import: "Import",
},
results: {
header: "Import results",
total:
"%{smart_count} entry in total |||| %{smart_count} entries in total",
successful: "%{smart_count} entries successfully imported",
skipped: "%{smart_count} entries skipped",
download_skipped: "Download skipped records",
with_error:
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated",
},
}, },
}, },
resources: { resources: {
users: { users: {
backtolist: "Back to list",
name: "User |||| Users", name: "User |||| Users",
email: "Email", email: "Email",
msisdn: "Phone", msisdn: "Phone",
threepid: "Email / Phone",
fields: { fields: {
avatar: "Avatar", avatar: "Avatar",
id: "User-ID", id: "User-ID",
name: "Name", name: "Name",
is_guest: "Guest", is_guest: "Guest",
admin: "Admin", admin: "Server Administrator",
deactivated: "Deactivated", deactivated: "Deactivated",
guests: "Show guests", guests: "Show guests",
show_deactivated: "Show deactivated users", show_deactivated: "Show deactivated users",
@ -31,12 +114,22 @@ export default {
displayname: "Displayname", displayname: "Displayname",
password: "Password", password: "Password",
avatar_url: "Avatar URL", avatar_url: "Avatar URL",
avatar_src: "Avatar",
medium: "Medium", medium: "Medium",
threepids: "3PIDs", threepids: "3PIDs",
address: "Address", address: "Address",
creation_ts_ms: "Creation timestamp",
consent_version: "Consent version",
auth_provider: "Provider",
user_type: "User type",
}, },
helper: { helper: {
deactivate: "Deactivated users cannot be reactivated", 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",
},
action: {
erase: "Erase user data",
}, },
}, },
rooms: { rooms: {
@ -46,6 +139,81 @@ export default {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Members", joined_members: "Members",
joined_local_members: "Local members",
joined_local_devices: "Local devices",
state_events: "State events / Complexity",
version: "Version",
is_encrypted: "Encrypted",
encryption: "Encryption",
federatable: "Federatable",
public: "Visible in room directory",
creator: "Creator",
join_rules: "Join rules",
guest_access: "Guest access",
history_visibility: "History visibility",
topic: "Topic",
avatar: "Avatar",
},
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: {
join_rules: {
public: "Public",
knock: "Knock",
invite: "Invite",
private: "Private",
},
guest_access: {
can_join: "Guests can join",
forbidden: "Guests can not join",
},
history_visibility: {
invited: "Since invited",
joined: "Since joined",
shared: "Since shared",
world_readable: "Anyone",
},
unencrypted: "Unencrypted",
},
action: {
erase: {
title: "Delete room",
content:
"Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
},
},
},
reports: {
name: "Reported event |||| Reported events",
fields: {
id: "ID",
received_ts: "report time",
user_id: "announcer",
name: "name of the room",
score: "score",
reason: "reason",
event_id: "event ID",
event_json: {
origin: "origin server",
origin_server_ts: "time of send",
type: "event type",
content: {
msgtype: "content type",
body: "content",
format: "format",
formatted_body: "formatted content",
algorithm: "algorithm",
},
},
},
action: {
erase: {
title: "Delete reported event",
content:
"Are you sure you want to delete the reported event? This cannot be undone.",
},
}, },
}, },
connections: { connections: {
@ -56,5 +224,167 @@ export default {
user_agent: "User agent", user_agent: "User agent",
}, },
}, },
devices: {
name: "Device |||| Devices",
fields: {
device_id: "Device-ID",
display_name: "Device name",
last_seen_ts: "Timestamp",
last_seen_ip: "IP address",
},
action: {
erase: {
title: "Removing %{id}",
content: 'Are you sure you want to remove the device "%{name}"?',
success: "Device successfully removed.",
failure: "An error has occurred.",
},
},
},
users_media: {
name: "Media",
fields: {
media_id: "Media ID",
media_length: "File Size (in Bytes)",
media_type: "Type",
upload_name: "File name",
quarantined_by: "Quarantined by",
safe_from_quarantine: "Safe from quarantine",
created_ts: "Created",
last_access_ts: "Last access",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "last access before",
size_gt: "Larger then (in bytes)",
keep_profiles: "Keep profile images",
},
action: {
send: "Delete media",
send_success: "Request successfully sent.",
send_failure: "An error has occurred.",
},
helper: {
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
},
},
protect_media: {
action: {
create: "Unprotected, create protection",
delete: "Protected, remove protection",
none: "In quarantine",
send_success: "Successfully changed the protection status.",
send_failure: "An error has occurred.",
},
},
quarantine_media: {
action: {
name: "Quarantine",
create: "Add to quarantine",
delete: "In quarantine, unquarantine",
none: "Protected from quarantine",
send_success: "Successfully changed the quarantine status.",
send_failure: "An error has occurred.",
},
},
pushers: {
name: "Pusher |||| Pushers",
fields: {
app: "App",
app_display_name: "App display name",
app_id: "App ID",
device_display_name: "Device display name",
kind: "Kind",
lang: "Language",
profile_tag: "Profile tag",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "Server Notices",
send: "Send server notices",
fields: {
body: "Message",
},
action: {
send: "Send note",
send_success: "Server notice successfully sent.",
send_failure: "An error has occurred.",
},
helper: {
send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
},
},
user_media_statistics: {
name: "Users' media",
fields: {
media_count: "Media count",
media_length: "Media length",
},
},
forward_extremities: {
name: "Forward Extremities",
fields: {
id: "Event ID",
received_ts: "Timestamp",
depth: "Depth",
state_group: "State group",
},
},
room_state: {
name: "State events",
fields: {
type: "Type",
content: "Content",
origin_server_ts: "time of send",
sender: "Sender",
},
},
room_directory: {
name: "Room directory",
fields: {
world_readable: "guest users may view without joining",
guest_can_join: "guest users may join",
},
action: {
title:
"Delete room from directory |||| Delete %{smart_count} rooms from directory",
content:
"Are you sure you want to remove this room from directory? |||| Are you sure you want to remove these %{smart_count} rooms from directory?",
erase: "Delete from room directory",
create: "Publish in room directory",
send_success: "Room successfully published.",
send_failure: "An error has occurred.",
},
},
destinations: {
name: "Federation",
fields: {
destination: "Destination",
failure_ts: "Failure timestamp",
retry_last_ts: "Last retry timestamp",
retry_interval: "Retry interval",
last_successful_stream_ordering: "Last successful stream",
stream_ordering: "Stream",
},
action: { reconnect: "Reconnect" },
},
},
registration_tokens: {
name: "Registration tokens",
fields: {
token: "Token",
valid: "Valid token",
uses_allowed: "Uses allowed",
pending: "Pending",
completed: "Completed",
expiry_time: "Expiry time",
length: "Length",
},
helper: { length: "Length of the token if no token is given." },
}, },
}; };
export default en;

382
src/i18n/fa.js Normal file
View File

@ -0,0 +1,382 @@
import farsiMessages from "ra-language-farsi";
const fa = {
...farsiMessages,
synapseadmin: {
auth: {
base_url: "آدرس سرور",
welcome: "به پنل مدیریت سیناپس خوش آمدید!",
server_version: "نسخه",
username_error: "لطفاً شناسه کاربر را وارد کنید: '@user:domain'",
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
url_error: "آدرس وارد شده یک سرور معتبر نیست",
sso_sign_in: "با SSO وارد شوید",
},
users: {
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "اصلی",
members: "اعضا",
detail: "جزئیات",
permission: "مجوزها",
},
},
reports: { tabs: { basic: "اصلی", detail: "جزئیات" } },
},
import_users: {
error: {
at_entry: "در هنگام ورود %{entry}: %{message}",
error: "Error",
required_field: "فیلد الزامی '%{field}' وجود ندارد",
invalid_value:
"خطا در خط %{row}. '%{field}' فیلد ممکن است فقط 'درست' یا 'نادرست' باشد",
unreasonably_big:
"از بارگذاری فایل هایی با حجم غیر منطقی خودداری کنید %{size} مگابایت",
already_in_progress: "یک بارگذاری از قبل در حال انجام است",
id_exits: "شناسه %{id} موجود است",
},
title: "کاربران را از طریق فایل CSV وارد کنید",
goToPdf: "رفتن به PDF",
cards: {
importstats: {
header: "وارد کردن کاربران",
users_total:
"%{smart_count} user in CSV file |||| %{smart_count} users in CSV file",
guest_count: "%{smart_count} guest |||| %{smart_count} guests",
admin_count: "%{smart_count} admin |||| %{smart_count} admins",
},
conflicts: {
header: "استراتژی متغارض",
mode: {
stop: "توقف",
skip: "نمایش خطا و رد شدن",
},
},
ids: {
header: "شناسنامه ها",
all_ids_present: "شناسه های موجود در هر ورودی",
count_ids_present:
"%{smart_count} ورود با شناسه |||| %{smart_count} ورودی با شناسه",
mode: {
ignore: "شناسه ها را در CSV نادیده بگیر و شناسه های جدید ایجاد کن",
update: "سوابق موجود را به روز کنید",
},
},
passwords: {
header: "رمز عبور",
all_passwords_present: "رمزهای عبور موجود در هر ورودی",
count_passwords_present:
"%{smart_count} ورود با رمز عبور |||| %{smart_count} ورودی با رمز عبور",
use_passwords: "از پسوردهای CSV استفاده کنید",
},
upload: {
header: "Input CSV file",
explanation:
"در اینجا می توانید فایلی را با مقادیر جدا شده با کاما بارگذاری کنید که برای ایجاد یا به روز رسانی کاربران پردازش می شود. فایل باید شامل فیلدهای 'id' و 'displayname' باشد. می توانید یک فایل نمونه را از اینجا دانلود و تطبیق دهید: ",
},
startImport: {
simulate_only: "فقط شبیه سازی",
run_import: "بارگذاری",
},
results: {
header: "بارگذاری نتایج",
total: "%{smart_count} ورودی در کل |||| %{smart_count} ورودی ها در کل",
successful: "%{smart_count} ورودی ها با موفقیت وارد شدند",
skipped: "%{smart_count} ورودی ها نادیده گرفته شدند",
download_skipped: "دانلود رکوردهای نادیده گرفته شده",
with_error:
"%{smart_count} ورود با خطا ||| %{smart_count} ورودی های دارای خطا",
simulated_only: "اجرا فقط شبیه سازی شد",
},
},
},
resources: {
users: {
name: "کاربر |||| کاربران",
email: "ایمیل",
msisdn: "شماره تلفن",
threepid: "ایمیل / شماره تلفن",
fields: {
avatar: "آواتار",
id: "شناسه کاربر",
name: "نام",
is_guest: "مهمان",
admin: "مدیر سرور",
deactivated: "غیرفعال",
guests: "نمایش مهمانان",
show_deactivated: "نمایش کاربران غیرفعال شده",
user_id: "جستجوی کاربر",
displayname: "نام نمایشی",
password: "رمز عبور",
avatar_url: "آواتار سرور",
avatar_src: "آواتار",
medium: "متوسط",
threepids: "سرویس احراز هویت",
address: "آدرس",
creation_ts_ms: "ساخته شده در",
consent_version: "Consent نسخه",
auth_provider: "ارائه دهنده",
user_type: "نوع کاربر",
},
helper: {
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
},
action: {
erase: "پاک کردن اطلاعات کاربر",
},
},
rooms: {
name: "اتاق |||| اتاق ها",
fields: {
room_id: "شناسه اتاق",
name: "نام",
canonical_alias: "نام مستعار",
joined_members: "اعضا",
joined_local_members: "اعضای محلی",
joined_local_devices: "دستگاه های محلی",
state_events: "رویدادهای حالت / پیچیدگی",
version: "نسخه",
is_encrypted: "رمزگذاری شده است",
encryption: "رمزگذاری",
federatable: "Federatable",
public: "قابل مشاهده در فهرست اتاق",
creator: "سازنده",
join_rules: "به قوانین بپیوندید",
guest_access: "دسترسی مهمان",
history_visibility: "مشاهده تاریخچه",
topic: "موضوع",
avatar: "آواتار",
},
helper: {
forward_extremities:
"اندام های رو به جلو، رویدادهای برگ در انتهای نمودار غیر چرخه ای جهت دار (DAG) در یک اتاق هستند، رویدادهایی که فرزند ندارند. هر چه تعداد بیشتری در یک اتاق وجود داشته باشد، وضوح حالت بیشتری را که سیناپس باید انجام دهد (نکته: این یک عملیات گران است). در حالی که 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:
"آیا مطمئن هستید که می خواهید اتاق را حذف کنید؟ این قابل بازگشت نیست. همه پیام ها و رسانه های مشترک در اتاق از سرور حذف می شوند!",
},
},
},
reports: {
name: "رویداد گزارش شده |||| رویدادهای گزارش شده",
fields: {
id: "شناسه",
received_ts: "زمان گزارش",
user_id: "گوینده",
name: "نام اتاق",
score: "نمره",
reason: "دلیل",
event_id: "شناسه رویداد",
event_json: {
origin: "سرور مبدا",
origin_server_ts: "زمان ارسال",
type: "نوع رویداد",
content: {
msgtype: "نوع محتوا",
body: "محتوا",
format: "قالب",
formatted_body: "محتوای قالب بندی شده",
algorithm: "الگوریتم",
},
},
},
},
connections: {
name: "اتصالات",
fields: {
last_seen: "تاریخ",
ip: "آدرس آی پی",
user_agent: "نماینده کاربر",
},
},
devices: {
name: "دستگاه |||| دستگاه ها",
fields: {
device_id: "شناسه دستگاه",
display_name: "نام دستگاه",
last_seen_ts: "مهر زمان",
last_seen_ip: "آدرس آی پی",
},
action: {
erase: {
title: "حذف کردن %{id}",
content:
'آیا مطمئن هستید که می خواهید دستگاه را حذف کنید؟ "%{name}"?',
success: "دستگاه با موفقیت حذف شد.",
failure: "خطایی رخ داده است.",
},
},
},
users_media: {
name: "رسانه ها",
fields: {
media_id: "شناسه رسانه",
media_length: "اندازه فایل (به بایت)",
media_type: "نوع",
upload_name: "نام فایل",
quarantined_by: "قرنطینه شده توسط",
safe_from_quarantine: "امان از قرنطینه",
created_ts: "ایجاد شده",
last_access_ts: "آخرین دسترسی",
},
},
delete_media: {
name: "رسانه ها",
fields: {
before_ts: "آخرین دسترسی قبل",
size_gt: "بزرگتر از آن (به بایت)",
keep_profiles: "تصاویر پروفایل را نگه دارید",
},
action: {
send: "حذف رسانه ها",
send_success: "درخواست با موفقیت ارسال شد.",
send_failure: "خطایی رخ داده است.",
},
helper: {
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانه‌هایی که در مخازن رسانه خارجی آپلود شده‌اند تأثیری نخواهد گذاشت.",
},
},
protect_media: {
action: {
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: "شناسه برنامه",
device_display_name: "نام نمایشی برنامه",
kind: "نوع",
lang: "زبان",
profile_tag: "برچسب پروفایل",
pushkey: "کلید",
data: { url: "URL" },
},
},
servernotices: {
name: "اطلاعیه های سرور",
send: "ارسال اعلانات سرور",
fields: {
body: "پیام",
},
action: {
send: "ارسال یادداشت",
send_success: "اعلان سرور با موفقیت ارسال شد.",
send_failure: "خطایی رخ داده است.",
},
helper: {
send: "اعلان سرور را برای کاربران انتخاب شده ارسال می کند. ویژگی 'اعلامیه های سرور' باید در سرور فعال شود.",
},
},
user_media_statistics: {
name: "رسانه کاربران",
fields: {
media_count: "شمارش رسانه ها",
media_length: "طول رسانه",
},
},
forward_extremities: {
name: "Forward Extremities",
fields: {
id: "شناسه رویداد",
received_ts: "مهر زمان",
depth: "عمق",
state_group: "گروه دولتی",
},
},
room_state: {
name: "رویدادهای وضعیت",
fields: {
type: "نوع",
content: "محتوا",
origin_server_ts: "زمان ارسال",
sender: "فرستنده",
},
},
room_directory: {
name: "راهنمای اتاق",
fields: {
world_readable: "کاربران مهمان می توانند بدون عضویت مشاهده کنند",
guest_can_join: "کاربران مهمان ممکن است ملحق شوند",
},
action: {
title:
"اتاق را از فهرست حذف کنید |||| حذف کنید %{smart_count} اتاق ها از دایرکتوری",
content:
"آیا مطمئنید که می خواهید این اتاق را از فهرست راهنمای حذف کنید؟ |||| آیا مطمئن هستید که می خواهید این موارد را %{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 fa;

377
src/i18n/fr.js Normal file
View File

@ -0,0 +1,377 @@
import frenchMessages from "ra-language-french";
const fr = {
...frenchMessages,
synapseadmin: {
auth: {
base_url: "URL du serveur daccueil",
welcome: "Bienvenue sur Synapse-admin",
server_version: "Version du serveur Synapse",
username_error:
"Veuillez entrer un nom d'utilisateur complet : « @utilisateur:domaine »",
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",
},
users: {
invalid_user_id:
"Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
tabs: { sso: "Authentification unique" },
},
rooms: {
tabs: {
basic: "Informations de base",
members: "Membres",
detail: "Détails",
permission: "Permissions",
},
},
reports: { tabs: { basic: "Informations de base", detail: "Détails" } },
},
import_users: {
error: {
at_entry: "Pour l'entrée %{entry} : %{message}",
error: "Erreur",
required_field: "Le champ requis « %{field} » est manquant",
invalid_value:
"Valeur non valide à la ligne %{row}. Le champ « %{field} » ne peut être que « true » ou « false »",
unreasonably_big:
"Refus de charger un fichier trop volumineux de %{size} mégaoctets",
already_in_progress: "Un import est déjà en cours",
id_exits: "L'identifiant %{id} déjà présent",
},
title: "Importer des utilisateurs à partir d'un fichier CSV",
goToPdf: "Voir le PDF",
cards: {
importstats: {
header: "Importer des utilisateurs",
users_total:
"%{smart_count} utilisateur dans le fichier CSV |||| %{smart_count} utilisateurs dans le fichier CSV",
guest_count: "%{smart_count} visiteur |||| %{smart_count} visiteurs",
admin_count:
"%{smart_count} administrateur |||| %{smart_count} administrateurs",
},
conflicts: {
header: "Stratégie de résolution des conflits",
mode: {
stop: "S'arrêter en cas de conflit",
skip: "Afficher l'erreur et ignorer le conflit",
},
},
ids: {
header: "Identifiants",
all_ids_present: "Identifiants présents pour chaque entrée",
count_ids_present:
"%{smart_count} entrée avec identifiant |||| %{smart_count} entrées avec identifiant",
mode: {
ignore:
"Ignorer les identifiants dans le ficher CSV et en créer de nouveaux",
update: "Mettre à jour les enregistrements existants",
},
},
passwords: {
header: "Mots de passe",
all_passwords_present: "Mots de passe présents pour chaque entrée",
count_passwords_present:
"%{smart_count} entrée avec mot de passe |||| %{smart_count} entrées avec mot de passe",
use_passwords: "Utiliser les mots de passe provenant du fichier CSV",
},
upload: {
header: "Fichier CSV en entrée",
explanation:
"Vous pouvez télécharger ici un fichier contenant des valeurs séparées par des virgules qui sera traité pour créer ou mettre à jour des utilisateurs. Le fichier doit inclure les champs « id » et « displayname ». Vous pouvez télécharger et adapter un fichier d'exemple ici : ",
},
startImport: {
simulate_only: "Simuler",
run_import: "Importer",
},
results: {
header: "Résultats de l'import",
total:
"%{smart_count} entrée au total |||| %{smart_count} entrées au total",
successful: "%{smart_count} entrées importées avec succès",
skipped: "%{smart_count} entrées ignorées",
download_skipped: "Télécharger les entrées ignorées",
with_error:
"%{smart_count} entrée avec des erreurs ||| %{smart_count} entrées avec des erreurs",
simulated_only: "L'import était simulé",
},
},
},
resources: {
users: {
name: "Utilisateur |||| Utilisateurs",
email: "Adresse électronique",
msisdn: "Numéro de téléphone",
threepid: "Adresse électronique / Numéro de téléphone",
fields: {
avatar: "Avatar",
id: "Identifiant",
name: "Nom",
is_guest: "Visiteur",
admin: "Administrateur du serveur",
deactivated: "Désactivé",
guests: "Afficher les visiteurs",
show_deactivated: "Afficher les utilisateurs désactivés",
user_id: "Rechercher un utilisateur",
displayname: "Nom d'affichage",
password: "Mot de passe",
avatar_url: "URL de l'avatar",
avatar_src: "Avatar",
medium: "Type",
threepids: "Identifiants tiers",
address: "Adresse",
creation_ts_ms: "Date de création",
consent_version: "Version du consentement",
auth_provider: "Fournisseur d'identité",
},
helper: {
deactivate:
"Vous devrez fournir un mot de passe pour réactiver le compte.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
},
action: {
erase: "Effacer les données de l'utilisateur",
},
},
rooms: {
name: "Salon |||| Salons",
fields: {
room_id: "Identifiant du salon",
name: "Nom",
canonical_alias: "Alias",
joined_members: "Membres",
joined_local_members: "Membres locaux",
joined_local_devices: "Appareils locaux",
state_events: "Événements d'État / Complexité",
version: "Version",
is_encrypted: "Chiffré",
encryption: "Chiffrement",
federatable: "Fédérable",
public: "Visible dans le répertoire des salons",
creator: "Créateur",
join_rules: "Règles d'adhésion",
guest_access: "Accès des visiteurs",
history_visibility: "Visibilité de l'historique",
topic: "Sujet",
avatar: "Avatar",
},
helper: {
forward_extremities:
"Les extrémités avant sont les événements feuilles à la fin d'un graphe orienté acyclique (DAG) dans un salon, c'est-à-dire les événements qui n'ont pas de descendants. Plus il y en a dans un salon, plus la résolution d'état que Synapse doit effectuer est importante (indice : c'est une opération coûteuse). Bien que Synapse dispose d'un algorithme pour éviter qu'un trop grand nombre de ces événements n'existent en même temps dans un salon, des bogues peuvent parfois les faire réapparaître. Si un salon présente plus de 10 extrémités avant, cela vaut la peine d'y prêter attention et éventuellement de les supprimer en utilisant les requêtes SQL mentionnées dans la discussion traitant du problème https://github.com/matrix-org/synapse/issues/1760.",
},
enums: {
join_rules: {
public: "Public",
knock: "Sur demande",
invite: "Sur invitation",
private: "Privé",
},
guest_access: {
can_join: "Les visiteurs peuvent rejoindre le salon",
forbidden: "Les visiteurs ne peuvent pas rejoindre le salon",
},
history_visibility: {
invited: "Depuis l'invitation",
joined: "Depuis l'adhésion",
shared: "Depuis le partage",
world_readable: "Tout le monde",
},
unencrypted: "Non chiffré",
},
action: {
erase: {
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 !",
},
},
},
reports: {
name: "Événement signalé |||| Événements signalés",
fields: {
id: "Identifiant",
received_ts: "Date du rapport",
user_id: "Rapporteur",
name: "Nom du salon",
score: "Score",
reason: "Motif",
event_id: "Identifiant de l'événement",
event_json: {
origin: "Serveur d'origine",
origin_server_ts: "Date d'envoi",
type: "Type d'événement",
content: {
msgtype: "Type de contenu",
body: "Contenu",
format: "Format",
formatted_body: "Contenu mis en forme",
algorithm: "Algorithme",
},
},
},
},
connections: {
name: "Connexions",
fields: {
last_seen: "Date",
ip: "Adresse IP",
user_agent: "Agent utilisateur",
},
},
devices: {
name: "Appareil |||| Appareils",
fields: {
device_id: "Identifiant de l'appareil",
display_name: "Nom de l'appareil",
last_seen_ts: "Date",
last_seen_ip: "Adresse IP",
},
action: {
erase: {
title: "Suppression de %{id}",
content: "Voulez-vous vraiment supprimer l'appareil « %{name} » ?",
success: "Appareil supprimé avec succès",
failure: "Une erreur s'est produite",
},
},
},
users_media: {
name: "Media",
fields: {
media_id: "Identifiant du média",
media_length: "Taille du fichier (en octets)",
media_type: "Type",
upload_name: "Nom du fichier",
quarantined_by: "Mis en quarantaine par",
safe_from_quarantine: "Protection contre la mise en quarantaine",
created_ts: "Date de création",
last_access_ts: "Dernier accès",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "Dernier accès avant",
size_gt: "Plus grand que (en octets)",
keep_profiles: "Conserver les images de profil",
},
action: {
send: "Supprimer le média",
send_success: "Requête envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
},
},
protect_media: {
action: {
create: "Protéger",
delete: "Révoquer la protection",
none: "En quarantaine",
send_success: "Le statut de protection a été modifié avec succès",
send_failure: "Une erreur s'est produite",
},
},
quarantine_media: {
action: {
name: "Quarantaine",
create: "Mettre en quarantaine",
delete: "Révoquer la mise en quarantaine",
none: "Protégé contre la mise en quarantaine",
send_success: "Le statut de la quarantaine a été modifié avec succès",
send_failure: "Une erreur s'est produite",
},
},
pushers: {
name: "Émetteur de notifications |||| Émetteurs de notifications",
fields: {
app: "Application",
app_display_name: "Nom d'affichage de l'application",
app_id: "Identifiant de l'application",
device_display_name: "Nom d'affichage de l'appareil",
kind: "Type",
lang: "Langue",
profile_tag: "Profil",
pushkey: "Identifiant de l'émetteur",
data: { url: "URL" },
},
},
servernotices: {
name: "Annonces du serveur",
send: "Envoyer des « Annonces du serveur »",
fields: {
body: "Message",
},
action: {
send: "Envoyer une annonce",
send_success: "Annonce envoyée avec succès",
send_failure: "Une erreur s'est produite",
},
helper: {
send: "Envoie une annonce au nom du serveur aux utilisateurs sélectionnés. La fonction « Annonces du serveur » doit être activée sur le serveur.",
},
},
user_media_statistics: {
name: "Médias des utilisateurs",
fields: {
media_count: "Nombre de médias",
media_length: "Taille des médias",
},
},
forward_extremities: {
name: "Extrémités avant",
fields: {
id: "Identifiant de l'événement",
received_ts: "Date de réception",
depth: "Profondeur",
state_group: "Groupe d'état",
},
},
room_state: {
name: "Événements d'état",
fields: {
type: "Type",
content: "Contenu",
origin_server_ts: "Date d'envoi",
sender: "Expéditeur",
},
},
room_directory: {
name: "Répertoire des salons",
fields: {
world_readable:
"Tout utilisateur peut avoir un aperçu du salon, sans en devenir membre",
guest_can_join: "Les visiteurs peuvent rejoindre le salon",
},
action: {
title:
"Supprimer un salon du répertoire |||| Supprimer %{smart_count} salons du répertoire",
content:
"Voulez-vous vraiment supprimer ce salon du répertoire ? |||| Voulez-vous vraiment supprimer ces %{smart_count} salons du répertoire ?",
erase: "Supprimer du répertoire des salons",
create: "Publier dans le répertoire des salons",
send_success: "Salon publié avec succès",
send_failure: "Une erreur s'est produite",
},
},
},
registration_tokens: {
name: "Jetons d'inscription",
fields: {
token: "Jeton",
valid: "Jeton valide",
uses_allowed: "Nombre d'inscription autorisées",
pending: "Nombre d'inscription en cours",
completed: "Nombre d'inscription accomplie",
expiry_time: "Date d'expiration",
length: "Longueur",
},
helper: {
length:
"Longueur du jeton généré aléatoirement si aucun jeton n'est pas spécifié",
},
},
};
export default fr;

385
src/i18n/it.js Normal file
View File

@ -0,0 +1,385 @@
import italianMessages from "ra-language-italian";
const it = {
...italianMessages,
synapseadmin: {
auth: {
base_url: "URL dell'homeserver",
welcome: "Benvenuto in Synapse-admin",
server_version: "Versione di Synapse",
username_error:
"Per favore inserisci un ID utente completo: '@utente:dominio'",
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
url_error: "URL del server Matrix non valido",
sso_sign_in: "Accedi con SSO",
},
users: {
invalid_user_id: "ID utente non valido su questo homeserver.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "Semplice",
members: "Membro",
detail: "Dettagli",
permission: "Permessi",
},
},
reports: { tabs: { basic: "Semplice", detail: "Dettagli" } },
},
import_users: {
error: {
at_entry: "Alla voce %{entry}: %{message}",
error: "Errore",
required_field: "Il campo '%{field}' non è presente",
invalid_value:
"Valore non valido alla riga %{row}. '%{field}' Il campo può essere solo 'true' o 'false'",
unreasonably_big:
"Impossibile caricare un file così grosso (%{size} megabyte)",
already_in_progress: "Un import è attualmente già in caricamento",
id_exits: "L'ID %{id} è già presente",
},
title: "Importa utenti tramite file CSV",
goToPdf: "Vai al PDF",
cards: {
importstats: {
header: "Importa utenti",
users_total:
"%{smart_count} utente nel file CSV |||| %{smart_count} utenti nel file CSV",
guest_count: "%{smart_count} ospite |||| %{smart_count} ospiti",
admin_count:
"%{smart_count} amministratore |||| %{smart_count} amministratori",
},
conflicts: {
header: "Strategia di conflitto",
mode: {
stop: "Stoppa al conflitto",
skip: "Mostra l'errore e ignora il conflitto",
},
},
ids: {
header: "ID",
all_ids_present: "ID presenti in ogni voce",
count_ids_present:
"%{smart_count} voce con ID |||| %{smart_count} voci con ID",
mode: {
ignore: "Ignora gli ID nel file CSV e creane di nuovi",
update: "Aggiorna le voci esistenti",
},
},
passwords: {
header: "Passwords",
all_passwords_present: "Password presenti in ogni voce",
count_passwords_present:
"%{smart_count} voce con password |||| %{smart_count} voci con password",
use_passwords: "Usa le password dal file CSV",
},
upload: {
header: "Input file CSV",
explanation:
"Qui puoi caricare un file con valori separati da virgole che verrà poi utilizzato per creare o aggiornare gli utenti. Il file deve includere i campi 'id' and 'displayname'. Puoi scaricare un file di esempio per adattarlo: ",
},
startImport: {
simulate_only: "Solo simulazione",
run_import: "Importa",
},
results: {
header: "Importa i risultati",
total:
"%{smart_count} voce in totale |||| %{smart_count} voci in totale",
successful: "%{smart_count} voci importate con successo",
skipped: "%{smart_count} voci ignorate",
download_skipped: "Scarica le voci ignorate",
with_error:
"%{smart_count} voce con errori ||| %{smart_count} voci con errori",
simulated_only: "Il processo era stato solamente simulato",
},
},
},
resources: {
users: {
name: "Utente |||| Utenti",
email: "Email",
msisdn: "Telefono",
threepid: "Email / Telefono",
fields: {
avatar: "Avatar",
id: "ID utente",
name: "Nome",
is_guest: "Ospite",
admin: "Amministratore",
deactivated: "Disattivato",
guests: "Mostra gli ospiti",
show_deactivated: "Mostra gli utenti disattivati",
user_id: "Cerca utente",
displayname: "Nickname",
password: "Password",
avatar_url: "URL dell'avatar",
avatar_src: "Avatar",
medium: "Medium",
threepids: "3PID",
address: "Indirizzo",
creation_ts_ms: "Creazione del timestamp",
consent_version: "Versione minima richiesta",
auth_provider: "Provider",
user_type: "Tipo d'utente",
},
helper: {
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",
},
action: {
erase: "Cancella i dati dell'utente",
},
},
rooms: {
name: "Stanza |||| Stanze",
fields: {
room_id: "ID della stanza",
name: "Nome",
canonical_alias: "Alias",
joined_members: "Membri",
joined_local_members: "Membri locali",
joined_local_devices: "Dispositivi locali",
state_events: "Eventi di stato / Complessità",
version: "Versione",
is_encrypted: "Criptato",
encryption: "Crittografia",
federatable: "Federabile",
public: "Visibile nella cartella della stanza",
creator: "Creatore",
join_rules: "Regole per entrare",
guest_access: "Entra come ospite",
history_visibility: "Visibilità temporale",
topic: "Topic",
avatar: "Avatar",
},
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: {
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",
},
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!",
},
},
},
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",
},
},
delete_media: {
name: "Media",
fields: {
before_ts: "ultimo accesso effettuato prima",
size_gt: "Più grande di (in byte)",
keep_profiles: "Mantieni le immagini del profilo",
},
action: {
send: "Cancella media",
send_success: "Richiesta inviata con successo.",
send_failure: "C'è stato un errore.",
},
helper: {
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
},
},
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;

390
src/i18n/ru.js Normal file
View File

@ -0,0 +1,390 @@
import russianMessages from "ra-language-russian";
const ru = {
...russianMessages,
synapseadmin: {
auth: {
base_url: "Домашняя страница",
welcome: "Добро пожаловать в Synapse-admin",
server_version: "Версия Synapse",
supports_specs: "поддерживает спецификации Matrix",
username_error: "Введите полный идентификатор пользователя: '@user:domain'",
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Некорректный сервер Matrix",
sso_sign_in: "Присоединиться с помощью SSO",
},
users: {
invalid_user_id: "Локальная часть идентификатора пользователя Matrix без домашнего сервера.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "Основное",
members: "Участники",
detail: "Подробности",
permission: "Права",
},
},
reports: { tabs: { basic: "Основное", detail: "Подробности" } },
},
import_users: {
error: {
at_entry: "При входе %{entry}: %{message}",
error: "Ошибка",
required_field: "Обязательное поле '%{field}' не представленно",
invalid_value:
"Недопустимое значение в строке %{row}. '%{field}' поле может быть только 'true' или 'false'",
unreasonably_big:
"Отказался загружать неоправданно большой файл %{size} Мбайт",
already_in_progress: "Импорт уже запущен",
id_exits: "Идентификатор %{id} уже существует",
},
title: "Импорт пользователей из CSV",
goToPdf: "В PDF",
cards: {
importstats: {
header: "Импорт пользователей",
users_total:
"%{smart_count} пользователь в CSV файл |||| %{smart_count} пользователей в CSV файл",
guest_count: "%{smart_count} гость |||| %{smart_count} гостей",
admin_count: "%{smart_count} администратор |||| %{smart_count} администраторов",
},
conflicts: {
header: "Решение конфликтов",
mode: {
stop: "Остановиться, если конфликт произошел",
skip: "Вывести ошибку и пропустить конфликт",
},
},
ids: {
header: "Идентификаторы",
all_ids_present: "Идентификаторы представлены для каждой записи",
count_ids_present:
"%{smart_count} запись с идентификатором |||| %{smart_count} записей с идентификатором",
mode: {
ignore: "Игнорировать идентификаторы в CSV и создавать новые",
update: "Обновлять существующие записи",
},
},
passwords: {
header: "Пароли",
all_passwords_present: "Пароли представлены для каждой записи",
count_passwords_present:
"%{smart_count} запись с паролем |||| %{smart_count} записей с паролем",
use_passwords: "Использовать пароли из CSV",
},
upload: {
header: "Загрузка CSV файла",
explanation:
"Здесь вы можете загрузить файл со значениями, разделенными запятыми, который будет обработан для создания или обновления пользователей. Файл должен содержать поля 'id' и 'displayname'. Вы можете загрузить пример здесь:",
},
startImport: {
simulate_only: "Не выполнять реальных действий",
run_import: "Импорт",
},
results: {
header: "Импорт результатов",
total:
"Всего %{smart_count} запись |||| Всего %{smart_count} записей",
successful: "%{smart_count} записей успешно импортировано",
skipped: "%{smart_count} записей пропущено",
download_skipped: "Загрузить пропущенные записи",
with_error:
"%{smart_count} запись с ошибками ||| %{smart_count} записей с ошибками",
simulated_only: "Результат не будет сохранен",
},
},
},
resources: {
users: {
name: "Пользователей |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
threepid: "Почта / Телефон",
fields: {
avatar: "Аватар",
id: "Идентификатор пользователя",
name: "Имя",
is_guest: "Гость",
admin: "Администратор",
deactivated: "Деактивирован",
guests: "Показать гостей",
show_deactivated: "Показать деактивированных пользователей",
user_id: "Найти пользователя",
displayname: "Отображаемое имя",
password: "Пароль",
avatar_url: "Ссылка на аватар",
avatar_src: "Аватар",
medium: "Тип",
threepids: "Иной идентификатор",
address: "Адрес",
creation_ts_ms: "Время создания",
consent_version: "Версия соглашения",
auth_provider: "Поставщик",
user_type: "Тип пользователя",
},
helper: {
password: "Изменение пароля приведет к выходу пользователя из всех сеансов.",
deactivate: "Вы должны ввести пароль для повторной активации учетной записи.",
erase: "Пометить пользователя как удаленного в связи с защитой персональных данных",
},
action: {
erase: "Удаление пользовательских данных",
},
},
rooms: {
name: "Комнат |||| Комнаты",
fields: {
room_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:
"Перенаправленные заключения, это такие события, которые не имеют потомков в рамках графа, отвечающего за их репрезентацию. Чем больше пользователей находится в комнате, тем больше операций требуется выполнить Synapse для разрешения коллиций, которые возникают при их проверке наступления событий (это дорогостоящая операция). Хотя в Synapse есть код, предотвращающий одновременное присутствие слишком большого количества таких объектов в комнате, ошибки иногда могут привести к их повторному появлению. Если в комнате содержится >10 перенаправленных заключений, имеет смысл выяснить, какая комната стала причиной их появления и, возможно, удалить их, используя SQL-запросы, упомянутые issue #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:
"Вы уверены, что хотите удалить комнату? Это невозможно отменить. Все сообщения и медиафайлы, находящиеся в общем доступе в комнате, будут удалены с сервера!",
},
},
},
reports: {
name: "Жалоб |||| Жалобы",
fields: {
id: "идентификатор",
received_ts: "время жалобы",
user_id: "коментатор",
name: "название комнаты",
score: "оценка",
reason: "причина",
event_id: "идентификатор события",
event_json: {
origin: "сервер",
origin_server_ts: "время отправки",
type: "тип события",
content: {
msgtype: "тип содержания",
body: "содержание",
format: "формат",
formatted_body: "форматированное содержание",
algorithm: "алгоритм",
},
},
},
action: {
erase: {
title: "Удалить жалобу",
content:
"Вы уверены, что хотите удалить жалобу? Это невозможно отменить.",
},
},
},
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "User agent",
},
},
devices: {
name: "Устройств |||| Устройства",
fields: {
device_id: "Идентификатор устройства",
display_name: "Название устройства",
last_seen_ts: "Метка времени",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Вы уверены, что хотите удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
},
},
},
users_media: {
name: "Медиа",
fields: {
media_id: "Идентификатор медиа",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "Отправлен в карантин",
safe_from_quarantine: "Защищен от карантина",
created_ts: "Создан",
last_access_ts: "Последнее обращение",
},
},
delete_media: {
name: "Медиа",
fields: {
before_ts: "последнее обращение",
size_gt: "Больше чем (в байтах)",
keep_profiles: "Сохранить аватары",
},
action: {
send: "Удалить медиафайлы",
send_success: "Запрос отправлен.",
send_failure: "Произошла ошибка.",
},
helper: {
send: "Этот метод удаляет медиафайлы с диска сервера. Это включает в себя любые миниатюры и копии загруженных файлов. Этот метод не повлияет на файлы, загруженные во внешние хранилища.",
},
},
protect_media: {
action: {
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: "Идентификатор приложения",
device_display_name: "Отображаемое имя устройства",
kind: "Тип",
lang: "Язык",
profile_tag: "Тег профиля",
pushkey: "Токен доступа",
data: { url: "URL" },
},
},
servernotices: {
name: "Уведомления",
send: "Отправить уведомление",
fields: {
body: "Сообщение",
},
action: {
send: "Отправить",
send_success: "Уведомление успешно отправлено.",
send_failure: "Произошла ошибка.",
},
helper: {
send: 'Отправляет уведомление выбранным пользователям. Функция "Server Notices" должна быть активирована на сервере.',
},
},
user_media_statistics: {
name: "Медиа",
fields: {
media_count: "Количество медиафайлов",
media_length: "Длина медиафайлов",
},
},
forward_extremities: {
name: "Перенаправленные заключения",
fields: {
id: "Идентификатор события",
received_ts: "Время",
depth: "Глубина",
state_group: "Группа состояния",
},
},
room_state: {
name: "События изменения состояния",
fields: {
type: "Тип",
content: "Содержание",
origin_server_ts: "время отправления",
sender: "Отправитель",
},
},
room_directory: {
name: "Публичных комнат |||| Публичные комнаты",
fields: {
world_readable: "Видимо для гостей",
guest_can_join: "Гости могут присоединиться",
},
action: {
title:
"Удалить комнату |||| Удалить %{smart_count} комнат",
content:
"Вы уверены, что хотите удалить комнату? |||| Вы уверены, что хотите удалить %{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;

290
src/i18n/zh.js Normal file
View File

@ -0,0 +1,290 @@
import chineseMessages from "ra-language-chinese";
const zh = {
...chineseMessages,
synapseadmin: {
auth: {
base_url: "服务器 URL",
welcome: "欢迎来到 Synapse-admin",
server_version: "Synapse 版本",
username_error: "请输入完整有效的用户 ID: '@user:domain'",
protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址",
sso_sign_in: "使用 SSO 登录",
},
users: {
invalid_user_id:
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "基本",
members: "成员",
detail: "细节",
permission: "权限",
},
delete: {
title: "删除房间",
message:
"您确定要删除这个房间吗?该操作无法被撤销。这个房间里所有的消息和分享的媒体都将被从服务器上删除!",
},
},
reports: { tabs: { basic: "基本", detail: "细节" } },
},
import_users: {
error: {
at_entry: "在条目 %{entry}: %{message}",
error: "错误",
required_field: "需要的值 '%{field}' 未被设置。",
invalid_value:
"第 %{row} 行出现无效值。 '%{field}' 只可以是 'true' 或 'false'。",
unreasonably_big: "拒绝加载过大的文件: %{size} MB",
already_in_progress: "一个导入进程已经在运行中",
id_exits: "ID %{id} 已经存在",
},
title: "通过 CSV 导入用户",
goToPdf: "转到 PDF",
cards: {
importstats: {
header: "导入用户",
users_total:
"%{smart_count} 用户在 CSV 文件中 |||| %{smart_count} 用户在 CSV 文件中",
guest_count: "%{smart_count} 访客 |||| %{smart_count} 访客",
admin_count: "%{smart_count} 管理员 |||| %{smart_count} 管理员",
},
conflicts: {
header: "冲突处理策略",
mode: {
stop: "在冲突处停止",
skip: "显示错误并跳过冲突",
},
},
ids: {
header: "IDs",
all_ids_present: "每条记录的 ID",
count_ids_present:
"%{smart_count} 个含 ID 的记录 |||| %{smart_count} 个含 ID 的记录",
mode: {
ignore: "忽略 CSV 中的 ID 并创建新的",
update: "更新已经存在的记录",
},
},
passwords: {
header: "密码",
all_passwords_present: "每条记录的密码",
count_passwords_present:
"%{smart_count} 个含密码的记录 |||| %{smart_count} 个含密码的记录",
use_passwords: "使用 CSV 中标记的密码",
},
upload: {
header: "导入 CSV 文件",
explanation:
"在这里,你可以上传一个用逗号分隔的文件,用于创建或更新用户。该文件必须包括 'id' 和 'displayname' 字段。你可以在这里下载并修改一个示例文件:",
},
startImport: {
simulate_only: "模拟模式",
run_import: "导入",
},
results: {
header: "导入结果",
total: "共计 %{smart_count} 条记录 |||| 共计 %{smart_count} 条记录",
successful: "%{smart_count} 条记录导入成功",
skipped: "跳过 %{smart_count} 条记录",
download_skipped: "下载跳过的记录",
with_error:
"%{smart_count} 条记录出现错误 ||| %{smart_count} 条记录出现错误",
simulated_only: "只是一次模拟运行",
},
},
},
resources: {
users: {
name: "用户",
email: "邮箱",
msisdn: "电话",
threepid: "邮箱 / 电话",
fields: {
avatar: "邮箱",
id: "用户 ID",
name: "用户名",
is_guest: "访客",
admin: "服务器管理员",
deactivated: "被禁用",
guests: "显示访客",
show_deactivated: "显示被禁用的账户",
user_id: "搜索用户",
displayname: "显示名字",
password: "密码",
avatar_url: "头像 URL",
avatar_src: "头像",
medium: "Medium",
threepids: "3PIDs",
address: "地址",
creation_ts_ms: "创建时间戳",
consent_version: "协议版本",
},
helper: {
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
},
action: {
erase: "抹除用户信息",
},
},
rooms: {
name: "房间",
fields: {
room_id: "房间 ID",
name: "房间名",
canonical_alias: "别名",
joined_members: "成员",
joined_local_members: "本地成员",
state_events: "状态事件",
version: "版本",
is_encrypted: "已经加密",
encryption: "加密",
federatable: "可联合的",
public: "公开",
creator: "创建者",
join_rules: "加入规则",
guest_access: "访客访问",
history_visibility: "历史可见性",
},
enums: {
join_rules: {
public: "公开",
knock: "申请",
invite: "邀请",
private: "私有",
},
guest_access: {
can_join: "访客可以加入",
forbidden: "访客不可加入",
},
history_visibility: {
invited: "自从被邀请",
joined: "自从加入",
shared: "自从分享",
world_readable: "任何人",
},
unencrypted: "未加密",
},
},
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: "算法",
},
},
},
},
connections: {
name: "连接",
fields: {
last_seen: "日期",
ip: "IP 地址",
user_agent: "用户代理 (UA)",
},
},
devices: {
name: "设备",
fields: {
device_id: "设备 ID",
display_name: "设备名",
last_seen_ts: "时间戳",
last_seen_ip: "IP 地址",
},
action: {
erase: {
title: "移除 %{id}",
content: '您确定要移除设备 "%{name}"?',
success: "设备移除成功。",
failure: "出现了一个错误。",
},
},
},
users_media: {
name: "媒体文件",
fields: {
media_id: "媒体文件 ID",
media_length: "长度",
media_type: "类型",
upload_name: "文件名",
quarantined_by: "被隔离",
safe_from_quarantine: "取消隔离",
created_ts: "创建",
last_access_ts: "上一次访问",
},
},
delete_media: {
name: "媒体文件",
fields: {
before_ts: "最后访问时间",
size_gt: "大于 (字节)",
keep_profiles: "保留头像",
},
action: {
send: "删除媒体",
send_success: "请求发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
},
},
pushers: {
name: "发布者",
fields: {
app: "App",
app_display_name: "App 名称",
app_id: "App ID",
device_display_name: "设备显示名",
kind: "类型",
lang: "语言",
profile_tag: "数据标签",
pushkey: "Pushkey",
data: { url: "URL" },
},
},
servernotices: {
name: "服务器提示",
send: "发送服务器提示",
fields: {
body: "信息",
},
action: {
send: "发送提示",
send_success: "服务器提示发送成功。",
send_failure: "出现了一个错误。",
},
helper: {
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
},
},
user_media_statistics: {
name: "用户的媒体文件",
fields: {
media_count: "媒体文件统计",
media_length: "媒体文件长度",
},
},
},
};
export default zh;

View File

@ -1,5 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));

9
src/index.jsx Normal file
View File

@ -0,0 +1,9 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -1,4 +1,3 @@
import { configure } from "enzyme"; import fetchMock from "jest-fetch-mock";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() }); fetchMock.enableMocks();

View File

@ -1,54 +1,69 @@
import { fetchUtils } from "react-admin"; import { fetchUtils } from "react-admin";
const ensureHttpsForUrl = url => {
if (/^https:\/\//i.test(url)) {
return url;
}
const domain = url.replace(/http.?:\/\//g, "");
return "https://" + domain;
};
const stripTrailingSlash = str => {
if (!str) {
return;
}
return str.endsWith("/") ? str.slice(0, -1) : str;
};
const authProvider = { const authProvider = {
// called when the user attempts to log in // called when the user attempts to log in
login: ({ homeserver, username, password }) => { login: async ({ base_url, username, password, loginToken }) => {
// force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url;
console.log("login "); console.log("login ");
const options = { const options = {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify(
type: "m.login.password", Object.assign(
user: username, {
password: password, device_id: localStorage.getItem("device_id"),
}), initial_device_display_name: "Synapse Admin",
},
loginToken
? {
type: "m.login.token",
token: loginToken,
}
: {
type: "m.login.password",
user: username,
password: password,
}
)
),
}; };
const url = window.decodeURIComponent(homeserver); // use the base_url from login instead of the well_known entry from the
const trimmed_url = url.trim().replace(/\s/g, ""); // server, since the admin might want to access the admin API via some
const login_api_url = // private address
ensureHttpsForUrl(trimmed_url) + "/_matrix/client/r0/login"; base_url = base_url.replace(/\/+$/g, "");
localStorage.setItem("base_url", base_url);
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => { const decoded_base_url = window.decodeURIComponent(base_url);
const normalized_base_url = stripTrailingSlash( const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
json.well_known["m.homeserver"].base_url
); const { json } = await fetchUtils.fetchJson(login_api_url, options);
localStorage.setItem("base_url", normalized_base_url); localStorage.setItem("home_server", json.home_server);
localStorage.setItem("home_server_url", json.home_server); localStorage.setItem("user_id", json.user_id);
localStorage.setItem("user_id", json.user_id); localStorage.setItem("access_token", json.access_token);
localStorage.setItem("access_token", json.access_token); localStorage.setItem("device_id", json.device_id);
localStorage.setItem("device_id", json.device_id);
});
}, },
// called when the user clicks on the logout button // called when the user clicks on the logout button
logout: () => { logout: async () => {
console.log("logout "); console.log("logout");
localStorage.removeItem("access_token");
return Promise.resolve(); const logout_api_url =
localStorage.getItem("base_url") + "/_matrix/client/r0/logout";
const access_token = localStorage.getItem("access_token");
const options = {
method: "POST",
user: {
authenticated: true,
token: `Bearer ${access_token}`,
},
};
if (typeof access_token === "string") {
await fetchUtils.fetchJson(logout_api_url, options);
localStorage.removeItem("access_token");
}
}, },
// called when the API returns an error // called when the API returns an error
checkError: ({ status }) => { checkError: ({ status }) => {
@ -62,7 +77,7 @@ const authProvider = {
checkAuth: () => { checkAuth: () => {
const access_token = localStorage.getItem("access_token"); const access_token = localStorage.getItem("access_token");
console.log("checkAuth " + access_token); console.log("checkAuth " + access_token);
return typeof access_token == "string" return typeof access_token === "string"
? Promise.resolve() ? Promise.resolve()
: Promise.reject(); : Promise.reject();
}, },

View File

@ -0,0 +1,135 @@
import authProvider from "./authProvider";
describe("authProvider", () => {
beforeEach(() => {
fetch.resetMocks();
localStorage.clear();
});
describe("login", () => {
it("should successfully login with username and password", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
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",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
headers: new Headers({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
});
it("should successfully login with token", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
const ret = await authProvider.login({
base_url: "https://example.com/",
loginToken: "login_token",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith(
"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({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
describe("logout", () => {
it("should remove the access_token from localStorage", async () => {
localStorage.setItem("base_url", "example.com");
localStorage.setItem("access_token", "foo");
fetch.mockResponse(JSON.stringify({}));
await authProvider.logout();
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
headers: new Headers({
Accept: ["application/json"],
Authorization: ["Bearer foo"],
}),
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
expect(localStorage.getItem("access_token")).toBeNull();
});
});
describe("checkError", () => {
it("should resolve if error.status is not 401 or 403", async () => {
await expect(
authProvider.checkError({ status: 200 })
).resolves.toBeUndefined();
});
it("should reject if error.status is 401", async () => {
await expect(
authProvider.checkError({ status: 401 })
).rejects.toBeUndefined();
});
it("should reject if error.status is 403", async () => {
await expect(
authProvider.checkError({ status: 403 })
).rejects.toBeUndefined();
});
});
describe("checkAuth", () => {
it("should reject when not logged in", async () => {
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
});
it("should resolve when logged in", async () => {
localStorage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});
});
describe("getPermissions", () => {
it("should do nothing", async () => {
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
});
});
});

View File

@ -14,22 +14,46 @@ const jsonClient = (url, options = {}) => {
return fetchUtils.fetchJson(url, options); return fetchUtils.fetchJson(url, options);
}; };
const mxcUrlToHttp = mxcUrl => {
const homeserver = localStorage.getItem("base_url");
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
var 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 resourceMap = { const resourceMap = {
users: { users: {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
map: u => ({ map: u => ({
...u, ...u,
id: u.name, id: u.name,
avatar_src: mxcUrlToHttp(u.avatar_url),
is_guest: !!u.is_guest, is_guest: !!u.is_guest,
admin: !!u.admin, admin: !!u.admin,
deactivated: !!u.deactivated, deactivated: !!u.deactivated,
// need timestamp in milliseconds
creation_ts_ms: u.creation_ts * 1000,
}), }),
data: "users", data: "users",
total: (json, from, perPage) => { total: json => json.total,
return json.next_token create: data => ({
? parseInt(json.next_token, 10) + perPage endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(
: from + json.users.length; data.id
}, )}:${localStorage.getItem("home_server")}`,
body: data,
method: "PUT",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(
params.id
)}`,
body: { erase: true },
method: "POST",
}),
}, },
rooms: { rooms: {
path: "/_synapse/admin/v1/rooms", path: "/_synapse/admin/v1/rooms",
@ -38,11 +62,45 @@ const resourceMap = {
id: r.room_id, id: r.room_id,
alias: r.canonical_alias, alias: r.canonical_alias,
members: r.joined_members, members: r.joined_members,
is_encrypted: !!r.encryption,
federatable: !!r.federatable,
public: !!r.public,
}), }),
data: "rooms", data: "rooms",
total: json => { total: json => {
return json.total_rooms; return json.total_rooms;
}, },
delete: params => ({
endpoint: `/_synapse/admin/v2/rooms/${params.id}`,
body: { block: false },
}),
},
reports: {
path: "/_synapse/admin/v1/event_reports",
map: er => ({
...er,
id: er.id,
}),
data: "event_reports",
total: json => json.total,
},
devices: {
map: d => ({
...d,
id: d.device_id,
}),
data: "devices",
total: json => {
return json.total;
},
reference: id => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`,
}),
delete: params => ({
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(
params.previousData.user_id
)}/devices/${params.id}`,
}),
}, },
connections: { connections: {
path: "/_synapse/admin/v1/whois", path: "/_synapse/admin/v1/whois",
@ -52,83 +110,329 @@ const resourceMap = {
}), }),
data: "connections", data: "connections",
}, },
room_members: {
map: m => ({
id: m,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/members`,
}),
data: "members",
total: json => {
return json.total;
},
},
room_state: {
map: rs => ({
...rs,
id: rs.event_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/state`,
}),
data: "state",
total: json => {
return json.state.length;
},
},
pushers: {
map: p => ({
...p,
id: p.pushkey,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`,
}),
data: "pushers",
total: json => {
return json.total;
},
},
joined_rooms: {
map: jr => ({
id: jr,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(
id
)}/joined_rooms`,
}),
data: "joined_rooms",
total: json => {
return json.total;
},
},
users_media: {
map: um => ({
...um,
id: um.media_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
}),
data: "media",
total: json => {
return json.total;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server"
)}/${params.id}`,
}),
},
delete_media: {
delete: params => ({
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem(
"home_server"
)}/delete?before_ts=${params.meta.before_ts}&size_gt=${
params.meta.size_gt
}&keep_profiles=${params.meta.keep_profiles}`,
method: "POST",
}),
},
protect_media: {
map: pm => ({ id: pm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`,
method: "POST",
}),
},
quarantine_media: {
map: qm => ({ id: qm.media_id }),
create: params => ({
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem(
"home_server"
)}/${params.media_id}`,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem(
"home_server"
)}/${params.id}`,
method: "POST",
}),
},
servernotices: {
map: n => ({ id: n.event_id }),
create: data => ({
endpoint: "/_synapse/admin/v1/send_server_notice",
body: {
user_id: data.id,
content: {
msgtype: "m.text",
body: data.body,
},
},
method: "POST",
}),
},
user_media_statistics: {
path: "/_synapse/admin/v1/statistics/users/media",
map: usms => ({
...usms,
id: usms.user_id,
}),
data: "users",
total: json => {
return json.total;
},
},
forward_extremities: {
map: fe => ({
...fe,
id: fe.event_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`,
}),
data: "results",
total: json => {
return json.count;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`,
}),
},
room_directory: {
path: "/_matrix/client/r0/publicRooms",
map: rd => ({
...rd,
id: rd.room_id,
public: !!rd.public,
guest_access: !!rd.guest_access,
avatar_src: mxcUrlToHttp(rd.avatar_url),
}),
data: "chunk",
total: json => {
return json.total_room_count_estimate;
},
create: params => ({
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
body: { visibility: "public" },
method: "PUT",
}),
delete: params => ({
endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`,
body: { visibility: "private" },
method: "PUT",
}),
},
destinations: {
path: "/_synapse/admin/v1/federation/destinations",
map: dst => ({
...dst,
id: dst.destination,
}),
data: "destinations",
total: json => {
return json.total;
},
delete: params => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`,
method: "POST",
}),
},
destination_rooms: {
map: dstroom => ({
...dstroom,
id: dstroom.room_id,
}),
reference: id => ({
endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`,
}),
data: "rooms",
total: json => {
return json.total;
},
},
registration_tokens: {
path: "/_synapse/admin/v1/registration_tokens",
map: rt => ({
...rt,
id: rt.token,
}),
data: "registration_tokens",
total: json => {
return json.registration_tokens.length;
},
create: params => ({
endpoint: "/_synapse/admin/v1/registration_tokens/new",
body: params,
method: "POST",
}),
delete: params => ({
endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
}),
},
}; };
function filterNullValues(key, value) { function filterNullValues(key, value) {
// Filtering out null properties // Filtering out null properties
if (value === null) { // to reset user_type from user, it must be null
if (value === null && key !== "user_type") {
return undefined; return undefined;
} }
return value; return value;
} }
function getSearchOrder(order) {
if (order === "DESC") {
return "b";
} else {
return "f";
}
}
const dataProvider = { const dataProvider = {
getList: (resource, params) => { getList: async (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { user_id, guests, deactivated } = params.filter; const {
user_id,
name,
guests,
deactivated,
search_term,
destination,
valid,
} = params.filter;
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage; const from = (page - 1) * perPage;
const query = { const query = {
from: from, from: from,
limit: perPage, limit: perPage,
user_id: user_id, user_id: user_id,
search_term: search_term,
name: name,
destination: destination,
guests: guests, guests: guests,
deactivated: deactivated, deactivated: deactivated,
valid: valid,
order_by: field,
dir: getSearchOrder(order),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const url = `${homeserver_url}?${stringify(query)}`; const url = `${endpoint_url}?${stringify(query)}`;
return jsonClient(url).then(({ json }) => ({ const { json } = await jsonClient(url);
return {
data: json[res.data].map(res.map), data: json[res.data].map(res.map),
total: res.total(json, from, perPage), total: res.total(json, from, perPage),
})); };
}, },
getOne: (resource, params) => { getOne: async (resource, params) => {
console.log("getOne " + resource); console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${homeserver_url}/${params.id}`).then(({ json }) => ({ const { json } = await jsonClient(
data: res.map(json), `${endpoint_url}/${encodeURIComponent(params.id)}`
})); );
return { data: res.map(json) };
}, },
getMany: (resource, params) => { getMany: async (resource, params) => {
console.log("getMany " + resource); console.log("getMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( const responses = await Promise.all(
params.ids.map(id => jsonClient(`${homeserver_url}/${id}`)) params.ids.map(id =>
).then(responses => ({ jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
)
);
return {
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
})); total: responses.length,
};
}, },
getManyReference: (resource, params) => { getManyReference: async (resource, params) => {
// FIXME
console.log("getManyReference " + resource); console.log("getManyReference " + resource);
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort; const { field, order } = params.sort;
const from = (page - 1) * perPage;
const query = { const query = {
sort: JSON.stringify([field, order]), from: from,
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]), limit: perPage,
filter: JSON.stringify({ order_by: field,
...params.filter, dir: getSearchOrder(order),
[params.target]: params.id,
}),
}; };
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
@ -136,98 +440,151 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const ref = res["reference"](params.id);
const url = `${homeserver_url}?${stringify(query)}`; const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
return jsonClient(url).then(({ headers, json }) => ({ const { json } = await jsonClient(endpoint_url);
data: json, return {
total: parseInt(headers.get("content-range").split("/").pop(), 10), data: json[res.data].map(res.map),
})); total: res.total(json, from, perPage),
};
}, },
update: (resource, params) => { update: async (resource, params) => {
console.log("update " + resource); console.log("update " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${homeserver_url}/${params.data.id}`, { const { json } = await jsonClient(
method: "PUT", `${endpoint_url}/${encodeURIComponent(params.id)}`,
body: JSON.stringify(params.data, filterNullValues), {
}).then(({ json }) => ({ method: "PUT",
data: res.map(json), body: JSON.stringify(params.data, filterNullValues),
})); }
);
return { data: res.map(json) };
}, },
updateMany: (resource, params) => { updateMany: async (resource, params) => {
console.log("updateMany " + resource); console.log("updateMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return Promise.all( const responses = await Promise.all(
params.ids.map(id => jsonClient(`${homeserver_url}/${id}`), { params.ids.map(
method: "PUT", id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
body: JSON.stringify(params.data, filterNullValues), {
}) method: "PUT",
).then(responses => ({ body: JSON.stringify(params.data, filterNullValues),
data: responses.map(({ json }) => json), }
})); )
);
return { data: responses.map(({ json }) => json) };
}, },
create: (resource, params) => { create: async (resource, params) => {
console.log("create " + resource); console.log("create " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
if (!("create" in res)) return Promise.reject();
const homeserver_url = homeserver + res.path; const create = res["create"](params.data);
return jsonClient(`${homeserver_url}/${params.data.id}`, { const endpoint_url = homeserver + create.endpoint;
method: "PUT", const { json } = await jsonClient(endpoint_url, {
body: JSON.stringify(params.data, filterNullValues), method: create.method,
}).then(({ json }) => ({ body: JSON.stringify(create.body, filterNullValues),
data: res.map(json), });
})); return { data: res.map(json) };
}, },
delete: (resource, params) => { createMany: async (resource, params) => {
console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource];
if (!("create" in res)) return Promise.reject();
const responses = await Promise.all(
params.ids.map(id => {
params.data.id = id;
const cre = res["create"](params.data);
const endpoint_url = homeserver + cre.endpoint;
return jsonClient(endpoint_url, {
method: cre.method,
body: JSON.stringify(cre.body, filterNullValues),
});
})
);
return { data: responses.map(({ json }) => json) };
},
delete: async (resource, params) => {
console.log("delete " + resource); console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; if ("delete" in res) {
return jsonClient(`${homeserver_url}/${params.id}`, { const del = res["delete"](params);
method: "DELETE", const endpoint_url = homeserver + del.endpoint;
}).then(({ json }) => ({ const { json } = await jsonClient(endpoint_url, {
data: json, method: "method" in del ? del.method : "DELETE",
})); body: "body" in del ? JSON.stringify(del.body) : null,
});
return { data: json };
} else {
const endpoint_url = homeserver + res.path;
const { json } = await jsonClient(`${endpoint_url}/${params.id}`, {
method: "DELETE",
body: JSON.stringify(params.previousData, filterNullValues),
});
return { data: json };
}
}, },
deleteMany: (resource, params) => { deleteMany: async (resource, params) => {
console.log("deleteMany " + resource); console.log("deleteMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
const res = resourceMap[resource]; const res = resourceMap[resource];
const homeserver_url = homeserver + res.path; if ("delete" in res) {
return Promise.all( const responses = await Promise.all(
params.ids.map(id => params.ids.map(id => {
jsonClient(`${homeserver_url}/${id}`, { const del = res["delete"]({ ...params, id: id });
method: "DELETE", const endpoint_url = homeserver + del.endpoint;
body: JSON.stringify(params.data, filterNullValues), return jsonClient(endpoint_url, {
}).then(responses => ({ method: "method" in del ? del.method : "DELETE",
data: responses.map(({ json }) => json), body: "body" in del ? JSON.stringify(del.body) : null,
})) });
) })
); );
return {
data: responses.map(({ json }) => json),
};
} else {
const endpoint_url = homeserver + res.path;
const responses = await Promise.all(
params.ids.map(id =>
jsonClient(`${endpoint_url}/${id}`, {
method: "DELETE",
body: JSON.stringify(params.data, filterNullValues),
})
)
);
return { data: responses.map(({ json }) => json) };
}
}, },
}; };

View File

@ -0,0 +1,78 @@
import dataProvider from "./dataProvider";
beforeEach(() => {
fetch.resetMocks();
});
describe("dataProvider", () => {
localStorage.setItem("base_url", "http://localhost");
localStorage.setItem("access_token", "access_token");
it("fetches all users", async () => {
fetch.mockResponseOnce(
JSON.stringify({
users: [
{
name: "user_id1",
password_hash: "password_hash1",
is_guest: 0,
admin: 0,
user_type: null,
deactivated: 0,
displayname: "User One",
},
{
name: "user_id2",
password_hash: "password_hash2",
is_guest: 0,
admin: 1,
user_type: null,
deactivated: 0,
displayname: "User Two",
},
],
next_token: "100",
total: 200,
})
);
const users = await dataProvider.getList("users", {
pagination: { page: 1, perPage: 5 },
sort: { field: "title", order: "ASC" },
filter: { author_id: 12 },
});
expect(users["data"][0]["id"]).toEqual("user_id1");
expect(users["total"]).toEqual(200);
expect(fetch).toHaveBeenCalledTimes(1);
});
it("fetches one user", async () => {
fetch.mockResponseOnce(
JSON.stringify({
name: "user_id1",
password: "user_password",
displayname: "User",
threepids: [
{
medium: "email",
address: "user@mail_1.com",
},
{
medium: "email",
address: "user@mail_2.com",
},
],
avatar_url: "mxc://localhost/user1",
admin: false,
deactivated: false,
})
);
const user = await dataProvider.getOne("users", { id: "user_id1" });
expect(user["data"]["id"]).toEqual("user_id1");
expect(user["data"]["displayname"]).toEqual("User");
expect(fetch).toHaveBeenCalledTimes(1);
});
});

55
src/synapse/synapse.js Normal file
View File

@ -0,0 +1,55 @@
import { fetchUtils } from "react-admin";
export const splitMxid = mxid => {
const re =
/^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
return re.exec(mxid)?.groups;
};
export const isValidBaseUrl = baseUrl =>
/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl);
/**
* Resolve the homeserver URL using the well-known lookup
* @param domain the domain part of an MXID
* @returns homeserver base URL
*/
export const getWellKnownUrl = async domain => {
const wellKnownUrl = `https://${domain}/.well-known/matrix/client`;
try {
const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" });
return json["m.homeserver"].base_url;
} catch {
// if there is no .well-known entry, return the domain itself
return `https://${domain}`;
}
};
/**
* Get synapse server version
* @param base_url the base URL of the homeserver
* @returns server version
*/
export const getServerVersion = async baseUrl => {
const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json.server_version;
};
/** Get supported Matrix features */
export const getSupportedFeatures = async baseUrl => {
const versionUrl = `${baseUrl}/_matrix/client/versions`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json;
};
/**
* Get supported login flows
* @param baseUrl the base URL of the homeserver
* @returns array of supported login flows
*/
export const getSupportedLoginFlows = async baseUrl => {
const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
return response.json.flows;
};

View File

@ -0,0 +1,31 @@
import { isValidBaseUrl, splitMxid } from "./synapse";
describe("splitMxid", () => {
it("splits valid MXIDs", () =>
expect(splitMxid("@name:domain.tld")).toEqual({
name: "name",
domain: "domain.tld",
}));
it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined());
});
describe("isValidBaseUrl", () => {
it("accepts a http URL", () =>
expect(isValidBaseUrl("http://foo.bar")).toBeTruthy());
it("accepts a https URL", () =>
expect(isValidBaseUrl("https://foo.bar")).toBeTruthy());
it("accepts a valid URL with port", () =>
expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy());
it("rejects undefined base URLs", () =>
expect(isValidBaseUrl(undefined)).toBeFalsy());
it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy());
it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy());
it("rejects non-string base URLs", () =>
expect(isValidBaseUrl({})).toBeFalsy());
it("rejects base URLs without protocol", () =>
expect(isValidBaseUrl("foo.bar")).toBeFalsy());
it("rejects base URLs with path", () =>
expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy());
it("rejects invalid base URLs", () =>
expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy());
});

16622
yarn.lock

File diff suppressed because it is too large Load Diff