Compare commits

149 Commits

Author SHA1 Message Date
Aine
392fec3186 refactoring (#178)
* unify components import

* refactor config and app context

* refactor icons

* refactor date, error, mxid and storage

* refactor synapse utils
2024-11-25 12:51:05 +02:00
dependabot[bot]
ea0c7a73fd Bump typescript from 5.6.3 to 5.7.2 (#175)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.3 to 5.7.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.3...v5.7.2)

---
updated-dependencies:
- dependency-name: typescript
  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-11-25 11:02:59 +02:00
Aine
21c2fdb5a6 increase the dependabot PRs limit 2024-11-25 11:02:13 +02:00
dependabot[bot]
0a55fc6182 Bump @types/node from 22.8.7 to 22.9.3 (#176)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.8.7 to 22.9.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 11:01:15 +02:00
dependabot[bot]
64c4bd760b Bump typescript-eslint from 8.13.0 to 8.15.0 (#174)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.13.0 to 8.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.15.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-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-11-25 11:00:56 +02:00
dependabot[bot]
e0c8fef365 Bump @mui/icons-material from 6.1.7 to 6.1.8 (#172)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 6.1.7 to 6.1.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/v6.1.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>
2024-11-25 11:00:42 +02:00
dependabot[bot]
29b757ac75 Bump ra-language-french from 5.3.4 to 5.4.0 (#173)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.3.4 to 5.4.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.4...v5.4.0)

---
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-11-25 11:00:31 +02:00
dependabot[bot]
6eb0aaea88 Bump @tanstack/react-query from 5.60.5 to 5.61.3 (#171)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.60.5 to 5.61.3.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.61.3/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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-11-25 10:39:36 +02:00
dependabot[bot]
b42a5c3b69 Bump @emotion/react from 11.13.3 to 11.13.5 (#169)
Bumps [@emotion/react](https://github.com/emotion-js/emotion) from 11.13.3 to 11.13.5.
- [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.13.3...@emotion/react@11.13.5)

---
updated-dependencies:
- dependency-name: "@emotion/react"
  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-11-25 10:39:23 +02:00
dependabot[bot]
66447ac025 Bump @mui/material from 6.1.7 to 6.1.8 (#167)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.1.7 to 6.1.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/v6.1.8/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-11-25 10:36:05 +02:00
dependabot[bot]
747818af6a Bump @emotion/styled from 11.13.0 to 11.13.5 (#166)
Bumps [@emotion/styled](https://github.com/emotion-js/emotion) from 11.13.0 to 11.13.5.
- [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.13.0...@emotion/styled@11.13.5)

---
updated-dependencies:
- dependency-name: "@emotion/styled"
  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-11-25 10:35:52 +02:00
dependabot[bot]
a86fa93608 Bump react-hook-form from 7.53.1 to 7.53.2 (#165)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.53.1 to 7.53.2.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.53.1...v7.53.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 10:35:41 +02:00
dependabot[bot]
f948318919 Bump @typescript-eslint/parser from 8.13.0 to 8.15.0 (#164)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.13.0 to 8.15.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.15.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-11-25 10:34:29 +02:00
dependabot[bot]
5fea8de023 Bump react-admin from 5.3.3 to 5.4.0 (#163)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.3.3 to 5.4.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.3...v5.4.0)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 10:33:30 +02:00
Aine
20417a67b9 User badges (#160)
* Users badges

* update readme
2024-11-22 00:37:16 +02:00
Aine
cfd8238edc migrate config away from localStorage; ensure config is properly loaded on access token auth 2024-11-21 23:31:07 +02:00
Aine
eae00a6ccd use first letter of display name / name / id as default avatar 2024-11-21 22:34:44 +02:00
Aine
3731410486 fix password input helper text (cover all 3 cases: system-managed user, update password, create a new password); add translations for 'generate password' button 2024-11-21 21:26:50 +02:00
Aine
099e349a90 Add missing "generate password" button to the user create form; fix password's helperText 2024-11-21 21:16:39 +02:00
Aine
de125d0d66 add missing labels in the rooms list 2024-11-21 15:56:17 +02:00
Aine
eb8e8f80a9 add explicit labels to the rooms list 2024-11-21 15:24:11 +02:00
ll-SKY-ll
1229266546 Update de.ts (#159)
* Update de.ts

not sure if i like the wording in line 193, but couldnt come up with something better, so I guess its fine

* Update de.ts
2024-11-21 15:01:58 +02:00
Aine
6620942928 add explicit labels to datagrids in destinations, registration tokens, reports, and users (list) 2024-11-21 14:59:03 +02:00
Aine
998d3817c6 Add rooms' avatars (#158)
* Add room avatars

* update readme
2024-11-21 13:46:11 +02:00
Aine
abca842eb9 fix DE message 2024-11-21 10:51:54 +02:00
Borislav Pantaleev
cb1314ab1d Add "Assign Admin" button to the rooms (#156)
* wip on make admin button

* Finish assign admin button

* fixes from pr

* update readme; remove unnecessary console.log; add assign admin to room directory and room view; rename user_id label; set own MXID by default; fix room name display when there is no name
2024-11-21 07:11:48 +02:00
Aine
dae6872fe8 add reverse proxy docs 2024-11-20 16:31:55 +02:00
Aine
61b91adf25 fix readme 2024-11-20 13:03:49 +02:00
Aine
5f7f587477 add config docs 2024-11-20 13:02:45 +02:00
Aine
75b9aeea01 add docs/ 2024-11-20 12:36:12 +02:00
Aine
fb71b5ed08 update readme 2024-11-20 11:56:35 +02:00
Aine
2e8e5e73bc unify project name across translations 2024-11-19 15:16:43 +02:00
Borislav Pantaleev
befcd15298 Add preferred theme colors to footer (#155)
* Add preferred theme colors to footer

* remove inline-block

* Add Footer to LoginPage

* update README, add new screenshot, update Footer
2024-11-19 11:14:06 +02:00
Aine
86d0fd04e6 make login form use dark/light theme 2024-11-18 12:48:20 +02:00
dependabot[bot]
c28ed94ca0 Bump @mui/icons-material from 6.1.5 to 6.1.7 (#153)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 6.1.5 to 6.1.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/v6.1.7/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-11-18 10:25:02 +02:00
dependabot[bot]
d139ab4a7d Bump ra-language-english from 5.3.3 to 5.3.4 (#143)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: ra-language-english
  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-11-18 10:23:31 +02:00
dependabot[bot]
6575a05a8a Bump @mui/material from 6.1.6 to 6.1.7 (#145)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.1.6 to 6.1.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/v6.1.7/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-11-18 10:21:33 +02:00
dependabot[bot]
3f2c3f2c0c Bump vite from 5.4.10 to 5.4.11 (#146)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.11/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.11/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 10:21:18 +02:00
dependabot[bot]
2c5ee7da10 Bump ra-i18n-polyglot from 5.3.3 to 5.3.4 (#147)
Bumps [ra-i18n-polyglot](https://github.com/marmelab/react-admin) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: ra-i18n-polyglot
  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-11-18 10:21:01 +02:00
dependabot[bot]
03bf4a5c5d Bump eslint from 9.14.0 to 9.15.0 (#148)
Bumps [eslint](https://github.com/eslint/eslint) from 9.14.0 to 9.15.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/v9.14.0...v9.15.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-11-18 10:20:47 +02:00
dependabot[bot]
3fce37b735 Bump @tanstack/react-query from 5.59.20 to 5.60.5 (#149)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.59.20 to 5.60.5.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.60.5/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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-11-18 10:20:33 +02:00
dependabot[bot]
6c6ec227fa Bump ra-language-french from 5.3.3 to 5.3.4 (#150)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.3...v5.3.4)

---
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-11-18 10:20:20 +02:00
dependabot[bot]
128fea726e Bump @typescript-eslint/eslint-plugin from 8.13.0 to 8.14.0 (#151)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.13.0 to 8.14.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.14.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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-11-18 10:17:41 +02:00
dependabot[bot]
97c44f85ef Bump ra-core from 5.3.3 to 5.3.4 (#152)
Bumps [ra-core](https://github.com/marmelab/react-admin) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: ra-core
  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-11-18 10:17:29 +02:00
Aine
ca2decda3a add nginx config example 2024-11-18 10:15:40 +02:00
dependabot[bot]
e394a7e186 Bump cross-spawn from 7.0.3 to 7.0.5 in the npm_and_yarn group (#142)
Bumps the npm_and_yarn group with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 06:23:40 +00:00
dependabot[bot]
c182f600f8 Bump @eslint/plugin-kit from 0.2.1 to 0.2.3 in the npm_and_yarn group (#141)
Bumps the npm_and_yarn group with 1 update: [@eslint/plugin-kit](https://github.com/eslint/rewrite).


Updates `@eslint/plugin-kit` from 0.2.1 to 0.2.3
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/release-please-config.json)
- [Commits](https://github.com/eslint/rewrite/compare/plugin-kit-v0.2.1...plugin-kit-v0.2.3)

---
updated-dependencies:
- dependency-name: "@eslint/plugin-kit"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 22:45:21 +00:00
Borislav Pantaleev
915e3564f8 Allow providing login form details via GET params (#140)
* Allow providing login form details via GET params

* add http:// to serverURL if it's only domian name

* update readme
2024-11-14 09:26:12 +02:00
Aine
1570ca5485 automatically check for app update 2024-11-13 23:44:54 +02:00
Borislav Pantaleev
b94b782547 Prevent accidental user overwrites (#139)
* Prevent accidental user overwrites

* cleanup

* update translation

* update readme
2024-11-12 10:15:33 +02:00
dependabot[bot]
90328a5b19 Bump eslint from 9.13.0 to 9.14.0 (#129)
Bumps [eslint](https://github.com/eslint/eslint) from 9.13.0 to 9.14.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/v9.13.0...v9.14.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-11-11 11:33:31 +02:00
dependabot[bot]
b42eb07a2f Bump @tanstack/react-query from 5.59.19 to 5.59.20 (#131)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.59.19 to 5.59.20.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.59.20/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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-11-11 11:33:19 +02:00
dependabot[bot]
131bfaaece Bump typescript-eslint from 8.12.2 to 8.13.0 (#132)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.12.2 to 8.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.13.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-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-11-11 11:33:10 +02:00
dependabot[bot]
b759d5d5fb Bump @typescript-eslint/parser from 8.12.2 to 8.13.0 (#135)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.12.2 to 8.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.13.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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-11-11 11:10:34 +02:00
dependabot[bot]
003e9da792 Bump ra-language-french from 5.3.1 to 5.3.3 (#136)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.3.1 to 5.3.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/v5.3.1...v5.3.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>
2024-11-11 11:10:18 +02:00
dependabot[bot]
908271060b Bump react-router-dom from 6.27.0 to 6.28.0 (#137)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.27.0 to 6.28.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.28.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 11:10:04 +02:00
dependabot[bot]
08cd427084 Bump react-admin from 5.3.1 to 5.3.3 (#138)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.3.1 to 5.3.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/v5.3.1...v5.3.3)

---
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-11-11 11:09:48 +02:00
Aine
73a1d952b1 update readme 2024-11-08 19:04:40 +02:00
Aine
1d5511421d better rate limits handling 2024-11-07 12:15:18 +02:00
Aine
b925c63171 clear storage on logout 2024-11-07 10:46:25 +02:00
Aine
6faebaf9df properly handle restrictBaseUrl config option, fixes #128 2024-11-07 10:34:06 +02:00
Aine
c698f57395 Add support for config from /.well-known/matrix/client (#126)
* Add support for config from /.well-known/matrix/client

* final fixes, refactoring, updated readme
2024-11-07 00:24:33 +02:00
Borislav Pantaleev
9adc13e722 Add User Rate Limits tab (#125)
* Add User Rate Limits tab

* update readme
2024-11-06 23:55:14 +02:00
Borislav Pantaleev
a04b24a5d5 add random password generation on user create/edit form (#123)
* Add button to generate password in UserEdit

* increase default random password length to 64, add more possible symbols to the generator

* update readme
2024-11-06 11:48:44 +02:00
Borislav Pantaleev
cd1ca7c039 Add option to control user's experimental features (#111)
* Add option to control user's experimental features

* Don't use ReferenceManyField, load experimental features manually

* cleanup

* Move experimental features to their own components, improve UI

* remove background from Stack

* update readme
2024-11-06 11:25:47 +02:00
dependabot[bot]
86b4987b7f Bump ra-i18n-polyglot from 5.3.1 to 5.3.2 (#113)
Bumps [ra-i18n-polyglot](https://github.com/marmelab/react-admin) from 5.3.1 to 5.3.2.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.1...v5.3.2)

---
updated-dependencies:
- dependency-name: ra-i18n-polyglot
  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-11-04 11:48:36 +02:00
dependabot[bot]
a7cf647669 Bump @types/lodash from 4.17.12 to 4.17.13 (#114)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.12 to 4.17.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 11:48:25 +02:00
dependabot[bot]
80d40d2fb5 Bump @tanstack/react-query from 5.59.16 to 5.59.19 (#116)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.59.16 to 5.59.19.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.59.19/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  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-11-04 11:43:55 +02:00
dependabot[bot]
b19e961a35 Bump typescript-eslint from 8.11.0 to 8.12.2 (#117)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.11.0 to 8.12.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.12.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-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-11-04 11:43:42 +02:00
dependabot[bot]
08f5f8ebd2 Bump @types/node from 22.8.1 to 22.8.7 (#118)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.8.1 to 22.8.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 11:43:30 +02:00
dependabot[bot]
1edf196049 Bump ra-language-english from 5.3.1 to 5.3.2 (#119)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.3.1 to 5.3.2.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.1...v5.3.2)

---
updated-dependencies:
- dependency-name: ra-language-english
  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-11-04 11:43:11 +02:00
dependabot[bot]
1f66b4d14a Bump ra-core from 5.3.1 to 5.3.2 (#120)
Bumps [ra-core](https://github.com/marmelab/react-admin) from 5.3.1 to 5.3.2.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.1...v5.3.2)

---
updated-dependencies:
- dependency-name: ra-core
  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-11-04 11:42:57 +02:00
dependabot[bot]
cec5b0af9a Bump @testing-library/jest-dom from 6.6.2 to 6.6.3 (#121)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.6.2 to 6.6.3.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.6.2...v6.6.3)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 11:42:44 +02:00
dependabot[bot]
2b0e1e7c0e Bump @mui/material from 6.1.5 to 6.1.6 (#122)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.1.5 to 6.1.6.
- [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/v6.1.6/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-11-04 11:42:31 +02:00
Aine
a613a88232 allow setting version using SYNAPSE_ADMIN_VERSION env var on build (if git is not available) 2024-10-31 17:18:28 +02:00
Aine
7afce71bef handle envs without git in the build hook 2024-10-31 17:11:27 +02:00
Aine
fe4ba22a03 update README 2024-10-30 18:49:32 +02:00
Aine
eb35d9e122 remove CI hack to include version into manifest.json (now generated on each build automatically) 2024-10-30 18:00:05 +02:00
Aine
fa79fecc9d Generate extended manifest.json on build 2024-10-30 17:58:33 +02:00
dependabot[bot]
61366b3792 Bump ra-language-english from 5.3.0 to 5.3.1 (#102)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.0...v5.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 10:55:00 +02:00
dependabot[bot]
b55033d983 Bump react-admin from 5.3.0 to 5.3.1 (#103)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.0...v5.3.1)

---
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-10-28 10:54:46 +02:00
dependabot[bot]
b1f42988c8 Bump ra-language-french from 5.3.0 to 5.3.1 (#104)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.0...v5.3.1)

---
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-10-28 10:54:27 +02:00
dependabot[bot]
ef05b366c3 Bump eslint-plugin-jsx-a11y from 6.10.1 to 6.10.2 (#105)
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.10.1 to 6.10.2.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.10.1...v6.10.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 10:53:30 +02:00
dependabot[bot]
b3d52e7d23 Bump @types/node from 22.7.9 to 22.8.1 (#106)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.9 to 22.8.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 10:53:18 +02:00
dependabot[bot]
d1c4250b46 Bump @tanstack/react-query from 5.59.15 to 5.59.16 (#107)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.59.15 to 5.59.16.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.59.16/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 10:53:04 +02:00
dependabot[bot]
8d44077b24 Bump ra-core from 5.3.0 to 5.3.1 (#109)
Bumps [ra-core](https://github.com/marmelab/react-admin) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.3.0...v5.3.1)

---
updated-dependencies:
- dependency-name: ra-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 10:52:37 +02:00
Aine
44e8b82412 Sanitize CSV on import (#101)
* Sanitize CSV on import

* update readme
2024-10-25 11:52:26 +03:00
Aine
791ae2c869 update naming 2024-10-24 21:49:00 +03:00
Aine
ee3753466a update manifest 2024-10-24 17:27:10 +03:00
Aine
b7dc703157 add community room to the footer 2024-10-24 14:17:02 +03:00
Aine
2eca0dcc33 better visual 2024-10-24 14:11:59 +03:00
Aine
d2219c1667 try adding community room to the readme 2024-10-24 14:11:23 +03:00
Aine
132ea6f97f add images 2024-10-24 13:44:12 +03:00
Aine
28ef08de03 Fix room state events display (#100)
* Fix room state events table display

* update readme
2024-10-23 13:48:21 +03:00
Aine
865e53388e fix yarn lint 2024-10-23 12:31:52 +03:00
dependabot[bot]
3a105bb8c7 Bump eslint from 8.57.1 to 9.13.0 (#99)
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.1 to 9.13.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.57.1...v9.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:14:33 +03:00
dependabot[bot]
edcda7a202 Bump @mui/icons-material from 6.1.1 to 6.1.5 (#98)
Bumps [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material) from 6.1.1 to 6.1.5.
- [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/v6.1.5/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-10-23 12:13:59 +03:00
dependabot[bot]
edd69273e2 Bump typescript-eslint from 8.10.0 to 8.11.0 (#88)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.10.0 to 8.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/typescript-eslint)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:10:43 +03:00
dependabot[bot]
444bfacbd9 Bump eslint-plugin-unused-imports from 3.2.0 to 4.1.4 (#93)
Bumps [eslint-plugin-unused-imports](https://github.com/sweepline/eslint-plugin-unused-imports) from 3.2.0 to 4.1.4.
- [Commits](https://github.com/sweepline/eslint-plugin-unused-imports/commits/v4.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:07:18 +03:00
dependabot[bot]
970e0a550f Bump @types/node from 22.7.7 to 22.7.9 (#91)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.7 to 22.7.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:06:41 +03:00
dependabot[bot]
b3ef68d66e Bump @types/jest from 29.5.13 to 29.5.14 (#95)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.13 to 29.5.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:06:26 +03:00
dependabot[bot]
31382a42ee Bump @typescript-eslint/eslint-plugin from 8.10.0 to 8.11.0 (#92)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 8.10.0 to 8.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:06:11 +03:00
dependabot[bot]
1a7748d1ef Bump @vitejs/plugin-react from 4.3.2 to 4.3.3 (#87)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.3/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:05:51 +03:00
dependabot[bot]
039b28cc5c Bump vite from 5.4.6 to 5.4.10 (#89)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.6 to 5.4.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.10/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:03:42 +03:00
dependabot[bot]
57eae3edb3 Bump @mui/material from 6.1.1 to 6.1.5 (#94)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.1.1 to 6.1.5.
- [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/v6.1.5/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-10-23 12:03:20 +03:00
dependabot[bot]
dadc9416c0 Bump eslint-plugin-jsx-a11y from 6.10.0 to 6.10.1 (#97)
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.10.0 to 6.10.1.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.10.0...v6.10.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:03:03 +03:00
dependabot[bot]
eab2342114 Bump @types/lodash from 4.17.7 to 4.17.12 (#96)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.7 to 4.17.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:02:40 +03:00
dependabot[bot]
9cf2f83936 Bump ra-language-french from 5.2.0 to 5.3.0 (#90)
Bumps [ra-language-french](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-language-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-10-23 12:02:09 +03:00
dependabot[bot]
d823856873 Bump @typescript-eslint/parser from 8.10.0 to 8.11.0 (#86)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.10.0 to 8.11.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.11.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:01:17 +03:00
dependabot[bot]
9b96c7cec8 Bump @eslint/js from 9.10.0 to 9.13.0 (#85)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.10.0 to 9.13.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/commits/v9.13.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:01:01 +03:00
dependabot[bot]
f211aba873 Bump @types/react from 18.3.8 to 18.3.12 (#84)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.8 to 18.3.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:00:50 +03:00
dependabot[bot]
c0fc2d8937 Bump @tanstack/react-query from 5.56.2 to 5.59.15 (#83)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.56.2 to 5.59.15.
- [Release notes](https://github.com/TanStack/query/releases)
- [Commits](https://github.com/TanStack/query/commits/v5.59.15/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 12:00:37 +03:00
Aine
a88b397748 increase dependabot PR limits 2024-10-23 11:58:50 +03:00
Aine
abc922c956 Enable visual customization (#81)
* Enable visual customization

* update readme
2024-10-23 00:03:06 +03:00
Aine
4f2cd38344 Add user profile to the top menu (#80)
* Add user profile to the top menu

* update readme
2024-10-22 12:18:55 +03:00
Aine
ca71038874 Custom Menu Items (#79)
* Custom Menu Items

* update readme

* fix indentation
2024-10-21 23:33:51 +03:00
Borislav Pantaleev
be867b6b0d Fix types and missing translations (#78) 2024-10-21 19:37:29 +03:00
Aine
f2f540b429 fix footer content overlapping and scrollbar 2024-10-21 13:27:32 +03:00
Aine
7feec4ba07 update eslint typescript 2024-10-21 12:01:12 +03:00
Aine
1d5fef1e53 add missing package 2024-10-21 11:43:26 +03:00
dependabot[bot]
9c40efde17 Bump typescript-eslint from 7.18.0 to 8.10.0 (#76)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 7.18.0 to 8.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.10.0/packages/typescript-eslint)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:34:26 +03:00
dependabot[bot]
53dff66978 Bump react-admin from 5.2.0 to 5.3.0 (#77)
Bumps [react-admin](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: react-admin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:31:09 +03:00
dependabot[bot]
3a595247e8 Bump vite-plugin-version-mark from 0.1.1 to 0.1.2 (#75)
Bumps [vite-plugin-version-mark](https://github.com/ZhongxuYang/vite-plugin-version-mark) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/ZhongxuYang/vite-plugin-version-mark/releases)
- [Changelog](https://github.com/ZhongxuYang/vite-plugin-version-mark/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ZhongxuYang/vite-plugin-version-mark/compare/v0.1.1...v0.1.2)

---
updated-dependencies:
- dependency-name: vite-plugin-version-mark
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:29:57 +03:00
dependabot[bot]
33f5f60e31 Bump eslint-plugin-import from 2.30.0 to 2.31.0 (#74)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.30.0 to 2.31.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.30.0...v2.31.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:29:35 +03:00
dependabot[bot]
9dd2ea57c9 Bump @types/papaparse from 5.3.14 to 5.3.15 (#73)
Bumps [@types/papaparse](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/papaparse) from 5.3.14 to 5.3.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/papaparse)

---
updated-dependencies:
- dependency-name: "@types/papaparse"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:29:16 +03:00
dependabot[bot]
fae7a696de Bump ra-i18n-polyglot from 5.2.0 to 5.3.0 (#68)
Bumps [ra-i18n-polyglot](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-i18n-polyglot
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:23:56 +03:00
dependabot[bot]
49e8b2d0f5 Bump vite-plugin-version-mark from 0.1.0 to 0.1.1 (#72)
Bumps [vite-plugin-version-mark](https://github.com/ZhongxuYang/vite-plugin-version-mark) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/ZhongxuYang/vite-plugin-version-mark/releases)
- [Changelog](https://github.com/ZhongxuYang/vite-plugin-version-mark/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ZhongxuYang/vite-plugin-version-mark/compare/v0.1.0...v0.1.1)

---
updated-dependencies:
- dependency-name: vite-plugin-version-mark
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:22:15 +03:00
dependabot[bot]
281d908d3f Bump ra-language-english from 5.2.0 to 5.3.0 (#71)
Bumps [ra-language-english](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.0...v5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:21:57 +03:00
dependabot[bot]
bacc42fe9c Bump react-router-dom from 6.26.2 to 6.27.0 (#70)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.26.2 to 6.27.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@6.27.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.27.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:21:44 +03:00
dependabot[bot]
1c26a28ca9 Bump react-hook-form from 7.53.0 to 7.53.1 (#69)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.53.0 to 7.53.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.53.0...v7.53.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:21:33 +03:00
dependabot[bot]
d3a04cd132 Bump @vitejs/plugin-react from 4.3.1 to 4.3.2 (#67)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.2/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:20:58 +03:00
dependabot[bot]
e6060a23ac Bump @types/node from 20.16.5 to 22.7.7 (#66)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.16.5 to 22.7.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:20:43 +03:00
dependabot[bot]
4b7fbf483a Bump typescript from 5.6.2 to 5.6.3 (#65)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.6.2 to 5.6.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.6.2...v5.6.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:20:31 +03:00
dependabot[bot]
bc3c30da92 Bump @testing-library/jest-dom from 6.5.0 to 6.6.2 (#64)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.5.0 to 6.6.2.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.5.0...v6.6.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:20:02 +03:00
dependabot[bot]
1896f770d1 Bump ra-core from 5.2.0 to 5.3.0 (#63)
Bumps [ra-core](https://github.com/marmelab/react-admin) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/marmelab/react-admin/releases)
- [Changelog](https://github.com/marmelab/react-admin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/marmelab/react-admin/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: ra-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:19:34 +03:00
dependabot[bot]
99d0b9ad72 Bump docker/setup-buildx-action from 1 to 3 (#62)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-19 16:19:06 +03:00
Aine
944afb9056 bring back dependabot config 2024-10-19 16:15:23 +03:00
Aine
23f5a24803 update README.md 2024-10-19 15:57:12 +03:00
Borislav Pantaleev
60ae00ac14 Add seprate login button for access token (#61) 2024-10-19 12:03:46 +00:00
Borislav Pantaleev
26862fa708 Add etke.cc to footer and package.json (#60)
* Add etke.cc to footer and package.json

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

* Add accessToken login

* Add confirmation for session destroy on accessToken logout

* add translations, fix tests, minor renaming

* update readme
2024-10-17 18:34:20 +03:00
Aine
19302466ef update README.md 2024-10-10 22:13:50 +03:00
Aine
0594259ae4 make avatar field sortable in the users list 2024-10-08 12:15:03 +03:00
Aine
ba485bbb18 Merge branch 'master' 2024-10-08 10:51:16 +03:00
Dirk Klimpel
9fc005032c Fix for empty user default tab after creation (#628) 2024-10-08 09:20:55 +02:00
Aine
f5d6f24b30 correctly treat empty or "almost empty" restrictBaseUrl config option, fixes #56 2024-10-07 12:00:33 +03:00
Aine
a42efe7eda do not color failed federation destinations, use an icon instead 2024-10-04 00:16:11 +03:00
64 changed files with 3101 additions and 1523 deletions

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

@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 30
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 30
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 30

View File

@@ -25,10 +25,6 @@ jobs:
cache: yarn
- name: Install dependencies
run: yarn install --immutable --network-timeout=300000
- name: Set version into manifest.json
run: |
TAG=$(git describe --tags --abbrev=0 || echo "latest")
sed -i "s|\"icons\"|\"version\": \"$TAG\",\\n \"icons\"|g" public/manifest.json
- name: Build
run: yarn build --base=${{ env.base_path }}
- uses: actions/upload-artifact@v4
@@ -52,7 +48,7 @@ jobs:
name: dist
path: dist/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:

175
README.md
View File

@@ -1,19 +1,33 @@
# Synapse Admin UI [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
<p align="center">
<img alt="Synapse Admin Logo" src="./public/images/logo.webp" height="140" />
<h3 align="center">
Synapse Admin<br>
<a href="https://matrix.to/#/#synapse-admin:etke.cc">
<img alt="Community room" src="https://img.shields.io/badge/room-community_room-green?logo=matrix&label=%23synapse-admin%3Aetke.cc">
</a><br>
<a href="./LICENSE">
<img alt="License" src="https://img.shields.io/github/license/etkecc/synapse-admin">
</a>
</h3>
<p align="center">Feature-packed and visually customizable: A better way to manage your Synapse homeserver.</p>
</p>
![Screenshots](./screenshots.jpg)
---
This project is built using [react-admin](https://marmelab.com/react-admin/).
![Login form showing dark and light modes](./screenshots/auth.webp)
![Screenshots](./screenshots/screenshots.jpg)
<!-- vim-markdown-toc GFM -->
* [Fork differences](#fork-differences)
* [Available via CDN](#available-via-cdn)
* [Changes](#changes)
* [Development](#development)
* [Support](#support)
* [Configuration](#configuration)
* [Prefilling login form](#prefilling-login-form)
* [Restricting available homeserver](#restricting-available-homeserver)
* [Protecting appservice managed users](#protecting-appservice-managed-users)
* [Providing support URL](#providing-support-url)
* [Adding custom menu items](#adding-custom-menu-items)
* [Usage](#usage)
* [Supported Synapse](#supported-synapse)
* [Prerequisites](#prerequisites)
@@ -22,47 +36,70 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
* [Steps for 1)](#steps-for-1)
* [Steps for 2)](#steps-for-2)
* [Steps for 3)](#steps-for-3)
* [Serving Synapse-Admin on a different path](#serving-synapse-admin-on-a-different-path)
* [Serving Synapse Admin on a different path](#serving-synapse-admin-on-a-different-path)
* [Development](#development-1)
<!-- vim-markdown-toc -->
## Fork differences
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
user-friendly interface for managing Synapse homeservers.
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream,
this fork introduces numerous enhancements to improve usability and extend functionality,
including support for authenticated media, advanced user management options, and visual customization.
The full list is described below in the [Changes](#changes) section.
### Available via CDN
**Availability**
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
* As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin)
* As a standalone app on [admin.etke.cc](https://admin.etke.cc)
* As a prebuilt distribution on [GitHub Releases](https://github.com/etkecc/synapse-admin/releases)
* As a Docker container on [GitHub Container Registry](https://github.com/etkecc/synapse-admin/pkgs/container/synapse-admin)
* As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md)
### Changes
The following changes are already implemented:
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
* [Fix user's default tab not being shown](https://github.com/etkecc/synapse-admin/pull/8)
* [Add identifier when authorizing with password](https://github.com/Awesome-Technologies/synapse-admin/pull/601)
* [Add ability to toggle whether to show locked users](https://github.com/Awesome-Technologies/synapse-admin/pull/573)
* [Fix user's display name in header on user's page](https://github.com/etkecc/synapse-admin/pull/9)
* [Fix footer overlapping content](https://github.com/Awesome-Technologies/synapse-admin/issues/574)
* Switch from nginx to [SWS](https://static-web-server.net/) for serving the app, reducing the size of the Docker image
* [Fix redirect URL after user creation](https://github.com/etkecc/synapse-admin/pull/16)
* [Display actual Synapse errors](https://github.com/etkecc/synapse-admin/pull/17)
* [Fix base_url being undefined on unsuccessful login](https://github.com/etkecc/synapse-admin/pull/18)
* [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (CI only)
* [Federation page improvements](https://github.com/Awesome-Technologies/synapse-admin/pull/583) (using theme colors)
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
* [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
* 🛑 [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
* 🐛 [Fix user's default tab not being shown](https://github.com/etkecc/synapse-admin/pull/8)
* 🔑 [Add identifier when authorizing with password](https://github.com/Awesome-Technologies/synapse-admin/pull/601)
* 🔒 [Add ability to toggle whether to show locked users](https://github.com/Awesome-Technologies/synapse-admin/pull/573)
* 🖊️ [Fix user's display name in header on user's page](https://github.com/etkecc/synapse-admin/pull/9)
* 🧹 [Fix footer overlapping content](https://github.com/Awesome-Technologies/synapse-admin/issues/574)
* 🐋 Switch from nginx to [SWS](https://static-web-server.net/) for serving the app, reducing the size of the Docker image
* 🔄 [Fix redirect URL after user creation](https://github.com/etkecc/synapse-admin/pull/16)
* 🔍 [Display actual Synapse errors](https://github.com/etkecc/synapse-admin/pull/17)
* ⚠️ [Fix base_url being undefined on unsuccessful login](https://github.com/etkecc/synapse-admin/pull/18)
* 📜 [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (later replaced with a proper manifest.json generation on build)
* 📊 [Federation page improvements](https://github.com/Awesome-Technologies/synapse-admin/pull/583) (using icons)
* 🚪 [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
* 🛠️ [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
* 🛡️ [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
* 🖼️ [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
* 🚀 [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
* 🔒 [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
* 📞 [Add `Contact support` menu item](https://github.com/etkecc/synapse-admin/pull/45)
* 🧹 [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
* 🎞️ [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
* 👁️ [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
* 🔐 [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
* 📏 [Fix footer causing vertical scrollbar](https://github.com/etkecc/synapse-admin/pull/60)
* 🍴 [Custom Menu Items](https://github.com/etkecc/synapse-admin/pull/79)
* 🧑‍💻 [Add user profile to the top menu](https://github.com/etkecc/synapse-admin/pull/80)
* 🎨 [Enable visual customization](https://github.com/etkecc/synapse-admin/pull/81)
* 🛋️ [Fix room state events display](https://github.com/etkecc/synapse-admin/pull/100)
* 🧹 [Sanitize CSV on import](https://github.com/etkecc/synapse-admin/pull/101)
* ⚙️ Allow setting version using `SYNAPSE_ADMIN_VERSION` environment variable on build (if git is not available)
* 🧪 [Add option to control user's experimental features](https://github.com/etkecc/synapse-admin/pull/111)
* 🔑 [Add random password generation on user create/edit form](https://github.com/etkecc/synapse-admin/pull/123)
* 🚦 [Add option to set user's rate limits](https://github.com/etkecc/synapse-admin/pull/125)
* 🌐 [Support configuration via /.well-known/matrix/client](https://github.com/etkecc/synapse-admin/pull/126)
* 🛑 [Prevent accidental user overwrites](https://github.com/etkecc/synapse-admin/pull/139)
* 🔍 [Allow providing login form details via GET params](https://github.com/etkecc/synapse-admin/pull/140)
* 🎨 [Add preferred theme colors to login page and footer](https://github.com/etkecc/synapse-admin/pull/155)
* 🔰 [Add "Assign Admin" button to the rooms](https://github.com/etkecc/synapse-admin/pull/156)
* 🖼️ [Add rooms' avatars](https://github.com/etkecc/synapse-admin/pull/158)
* 🤖 [User Badges](https://github.com/etkecc/synapse-admin/pull/160)
_the list will be updated as new changes are added_
@@ -71,15 +108,32 @@ _the list will be updated as new changes are added_
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
user creation fails)
This command initializes the development environment (local Synapse server and Postgres DB),
and launches the app in a dev mode at `http://localhost:5173`
After that open `http://localhost:5173` in your browser, login using the following credentials:
* Login: admin
* Password: admin
* Homeserver URL: http://localhost:8008
### Support
If you have any questions or need help, feel free to join the [community room](https://matrix.to/#/#synapse-admin:etke.cc) or create an issue on GitHub.
## Configuration
You can use `config.json` file to configure synapse-admin
You can use `config.json` file to configure Synapse Admin instance,
and `/.well-known/matrix/client` file to provide Synapse Admin configuration specifically for your homeserver.
In the latter case, any instance of Synapse Admin will automatically pick up the configuration from the homeserver.
Note that configuration inside the `/.well-known/matrix/client` file should go under the `cc.etke.synapse-admin` key,
and it will override the configuration from the `config.json` file.
In case you use [spantaleev/matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy) or
[etkecc/ansible](https://github.com/etkecc/ansible),
configuration will be automatically added to the `/.well-known/matrix/client` file.
[Configuration options](./docs/config.md)
The `config.json` can be injected into a Docker container using a bind mount.
@@ -92,48 +146,35 @@ services:
...
```
### Prefilling login form
You can prefill `username` and `homeserver` fields on the login page using GET parameters, example:
```
https://matrix.example.com/synapse-admin/?username=admin&server=matrix.example.com
```
That way `username` and `homeserver` fields will be pre-filled with `admin` and `https://matrix.example.com` respectively.
### Restricting available homeserver
You can restrict the homeserver(s), so that the user can no longer define it himself.
Edit `config.json` to restrict either to a single homeserver:
```json
{
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
}
```
or to a list of homeservers:
```json
{
"restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
}
```
[Documentation](./docs/restrict-hs.md)
### Protecting appservice managed users
To avoid accidental adjustments of appservice-managed users (e.g., puppets created by a bridge) and breaking the bridge,
you can specify the list of MXIDs (regexp) that should be prohibited from any changes, except display name and avatar.
Example for [mautrix-telegram](https://github.com/mautrix/telegram)
[Documentation](./docs/system-users.md)
```json
{
"asManagedUsers": ["^@telegram_[a-zA-Z0-9]+:example\\.com$"]
}
```
### Adding custom menu items
### Providing support URL
You can add custom menu items to the main menu by providing a `menu` array in the config.
Synapse-Admin provides a support link in the main menu - `Contact support`. By default, the link points to the GitHub issues page of the project. You can change this link by providing a `supportURL` in the `config.json`.
```json
{
"supportURL": "https://example.com/support"
}
```
[Documentation](./docs/custom-menu.md)
## Usage
@@ -182,6 +223,8 @@ You have three options:
- move or symlink the `synapse-admin` into your vhosts root dir
- open the url of the vhost in your browser
[Reverse Proxy Documentation with Examples](./docs/reverse-proxy.md)
#### Steps for 2)
- make sure you have installed the following: git, yarn, nodejs
@@ -214,7 +257,7 @@ You have three options:
- browse to http://localhost:8080
### Serving Synapse-Admin on a different path
### Serving Synapse Admin on a different path
The path prefix where synapse-admin is served can only be changed during the build step.
@@ -222,7 +265,7 @@ If you downloaded the source code, use `yarn build --base=/my-prefix` to set a p
If you want to build your own Docker container, use the `BASE_PATH` argument.
We do not support directly changing the path where Synapse-Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse-Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
We do not support directly changing the path where Synapse Admin is served in the pre-built Docker container. Instead please use a reverse proxy if you need to move Synapse Admin to a different base path. If you want to serve multiple applications with different paths on the same domain, you need a reverse proxy anyway.
Example for Traefik:
@@ -240,7 +283,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
synapse-admin:
image: etkecc/synapse-admin:latest
image: ghcr.io/etkecc/synapse-admin:latest
restart: unless-stopped
labels:
- "traefik.enable=true"

94
docs/config.md Normal file
View File

@@ -0,0 +1,94 @@
# Configuration
Synapse Admin could be configured using the following ways (both are optional, and both could be used together):
* By providing the `config.json` file alongside with the Synapse Admin deployment, example: [admin.etke.cc/config.json](https://admin.etke.cc/config.json)
* By providing configuration under the `cc.etke.synapse-admin` key in the `/.well-known/matrix/client` file, example:
[demo.etke.host/.well-known/matrix/client](https://demo.etke.host/.well-known/matrix/client)
In case you are an [etke.cc](https://etke.cc) customer,
or use [spantaleev/matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy),
or [etkecc/ansible](https://github.com/etkecc/ansible),
configuration will be automatically added to the `/.well-known/matrix/client` file.
**Why `/.well-known/matrix/client`?**
Because any instance of Synapse Admin will automatically pick up the configuration from the homeserver.
Common use case when you have a Synapse server running, but don't want (or can't) deploy Synapse Admin alongside with it.
In this case, you could provide the configuration in the `/.well-known/matrix/client` file,
and any Synapse Admin instance (e.g., [admin.etke.cc](https://admin.etke.cc) will pick it up.
Another common case is when you have multiple Synapse servers running and want to use a single Synapse Admin instance to manage them all.
In this case, you could provide the configuration in the `/.well-known/matrix/client` file for each of the servers.
## Configuration options
* `restrictBaseUrl` - restrictBaseUrl restricts the Synapse Admin instance to work only with specific homeserver(-s).
It accepts both a string and an array of strings.
The homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
Example: `https://matrix.example.com` or `https://synapse.example.net`
[More details](restrict-hs.md)
* `asManagedUsers` - protect system user accounts managed by appservices (such as bridges) / system (such as bots) from accidental changes.
By defining a list of MXID regex patterns, you can protect these accounts from accidental changes.
Example: `^@baibot:example\\.com$`, `^@slackbot:example\\.com$`, `^@slack_[a-zA-Z0-9\\-]+:example\\.com$`, `^@telegrambot:example\\.com$`, `^@telegram_[a-zA-Z0-9]+:example\\.com$`
[More details](system-users.md)
* `menu` - add custom menu items to the main menu (sidebar) by providing a `menu` array in the config.
Each `menu` item can contain the following fields:
* `label` (required): The text to display in the menu.
* `icon` (optional): The icon to display next to the label, one of the [src/utils/icons.ts](../src/utils/icons.ts) icons, otherwise a default icon will be used.
* `url` (required): The URL to navigate to when the menu item is clicked.
[More details](custom-menu.md)
## Examples
### config.json
```json
{
"restrictBaseUrl": [
"https://matrix.example.com",
"https://synapse.example.net"
],
"asManagedUsers": [
"^@baibot:example\\.com$",
"^@slackbot:example\\.com$",
"^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
"^@telegrambot:example\\.com$",
"^@telegram_[a-zA-Z0-9]+:example\\.com$"
],
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
```
### `/.well-known/matrix/client`
```json
{
"cc.etke.synapse-admin": {
"restrictBaseUrl": [
"https://matrix.example.com",
"https://synapse.example.net"
],
"asManagedUsers": [
"^@baibot:example\\.com$",
"^@slackbot:example\\.com$",
"^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
"^@telegrambot:example\\.com$",
"^@telegram_[a-zA-Z0-9]+:example\\.com$"
],
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
}
```

47
docs/custom-menu.md Normal file
View File

@@ -0,0 +1,47 @@
# Custom Menu Items
You can add custom menu items to the main menu (sidebar) by providing a `menu` array in the config.
This is useful for adding links to external sites or other pages in your documentation, like a support page or internal wiki.
## Configuration
The examples below contain the configuration settings to add a link to the [Synapse Admin issues](https://github.com/etke.cc/synapse-admin/issues).
Each `menu` item can contain the following fields:
* `label` (required): The text to display in the menu.
* `icon` (optional): The icon to display next to the label, one of the [src/utils/icons.ts](../src/utils/icons.ts) icons, otherwise a
default icon will be used.
* `url` (required): The URL to navigate to when the menu item is clicked.
[Configuration options](config.md)
### config.json
```json
{
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
```
### `/.well-known/matrix/client`
```json
{
"cc.etke.synapse-admin": {
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
}
```

44
docs/restrict-hs.md Normal file
View File

@@ -0,0 +1,44 @@
# Restricting available homeserver
If you want to have your Synapse Admin instance work only with specific homeserver(-s),
you can do that by setting `restrictBaseUrl` in the configuration.
## Configuration
You can do that for a single homeserver or multiple homeservers at once, as `restrictBaseUrl` accepts both a string and
an array of strings.
The examples below contain the configuration settings to restrict the Synapse Admin instance to work only with
`example.com` (with Synapse runing at `matrix.example.com`) and
`example.net` (with Synapse running at `synapse.example.net`) homeservers.
Note that the homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
So, if you have a homeserver `example.com` where users have MXIDs like `@user:example.com`,
but actual Synapse is installed on `matrix.example.com` subdomain, you should use `https://matrix.example.com` in the
configuration.
[Configuration options](config.md)
### config.json
```json
{
"restrictBaseUrl": [
"https://matrix.example.com",
"https://synapse.example.net"
]
}
```
### `/.well-known/matrix/client`
```json
{
"cc.etke.synapse-admin": {
"restrictBaseUrl": [
"https://matrix.example.com",
"https://synapse.example.net"
]
}
}
```

53
docs/reverse-proxy.md Normal file
View File

@@ -0,0 +1,53 @@
# Serving Synapse Admin behind a reverse proxy
Your are supposed to do so for any service you want to expose to the internet,
and here you can find specific instructions and example configurations for Synapse Admin.
## Nginx
Place the config below into `/etc/nginx/conf.d/synapse-admin.conf` (don't forget to replace `server_name` and `root`):
```nginx
server {
listen 80;
listen [::]:80;
server_name example.com; # REPLACE with your domain
root /var/www/synapse-admin; # REPLACE with path where you extracted synapse admin
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|woff|woff2|ttf|eot|webp)$ {
expires 30d; # Set caching for static assets
add_header Cache-Control "public";
}
gzip on;
gzip_types text/plain application/javascript application/json text/css text/xml application/xml+rss;
gzip_min_length 1000;
}
```
After you've done that, ensure that the configuration is correct by running `nginx -t` and then reload Nginx
(e.g. `systemctl reload nginx`).
> **Note:** This configuration doesn't cover HTTPS, which is highly recommended to use. You can find more information
about setting up HTTPS in the [Nginx documentation](https://nginx.org/en/docs/http/configuring_https_servers.html).
## Traefik (docker labels)
If you are using Traefik as a reverse proxy, you can use the following labels, `docker-compose.yml` example:
```yaml
services:
synapse-admin:
image: ghcr.io/etkecc/synapse-admin:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)"
```
## Other reverse proxies
There is no examples for other reverse proxies yet, and so PRs are greatly appreciated.

44
docs/system-users.md Normal file
View File

@@ -0,0 +1,44 @@
# System / Appservice-managed Users
Inadvertently altering system user accounts managed by appservices (such as bridges) / system (such as bots) is a common issue.
Editing, deleting, locking, or changing the passwords of these appservice-managed accounts can cause serious problems.
To prevent this, we've added a new feature that blocks these types of modifications to such accounts,
while still allowing other risk-free changes (changing display names and avatars).
By defining a list of MXID regex patterns in the new `asManagedUsers` configuration setting,
you can protect these accounts from accidental changes.
## Configuration
The examples below contain the configuration settings to mark
[Telegram bridge (mautrix-telegram)](https://github.com/mautrix/telegram),
[Slack bridge (mautrix-slack)](https://github.com/mautrix/slack),
and [Baibot](https://github.com/etkecc/baibot) users of `example.com` homeserver as appservice-managed users,
just to illustrate the options to protect both specific MXIDs (as in the Baibot example) and all puppets of a bridge (as in the Telegram and Slack examples).
[Configuration options](config.md)
### config.json
```json
"asManagedUsers": [
"^@baibot:example\\.com$",
"^@slackbot:example\\.com$",
"^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
"^@telegrambot:example\\.com$",
"^@telegram_[a-zA-Z0-9]+:example\\.com$"
]
```
### `/.well-known/matrix/client`
```json
"cc.etke.synapse-admin": {
"asManagedUsers": [
"^@baibot:example\\.com$",
"^@slackbot:example\\.com$",
"^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
"^@telegrambot:example\\.com$",
"^@telegram_[a-zA-Z0-9]+:example\\.com$"
]
}
```

36
docs/user-badges.md Normal file
View File

@@ -0,0 +1,36 @@
# User Badges
To help with identifying users with certain roles or permissions, we have implemented a badge system.
These badges are displayed on the user's avatar and have a handy tooltip that explains what the badge means.
## Available Badges
### 🧙‍ You
This badge is displayed on your user's avatar.
Tooltip for this badge will contain additional information, e.g.: `You (Admin)`.
### 👑 Admin
This badge is displayed on homeserver admins' avatars.
Tooltip for this badge is `Admin`.
### 🛡️ Appservice/System-managed
This badge is displayed on users that are managed by an appservices (or system), [more details](./system-users.md).
Tooltip for this badge will contain additional information, e.g.: `System-managed (Bot)`.
### 🤖 Bot
This badge is displayed on bots' avatars (users with the `user_type` set to `bot`).
Tooltip for this badge is `Bot`.
### 📞 Support
This badge is displayed on users that are part of the support team (users with the `user_type` set to `support`).
Tooltip for this badge is `Support`.
### 👤 Regular User
This badge is displayed on regular users' avatars.
Tooltip for this badge is `Regular User`.

View File

@@ -4,17 +4,14 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Synapse-Admin"
/>
<meta name="description" content="Synapse Admin" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>Synapse-Admin</title>
<title>Synapse Admin</title>
<style>
body {
margin: 0;
@@ -22,6 +19,11 @@
font-family: sans-serif;
}
.layout {
min-height: 90vh !important;
height: 90vh;
}
.loader-container {
display: flex;
align-items: center;
@@ -120,13 +122,7 @@
</div>
</div>
<script type="module" src="/src/index.tsx"></script>
<footer
style="position: relative; z-index: 2; height: 2em; margin-top: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
<a id="copyright" href="https://github.com/etkecc/synapse-admin"
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
</a>
</footer>
<span id="js-version" style="display: none;"></span>
</body>
<script>document.getElementById("version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
<script>document.getElementById("js-version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
</html>

View File

@@ -25,11 +25,15 @@ run-dev:
stop-dev:
@docker-compose -f docker-compose-dev.yml stop
# register a user in the dev stack
register-user localpart password *admin:
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
# run yarn {fix,lint,test} commands
test:
@-yarn run fix
@-yarn run lint
@-yarn run test
# run the app in a production mode
run-prod: build

View File

@@ -3,7 +3,7 @@
"version": "0.10.3",
"description": "Admin GUI for the Matrix.org server Synapse",
"type": "module",
"author": "Awesome Technologies Innovationslabor GmbH",
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
"license": "Apache-2.0",
"homepage": ".",
"repository": {
@@ -11,25 +11,25 @@
"url": "https://github.com/etkecc/synapse-admin"
},
"devDependencies": {
"@eslint/js": "^9.7.0",
"@eslint/js": "^9.13.0",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.7",
"@types/node": "^20.14.12",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/node": "^22.9.3",
"@types/papaparse": "^5.3.15",
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitejs/plugin-react": "^4.3.3",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^3.2.0",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-yaml": "^1.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -38,41 +38,42 @@
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.16.1",
"vite": "^5.4.6",
"vite-plugin-version-mark": "^0.1.0"
"typescript": "^5.7.2",
"typescript-eslint": "^8.15.0",
"vite": "^5.4.11",
"vite-plugin-version-mark": "^0.1.2"
},
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5",
"@haleos/ra-language-german": "^1.0.0",
"@haxqer/ra-language-chinese": "^4.16.2",
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@tanstack/react-query": "^5.56.2",
"@mui/icons-material": "^6.1.8",
"@mui/material": "^6.1.8",
"@mui/utils": "^5.16.6",
"@tanstack/react-query": "^5.61.3",
"history": "^5.3.0",
"lodash": "^4.17.21",
"papaparse": "^5.4.1",
"ra-core": "^5.2.0",
"ra-i18n-polyglot": "^5.2.0",
"ra-language-english": "^5.2.0",
"ra-core": "^5.3.4",
"ra-i18n-polyglot": "^5.3.4",
"ra-language-english": "^5.3.4",
"ra-language-farsi": "^5.0.0",
"ra-language-french": "^5.2.0",
"ra-language-french": "^5.4.0",
"ra-language-italian": "^3.13.1",
"ra-language-russian": "^4.14.2",
"react": "^18.3.1",
"react-admin": "^5.2.0",
"react-admin": "^5.4.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.53.2",
"react-is": "^18.3.1",
"react-router": "^6.26.2",
"react-router-dom": "^6.26.2"
"react-router-dom": "^6.28.0"
},
"scripts": {
"start": "vite serve",
"build": "vite build",
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx,.yml,.yaml .",
"fix": "yarn lint --fix",
"test": "yarn jest",
"test:watch": "yarn jest --watch"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/images/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,15 +0,0 @@
{
"short_name": "Synapse-Admin",
"name": "Synapse-Admin",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

BIN
screenshots/auth.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import fetchMock from "jest-fetch-mock";
fetchMock.enableMocks();
@@ -7,6 +7,6 @@ import App from "./App";
describe("App", () => {
it("renders", async () => {
render(<App />);
await screen.findAllByText("Welcome to Synapse-admin");
await screen.findAllByText("Welcome to Synapse Admin");
});
});
});

View File

@@ -2,10 +2,11 @@ import { merge } from "lodash";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { createContext, useContext } from "react";
import { Route } from "react-router-dom";
import { AdminLayout } from "./components/AdminLayout";
import { ImportFeature } from "./components/ImportFeature";
import AdminLayout from "./components/AdminLayout";
import UserImport from "./components/UserImport";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
@@ -23,6 +24,7 @@ import users from "./resources/users";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Config } from "./utils/config";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
@@ -49,7 +51,7 @@ const i18nProvider = polyglotI18nProvider(
const queryClient = new QueryClient();
const App = () => (
export const App = () => (
<QueryClientProvider client={queryClient}>
<Admin
disableTelemetry
@@ -61,7 +63,7 @@ const App = () => (
i18nProvider={i18nProvider}
>
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
<Route path="/import_users" element={<UserImport />} />
</CustomRoutes>
<Resource {...users} />
<Resource {...rooms} />
@@ -84,4 +86,8 @@ const App = () => (
</QueryClientProvider>
);
export const AppContext = createContext({});
export const useAppContext = () => useContext(AppContext) as Config;
export default App;

View File

@@ -1,11 +0,0 @@
import { createContext, useContext } from "react";
interface AppContextType {
restrictBaseUrl: string | string[];
asManagedUsers: string[];
supportURL: string;
}
export const AppContext = createContext({});
export const useAppContext = () => useContext(AppContext) as AppContextType;

View File

@@ -1,26 +1,100 @@
import { Layout, Menu } from 'react-admin';
import LiveHelpIcon from '@mui/icons-material/LiveHelp';
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react";
import { Icons, DefaultIcon } from "../utils/icons";
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
import Footer from "./Footer";
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
const supportLink = (): string => {
try {
new URL(localStorage.getItem("support_url") || ''); // Check if the URL is valid
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
} catch (e) {
return DEFAULT_SUPPORT_LINK;
const AdminUserMenu = () => {
const [open, setOpen] = useState(false);
const logout = useLogout();
const checkLoginType = (ev: React.MouseEvent<HTMLDivElement>) => {
const loginType: LoginMethod = (localStorage.getItem("login_type") || "credentials") as LoginMethod;
if (loginType === "accessToken") {
ev.stopPropagation();
setOpen(true);
}
};
const handleConfirm = () => {
setOpen(false);
logout();
};
const handleDialogClose = () => {
setOpen(false);
ClearConfig();
window.location.reload();
};
return (
<UserMenu>
<div onClickCapture={checkLoginType}>
<Logout />
</div>
<Confirm
isOpen={open}
title="synapseadmin.auth.logout_acces_token_dialog.title"
content="synapseadmin.auth.logout_acces_token_dialog.content"
onConfirm={handleConfirm}
onClose={handleDialogClose}
confirm="synapseadmin.auth.logout_acces_token_dialog.confirm"
cancel="synapseadmin.auth.logout_acces_token_dialog.cancel"
/>
</UserMenu>
);
};
const AdminAppBar = () => {
return (<AppBar userMenu={<AdminUserMenu />}>
<TitlePortal />
<InspectorButton />
</AppBar>);
};
const AdminMenu = () => (
<Menu>
<Menu.ResourceItems />
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
const AdminMenu = (props) => {
const [menu, setMenu] = useState([] as MenuItem[]);
useEffect(() => setMenu(GetConfig().menu), []);
return (
<Menu {...props}>
<Menu.ResourceItems />
{menu.map((item, index) => {
const { url, icon, label } = item;
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
return (
<Suspense key={index}>
<Menu.Item
to={url}
target="_blank"
primaryText={label}
leftIcon={IconComponent ? <IconComponent /> : <DefaultIcon />}
onClick={props.onMenuClick}
/>
</Suspense>
);
})}
</Menu>
);
);
};
export const AdminLayout = ({ children }) => (
<Layout menu={AdminMenu}>
{children}
export const AdminLayout = ({ children }) => {
return <>
<Layout appBar={AdminAppBar} menu={AdminMenu} sx={{
['& .RaLayout-appFrame']: {
minHeight: '90vh',
height: '90vh',
},
['& .RaLayout-content']: {
marginBottom: '3rem',
},
}}>
{children}
<CheckForApplicationUpdate />
</Layout>
);
<Footer />
</>
};
export default AdminLayout;

View File

@@ -1,12 +1,12 @@
import { get } from "lodash";
import { Avatar, AvatarProps } from "@mui/material";
import { useRecordContext } from "react-admin";
import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material";
import { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { useState, useEffect, useCallback } from "react";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { isMXID, isASManaged } from "../utils/mxid";
const AvatarField = ({ source, ...rest }: AvatarProps & { source: string, label?: string }) => {
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
const { alt, classes, sizes, sx, variant } = rest;
const record = useRecordContext(rest);
const mxcURL = get(record, source)?.toString();
@@ -32,7 +32,76 @@ const AvatarField = ({ source, ...rest }: AvatarProps & { source: string, label?
};
}, [mxcURL, fetchAvatar]);
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
// a hacky way to handle both users and rooms,
// where users have an ID, may have a name, and may have a displayname
// and rooms have an ID and may have a name
let letter = "";
if (record?.id) {
letter = record.id[0].toUpperCase();
}
if (record?.name) {
letter = record.name[0].toUpperCase();
}
if (record?.displayname) {
letter = record.displayname[0].toUpperCase();
}
// hacky way to determine the user type
let badge = "";
let tooltip = "";
if (isMXID(record?.id)) {
const translate = useTranslate();
switch (record?.user_type) {
case "bot":
badge = "🤖";
tooltip = translate("resources.users.badge.bot");
break;
case "support":
badge = "📞";
tooltip = translate("resources.users.badge.support");
break;
default:
badge = "👤";
tooltip = translate("resources.users.badge.regular");
break;
}
if (record?.admin) {
badge = "👑";
tooltip = translate("resources.users.badge.admin");
}
if (isASManaged(record?.name)) {
badge = "🛡️";
tooltip = `${translate("resources.users.badge.system_managed")} (${tooltip})`;
}
if (localStorage.getItem("user_id") === record?.id) {
badge = "🧙‍";
tooltip = `${translate("resources.users.badge.you")} (${tooltip})`;
}
}
// if there is a badge, wrap the Avatar in a Badge and a Tooltip
if (badge) {
return (
<Tooltip title={tooltip}>
<Badge
badgeContent={badge}
overlap="circular"
sx={{ "& .MuiBadge-badge": { width: "10px" } }} // we deliberately set a very small width here, to make the badge actually circular
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
>
<Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}>
{letter}
</Avatar>
</Badge>
</Tooltip>);
}
return (<Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant}>
{letter}
</Avatar>);
};
export default AvatarField;

View File

@@ -1,5 +1,5 @@
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
import { isASManaged } from "./mxid";
import { isASManaged } from "../utils/mxid";
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
const record = useRecordContext();
@@ -26,3 +26,5 @@ export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
/>
);
};
export default DeviceRemoveButton;

View File

@@ -0,0 +1,97 @@
import { useRecordContext } from "react-admin";
import { useNotify } from "react-admin";
import { useDataProvider } from "react-admin";
import { useState, useEffect } from "react";
import { Stack, Switch, Typography } from "@mui/material";
import { ExperimentalFeaturesModel, SynapseDataProvider } from "../synapse/dataProvider";
const experimentalFeaturesMap = {
msc3881: "enable remotely toggling push notifications for another client",
msc3575: "enable experimental sliding sync support",
};
const ExperimentalFeatureRow = (props: { featureKey: string, featureValue: boolean, updateFeature: (feature_name: string, feature_value: boolean) => void}) => {
const featureKey = props.featureKey;
const featureValue = props.featureValue;
const featureDescription = experimentalFeaturesMap[featureKey] ?? "";
const [checked, setChecked] = useState(featureValue);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
props.updateFeature(featureKey, event.target.checked);
};
return <Stack
direction="row"
spacing={2}
alignItems="start"
sx={{
padding: 2,
}}
>
<Switch checked={checked} onChange={handleChange} />
<Stack>
<Typography
variant="subtitle1"
sx={{
fontWeight: "medium",
color: "text.primary"
}}
>
{featureKey}
</Typography>
<Typography
variant="body2"
color="text.secondary"
>
{featureDescription}
</Typography>
</Stack>
</Stack>
}
export const ExperimentalFeaturesList = () => {
const record = useRecordContext();
const notify = useNotify();
const dataProvider = useDataProvider() as SynapseDataProvider;
const [features, setFeatures] = useState({});
if (!record) {
return null;
}
useEffect(() => {
const fetchFeatures = async () => {
const features = await dataProvider.getFeatures(record.id);
setFeatures(features);
}
fetchFeatures();
}, []);
const updateFeature = async (feature_name: string, feature_value: boolean) => {
const updatedFeatures = {...features, [feature_name]: feature_value} as ExperimentalFeaturesModel;
setFeatures(updatedFeatures);
const reponse = await dataProvider.updateFeatures(record.id, updatedFeatures);
notify("ra.notification.updated", {
messageArgs: { smart_count: 1 },
type: "success",
});
};
return <>
<Stack
direction="column"
spacing={1}
>
{Object.keys(features).map((featureKey: string) =>
<ExperimentalFeatureRow
key={featureKey}
featureKey={featureKey}
featureValue={features[featureKey]}
updateFeature={updateFeature}
/>
)}
</Stack>
</>
}
export default ExperimentalFeaturesList;

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

@@ -0,0 +1,49 @@
import { Avatar, Box, Link, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@mui/material/styles";
const Footer = () => {
const [version, setVersion] = useState<string | null>(null);
const theme = useTheme();
useEffect(() => {
const version = document.getElementById("js-version")?.textContent;
if (version) {
setVersion(version);
}
}, []);
return (<Box
component="footer"
sx={{
position: 'fixed',
zIndex: 100,
bottom: 0,
width: '100%',
bgcolor: theme.palette.background.default,
color: theme.palette.text.primary,
borderTop: '1px solid',
borderColor: theme.palette.divider,
fontSize: '0.89rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start',
p: 1,
gap: '10px'
}}>
<Avatar src="./images/logo.webp" sx={{ width: "1rem", height: "1rem", display: "inline-block", verticalAlign: "sub" }} />
<Link href="https://github.com/etkecc/synapse-admin" target="_blank">
Synapse Admin {version}
</Link>
by
<Link href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin" target="_blank">
etke.cc
</Link>
(originally developed by Awesome Technologies Innovationslabor GmbH).
<Link sx={{ fontWeight: 'bold' }} href="https://matrix.to/#/#synapse-admin:etke.cc" target="_blank">#synapse-admin:etke.cc</Link>
</Box>
);
};
export default Footer;

View File

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

View File

@@ -15,7 +15,8 @@ import {
import { DataProvider, useTranslate } from "ra-core";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
import { generateRandomMxId, generateRandomPassword, returnMXID } from "../synapse/synapse";
import { generateRandomMXID, returnMXID } from "../utils/mxid";
import { generateRandomPassword } from "../utils/password";
const LOGGING = true;
@@ -121,11 +122,8 @@ const FilePicker = () => {
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
/* First, verify the presence of required fields */
const missingFields = expectedFields.filter(eF => {
const result = meta.fields?.find(mF => eF === mF);
if (result === undefined) { return eF; } // missing field
return undefined; // field found
});
meta.fields = meta.fields?.map(f => f.trim().toLowerCase());
const missingFields = expectedFields.filter(eF => !meta.fields?.find(mF => eF === mF));
if (missingFields.length > 0) {
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
@@ -151,6 +149,15 @@ const FilePicker = () => {
};
const errorMessages = errors.map(e => e.message);
// sanitize the data first
data = data.map(line => {
const newLine = {} as ImportLine;
for (const [key, value] of Object.entries(line)) {
newLine[key.trim().toLowerCase()] = value;
}
return newLine;
});
// process the data
data.forEach((line, idx) => {
if (line.user_type === undefined || line.user_type === "") {
stats.user_types.default++;
@@ -177,6 +184,7 @@ const FilePicker = () => {
line[f] = true; // we need true booleans instead of strings
} else {
if (line[f] !== "false" && line[f] !== "") {
console.log("invalid value", line[f], "for field " + f + " in row " + idx);
errorMessages.push(
translate("import_users.error.invalid_value", {
field: f,
@@ -267,7 +275,7 @@ const FilePicker = () => {
// 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" || userRecord.id === undefined || userRecord.id === "") {
userRecord.id = generateRandomMxId();
userRecord.id = generateRandomMXID();
}
if (passwordMode === false || entry.password === undefined || entry.password === "") {
userRecord.password = generateRandomPassword();
@@ -318,7 +326,7 @@ const FilePicker = () => {
);
} else {
const newRecordData = Object.assign({}, recordData, {
id: generateRandomMxId(),
id: generateRandomMXID(),
});
retries++;
if (retries > 512) {
@@ -563,4 +571,5 @@ const FilePicker = () => {
return [<Title defaultTitle={translate("import_users.title")} />, cardContainer];
};
export const ImportFeature = FilePicker;
export const UserImport = FilePicker;
export default UserImport;

View File

@@ -0,0 +1,97 @@
import { Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useDataProvider, useNotify, useRecordContext, useTranslate } from "react-admin";
import { TextField } from "@mui/material";
import { useFormContext } from "react-hook-form";
const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value: any, updateRateLimit: (limit: string, value: any) => void }) => {
const translate = useTranslate();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value);
if (isNaN(value)) {
updateRateLimit(limit, null);
return;
}
updateRateLimit(limit, value);
};
return <Stack
spacing={1}
alignItems="start"
sx={{
padding: 2,
}}
>
<TextField
id="outlined-number"
type="number"
value={value}
onChange={handleChange}
slotProps={{
inputLabel: {
shrink: true,
},
}}
label={translate(`resources.users.limits.${limit}`)}
/>
<Stack>
<Typography
variant="body2"
color="text.secondary"
>
{translate(`resources.users.limits.${limit}_text`)}
</Typography>
</Stack>
</Stack>
}
const UserRateLimits = () => {
const translate = useTranslate();
const notify = useNotify();
const record = useRecordContext();
const form = useFormContext();
const dataProvider = useDataProvider();
const [rateLimits, setRateLimits] = useState({
messages_per_second: "", // we are setting string here to make the number field empty by default, null is prohibited by the field validation
burst_count: "",
});
if (!record) {
return null;
}
useEffect(() => {
const fetchRateLimits = async () => {
const rateLimits = await dataProvider.getRateLimits(record.id);
if (Object.keys(rateLimits).length > 0) {
setRateLimits(rateLimits);
}
}
fetchRateLimits();
}, []);
const updateRateLimit = async (limit: string, value: any) => {
let updatedRateLimits = { ...rateLimits, [limit]: value };
setRateLimits(updatedRateLimits);
form.setValue(`rates.${limit}`, value, { shouldDirty: true });
};
return <>
<Stack
direction="column"
>
{Object.keys(rateLimits).map((limit: string) =>
<RateLimitRow
key={limit}
limit={limit}
value={rateLimits[limit]}
updateRateLimit={updateRateLimit}
/>
)}
</Stack>
</>
};
export default UserRateLimits;

View File

@@ -32,9 +32,8 @@ import {
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import { dateParser } from "./date";
import { dateParser } from "../utils/date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
import storage from "../storage";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
@@ -385,7 +384,7 @@ export const MediaIDField = ({ source }) => {
if (!record) {
return null;
}
const homeserver = storage.getItem("home_server");
const homeserver = localStorage.getItem("home_server");
const mediaID = get(record, source)?.toString();
if (!mediaID) {

View File

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

View File

@@ -2,7 +2,7 @@ import { formalGermanMessages } from "@haleos/ra-language-german";
import { SynapseTranslationMessages } from ".";
const de: SynapseTranslationMessages = {
const fixedGermanMessages = {
...formalGermanMessages,
ra: {
...formalGermanMessages.ra,
@@ -10,22 +10,52 @@ const de: SynapseTranslationMessages = {
...formalGermanMessages.ra.navigation,
no_filtered_results: "Keine Ergebnisse",
clear_filters: "Alle Filter entfernen",
add_filter: "Filter hinzufügen",
},
action: {
...formalGermanMessages.ra.action,
update_application: "Anwendung aktualisieren",
},
page: {
...formalGermanMessages.ra.page,
empty: "Leer",
access_denied: "Zugriff verweigert",
authentication_error: "Authentifizierungsfehler",
},
message: {
...formalGermanMessages.ra.message,
access_denied:
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
authentication_error:
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
},
},
}
const de: SynapseTranslationMessages = {
...fixedGermanMessages,
synapseadmin: {
auth: {
base_url: "Heimserver URL",
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",
credentials: "Anmeldedaten",
access_token: "Zugriffstoken",
logout_acces_token_dialog: {
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
confirm: "Sitzung beenden",
cancel: "Nur vom Admin-Panel abmelden",
},
},
users: {
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
tabs: { sso: "SSO" },
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits" },
},
rooms: {
details: "Raumdetails",
@@ -149,18 +179,39 @@ const de: SynapseTranslationMessages = {
},
helper: {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
create_password: "Generiere ein starkes und sicheres Passwort mit dem Button unten.",
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
erase: "DSGVO konformes Löschen der Benutzerdaten.",
erase_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
username_available: "Benutzername verfügbar",
},
badge: {
you: "Sie",
bot: "Bot",
admin: "Administrator",
support: "Unterstützung",
regular: "Normaler Benutzer",
system_managed: "Systemverwalteter Benutzer",
},
action: {
erase: "Lösche Benutzerdaten",
erase_avatar: "Avatar löschen",
delete_media: "Alle von dem/den Benutzer(n) hochgeladenen Medien löschen",
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
generate_password: "Passwort generieren",
overwrite_title: "Warnung!",
overwrite_content: "Dieser Benutzername ist bereits vergeben. Sind Sie sicher, dass Sie den vorhandenen Benutzer überschreiben möchten?",
overwrite_cancel: "Abbrechen",
overwrite_confirm: "Überschreiben",
},
limits: {
messages_per_second: "Nachrichten pro Sekunde",
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können.",
burst_count: "Burst-Anzahl",
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
}
},
rooms: {
name: "Raum |||| Räume",
@@ -183,10 +234,11 @@ const de: SynapseTranslationMessages = {
history_visibility: "Historie-Sichtbarkeit",
topic: "Thema",
avatar: "Avatar",
actions: "Aktionen",
},
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.",
"Vorderextremitäten sind Blatt-Ereignisse am Ende eines gerichteten azyklischen Graphens (DAG) in einem Raum, auch bekannt als Ereignisse ohne Nachkommen. Je mehr in einem Raum existieren, umso mehr Zustandsauflösungen muss Synapse absolvieren (Hinweis: dies ist eine sehr aufwendige Operation). Obwohl Synapse Code hat um zu verhindern, dass zuviele davon gleichzeitig in einem Raum existieren, können Bugs manchmal dafür sorgen, dass sie sich ansammeln. Wenn ein Raum >10 Vorderextremitäten hat ist es sinnvoll zu überprüfen um welchen Raum es sich handelt und sie gegebenenfalls, wie in #1769 beschrieben, mittels SQL-Queries zu entfernen.",
},
enums: {
join_rules: {
@@ -211,13 +263,21 @@ const de: SynapseTranslationMessages = {
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!",
"Sind Sie sicher, dass Sie den Raum löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. Alle Nachrichten und Medien, die der Raum beinhaltet werden vom Server gelöscht!",
fields: {
block: "Benutzer blockieren und daran hindern, dem Raum beizutreten",
block: "Blockieren und Benutzer daran hindern, dem Raum beizutreten",
},
success: "Raum/Räume erfolgreich gelöscht.",
failure: "Der/die Raum/Räume konnten nicht gelöscht werden.",
},
make_admin: {
assign_admin: "Raumadministrator zuweisen",
title: "Raumadministrator zu %{roomName} zuweisen",
confirm: "Raumadministrator zuweisen",
content: "Geben Sie die vollständige MXID des Benutzers an, der als Administrator gesetzt werden soll.\nWarnung: Damit dies funktioniert, muss der Raum mindestens ein lokales Mitglied als Administrator haben.",
success: "Der/die Benutzer wurde/n als Raumadministrator gesetzt.",
failure: "Der/die Benutzer konnte/n nicht als Raumadministrator gesetzt werden. %{errMsg}",
}
},
},
reports: {
@@ -251,7 +311,7 @@ const de: SynapseTranslationMessages = {
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.",
"Sind Sie sicher, dass Sie das gemeldete Event löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
},
},
},
@@ -298,8 +358,8 @@ const de: SynapseTranslationMessages = {
},
protect_media: {
action: {
create: "Ungeschützt, Schutz erstellen",
delete: "Geschützt, Schutz aufheben",
create: "Ungeschützt, Schutz aktivieren",
delete: "Geschützt, Schutz deaktivieren",
none: "In Quarantäne",
send_success: "Erfolgreich den Schutz-Status geändert.",
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
@@ -379,8 +439,8 @@ const de: SynapseTranslationMessages = {
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",
erase: "Aus Verzeichnis löschen",
create: "Ins Verzeichnis eintragen",
send_success: "Raum erfolgreich eingetragen.",
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
@@ -402,7 +462,7 @@ const de: SynapseTranslationMessages = {
fields: {
token: "Token",
valid: "Gültige Token",
uses_allowed: "Anzahl",
uses_allowed: "Verwendungen erlaubt",
pending: "Ausstehend",
completed: "Abgeschlossen",
expiry_time: "Ablaufzeit",

View File

@@ -7,17 +7,29 @@ const en: SynapseTranslationMessages = {
synapseadmin: {
auth: {
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",
credentials: "Credentials",
access_token: "Access token",
logout_acces_token_dialog: {
title: "You are using an existing Matrix access token.",
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
confirm: "Destroy session",
cancel: "Just logout from admin panel",
},
},
users: {
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
tabs: { sso: "SSO" },
tabs: {
sso: "SSO",
experimental: "Experimental",
limits: "Rate Limits",
},
},
rooms: {
details: "Room details",
@@ -26,7 +38,7 @@ const en: SynapseTranslationMessages = {
members: "Members",
detail: "Details",
permission: "Permissions",
},
}
},
reports: { tabs: { basic: "Basic", detail: "Details" } },
},
@@ -140,18 +152,39 @@ const en: SynapseTranslationMessages = {
},
helper: {
password: "Changing password will log user out of all sessions.",
create_password: "Generate a strong and secure password using the button below.",
deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased",
erase_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
erase_admin_error: "Deleting own user is not allowed.",
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
username_available: "Username is available",
},
action: {
erase: "Erase user data",
erase_avatar: "Erase avatar",
delete_media: "Delete all media uploaded by the user(-s)",
redact_events: "Redact all events sent by the user(-s)",
generate_password: "Generate password",
overwrite_title: "Warning!",
overwrite_content: "This username is already taken. Are you sure that you want to overwrite the existing user?",
overwrite_cancel: "Cancel",
overwrite_confirm: "Overwrite",
},
badge: {
you: "You",
bot: "Bot",
admin: "Admin",
support: "Support",
regular: "Regular User",
system_managed: "System-managed",
},
limits: {
messages_per_second: "Messages per second",
messages_per_second_text: "The number of actions that can be performed in a second.",
burst_count: "Burst count",
burst_count_text: "How many actions that can be performed before being limited.",
}
},
rooms: {
name: "Room |||| Rooms",
@@ -174,6 +207,7 @@ const en: SynapseTranslationMessages = {
history_visibility: "History visibility",
topic: "Topic",
avatar: "Avatar",
actions: "Actions",
},
helper: {
forward_extremities:
@@ -209,6 +243,14 @@ const en: SynapseTranslationMessages = {
success: "Room/s successfully deleted.",
failure: "The room/s could not be deleted.",
},
make_admin: {
assign_admin: "Assign admin",
title: "Assign a room admin to %{roomName}",
confirm: "Make admin",
content: "Put the full MXID of the user which will be set as admin.\nWarning: for this to work, the room needs to have at least one local member as admin.",
success: "The user has been set as room admin.",
failure: "The user could not be set as room admin. %{errMsg}",
}
},
},
reports: {

View File

@@ -13,10 +13,18 @@ const fa: SynapseTranslationMessages = {
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
url_error: "آدرس وارد شده یک سرور معتبر نیست",
sso_sign_in: "با SSO وارد شوید",
credentials: "اعتبارنامه",
access_token: "توکن دسترسی",
logout_acces_token_dialog: {
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
content: "آیا می‌خواهید این جلسه (که می‌تواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
confirm: "نابودی جلسه",
cancel: "فقط خروج از پنل مدیریت",
},
},
users: {
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
tabs: { sso: "SSO" },
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها" },
},
rooms: {
tabs: {
@@ -136,18 +144,39 @@ const fa: SynapseTranslationMessages = {
},
helper: {
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
create_password: "رمز عبور قوی و امنی را با استفاده از دکمه زیر ایجاد کنید.",
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
username_available: "نام کاربری موجود",
},
badge: {
you: "شما",
bot: "ربات",
admin: "مدیر",
support: "پشتیبان",
regular: "کاربر عادی",
system_managed: "مدیریت سیستم",
},
action: {
erase: "پاک کردن اطلاعات کاربر",
erase_avatar: "محو الصورة الرمزية",
delete_media: "حذف جميع الوسائط التي تم تحميلها بواسطة المستخدم (المستخدمين)",
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
generate_password: "توليد رمز عبور",
overwrite_title: "هشدار!",
overwrite_content: "این نام کاربری قبلا استفاده شده است. آیا مطمئن هستید که می خواهید کاربر موجود را بازنویسی کنید؟",
overwrite_cancel: "انصراف",
overwrite_confirm: "بازنویسی",
},
limits: {
messages_per_second: "پیام در ثانیه",
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود.",
burst_count: "تعداد پیچیدگی",
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
}
},
rooms: {
name: "اتاق |||| اتاق ها",
@@ -170,6 +199,7 @@ const fa: SynapseTranslationMessages = {
history_visibility: "مشاهده تاریخچه",
topic: "موضوع",
avatar: "آواتار",
actions: "عملیات",
},
helper: {
forward_extremities:
@@ -199,7 +229,20 @@ const fa: SynapseTranslationMessages = {
title: "حذف اتاق",
content:
"آیا مطمئن هستید که می خواهید اتاق را حذف کنید؟ این قابل بازگشت نیست. همه پیام ها و رسانه های مشترک در اتاق از سرور حذف می شوند!",
fields: {
block: "حذف",
},
success: "اتاق با موفقیت حذف شد.",
failure: "خطایی رخ داده است.",
},
make_admin: {
assign_admin: "مدیر انتخاب کنید",
title: "مدیر اتاق %{roomName} را انتخاب کنید",
confirm: "مدیر انتخاب کنید",
content: "کامل MXID کاربری را وارد کنید که به عنوان مدیر تنظیم شود.\nهشدار: برای این کار، اتاق باید حداقل یک اعضای محلی به عنوان مدیر داشته باشد.",
success: "کاربر به عنوان مدیر اتاق تنظیم شد.",
failure: "کاربر به عنوان مدیر اتاق تنظیم نشد. %{errMsg}",
}
},
},
reports: {

View File

@@ -7,16 +7,24 @@ const fr: SynapseTranslationMessages = {
synapseadmin: {
auth: {
base_url: "URL du serveur daccueil",
welcome: "Bienvenue sur Synapse-admin",
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",
credentials: "Identifiants",
access_token: "Jeton d'accès",
logout_acces_token_dialog: {
title: "Vous utilisez un jeton d'accès Matrix existant.",
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
confirm: "Détruire la session",
cancel: "Se déconnecter simplement du panneau d'administration",
},
},
users: {
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur daccueil.",
tabs: { sso: "Authentification unique" },
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites" },
},
rooms: {
tabs: {
@@ -138,18 +146,40 @@ const fr: SynapseTranslationMessages = {
auth_provider: "Fournisseur d'identité",
},
helper: {
password: "Changer le mot de passe déconnectera l'utilisateur de toutes les sessions.",
create_password: "Générer un mot de passe fort et sécurisé en utilisant le bouton ci-dessous.",
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
erase_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
username_available: "Nom d'utilisateur disponible",
},
badge: {
you: "Vous",
bot: "Bot",
admin: "Admin",
support: "Support",
regular: "Utilisateur régulier",
system_managed: "Géré par le système",
},
action: {
erase: "Effacer les données de l'utilisateur",
erase_avatar: "Effacer l'avatar",
delete_media: "Supprimer tous les médias téléchargés par le(s) utilisateur(s)",
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
generate_password: "Générer un mot de passe",
overwrite_title: "Attention !",
overwrite_content: "Ce nom d'utilisateur est déjà pris. Êtes-vous sûr de vouloir écraser l'utilisateur existant ?",
overwrite_cancel: "Annuler",
overwrite_confirm: "Écraser",
},
limits: {
messages_per_second: "Messages par seconde",
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde.",
burst_count: "Compteur de pics",
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
}
},
rooms: {
name: "Salon |||| Salons",
@@ -172,6 +202,7 @@ const fr: SynapseTranslationMessages = {
history_visibility: "Visibilité de l'historique",
topic: "Sujet",
avatar: "Avatar",
actions: "Actions",
},
helper: {
forward_extremities:
@@ -201,12 +232,20 @@ const fr: SynapseTranslationMessages = {
title: "Supprimer le salon",
content:
"Voulez-vous vraiment supprimer le salon ? Cette opération ne peut être annulée. Tous les messages et médias partagés du salon seront supprimés du serveur !",
fields: {
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
},
fields: {
block: "Bloquer et empêcher les utilisateurs de rejoindre la salle",
},
success: "Salle/s supprimées avec succès.",
failure: "La/les salle/s n'ont pas pu être supprimées.",
},
make_admin: {
assign_admin: "Assigner un administrateur",
title: "Assigner un administrateur au salon %{roomName}",
confirm: "Assigner un administrateur",
content: "Entrez la MXID complète de l'utilisateur qui sera désigné comme administrateur.\nAttention : pour que cela fonctionne, la salle doit avoir au moins un membre local en tant qu'administrateur.",
success: "L'utilisateur a été désigné comme administrateur de la salle.",
failure: "L'utilisateur n'a pas pu être désigné comme administrateur de la salle. %{errMsg}",
}
},
},
reports: {

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

@@ -11,10 +11,18 @@ interface SynapseTranslationMessages extends TranslationMessages {
protocol_error: string;
url_error: string;
sso_sign_in: string;
credentials: string;
access_token: string;
logout_acces_token_dialog: {
title: string;
content: string;
confirm: string;
cancel: string;
};
};
users: {
invalid_user_id: string;
tabs: { sso: string };
tabs: { sso: string; experimental: string; limits: string; };
};
rooms: {
details?: string; // TODO: fa, fr, it, zh
@@ -135,18 +143,39 @@ interface SynapseTranslationMessages extends TranslationMessages {
user_type?: string;
};
helper: {
password?: string;
password: string;
create_password: string;
deactivate: string;
erase: string;
erase_text: string;
erase_admin_error: string;
modify_managed_user_error: string;
username_available: string;
};
action: {
erase: string;
erase_avatar: string;
delete_media: string;
redact_events: string;
generate_password: string;
overwrite_title: string;
overwrite_content: string;
overwrite_cancel: string;
overwrite_confirm: string;
};
badge: {
you: string;
bot: string;
admin: string;
support: string;
regular: string;
system_managed: string;
}
limits: {
messages_per_second: string;
messages_per_second_text: string;
burst_count: string;
burst_count_text: string;
};
};
rooms: {
@@ -170,6 +199,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
history_visibility: string;
topic?: string;
avatar?: string;
actions: string;
};
helper?: {
forward_extremities: string;
@@ -203,6 +233,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
success: string;
failure: string;
};
make_admin: {
assign_admin: string;
title: string;
confirm: string;
content: string;
success: string;
failure: string;
};
};
};
reports: {

View File

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

View File

@@ -2,7 +2,7 @@ import russianMessages from "ra-language-russian";
import { SynapseTranslationMessages } from ".";
const ru: SynapseTranslationMessages = {
const fixedRussianMessages = {
...russianMessages,
ra: {
...russianMessages.ra,
@@ -11,21 +11,46 @@ const ru: SynapseTranslationMessages = {
no_filtered_results: "Нет результатов",
clear_filters: "Все фильтры сбросить",
},
page: {
...russianMessages.ra.page,
empty: "Пусто",
access_denied: "Доступ запрещен",
authentication_error: "Ошибка аутентификации",
},
message: {
...russianMessages.ra.message,
access_denied:
"У вас нет прав доступа к этой странице.",
authentication_error:
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
},
},
}
const ru: SynapseTranslationMessages = {
...fixedRussianMessages,
synapseadmin: {
auth: {
base_url: "Адрес домашнего сервера",
welcome: "Добро пожаловать в Synapse-admin",
welcome: "Добро пожаловать в Synapse Admin",
server_version: "Версия Synapse",
supports_specs: "поддерживает спецификации Matrix",
username_error: "Пожалуйста, укажите полный ID пользователя: '@user:domain'",
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Неверный адрес сервера Matrix",
sso_sign_in: "Вход через SSO",
credentials: "Учетные данные",
access_token: "Токен доступа",
logout_acces_token_dialog: {
title: "Вы используете существующий токен доступа Matrix.",
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
confirm: "Завершить сессию",
cancel: "Просто выйти из панели администрирования",
},
},
users: {
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
tabs: { sso: "SSO" },
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения" },
},
rooms: {
details: "Данные комнаты",
@@ -87,7 +112,7 @@ const ru: SynapseTranslationMessages = {
header: "Загрузить CSV файл",
explanation:
"Здесь вы можете загрузить файл со значениями, разделёнными запятыми, которые будут использованы для создания или обновления данных пользователей. \
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
В файле должны быть поля 'id' и 'displayname'. Вы можете скачать и изменить файл-образец отсюда: ",
},
startImport: {
simulate_only: "Только симулировать",
@@ -157,18 +182,39 @@ const ru: SynapseTranslationMessages = {
},
helper: {
password: "Смена пароля завершит все сессии пользователя.",
create_password: "Сгенерировать надёжный и безопасный пароль, используя кнопку ниже.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
erase_admin_error: "Удаление собственного пользователя запрещено.",
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
username_available: "Имя пользователя доступно",
},
badge: {
you: "Вы",
bot: "Бот",
admin: "Админ",
support: "Поддержка",
regular: "Обычный пользователь",
system_managed: "Управляемый системой",
},
action: {
erase: "Удалить данные пользователя",
erase_avatar: "Удалить аватар",
delete_media: "Удаление всех медиафайлов, загруженных пользователем (-ами)",
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
generate_password: "Сгенерировать пароль",
overwrite_title: "Предупреждение!",
overwrite_content: "Это имя пользователя уже занято. Вы уверены, что хотите перезаписать существующего пользователя?",
overwrite_cancel: "Отмена",
overwrite_confirm: "Перезаписать",
},
limits: {
messages_per_second: "Сообщений в секунду",
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду.",
burst_count: "Burst-счётчик",
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
}
},
rooms: {
name: "Комната |||| Комнаты",
@@ -191,13 +237,14 @@ const ru: SynapseTranslationMessages = {
history_visibility: "Видимость истории",
topic: "Тема",
avatar: "Аватар",
actions: "Действия",
},
helper: {
forward_extremities:
"Оконечности — это события-листья в конце ориентированного ациклического графа (DAG) в комнате, т.е. события без дочерних элементов. \
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
Чем больше их в комнате, тем больше Synapse работает над разрешением состояния (это дорогостоящая операция). \
Хотя Synapse старается не допускать существования слишком большого числа таких событий в комнате, из-за ошибок они иногда снова появляются. \
Если в комнате >10 оконечностей, стоит найти комнату-виновника и попробовать удалить их с помощью SQL-запросов из #1760.",
},
enums: {
join_rules: {
@@ -229,6 +276,14 @@ const ru: SynapseTranslationMessages = {
success: "Комната/ы успешно удалены",
failure: "Комната/ы не могут быть удалены.",
},
make_admin: {
assign_admin: "Назначить администратора",
title: "Назначить администратора комнате %{roomName}",
confirm: "Назначить администратора",
content: "Введите полную MXID пользователя, которого нужно назначить администратором.\nПредупреждение: для этого должен быть назначен хотя бы один локальный участник в качестве администратора.",
success: "Пользователь назначен администратором комнаты.",
failure: "Пользователь не может быть назначен администратором комнаты. %{errMsg}",
}
},
},
reports: {

View File

@@ -2,7 +2,7 @@ import chineseMessages from "@haxqer/ra-language-chinese";
import { SynapseTranslationMessages } from ".";
const zh: SynapseTranslationMessages = {
const fixedChineseMessages = {
...chineseMessages,
ra: {
...chineseMessages.ra,
@@ -11,20 +11,48 @@ const zh: SynapseTranslationMessages = {
no_filtered_results: "没有结果",
clear_filters: "清除所有过滤器",
},
action: {
...chineseMessages.ra.action,
update_application: "Anwendung aktualisieren",
},
page: {
...chineseMessages.ra.page,
access_denied: "拒绝访问",
authentication_error: "认证错误",
},
message: {
...chineseMessages.ra.message,
access_denied:
"您没有访问此页面的权限。",
authentication_error:
"身份验证服务器返回错误,无法验证您的凭据。",
},
},
}
const zh: SynapseTranslationMessages = {
...fixedChineseMessages,
synapseadmin: {
auth: {
base_url: "服务器 URL",
welcome: "欢迎来到 Synapse-admin",
welcome: "欢迎来到 Synapse Admin",
server_version: "Synapse 版本",
username_error: "请输入完整有效的用户 ID: '@user:domain'",
protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址",
sso_sign_in: "使用 SSO 登录",
credentials: "凭证",
access_token: "访问令牌",
logout_acces_token_dialog: {
title: "您正在使用现有的 Matrix 访问令牌。",
content: "您想销毁此会话(可能在其他地方使用,例如在 Matrix 客户端中)还是仅从管理面板退出?",
confirm: "销毁会话",
cancel: "仅从管理面板退出",
},
},
users: {
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
tabs: { sso: "SSO" },
tabs: { sso: "SSO", experimental: "实验性", limits: "限制" },
},
rooms: {
tabs: {
@@ -141,18 +169,40 @@ const zh: SynapseTranslationMessages = {
consent_version: "协议版本",
},
helper: {
password: "更改密码会使用户注销所有会话。",
create_password: "使用下面的按钮生成一个强大和安全的密码。",
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
erase_admin_error: "不允许删除自己的用户",
modify_managed_user_error: "不允许修改系统管理的用户。",
username_available: "用户名可用",
},
badge: {
you: "您",
bot: "机器人",
admin: "管理员",
support: "支持",
regular: "普通用户",
system_managed: "系统管理",
},
action: {
erase: "抹除用户信息",
erase_avatar: "抹掉头像",
delete_media: "删除用户上传的所有媒体",
redact_events: "重新编辑用户(-s发送的所有事件",
generate_password: "生成密码",
overwrite_title: "警告!",
overwrite_content: "这个用户名已经被占用。您确定要覆盖现有的用户吗?",
overwrite_cancel: "取消",
overwrite_confirm: "覆盖",
},
limits: {
messages_per_second: "每秒消息数",
messages_per_second_text: "每秒可以执行的操作数。",
burst_count: "Burst-计数",
burst_count_text: "在限制之前可以执行的操作数。",
}
},
rooms: {
name: "房间",
@@ -172,6 +222,9 @@ const zh: SynapseTranslationMessages = {
join_rules: "加入规则",
guest_access: "访客访问",
history_visibility: "历史可见性",
topic: "主题",
avatar: "头像",
actions: "操作",
},
enums: {
join_rules: {

View File

@@ -2,20 +2,15 @@ import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { AppContext } from "./AppContext";
import storage from "./storage";
import {App, AppContext } from "./App";
import { FetchConfig, GetConfig } from "./utils/config";
fetch("config.json")
.then(res => res.json())
.then(props => {
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
storage.setItem("support_url", props.supportURL);
return createRoot(document.getElementById("root")).render(
await FetchConfig();
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AppContext.Provider value={props}>
<AppContext.Provider value={GetConfig()}>
<App />
</AppContext.Provider>
</React.StrictMode>
)
});
);

View File

@@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage";
import { AppContext } from "../AppContext";
import { AppContext } from "../App";
import englishMessages from "../i18n/en";
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);

View File

@@ -1,8 +1,6 @@
import { useState, useEffect } from "react";
import LockIcon from "@mui/icons-material/Lock";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
import {
Form,
FormDataConsumer,
@@ -17,8 +15,8 @@ import {
useLocales,
} from "react-admin";
import { useFormContext } from "react-hook-form";
import { useAppContext } from "../AppContext";
import LoginFormBox from "../components/LoginFormBox";
import { useAppContext } from "../App";
import {
getServerVersion,
getSupportedFeatures,
@@ -26,86 +24,43 @@ import {
getWellKnownUrl,
isValidBaseUrl,
splitMxid,
} from "../synapse/synapse";
import storage from "../storage";
} from "../synapse/matrix";
import Footer from "../components/Footer";
const FormBox = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
minHeight: "calc(100vh - 1rem)",
alignItems: "center",
justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)",
backgroundColor: "#f9f9f9",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
[`& .card`]: {
width: "30rem",
marginTop: "6rem",
marginBottom: "6rem",
},
[`& .avatar`]: {
margin: "1rem",
display: "flex",
justifyContent: "center",
},
[`& .icon`]: {
backgroundColor: theme.palette.grey[500],
},
[`& .hint`]: {
marginTop: "1em",
marginBottom: "1em",
display: "flex",
justifyContent: "center",
color: theme.palette.grey[600],
},
[`& .form`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .select`]: {
marginBottom: "2rem",
},
[`& .actions`]: {
padding: "0 1rem 1rem 1rem",
},
[`& .serverVersion`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem",
},
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
},
}));
export type LoginMethod = "credentials" | "accessToken";
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const { restrictBaseUrl } = useAppContext();
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
const allowSingleBaseUrl = typeof restrictBaseUrl === "string" && restrictBaseUrl !== "";
const allowMultipleBaseUrls =
Array.isArray(restrictBaseUrl) &&
restrictBaseUrl.length > 0 &&
restrictBaseUrl[0] !== "" &&
restrictBaseUrl[0] !== null;
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
const [locale, setLocale] = useLocaleState();
const locales = useLocales();
const translate = useTranslate();
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
useEffect(() => {
if (!loginToken) {
return;
}
if (loginToken) {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
// Prevent further requests
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
const baseUrl = storage.getItem("sso_base_url");
storage.removeItem("sso_base_url");
const baseUrl = localStorage.getItem("sso_base_url");
localStorage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
@@ -127,7 +82,7 @@ const LoginPage = () => {
console.error(error);
});
}
}
}, [loginToken]);
const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) {
@@ -141,6 +96,9 @@ const LoginPage = () => {
const handleSubmit = auth => {
setLoading(true);
const cleanUrl = window.location.href.replace(window.location.search, "");
window.history.replaceState({}, "", cleanUrl);
login(auth).catch(error => {
setLoading(false);
notify(
@@ -155,7 +113,7 @@ const LoginPage = () => {
};
const handleSSO = () => {
storage.setItem("sso_base_url", ssoBaseUrl);
localStorage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
@@ -211,38 +169,78 @@ const LoginPage = () => {
.catch(() => setSSOBaseUrl(""));
}, [formData.base_url, form]);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const username = params.get("username");
let serverURL = params.get("server");
if (username) {
form.setValue("username", username);
}
if (serverURL) {
const isFullUrl = serverURL.match(/^(http|https):\/\//);
if (!isFullUrl) {
serverURL = `https://${serverURL}`;
}
form.setValue("base_url", serverURL);
}
}, [window.location.search]);
return (
<>
<Box>
<TextInput
autoFocus
source="username"
label="ra.auth.username"
autoComplete="username"
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
validate={required()}
/>
</Box>
<Box>
<PasswordInput
source="password"
label="ra.auth.password"
type="password"
autoComplete="current-password"
disabled={loading || !supportPassAuth}
resettable
validate={required()}
/>
</Box>
<Tabs
value={loginMethod}
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
indicatorColor="primary"
textColor="primary"
centered
>
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
</Tabs>
{loginMethod === "credentials" ? (
<>
<Box>
<TextInput
autoFocus
source="username"
label="ra.auth.username"
autoComplete="username"
onBlur={handleUsernameChange}
resettable
validate={required()}
{...(loading || !supportPassAuth ? { disabled: true } : {})}
/>
</Box>
<Box>
<PasswordInput
source="password"
label="ra.auth.password"
type="password"
autoComplete="current-password"
{...(loading || !supportPassAuth ? { disabled: true } : {})}
resettable
validate={required()}
/>
</Box>
</>
) : (
<Box>
<TextInput
source="accessToken"
label="synapseadmin.auth.access_token"
{...(loading ? { disabled: true } : {})}
resettable
validate={required()}
/>
</Box>
)}
<Box>
<TextInput
source="base_url"
label="synapseadmin.auth.base_url"
select={allowMultipleBaseUrls}
autoComplete="url"
disabled={loading}
{...(loading ? { disabled: true } : {})}
readOnly={allowSingleBaseUrl}
resettable={allowAnyBaseUrl}
validate={[required(), validateBaseUrl]}
@@ -263,15 +261,13 @@ const LoginPage = () => {
return (
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
<FormBox>
<LoginFormBox>
<Card className="card">
<Box className="avatar">
{loading ? (
<CircularProgress size={25} thickness={2} />
) : (
<Avatar className="icon">
<LockIcon />
</Avatar>
<Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp"/>
)}
</Box>
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
@@ -290,7 +286,7 @@ const LoginPage = () => {
))}
</Select>
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
<CardActions className="actions">
{loginMethod === "credentials" && <CardActions className="actions">
<Button
variant="contained"
type="submit"
@@ -309,11 +305,23 @@ const LoginPage = () => {
>
{translate("synapseadmin.auth.sso_sign_in")}
</Button>
</CardActions>
</CardActions>}
{loginMethod === "accessToken" && <CardActions className="actions">
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
fullWidth
>
{translate("ra.auth.sign_in")}
</Button>
</CardActions>}
</Box>
</Card>
</FormBox>
</LoginFormBox>
<Notification />
<Footer />
</Form>
);
};

View File

@@ -4,9 +4,11 @@ import AutorenewIcon from "@mui/icons-material/Autorenew";
import DestinationsIcon from "@mui/icons-material/CloudQueue";
import FolderSharedIcon from "@mui/icons-material/FolderShared";
import ViewListIcon from "@mui/icons-material/ViewList";
import ErrorIcon from '@mui/icons-material/Error';
import {
Button,
Datagrid,
DatagridConfigurable,
DateField,
List,
ListProps,
@@ -21,6 +23,7 @@ import {
Tab,
TabbedShowLayout,
TextField,
FunctionField,
TopToolbar,
useRecordContext,
useDelete,
@@ -30,18 +33,11 @@ import {
DateFieldProps,
} from "react-admin";
import { DATE_FORMAT } from "../components/date";
import { DATE_FORMAT } from "../utils/date";
import { get } from "lodash";
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const destinationRowSx = (record: RaRecord) => ({
backgroundColor: record.retry_last_ts > 0 ? "warning.light" : "primary.contrastText",
"& .MuiButtonBase-root": {
color: "primary.dark",
},
});
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
export const DestinationReconnectButton = () => {
@@ -105,7 +101,22 @@ const RetryDateField = (props: DateFieldProps) => {
return <DateField {...props} />;
};
const destinationFieldRender = (record: RaRecord) => {
if (record.retry_last_ts > 0) {
return (
<>
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/>
{record.destination}
</>
);
}
return <> {record.destination} </>;
}
export const DestinationList = (props: ListProps) => {
const record = useRecordContext(props);
return (
<List
{...props}
@@ -113,14 +124,14 @@ export const DestinationList = (props: ListProps) => {
pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }}
>
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<TextField source="destination" />
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
<TextField source="retry_interval" />
<TextField source="last_successful_stream_ordering" />
<DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
<FunctionField source="destination" render={destinationFieldRender} label="resources.destinations.fields.destination" />
<DateField source="failure_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.failure_ts" />
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} label="resources.destinations.fields.retry_last_ts" />
<TextField source="retry_interval" label="resources.destinations.fields.retry_interval" />
<TextField source="last_successful_stream_ordering" label="resources.destinations.fields.last_successful_stream_ordering" />
<DestinationReconnectButton />
</Datagrid>
</DatagridConfigurable>
</List>
);
};

View File

@@ -4,6 +4,7 @@ import {
Create,
CreateProps,
Datagrid,
DatagridConfigurable,
DateField,
DateTimeInput,
Edit,
@@ -23,7 +24,7 @@ import {
Toolbar,
} from "react-admin";
import { DATE_FORMAT, dateFormatter, dateParser } from "../components/date";
import { DATE_FORMAT, dateFormatter, dateParser } from "../utils/date";
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];
@@ -39,13 +40,13 @@ export const RegistrationTokenList = (props: ListProps) => (
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>
<DatagridConfigurable rowClick="edit">
<TextField source="token" sortable={false} label="resources.registration_tokens.fields.token"/>
<NumberField source="uses_allowed" sortable={false} label="resources.registration_tokens.fields.uses_allowed" />
<NumberField source="pending" sortable={false} label="resources.registration_tokens.fields.pending" />
<NumberField source="completed" sortable={false} label="resources.registration_tokens.fields.completed" />
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} label="resources.registration_tokens.fields.expiry_time" />
</DatagridConfigurable>
</List>
);

View File

@@ -3,6 +3,7 @@ import ViewListIcon from "@mui/icons-material/ViewList";
import ReportIcon from "@mui/icons-material/Warning";
import {
Datagrid,
DatagridConfigurable,
DateField,
DeleteButton,
List,
@@ -21,7 +22,7 @@ import {
useTranslate,
} from "react-admin";
import { DATE_FORMAT } from "../components/date";
import { DATE_FORMAT } from "../utils/date";
import { ReportMediaContent } from "../components/media";
const ReportPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
@@ -90,13 +91,13 @@ const ReportShowActions = () => {
export const ReportList = (props: ListProps) => (
<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>
<DatagridConfigurable rowClick="show" bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.reports.fields.id" />
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} label="resources.reports.fields.received_ts" />
<TextField sortable={false} source="user_id" label="resources.reports.fields.user_id" />
<TextField sortable={false} source="name" label="resources.reports.fields.name" />
<TextField sortable={false} source="score" label="resources.reports.fields.score" />
</DatagridConfigurable>
</List>
);

View File

@@ -27,8 +27,7 @@ import {
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
import AvatarField from "../components/AvatarField";
import { MakeAdminBtn } from "./rooms";
const RoomDirectoryPagination = () => <Pagination rowsPerPageOptions={[100, 500, 1000, 2000]} />;
export const RoomDirectoryUnpublishButton = (props: DeleteButtonProps) => {
@@ -154,6 +153,7 @@ export const RoomDirectoryList = () => (
<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" />
<MakeAdminBtn />
</DatagridConfigurable>
</List>
);

View File

@@ -11,11 +11,11 @@ import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import {
BooleanField,
BulkDeleteButton,
DateField,
EditButton,
WrapperField,
Datagrid,
DatagridConfigurable,
DeleteButton,
ExportButton,
FunctionField,
List,
@@ -32,23 +32,32 @@ import {
ShowProps,
Tab,
TabbedShowLayout,
TextField,
TextField as RaTextField,
TopToolbar,
useRecordContext,
useTranslate,
useListContext,
useNotify,
} from "react-admin";
import TextField from "@mui/material/TextField";
import {
RoomDirectoryBulkUnpublishButton,
RoomDirectoryBulkPublishButton,
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./room_directory";
import { DATE_FORMAT } from "../components/date";
import { DATE_FORMAT } from "../utils/date";
import DeleteRoomButton from "../components/DeleteRoomButton";
import AvatarField from "../components/AvatarField";
import { Room } from "../synapse/dataProvider";
import { useMutation } from "@tanstack/react-query";
import { useDataProvider } from "react-admin";
import { Confirm } from "react-admin";
import { useState } from "react";
import Button from "@mui/material/Button";
import PersonIcon from '@mui/icons-material/Person';
import Typography from "@mui/material/Typography";
const RoomPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
const RoomTitle = () => {
@@ -76,6 +85,7 @@ const RoomShowActions = () => {
return (
<TopToolbar>
{publishButton}
<MakeAdminBtn />
<DeleteRoomButton
selectedIds={[record.id]}
confirmTitle="resources.rooms.action.erase.title"
@@ -85,8 +95,97 @@ const RoomShowActions = () => {
);
};
export const MakeAdminBtn = () => {
const record = useRecordContext() as Room;
if (!record) {
return null;
}
if (record.joined_local_members < 1) {
return null;
}
const ownMXID = localStorage.getItem("user_id") || "";
const [open, setOpen] = useState(false);
const [userIdValue, setUserIdValue] = useState(ownMXID);
const dataProvider = useDataProvider();
const notify = useNotify();
const translate = useTranslate();
const { mutate, isPending } = useMutation({
mutationFn: async () => {
try {
const result = await dataProvider.makeRoomAdmin(record.room_id, userIdValue);
if (!result.success) {
throw new Error(result.error);
}
} catch (error) {
throw error;
}
},
onSuccess: () => {
notify("resources.rooms.action.make_admin.success", { type: "success" });
setOpen(false);
setUserIdValue("");
},
onError: (err) => {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
notify("resources.rooms.action.make_admin.failure", { type: "error", messageArgs: { errMsg: errorMessage } });
setOpen(false);
setUserIdValue("");
}
});
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUserIdValue(event.target.value);
};
const handleConfirm = async () => {
mutate();
setOpen(false);
};
const handleDialogClose = () => {
setOpen(false);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
handleConfirm();
}
};
return (<>
<Button size="small" onClick={(e) => { e.stopPropagation(); setOpen(true); }} disabled={isPending}>
<PersonIcon /> {translate("resources.rooms.action.make_admin.assign_admin")}
</Button>
<Confirm
isOpen={open}
onConfirm={handleConfirm}
onClose={handleDialogClose}
confirm="resources.rooms.action.make_admin.confirm"
cancel="ra.action.cancel"
title={translate("resources.rooms.action.make_admin.title", { roomName: record.name ? record.name : record.room_id })}
content={<>
<Typography sx={{ marginBottom: 2, whiteSpace: "pre-line"}}>{translate("resources.rooms.action.make_admin.content")}</Typography>
<TextField
type="text"
variant="filled"
value={userIdValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
label={"Matrix ID"}
/>
</>}
/>
</>);
};
export const RoomShow = (props: ShowProps) => {
const translate = useTranslate();
const record = useRecordContext();
return (
<Show {...props} actions={<RoomShowActions />} title={<RoomTitle />}>
<TabbedShowLayout>
@@ -96,28 +195,29 @@ export const RoomShow = (props: ShowProps) => {
sx={{ height: "120px", width: "120px" }}
label="resources.rooms.fields.avatar"
/>
<TextField source="room_id" />
<TextField source="name" />
<TextField source="topic" />
<TextField source="canonical_alias" />
<RaTextField source="room_id" />
<RaTextField source="name" />
<RaTextField source="topic" />
<RaTextField source="canonical_alias" />
<ReferenceField source="creator" reference="users">
<TextField source="id" />
<RaTextField 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")} />
<RaTextField source="joined_members" />
<RaTextField source="joined_local_members" />
<RaTextField source="joined_local_devices" />
<RaTextField source="state_events" />
<RaTextField source="version" />
<RaTextField source="encryption" emptyText={translate("resources.rooms.enums.unencrypted")} />
</Tab>
<Tab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />} path="members">
<MakeAdminBtn />
<ReferenceManyField reference="room_members" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}>
<TextField source="id" sortable={false} label="resources.users.fields.id" />
<Datagrid sx={{ width: "100%" }} rowClick={id => "/users/" + id} bulkActionButtons={false}>
<RaTextField source="id" sortable={false} label="resources.users.fields.id" />
<ReferenceField
label="resources.users.fields.displayname"
source="id"
@@ -125,7 +225,7 @@ export const RoomShow = (props: ShowProps) => {
sortable={false}
link=""
>
<TextField source="displayname" sortable={false} />
<RaTextField source="displayname" sortable={false} />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
@@ -184,12 +284,12 @@ export const RoomShow = (props: ShowProps) => {
<Tab label={translate("resources.room_state.name", { smart_count: 2 })} icon={<EventIcon />} path="state">
<ReferenceManyField reference="room_state" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="type" sortable={false} />
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<RaTextField source="type" sortable={false} />
<DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} />
<TextField source="content" sortable={false} />
<FunctionField source="content" sortable={false} render={record => `${JSON.stringify(record.content, null, 2)}`} />
<ReferenceField source="sender" reference="users" sortable={false}>
<TextField source="id" />
<RaTextField source="id" />
</ReferenceField>
</Datagrid>
</ReferenceManyField>
@@ -205,11 +305,11 @@ export const RoomShow = (props: ShowProps) => {
{translate("resources.rooms.helper.forward_extremities")}
</Box>
<ReferenceManyField reference="forward_extremities" target="room_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="id" sortable={false} />
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<RaTextField 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} />
<RaTextField source="state_group" sortable={false} />
</Datagrid>
</ReferenceManyField>
</Tab>
@@ -258,24 +358,32 @@ export const RoomList = (props: ListProps) => {
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" />
<ReferenceField reference="rooms" source="id" label="resources.users.fields.avatar" link={false} sortable={false}>
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
</ReferenceField>
<WrapperField source="encryption" sortBy="is_encrypted" label="resources.rooms.fields.encryption">
<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 },
}}
/>
</WrapperField>
<FunctionField source="name" render={record => record["name"] || record["canonical_alias"] || record["id"]} label="resources.rooms.fields.name" />
<RaTextField source="joined_members" label="resources.rooms.fields.joined_members" />
<RaTextField source="joined_local_members" label="resources.rooms.fields.joined_local_members" />
<RaTextField source="state_events" label="resources.rooms.fields.state_events" />
<RaTextField source="version" label="resources.rooms.fields.version" />
<BooleanField source="federatable" label="resources.rooms.fields.federatable" />
<BooleanField source="public" label="resources.rooms.fields.public" />
<WrapperField label="resources.rooms.fields.actions">
<MakeAdminBtn />
</WrapperField>
</DatagridConfigurable>
</List>
);

View File

@@ -1,6 +1,7 @@
import PermMediaIcon from "@mui/icons-material/PermMedia";
import {
Datagrid,
DatagridConfigurable,
ExportButton,
List,
ListProps,
@@ -37,12 +38,12 @@ export const UserMediaStatsList = (props: ListProps) => (
pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }}
>
<Datagrid rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
<DatagridConfigurable rowClick={id => "/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>
</DatagridConfigurable>
</List>
);

View File

@@ -7,14 +7,18 @@ 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 ScienceIcon from "@mui/icons-material/Science";
import LockClockIcon from '@mui/icons-material/LockClock';
import ViewListIcon from "@mui/icons-material/ViewList";
import { useEffect, useState } from "react";
import { Alert, ownerDocument } from "@mui/material";
import { Alert, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import {
ArrayInput,
ArrayField,
Button,
Datagrid,
DatagridConfigurable,
DateField,
Create,
CreateProps,
@@ -51,22 +55,31 @@ import {
NumberField,
useListContext,
useNotify,
ToolbarClasses,
Identifier,
RaRecord,
ToolbarClasses,
ImageInput,
ImageField,
FunctionField,
useDataProvider,
Confirm,
useCreate,
useRedirect,
} from "react-admin";
import { Link } from "react-router-dom";
import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton";
import { isASManaged } from "../components/mxid";
import { isASManaged } from "../utils/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../components/date";
import { DeviceRemoveButton } from "../components/devices";
import { DATE_FORMAT } from "../utils/date";
import DeviceRemoveButton from "../components/DeviceRemoveButton";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
import { generateRandomPassword } from "../utils/password";
import { useFormContext } from "react-hook-form";
import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
import UserRateLimits from "../components/UserRateLimits";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { MakeAdminBtn } from "./rooms";
const choices_medium = [
{ id: "email", name: "resources.users.email" },
@@ -125,8 +138,6 @@ const UserBulkActionButtons = () => {
const [asManagedUserIsSelected, setAsManagedUserIsSelected] = useState(false);
const selectedIds = record.selectedIds;
const ownUserId = localStorage.getItem("user_id");
const notify = useNotify();
const translate = useTranslate();
useEffect(() => {
setOwnUserIsSelected(selectedIds.includes(ownUserId));
@@ -147,10 +158,6 @@ const UserBulkActionButtons = () => {
);
};
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
return `/users/${id}`;
};
export const UserList = (props: ListProps) => (
<List
{...props}
@@ -160,17 +167,20 @@ export const UserList = (props: ListProps) => (
actions={<UserListActions />}
pagination={<UserPagination />}
>
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} />
<TextField source="id" sortBy="name" />
<TextField source="displayname" />
<BooleanField source="is_guest" />
<BooleanField source="admin" />
<BooleanField source="deactivated" />
<BooleanField source="locked" />
<BooleanField source="erased" sortable={false} />
<DatagridConfigurable
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
bulkActionButtons={<UserBulkActionButtons />}
>
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" label="resources.users.fields.avatar" />
<TextField source="id" sortBy="name" label="resources.users.fields.id" />
<TextField source="displayname" label="resources.users.fields.displayname" />
<BooleanField source="is_guest" label="resources.users.fields.is_guest" />
<BooleanField source="admin" label="resources.users.fields.admin" />
<BooleanField source="deactivated" label="resources.users.fields.deactivated" />
<BooleanField source="locked" label="resources.users.fields.locked" />
<BooleanField source="erased" sortable={false} label="resources.users.fields.erased" />
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
</Datagrid>
</DatagridConfigurable>
</List>
);
@@ -209,17 +219,72 @@ const UserEditActions = () => {
);
};
export const UserCreate = (props: CreateProps) => (
<Create
export const UserCreate = (props: CreateProps) => {
const dataProvider = useDataProvider();
const translate = useTranslate();
const redirect = useRedirect();
const notify = useNotify();
const theme = useTheme();
const [open, setOpen] = useState(false);
const [userIsAvailable, setUserIsAvailable] = useState<boolean | undefined>();
const [userAvailabilityEl, setUserAvailabilityEl] = useState<React.ReactElement | false>(<Typography component="span"></Typography>);
const [formData, setFormData] = useState<Record<string, any>>({});
const [create] = useCreate();
const checkAvailability = async(event: React.FocusEvent<HTMLInputElement>) => {
const username = event.target.value;
const result: UsernameAvailabilityResult = await dataProvider.checkUsernameAvailability(username);
setUserIsAvailable(!!result?.available);
if (result?.available) {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.success.main }}> {translate("resources.users.helper.username_available")}</Typography>);
} else {
setUserAvailabilityEl(<Typography component="span" variant="body2" sx={{ color: theme.palette.warning.main }}> {result?.error || "unknown error"}</Typography>);
}
};
const postSave = (data: Record<string, any>) => {
setFormData(data);
if (!userIsAvailable) {
setOpen(true);
return;
}
create("users", { data: data }, {
onSuccess: (resource: User) => {
notify("ra.notification.created", { messageArgs: { smart_count: 1 } });
redirect(() => { return `users/${resource.id}` });
}
});
};
const handleConfirm = () => {
setOpen(false);
updateUser();
};
const handleDialogClose = () => {
setOpen(false);
};
const updateUser = () => {
create("users", { data: formData }, {
onSuccess: (resource: User) => {
notify("ra.notification.updated", { messageArgs: { smart_count: 1 } });
redirect(() => { return `users/${resource.id}` });
}
});
}
return <Create
{...props}
redirect={(resource, id, data) => {
return `users/${id}`;
}}
>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<SimpleForm
onSubmit={postSave}
>
<TextInput source="id" autoComplete="off" validate={validateUser} onBlur={checkAvailability} helperText={userAvailabilityEl}/>
<TextInput source="displayname" validate={maxLength(256)} />
<PasswordInput source="password" autoComplete="new-password" validate={maxLength(512)} />
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" />
<ArrayInput source="threepids">
@@ -235,20 +300,26 @@ export const UserCreate = (props: CreateProps) => (
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
<Confirm
isOpen={open}
title="resources.users.action.overwrite_title"
content="resources.users.action.overwrite_content"
onConfirm={handleConfirm}
onClose={handleDialogClose}
confirm="resources.users.action.overwrite_confirm"
cancel="resources.users.action.overwrite_cancel"
/>
</Create>
);
};
const UserTitle = () => {
const record = useRecordContext();
const translate = useTranslate();
if (!record) {
return null;
}
const translate = useTranslate();
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""
if (isASManaged(record?.id)) {
username += " 🤖";
}
return (
<span>
{translate("resources.users.name", {
@@ -303,12 +374,29 @@ const UserBooleanInput = props => {
const UserPasswordInput = props => {
const record = useRecordContext();
let asManagedUserIsSelected = false;
// Get form context to update field value
const form = useFormContext();
if (record) {
asManagedUserIsSelected = isASManaged(record.id);
}
const generatePassword = () => {
const password = generateRandomPassword();
form.setValue("password", password, { shouldDirty: true });
};
return (
<PasswordInput {...props} helperText="resources.users.helper.modify_managed_user_error" disabled={asManagedUserIsSelected} />
<>
<PasswordInput {...props} helperText={asManagedUserIsSelected ? "resources.users.helper.modify_managed_user_error" : (record ? "resources.users.helper.password" : "resources.users.helper.create_password")} disabled={asManagedUserIsSelected} />
<Button
variant="outlined"
label="resources.users.action.generate_password"
onClick={generatePassword}
sx={{ marginBottom: "10px" }}
disabled={asManagedUserIsSelected}
/>
</>
);
};
@@ -316,7 +404,11 @@ export const UserEdit = (props: EditProps) => {
const translate = useTranslate();
return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic" queryOptions={{
meta: {
include: ["features"] // Tell your dataProvider to include features
}
}}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sx={{ height: "120px", width: "120px" }} />
@@ -365,7 +457,7 @@ export const UserEdit = (props: EditProps) => {
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
<ReferenceManyField reference="devices" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="device_id" sortable={false} />
<TextField source="display_name" sortable={false} />
<TextField source="last_seen_ip" sortable={false} />
@@ -378,7 +470,7 @@ export const UserEdit = (props: EditProps) => {
<FormTab label="resources.connections.name" icon={<SettingsInputComponentIcon />} path="connections">
<ReferenceField reference="connections" source="id" label={false} link={false}>
<ArrayField source="devices[].sessions[0].connections" label="resources.connections.name">
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<Datagrid sx={{ 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%" }} />
@@ -400,7 +492,7 @@ export const UserEdit = (props: EditProps) => {
perPage={50}
sort={{ field: "created_ts", order: "DESC" }}
>
<Datagrid style={{ width: "100%" }}>
<Datagrid sx={{ width: "100%" }}>
<MediaIDField source="media_id" />
<DateField source="created_ts" showTime options={DATE_FORMAT} />
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
@@ -417,18 +509,19 @@ export const UserEdit = (props: EditProps) => {
<FormTab label={translate("resources.rooms.name", { smart_count: 2 })} icon={<ViewListIcon />} path="rooms">
<ReferenceManyField reference="joined_rooms" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} rowClick={id => "/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>
<Datagrid sx={{ width: "100%" }} rowClick={id => "/rooms/" + id + "/show"} bulkActionButtons={false}>
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
<AvatarField source="avatar" sx={{ height: "40px", width: "40px" }} />
</ReferenceField>
<TextField source="id" label="resources.rooms.fields.room_id" sortable={false}/>
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.name" link={false} sortable={false}>
<TextField source="name" />
</ReferenceField>
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}>
<TextField source="joined_members" sortable={false} />
</ReferenceField>
<MakeAdminBtn />
</Datagrid>
</ReferenceManyField>
</FormTab>
@@ -438,7 +531,7 @@ export const UserEdit = (props: EditProps) => {
path="pushers"
>
<ReferenceManyField reference="pushers" target="user_id" label={false}>
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
<Datagrid sx={{ width: "100%" }} bulkActionButtons={false}>
<TextField source="kind" sortable={false} />
<TextField source="app_display_name" sortable={false} />
<TextField source="app_id" sortable={false} />
@@ -450,6 +543,14 @@ export const UserEdit = (props: EditProps) => {
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab label="synapseadmin.users.tabs.experimental" icon={<ScienceIcon />} path="experimental">
<ExperimentalFeaturesList />
</FormTab>
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
<UserRateLimits />
</FormTab>
</TabbedForm>
</Edit>
);

View File

@@ -1,3 +0,0 @@
const storage = localStorage;
export default storage;

View File

@@ -1,7 +1,6 @@
import fetchMock from "jest-fetch-mock";
import authProvider from "./authProvider";
import storage from "../storage";
import { HttpError } from "ra-core";
fetchMock.enableMocks();
@@ -9,7 +8,7 @@ fetchMock.enableMocks();
describe("authProvider", () => {
beforeEach(() => {
fetchMock.resetMocks();
storage.clear();
localStorage.clear();
});
describe("login", () => {
@@ -23,14 +22,14 @@ describe("authProvider", () => {
})
);
const ret: undefined = await authProvider.login({
const ret = await authProvider.login({
base_url: "http://example.com",
username: "@user:example.com",
password: "secret",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
headers: new Headers({
Accept: "application/json",
@@ -38,10 +37,10 @@ describe("authProvider", () => {
}),
method: "POST",
});
expect(storage.getItem("base_url")).toEqual("http://example.com");
expect(storage.getItem("user_id")).toEqual("@user:example.com");
expect(storage.getItem("access_token")).toEqual("foobar");
expect(storage.getItem("device_id")).toEqual("some_device");
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");
});
});
@@ -55,13 +54,13 @@ describe("authProvider", () => {
})
);
const ret: undefined = await authProvider.login({
const ret = await authProvider.login({
base_url: "https://example.com/",
loginToken: "login_token",
});
expect(ret).toBe(undefined);
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
expect(ret).toEqual({redirectTo: "/"});
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", {
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
Accept: "application/json",
@@ -69,21 +68,21 @@ describe("authProvider", () => {
}),
method: "POST",
});
expect(storage.getItem("base_url")).toEqual("https://example.com");
expect(storage.getItem("user_id")).toEqual("@user:example.com");
expect(storage.getItem("access_token")).toEqual("foobar");
expect(storage.getItem("device_id")).toEqual("some_device");
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 storage", async () => {
storage.setItem("base_url", "example.com");
storage.setItem("access_token", "foo");
localStorage.setItem("base_url", "example.com");
localStorage.setItem("access_token", "foo");
fetchMock.mockResponse(JSON.stringify({}));
await authProvider.logout(null);
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
headers: new Headers({
Accept: "application/json",
Authorization: "Bearer foo",
@@ -91,7 +90,7 @@ describe("authProvider", () => {
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
expect(storage.getItem("access_token")).toBeNull();
expect(localStorage.getItem("access_token")).toBeNull();
});
});
@@ -115,7 +114,7 @@ describe("authProvider", () => {
});
it("should resolve when logged in", async () => {
storage.setItem("access_token", "foobar");
localStorage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});
@@ -123,7 +122,9 @@ describe("authProvider", () => {
describe("getPermissions", () => {
it("should do nothing", async () => {
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
if (authProvider.getPermissions) {
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
}
});
});
});

View File

@@ -1,7 +1,8 @@
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin";
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
import storage from "../storage";
import { MatrixError, displayError } from "../components/error";
import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
import { FetchConfig, ClearConfig } from "../utils/config";
const authProvider: AuthProvider = {
// called when the user attempts to log in
@@ -10,19 +11,21 @@ const authProvider: AuthProvider = {
username,
password,
loginToken,
accessToken,
}: {
base_url: string;
username: string;
password: string;
loginToken: string;
accessToken: string;
}) => {
console.log("login ");
const options: Options = {
let options: Options = {
method: "POST",
body: JSON.stringify(
Object.assign(
{
device_id: storage.getItem("device_id"),
device_id: localStorage.getItem("device_id"),
initial_device_display_name: "Synapse Admin",
},
loginToken
@@ -48,18 +51,42 @@ const authProvider: AuthProvider = {
if (!base_url) {
// there is some kind of bug with base_url being present in the form, but not submitted
// ref: https://github.com/etkecc/synapse-admin/issues/14
storage.removeItem("base_url")
localStorage.removeItem("base_url")
throw new Error("Homeserver URL is required.");
}
base_url = base_url.replace(/\/+$/g, "");
storage.setItem("base_url", base_url);
localStorage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
let response;
try {
if (accessToken) {
// this a login with an already obtained access token, let's just validate it
options = {
headers: new Headers({
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
}),
};
}
response = await fetchUtils.fetchJson(login_api_url, options);
const json = response.json;
localStorage.setItem("home_server", accessToken ? json.user_id.split(":")[1] : json.home_server);
localStorage.setItem("user_id", json.user_id);
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
localStorage.setItem("device_id", json.device_id);
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
// when doing access token auth, config is not fetched, so we need to do it here
if (accessToken) {
await FetchConfig();
}
return Promise.resolve({redirectTo: "/"});
} catch(err) {
const error = err as HttpError;
const errorStatus = error.status;
@@ -71,21 +98,52 @@ const authProvider: AuthProvider = {
errMsg,
errorStatus,
)
);
);
}
},
getIdentity: async () => {
const access_token = localStorage.getItem("access_token");
const user_id = localStorage.getItem("user_id");
const base_url = localStorage.getItem("base_url");
if (typeof access_token !== "string" || typeof user_id !== "string" || typeof base_url !== "string") {
return Promise.reject();
}
const json = response.json;
storage.setItem("home_server", json.home_server);
storage.setItem("user_id", json.user_id);
storage.setItem("access_token", json.access_token);
storage.setItem("device_id", json.device_id);
const options: Options = {
headers: new Headers({
Accept: "application/json",
Authorization: `Bearer ${access_token}`,
}),
};
const whoami_api_url = base_url + `/_matrix/client/v3/profile/${user_id}`;
try {
let avatar_url = "";
const response = await fetchUtils.fetchJson(whoami_api_url, options);
if (response.json.avatar_url) {
const mediaresp = await fetchAuthenticatedMedia(response.json.avatar_url, "thumbnail");
const blob = await mediaresp.blob();
avatar_url = URL.createObjectURL(blob);
}
return Promise.resolve({
id: user_id,
fullName: response.json.displayname,
avatar: avatar_url,
});
} catch (err) {
console.log("Error getting identity", err);
return Promise.reject();
}
},
// called when the user clicks on the logout button
logout: async () => {
console.log("logout");
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/r0/logout";
const access_token = storage.getItem("access_token");
const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/v3/logout";
const access_token = localStorage.getItem("access_token");
const options: Options = {
method: "POST",
@@ -101,7 +159,7 @@ const authProvider: AuthProvider = {
} catch (err) {
console.log("Error logging out", err);
} finally {
storage.removeItem("access_token");
ClearConfig();
}
}
},
@@ -117,8 +175,7 @@ const authProvider: AuthProvider = {
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
const access_token = storage.getItem("access_token");
console.log("checkAuth " + access_token);
const access_token = localStorage.getItem("access_token");
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
},
// called when the user navigates to a new location, to check for permissions / roles

View File

@@ -1,7 +1,6 @@
import fetchMock from "jest-fetch-mock";
import dataProvider from "./dataProvider";
import storage from "../storage";
fetchMock.enableMocks();
@@ -10,8 +9,8 @@ beforeEach(() => {
});
describe("dataProvider", () => {
storage.setItem("base_url", "http://localhost");
storage.setItem("access_token", "access_token");
localStorage.setItem("base_url", "http://localhost");
localStorage.setItem("access_token", "access_token");
it("fetches all users", async () => {
fetchMock.mockResponseOnce(

View File

@@ -13,13 +13,12 @@ import {
withLifecycleCallbacks,
} from "react-admin";
import storage from "../storage";
import { returnMXID } from "./synapse";
import { MatrixError, displayError } from "../components/error";
import { returnMXID } from "../utils/mxid";
import { MatrixError, displayError } from "../utils/error";
// Adds the access token to all requests
const jsonClient = async (url: string, options: Options = {}) => {
const token = storage.getItem("access_token");
const token = localStorage.getItem("access_token");
console.log("httpClient " + url);
if (token !== null) {
options.user = {
@@ -52,7 +51,7 @@ interface Action {
body?: Record<string, any>;
}
interface Room {
export interface Room {
room_id: string;
name?: string;
canonical_alias?: string;
@@ -120,7 +119,8 @@ interface ExternalId {
external_id: string;
}
interface User {
export interface User {
id?: string;
name: string;
displayname?: string;
threepids: Threepid[];
@@ -248,9 +248,31 @@ export interface UploadMediaResult {
content_uri: string;
}
export interface ExperimentalFeaturesModel {
features: {
[key: string]: boolean;
};
}
export interface RateLimitsModel {
messages_per_second?: number;
burst_count?: number;
}
export interface UsernameAvailabilityResult {
available?: boolean;
error?: string;
errcode?: string;
}
export interface SynapseDataProvider extends DataProvider {
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
}
const resourceMap = {
@@ -378,7 +400,7 @@ const resourceMap = {
data: "media",
total: json => json.total,
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/${storage.getItem("home_server")}/${params.id}`,
endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
}),
},
protect_media: {
@@ -395,11 +417,11 @@ const resourceMap = {
quarantine_media: {
map: (qm: UserMedia) => ({ id: qm.media_id }),
create: (params: UserMedia) => ({
endpoint: `/_synapse/admin/v1/media/quarantine/${storage.getItem("home_server")}/${params.media_id}`,
endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
method: "POST",
}),
delete: (params: DeleteParams) => ({
endpoint: `/_synapse/admin/v1/media/unquarantine/${storage.getItem("home_server")}/${params.id}`,
endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
method: "POST",
}),
},
@@ -544,7 +566,7 @@ const baseDataProvider: SynapseDataProvider = {
order_by: field,
dir: getSearchOrder(order),
};
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -563,7 +585,7 @@ const baseDataProvider: SynapseDataProvider = {
getOne: async (resource, params) => {
console.log("getOne " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -575,13 +597,28 @@ const baseDataProvider: SynapseDataProvider = {
getMany: async (resource, params) => {
console.log("getMany " + resource);
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
const base_url = localStorage.getItem("base_url");
const homeserver = localStorage.getItem("home_server");
if (!base_url || !(resource in resourceMap)) throw Error("base_url not set");
const res = resourceMap[resource];
const endpoint_url = homeserver + res.path;
const responses = await Promise.all(params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)));
const endpoint_url = base_url + res.path;
const responses = await Promise.all(params.ids.map(id => {
// edge case: when user is external / federated, homeserver will return error, as querying external users via
// /_synapse/admin/v2/users is not allowed.
// That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty.
// To avoid that, we fake the response with one specific field (name) which is used in the datagrid.
if (homeserver && resource === "users") {
if (!(<string>id).endsWith(homeserver)) {
const json = {
name: id,
};
return Promise.resolve({ json });
}
}
return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`);
}));
return {
data: responses.map(({ json }) => res.map(json)),
total: responses.length,
@@ -600,7 +637,7 @@ const baseDataProvider: SynapseDataProvider = {
dir: getSearchOrder(order),
};
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -617,7 +654,7 @@ const baseDataProvider: SynapseDataProvider = {
update: async (resource, params) => {
console.log("update " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -632,7 +669,7 @@ const baseDataProvider: SynapseDataProvider = {
updateMany: async (resource, params) => {
console.log("updateMany " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -649,7 +686,7 @@ const baseDataProvider: SynapseDataProvider = {
create: async (resource, params) => {
console.log("create " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -666,7 +703,7 @@ const baseDataProvider: SynapseDataProvider = {
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
console.log("createMany " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -688,7 +725,7 @@ const baseDataProvider: SynapseDataProvider = {
delete: async (resource, params) => {
console.log("delete " + resource);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -713,7 +750,7 @@ const baseDataProvider: SynapseDataProvider = {
deleteMany: async (resource, params) => {
console.log("deleteMany " + resource, "params", params);
const homeserver = storage.getItem("base_url");
const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -760,17 +797,17 @@ const baseDataProvider: SynapseDataProvider = {
* @returns
*/
deleteMedia: async ({ before_ts, size_gt = 0, keep_profiles = true }) => {
const homeserver = storage.getItem("home_server"); // TODO only required for synapse < 1.78.0
const homeserver = localStorage.getItem("home_server"); // TODO only required for synapse < 1.78.0
const endpoint = `/_synapse/admin/v1/media/${homeserver}/delete?before_ts=${before_ts}&size_gt=${size_gt}&keep_profiles=${keep_profiles}`;
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const endpoint_url = base_url + endpoint;
const { json } = await jsonClient(endpoint_url, { method: "POST" });
return json as DeleteMediaResult;
},
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
const { json } = await jsonClient(`${uploadMediaURL}?filename=${filename}`, {
@@ -783,6 +820,67 @@ const baseDataProvider: SynapseDataProvider = {
});
return json as UploadMediaResult;
},
getFeatures: async (id: Identifier) => {
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
const { json } = await jsonClient(endpoint_url);
return json.features as ExperimentalFeaturesModel;
},
updateFeatures: async (id: Identifier, features: ExperimentalFeaturesModel) => {
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
await jsonClient(endpoint_url, { method: "PUT", body: JSON.stringify({ features }) });
},
getRateLimits: async (id: Identifier) => {
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
const { json } = await jsonClient(endpoint_url);
return json as RateLimitsModel;
},
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
const filtered = Object.entries(rateLimits).
filter(([key, value]) => value !== null && value !== undefined).
reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
if (Object.keys(filtered).length === 0) {
await jsonClient(endpoint_url, { method: "DELETE" });
return
}
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) });
},
checkUsernameAvailability: async (username: string) => {
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/username_available?username=${encodeURIComponent(username)}`;
try {
const { json } = await jsonClient(endpoint_url);
return json as UsernameAvailabilityResult;
} catch (error) {
if (error instanceof HttpError) {
return { available: false, error: error.body.error, errcode: error.body.errcode } as UsernameAvailabilityResult;
}
throw error;
}
},
makeRoomAdmin: async (room_id: string, user_id: string) => {
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/rooms/${encodeURIComponent(room_id)}/make_room_admin`;
try {
const { json } = await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ user_id }) });
return { success: true };
} catch (error) {
if (error instanceof HttpError) {
return { success: false, error: error.body.error, errcode: error.body.errcode };
}
throw error;
}
}
};
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
@@ -791,6 +889,12 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
const avatarFile = params.data.avatar_file?.rawFile;
const avatarErase = params.data.avatar_erase;
const rates = params.data.rates;
if (rates) {
await dataProvider.setRateLimits(params.id, rates);
delete params.data.rates;
}
if (avatarErase) {
params.data.avatar_url = "";
@@ -809,13 +913,13 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
},
beforeDelete: async (params: DeleteParams<any>, dataProvider: DataProvider) => {
if (params.meta?.deleteMedia) {
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(params.id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}
@@ -826,13 +930,13 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
await Promise.all(
params.ids.map(async id => {
if (params.meta?.deleteMedia) {
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
const base_url = storage.getItem("base_url");
const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}

View File

@@ -1,4 +1,4 @@
import { isValidBaseUrl, splitMxid } from "./synapse";
import { isValidBaseUrl, splitMxid } from "./matrix";
describe("splitMxid", () => {
it("splits valid MXIDs", () =>

View File

@@ -1,6 +1,6 @@
import { Identifier, fetchUtils } from "react-admin";
import storage from "../storage";
import { isMXID } from "../utils/mxid";
export const splitMxid = mxid => {
const re = /^@(?<name>[a-zA-Z0-9._=\-/]+):(?<domain>[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
@@ -49,51 +49,7 @@ export const getSupportedFeatures = async baseUrl => {
* @returns array of supported login flows
*/
export const getSupportedLoginFlows = async baseUrl => {
const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
const loginFlowsUrl = `${baseUrl}/_matrix/client/v3/login`;
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
return response.json.flows;
};
/**
* Generate a random MXID for current homeserver
* @returns full MXID as string
*/
export function generateRandomMxId(): string {
const homeserver = storage.getItem("home_server");
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
.map(x => characters[x % characters.length])
.join("");
return `@${localpart}:${homeserver}`;
}
/**
* Return the full MXID from an arbitrary input
* @param input the input string
* @returns full MXID as string
*/
export function returnMXID(input: string | Identifier): string {
const homeserver = storage.getItem("home_server");
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
const mxidPattern = /^@[^@:]+:[^@:]+$/;
if (typeof input === 'string' && mxidPattern.test(input)) {
return input; // Already a valid MXID
}
// If input is not a valid MXID, assume it's a localpart and construct the MXID
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
return `@${localpart}:${homeserver}`;
}
/**
* Generate a random user password
* @returns a new random password as string
*/
export function generateRandomPassword(length = 20): string {
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$";
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
.map(x => characters[x % characters.length])
.join("");
}

82
src/utils/config.ts Normal file
View File

@@ -0,0 +1,82 @@
export interface Config {
restrictBaseUrl: string | string[];
asManagedUsers: RegExp[];
menu: MenuItem[];
}
export interface MenuItem {
label: string;
icon: string;
url: string;
}
export const WellKnownKey = "cc.etke.synapse-admin";
// current configuration
let config: Config = {
restrictBaseUrl: "",
asManagedUsers: [],
menu: [],
};
export const FetchConfig = async () => {
try {
const resp = await fetch("config.json");
const configJSON = await resp.json();
console.log("Loaded config.json", configJSON);
LoadConfig(configJSON);
} catch (e) {
console.error(e);
}
// if home_server is set, try to load https://home_server/.well-known/matrix/client
const homeserver = localStorage.getItem("home_server");
if (homeserver) {
try {
const resp = await fetch(`https://${homeserver}/.well-known/matrix/client`);
const configWK = await resp.json();
if (!configWK[WellKnownKey]) {
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
} else {
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
LoadConfig(configWK[WellKnownKey]);
}
} catch (e) {
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
}
}
}
// load config from context
export const LoadConfig = (context: any) => {
if (context?.restrictBaseUrl) {
config.restrictBaseUrl = context.restrictBaseUrl as string | string[];
}
if (context?.asManagedUsers) {
config.asManagedUsers = context.asManagedUsers.map((regex: string) => new RegExp(regex));
}
let menu: MenuItem[] = [];
if (context?.menu) {
menu = context.menu as MenuItem[];
}
if (menu.length > 0) {
config.menu = menu;
}
}
// get config
export const GetConfig = (): Config => {
return config;
}
// clear config
export const ClearConfig = () => {
// config.json
config = {} as Config;
// session
localStorage.clear();
}

View File

@@ -1,9 +1,6 @@
import storage from "../storage";
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => {
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl);
console.log("mxcClient " + ret);
if (ret == null) {
throw new Error("Invalid mxcUrl");
}
@@ -15,8 +12,8 @@ export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: str
export type MediaType = "thumbnail" | "original";
export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType): Promise<Response> => {
const homeserver = storage.getItem("base_url");
const accessToken = storage.getItem("access_token");
const homeserver = localStorage.getItem("base_url");
const accessToken = localStorage.getItem("access_token");
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
if (!serverName || !mediaId) {
@@ -40,4 +37,4 @@ export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType):
});
return response;
};
};

16
src/utils/icons.ts Normal file
View File

@@ -0,0 +1,16 @@
import AnnouncementIcon from '@mui/icons-material/Announcement';
import EngineeringIcon from '@mui/icons-material/Engineering';
import HelpCenterIcon from '@mui/icons-material/HelpCenter';
import SupportAgentIcon from '@mui/icons-material/SupportAgent';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
export const Icons = {
Announcement: AnnouncementIcon,
Engineering: EngineeringIcon,
HelpCenter: HelpCenterIcon,
SupportAgent: SupportAgentIcon,
Default: OpenInNewIcon,
// Add more icons as needed
};
export const DefaultIcon = Icons.Default;

52
src/utils/mxid.ts Normal file
View File

@@ -0,0 +1,52 @@
import { Identifier } from "ra-core";
import { GetConfig } from "../utils/config";
const mxidPattern = /^@[^@:]+:[^@:]+$/;
/*
* Check if id is a valid Matrix ID (user)
* @param id The ID to check
* @returns Whether the ID is a valid Matrix ID
*/
export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id as string);
/**
* Check if a user is managed by an application service
* @param id The user ID to check
* @returns Whether the user is managed by an application service
*/
export const isASManaged = (id: string | Identifier): boolean => {
return GetConfig().asManagedUsers.some(regex => regex.test(id as string));
};
/**
* Generate a random MXID for current homeserver
* @returns full MXID as string
*/
export function generateRandomMXID(): string {
const homeserver = localStorage.getItem("home_server");
const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
.map(x => characters[x % characters.length])
.join("");
return `@${localpart}:${homeserver}`;
}
/**
* Return the full MXID from an arbitrary input
* @param input the input string
* @returns full MXID as string
*/
export function returnMXID(input: string | Identifier): string {
const homeserver = localStorage.getItem("home_server");
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
const mxidPattern = /^@[^@:]+:[^@:]+$/;
if (isMXID(input)) {
return input as string; // Already a valid MXID
}
// If input is not a valid MXID, assume it's a localpart and construct the MXID
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
return `@${localpart}:${homeserver}`;
}

10
src/utils/password.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Generate a random user password
* @returns a new random password as string
*/
export function generateRandomPassword(length = 64): string {
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~`!@#$%^&*()_-+={[}]|:;'.?/<>,";
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
.map(x => characters[x % characters.length])
.join("");
}

View File

@@ -5,13 +5,41 @@ import { defineConfig } from "vite";
export default defineConfig({
base: "./",
build: {
target: "esnext",
},
plugins: [
react(),
vitePluginVersionMark({
command: "git describe --tags || git rev-parse --short HEAD",
ifMeta: true,
ifLog: true,
name: "Synapse Admin",
command: 'git describe --tags || git rev-parse --short HEAD || echo "${SYNAPSE_ADMIN_VERSION:-unknown}"',
ifMeta: false,
ifLog: false,
ifGlobal: true,
outputFile: (version) => ({
path: "manifest.json",
content: JSON.stringify({
name: "Synapse Admin",
version: version,
description: "Synapse Admin is an admin console for synapse Matrix homeserver with additional features.",
categories: ["productivity", "utilities"],
orientation: "landscape",
icons: [{
src: "favicon.ico",
sizes: "32x32",
type: "image/x-icon"
},{
src: "images/logo.webp",
sizes: "512x512",
type: "image/webp",
purpose: "any maskable"
}],
start_url: ".",
display: "standalone",
theme_color: "#000000",
background_color: "#ffffff"
}),
}),
}),
],
});

1315
yarn.lock

File diff suppressed because it is too large Load Diff