Compare commits
8 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
02bee92806 | ||
![]() |
b7f8e03894 | ||
![]() |
59bb4b4183 | ||
![]() |
bb53d53692 | ||
![]() |
52a2f1c936 | ||
![]() |
e328380c77 | ||
![]() |
a277ded227 | ||
![]() |
48d933e028 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -5,6 +5,6 @@
|
|||||||
},
|
},
|
||||||
"eslint.nodePath": ".yarn/sdks",
|
"eslint.nodePath": ".yarn/sdks",
|
||||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
}
|
}
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
|
135
README.md
135
README.md
@@ -1,7 +1,32 @@
|
|||||||
# Synapse Admin UI [](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
# Synapse Admin UI [](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
|
* [Fork differences](#fork-differences)
|
||||||
|
* [Available via CDN](#available-via-cdn)
|
||||||
|
* [Changes](#changes)
|
||||||
|
* [Development](#development)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [Restricting available homeserver](#restricting-available-homeserver)
|
||||||
|
* [Protecting appservice managed users](#protecting-appservice-managed-users)
|
||||||
|
* [Providing support URL](#providing-support-url)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [Supported Synapse](#supported-synapse)
|
||||||
|
* [Prerequisites](#prerequisites)
|
||||||
|
* [Use without install](#use-without-install)
|
||||||
|
* [Step-By-Step install](#step-by-step-install)
|
||||||
|
* [Steps for 1)](#steps-for-1)
|
||||||
|
* [Steps for 2)](#steps-for-2)
|
||||||
|
* [Steps for 3)](#steps-for-3)
|
||||||
|
* [Serving Synapse-Admin on a different path](#serving-synapse-admin-on-a-different-path)
|
||||||
|
* [Development](#development-1)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
## Fork differences
|
## Fork differences
|
||||||
|
|
||||||
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
||||||
@@ -32,6 +57,9 @@ The following changes are already implemented:
|
|||||||
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
|
* [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)
|
* [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)
|
* [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)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
@@ -46,6 +74,64 @@ After that open `http://localhost:5173` in your browser, login using the followi
|
|||||||
* Password: admin
|
* Password: admin
|
||||||
* Homeserver URL: http://localhost:8008
|
* Homeserver URL: http://localhost:8008
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can use `config.json` file to configure synapse-admin
|
||||||
|
|
||||||
|
The `config.json` can be injected into a Docker container using a bind mount.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
services:
|
||||||
|
synapse-admin:
|
||||||
|
...
|
||||||
|
volumes:
|
||||||
|
./config.json:/app/config.json:ro
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"asManagedUsers": ["^@telegram_[a-zA-Z0-9]+:example\\.com$"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Providing support URL
|
||||||
|
|
||||||
|
Synapse-Admin provides a support link in the main menu - `Contact support`. By default, the link points to the GitHub issues page of the project. You can change this link by providing a `supportURL` in the `config.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"supportURL": "https://example.com/support"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Supported Synapse
|
### Supported Synapse
|
||||||
@@ -69,7 +155,7 @@ See also [Synapse administration endpoints](https://element-hq.github.io/synapse
|
|||||||
### Use without install
|
### Use without install
|
||||||
|
|
||||||
You can use the current version of Synapse Admin without own installation direct
|
You can use the current version of Synapse Admin without own installation direct
|
||||||
via [GitHub Pages](https://awesome-technologies.github.io/synapse-admin/).
|
via [admin.etke.cc](https://admin.etke.cc).
|
||||||
|
|
||||||
**Note:**
|
**Note:**
|
||||||
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
|
If you want to use the deployment, you have to make sure that the admin endpoints (`/_synapse/admin`) are accessible for your browser.
|
||||||
@@ -88,22 +174,22 @@ You have three options:
|
|||||||
|
|
||||||
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
|
- make sure you have a webserver installed that can serve static files (any webserver like nginx or apache will do)
|
||||||
- configure a vhost for synapse admin on your webserver
|
- configure a vhost for synapse admin on your webserver
|
||||||
- download the .tar.gz from the latest release: https://github.com/Awesome-Technologies/synapse-admin/releases/latest
|
- download the .tar.gz [from the latest release](https://github.com/etkecc/synapse-admin/releases/latest)
|
||||||
- unpack the .tar.gz
|
- unpack the .tar.gz
|
||||||
- move or symlink the `synapse-admin-x.x.x` into your vhosts root dir
|
- move or symlink the `synapse-admin` into your vhosts root dir
|
||||||
- open the url of the vhost in your browser
|
- open the url of the vhost in your browser
|
||||||
|
|
||||||
#### Steps for 2)
|
#### Steps for 2)
|
||||||
|
|
||||||
- make sure you have installed the following: git, yarn, nodejs
|
- make sure you have installed the following: git, yarn, nodejs
|
||||||
- download the source code: `git clone https://github.com/Awesome-Technologies/synapse-admin.git`
|
- download the source code: `git clone https://github.com/etkecc/synapse-admin.git`
|
||||||
- change into downloaded directory: `cd synapse-admin`
|
- change into downloaded directory: `cd synapse-admin`
|
||||||
- download dependencies: `yarn install`
|
- download dependencies: `yarn install`
|
||||||
- start web server: `yarn start`
|
- start web server: `yarn start`
|
||||||
|
|
||||||
#### Steps for 3)
|
#### Steps for 3)
|
||||||
|
|
||||||
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
- run the Docker container from the public docker registry: `docker run -p 8080:80 ghcr.io/etkecc/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
||||||
|
|
||||||
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
||||||
|
|
||||||
@@ -113,7 +199,7 @@ You have three options:
|
|||||||
container_name: synapse-admin
|
container_name: synapse-admin
|
||||||
hostname: synapse-admin
|
hostname: synapse-admin
|
||||||
build:
|
build:
|
||||||
context: https://github.com/Awesome-Technologies/synapse-admin.git
|
context: https://github.com/etkecc/synapse-admin.git
|
||||||
args:
|
args:
|
||||||
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||||
@@ -125,37 +211,6 @@ You have three options:
|
|||||||
|
|
||||||
- browse to http://localhost:8080
|
- browse to http://localhost:8080
|
||||||
|
|
||||||
### 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"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `config.json` can be injected into a Docker container using a bind mount.
|
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
synapse-admin:
|
|
||||||
...
|
|
||||||
volumes:
|
|
||||||
./config.json:/app/config.json:ro
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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.
|
The path prefix where synapse-admin is served can only be changed during the build step.
|
||||||
@@ -182,7 +237,7 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
synapse-admin:
|
synapse-admin:
|
||||||
image: awesometechnologies/synapse-admin:latest
|
image: etkecc/synapse-admin:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
@@ -193,10 +248,6 @@ services:
|
|||||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
- See https://yarnpkg.com/getting-started/editor-sdks how to setup your IDE
|
||||||
|
34
package.json
34
package.json
@@ -10,21 +10,20 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/etkecc/synapse-admin"
|
"url": "https://github.com/etkecc/synapse-admin"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.1",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.7.0",
|
"@eslint/js": "^9.7.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/lodash": "^4.17.7",
|
"@types/lodash": "^4.17.7",
|
||||||
"@types/node": "^20.14.12",
|
"@types/node": "^20.14.12",
|
||||||
"@types/papaparse": "^5.3.14",
|
"@types/papaparse": "^5.3.14",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.16.1",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
@@ -37,11 +36,11 @@
|
|||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1",
|
||||||
"ts-jest": "^29.2.3",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"typescript-eslint": "^7.16.1",
|
"typescript-eslint": "^7.16.1",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.4.6",
|
||||||
"vite-plugin-version-mark": "^0.1.0"
|
"vite-plugin-version-mark": "^0.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -49,27 +48,26 @@
|
|||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@haleos/ra-language-german": "^1.0.0",
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^5.16.4",
|
"@mui/icons-material": "^6.1.1",
|
||||||
"@mui/material": "^5.16.4",
|
"@mui/material": "^6.1.1",
|
||||||
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"query-string": "^7.1.3",
|
"ra-core": "^5.2.0",
|
||||||
"ra-core": "^4.16.20",
|
"ra-i18n-polyglot": "^5.2.0",
|
||||||
"ra-i18n-polyglot": "^4.16.20",
|
"ra-language-english": "^5.2.0",
|
||||||
"ra-language-english": "^4.16.20",
|
"ra-language-farsi": "^5.0.0",
|
||||||
"ra-language-farsi": "^4.2.0",
|
"ra-language-french": "^5.2.0",
|
||||||
"ra-language-french": "^4.16.20",
|
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^4.14.2",
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^4.16.20",
|
"react-admin": "^5.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-query": "^3.39.3",
|
"react-router": "^6.26.2",
|
||||||
"react-router": "^6.25.1",
|
"react-router-dom": "^6.26.2"
|
||||||
"react-router-dom": "^6.25.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import fetchMock from "jest-fetch-mock";
|
||||||
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
@@ -7,4 +9,4 @@ describe("App", () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
await screen.findAllByText("Welcome to Synapse-admin");
|
await screen.findAllByText("Welcome to Synapse-admin");
|
||||||
});
|
});
|
||||||
});
|
});
|
66
src/App.tsx
66
src/App.tsx
@@ -4,6 +4,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
|||||||
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
|
import { AdminLayout } from "./components/AdminLayout";
|
||||||
import { ImportFeature } from "./components/ImportFeature";
|
import { ImportFeature } from "./components/ImportFeature";
|
||||||
import germanMessages from "./i18n/de";
|
import germanMessages from "./i18n/de";
|
||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
@@ -21,6 +22,7 @@ import userMediaStats from "./resources/user_media_statistics";
|
|||||||
import users from "./resources/users";
|
import users from "./resources/users";
|
||||||
import authProvider from "./synapse/authProvider";
|
import authProvider from "./synapse/authProvider";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -45,37 +47,41 @@ const i18nProvider = polyglotI18nProvider(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Admin
|
<QueryClientProvider client={queryClient}>
|
||||||
disableTelemetry
|
<Admin
|
||||||
requireAuth
|
disableTelemetry
|
||||||
loginPage={LoginPage}
|
requireAuth
|
||||||
authProvider={authProvider}
|
layout={AdminLayout}
|
||||||
dataProvider={dataProvider}
|
loginPage={LoginPage}
|
||||||
i18nProvider={i18nProvider}
|
authProvider={authProvider}
|
||||||
darkTheme={{ palette: { mode: "dark" } }}
|
dataProvider={dataProvider}
|
||||||
>
|
i18nProvider={i18nProvider}
|
||||||
<CustomRoutes>
|
>
|
||||||
<Route path="/import_users" element={<ImportFeature />} />
|
<CustomRoutes>
|
||||||
</CustomRoutes>
|
<Route path="/import_users" element={<ImportFeature />} />
|
||||||
<Resource {...users} />
|
</CustomRoutes>
|
||||||
<Resource {...rooms} />
|
<Resource {...users} />
|
||||||
<Resource {...userMediaStats} />
|
<Resource {...rooms} />
|
||||||
<Resource {...reports} />
|
<Resource {...userMediaStats} />
|
||||||
<Resource {...roomDirectory} />
|
<Resource {...reports} />
|
||||||
<Resource {...destinations} />
|
<Resource {...roomDirectory} />
|
||||||
<Resource {...registrationToken} />
|
<Resource {...destinations} />
|
||||||
<Resource name="connections" />
|
<Resource {...registrationToken} />
|
||||||
<Resource name="devices" />
|
<Resource name="connections" />
|
||||||
<Resource name="room_members" />
|
<Resource name="devices" />
|
||||||
<Resource name="users_media" />
|
<Resource name="room_members" />
|
||||||
<Resource name="joined_rooms" />
|
<Resource name="users_media" />
|
||||||
<Resource name="pushers" />
|
<Resource name="joined_rooms" />
|
||||||
<Resource name="servernotices" />
|
<Resource name="pushers" />
|
||||||
<Resource name="forward_extremities" />
|
<Resource name="servernotices" />
|
||||||
<Resource name="room_state" />
|
<Resource name="forward_extremities" />
|
||||||
<Resource name="destination_rooms" />
|
<Resource name="room_state" />
|
||||||
</Admin>
|
<Resource name="destination_rooms" />
|
||||||
|
</Admin>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@@ -2,6 +2,8 @@ import { createContext, useContext } from "react";
|
|||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
restrictBaseUrl: string | string[];
|
restrictBaseUrl: string | string[];
|
||||||
|
asManagedUsers: string[];
|
||||||
|
supportURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
export const AppContext = createContext({});
|
||||||
|
26
src/components/AdminLayout.tsx
Normal file
26
src/components/AdminLayout.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Layout, Menu } from 'react-admin';
|
||||||
|
import LiveHelpIcon from '@mui/icons-material/LiveHelp';
|
||||||
|
|
||||||
|
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 AdminMenu = () => (
|
||||||
|
<Menu>
|
||||||
|
<Menu.ResourceItems />
|
||||||
|
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AdminLayout = ({ children }) => (
|
||||||
|
<Layout menu={AdminMenu}>
|
||||||
|
{children}
|
||||||
|
</Layout>
|
||||||
|
);
|
@@ -74,7 +74,6 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
|||||||
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
||||||
<SimpleForm toolbar={false}>
|
<SimpleForm toolbar={false}>
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
fullWidth
|
|
||||||
source="block"
|
source="block"
|
||||||
value={block}
|
value={block}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
||||||
|
@@ -20,7 +20,7 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
@@ -43,7 +43,6 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
source="body"
|
source="body"
|
||||||
label="resources.servernotices.fields.body"
|
label="resources.servernotices.fields.body"
|
||||||
fullWidth
|
|
||||||
multiline
|
multiline
|
||||||
rows="4"
|
rows="4"
|
||||||
resettable
|
resettable
|
||||||
@@ -64,6 +63,10 @@ export const ServerNoticeButton = () => {
|
|||||||
const handleDialogOpen = () => setOpen(true);
|
const handleDialogOpen = () => setOpen(true);
|
||||||
const handleDialogClose = () => setOpen(false);
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = (values: Partial<RaRecord>) => {
|
const handleSend = (values: Partial<RaRecord>) => {
|
||||||
create(
|
create(
|
||||||
"servernotices",
|
"servernotices",
|
||||||
@@ -100,28 +103,26 @@ export const ServerNoticeBulkButton = () => {
|
|||||||
const unselectAllUsers = useUnselectAll("users");
|
const unselectAllUsers = useUnselectAll("users");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
|
|
||||||
const { mutate: sendNotices, isLoading } = useMutation(
|
const { mutate: sendNotices, isPending } = useMutation({
|
||||||
data =>
|
mutationFn: (data) =>
|
||||||
dataProvider.createMany("servernotices", {
|
dataProvider.createMany("servernotices", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: data,
|
data: data,
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.servernotices.action.send_success");
|
||||||
notify("resources.servernotices.action.send_success");
|
unselectAllUsers();
|
||||||
unselectAllUsers();
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.servernotices.action.send_failure", {
|
||||||
notify("resources.servernotices.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isPending}>
|
||||||
<MessageIcon />
|
<MessageIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
||||||
|
import { isASManaged } from "./mxid";
|
||||||
|
|
||||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
|
let isASManagedUser = false;
|
||||||
|
if (record.user_id) {
|
||||||
|
isASManagedUser = isASManaged(record.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeleteWithConfirmButton
|
<DeleteWithConfirmButton
|
||||||
{...props}
|
{...props}
|
||||||
@@ -12,6 +18,7 @@ export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
|||||||
confirmContent="resources.devices.action.erase.content"
|
confirmContent="resources.devices.action.erase.content"
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
redirect={false}
|
redirect={false}
|
||||||
|
disabled={isASManagedUser}
|
||||||
translateOptions={{
|
translateOptions={{
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name: record.display_name ? record.display_name : record.id,
|
name: record.display_name ? record.display_name : record.id,
|
||||||
|
@@ -28,7 +28,7 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { dateParser } from "./date";
|
import { dateParser } from "./date";
|
||||||
@@ -55,14 +55,12 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
fullWidth
|
|
||||||
source="before_ts"
|
source="before_ts"
|
||||||
label="delete_media.fields.before_ts"
|
label="delete_media.fields.before_ts"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
parse={dateParser}
|
parse={dateParser}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
fullWidth
|
|
||||||
source="size_gt"
|
source="size_gt"
|
||||||
label="delete_media.fields.size_gt"
|
label="delete_media.fields.size_gt"
|
||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
@@ -70,7 +68,6 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
|||||||
step={1024}
|
step={1024}
|
||||||
/>
|
/>
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
fullWidth
|
|
||||||
source="keep_profiles"
|
source="keep_profiles"
|
||||||
label="delete_media.fields.keep_profiles"
|
label="delete_media.fields.keep_profiles"
|
||||||
defaultValue={true}
|
defaultValue={true}
|
||||||
@@ -86,20 +83,18 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
const { mutate: deleteMedia, isLoading } = useMutation(
|
const { mutate: deleteMedia, isPending } = useMutation({
|
||||||
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("delete_media.action.send_success");
|
||||||
notify("delete_media.action.send_success");
|
closeDialog();
|
||||||
closeDialog();
|
},
|
||||||
},
|
onError: () => {
|
||||||
onError: () => {
|
notify("delete_media.action.send_failure", {
|
||||||
notify("delete_media.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
});
|
||||||
});
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const openDialog = () => setOpen(true);
|
const openDialog = () => setOpen(true);
|
||||||
const closeDialog = () => setOpen(false);
|
const closeDialog = () => setOpen(false);
|
||||||
@@ -110,7 +105,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
label="delete_media.action.send"
|
label="delete_media.action.send"
|
||||||
onClick={openDialog}
|
onClick={openDialog}
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
|
16
src/components/mxid.tsx
Normal file
16
src/components/mxid.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const de: SynapseTranslationMessages = {
|
const de: SynapseTranslationMessages = {
|
||||||
...formalGermanMessages,
|
...formalGermanMessages,
|
||||||
|
ra: {
|
||||||
|
...formalGermanMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...formalGermanMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Keine Ergebnisse",
|
||||||
|
clear_filters: "Alle Filter entfernen",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Heimserver URL",
|
base_url: "Heimserver URL",
|
||||||
@@ -142,8 +150,9 @@ const de: SynapseTranslationMessages = {
|
|||||||
helper: {
|
helper: {
|
||||||
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
|
||||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
erase: "DSGVO konformes Löschen der Benutzerdaten.",
|
||||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
|
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
|
||||||
|
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Lösche Benutzerdaten",
|
erase: "Lösche Benutzerdaten",
|
||||||
|
@@ -143,6 +143,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
deactivate: "You must provide a password to re-activate an account.",
|
deactivate: "You must provide a password to re-activate an account.",
|
||||||
erase: "Mark the user as GDPR-erased",
|
erase: "Mark the user as GDPR-erased",
|
||||||
erase_admin_error: "Deleting own user is not allowed.",
|
erase_admin_error: "Deleting own user is not allowed.",
|
||||||
|
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Erase user data",
|
erase: "Erase user data",
|
||||||
|
@@ -138,6 +138,8 @@ const fa: SynapseTranslationMessages = {
|
|||||||
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
|
||||||
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
|
||||||
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
|
||||||
|
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
|
||||||
|
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "پاک کردن اطلاعات کاربر",
|
erase: "پاک کردن اطلاعات کاربر",
|
||||||
|
@@ -141,6 +141,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||||
|
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Effacer les données de l'utilisateur",
|
erase: "Effacer les données de l'utilisateur",
|
||||||
|
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
@@ -139,6 +139,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
deactivate: string;
|
deactivate: string;
|
||||||
erase: string;
|
erase: string;
|
||||||
erase_admin_error: string;
|
erase_admin_error: string;
|
||||||
|
modify_managed_user_error: string;
|
||||||
};
|
};
|
||||||
action: {
|
action: {
|
||||||
erase: string;
|
erase: string;
|
||||||
|
@@ -143,6 +143,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
action: {
|
action: {
|
||||||
erase: "Cancella i dati dell'utente",
|
erase: "Cancella i dati dell'utente",
|
||||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||||
|
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const ru: SynapseTranslationMessages = {
|
const ru: SynapseTranslationMessages = {
|
||||||
...russianMessages,
|
...russianMessages,
|
||||||
|
ra: {
|
||||||
|
...russianMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...russianMessages.ra.navigation,
|
||||||
|
no_filtered_results: "Нет результатов",
|
||||||
|
clear_filters: "Все фильтры сбросить",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "Адрес домашнего сервера",
|
base_url: "Адрес домашнего сервера",
|
||||||
@@ -152,6 +160,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
|
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Удалить данные пользователя",
|
erase: "Удалить данные пользователя",
|
||||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
|||||||
|
|
||||||
const zh: SynapseTranslationMessages = {
|
const zh: SynapseTranslationMessages = {
|
||||||
...chineseMessages,
|
...chineseMessages,
|
||||||
|
ra: {
|
||||||
|
...chineseMessages.ra,
|
||||||
|
navigation: {
|
||||||
|
...chineseMessages.ra.navigation,
|
||||||
|
no_filtered_results: "没有结果",
|
||||||
|
clear_filters: "清除所有过滤器",
|
||||||
|
},
|
||||||
|
},
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
base_url: "服务器 URL",
|
base_url: "服务器 URL",
|
||||||
@@ -136,6 +144,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
|
modify_managed_user_error: "不允许修改系统管理的用户。",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "抹除用户信息",
|
erase: "抹除用户信息",
|
||||||
|
@@ -4,15 +4,18 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { AppContext } from "./AppContext";
|
import { AppContext } from "./AppContext";
|
||||||
|
import storage from "./storage";
|
||||||
|
|
||||||
fetch("config.json")
|
fetch("config.json")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props =>
|
.then(props => {
|
||||||
createRoot(document.getElementById("root")).render(
|
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||||
|
storage.setItem("support_url", props.supportURL);
|
||||||
|
return createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppContext.Provider value={props}>
|
<AppContext.Provider value={props}>
|
||||||
<App />
|
<App />
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
);
|
});
|
||||||
|
@@ -8,14 +8,17 @@ import { AppContext } from "../AppContext";
|
|||||||
import englishMessages from "../i18n/en";
|
import englishMessages from "../i18n/en";
|
||||||
|
|
||||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||||
|
import { act } from "@testing-library/react";
|
||||||
|
|
||||||
describe("LoginForm", () => {
|
describe("LoginForm", () => {
|
||||||
it("renders with no restriction to homeserver", () => {
|
it("renders with no restriction to homeserver", async () => {
|
||||||
render(
|
await act(async () => {
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
render(
|
||||||
<LoginPage />
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
</AdminContext>
|
<LoginPage />
|
||||||
);
|
</AdminContext>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
||||||
screen.getByRole("combobox", { name: "" });
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
@@ -222,7 +222,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
onBlur={handleUsernameChange}
|
onBlur={handleUsernameChange}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -234,7 +233,6 @@ const LoginPage = () => {
|
|||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={loading || !supportPassAuth}
|
disabled={loading || !supportPassAuth}
|
||||||
resettable
|
resettable
|
||||||
fullWidth
|
|
||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -247,7 +245,6 @@ const LoginPage = () => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
readOnly={allowSingleBaseUrl}
|
readOnly={allowSingleBaseUrl}
|
||||||
resettable={allowAnyBaseUrl}
|
resettable={allowAnyBaseUrl}
|
||||||
fullWidth
|
|
||||||
validate={[required(), validateBaseUrl]}
|
validate={[required(), validateBaseUrl]}
|
||||||
>
|
>
|
||||||
{allowMultipleBaseUrls &&
|
{allowMultipleBaseUrls &&
|
||||||
@@ -280,9 +277,9 @@ const LoginPage = () => {
|
|||||||
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
||||||
<Box className="form">
|
<Box className="form">
|
||||||
<Select
|
<Select
|
||||||
|
fullWidth
|
||||||
value={locale}
|
value={locale}
|
||||||
onChange={e => setLocale(e.target.value)}
|
onChange={e => setLocale(e.target.value)}
|
||||||
fullWidth
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="select"
|
className="select"
|
||||||
>
|
>
|
||||||
|
@@ -25,7 +25,7 @@ import {
|
|||||||
useRefresh,
|
useRefresh,
|
||||||
useUnselectAll,
|
useUnselectAll,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
|
||||||
@@ -70,27 +70,25 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const unselectAllRooms = useUnselectAll("rooms");
|
const unselectAllRooms = useUnselectAll("rooms");
|
||||||
const dataProvider = useDataProvider();
|
const dataProvider = useDataProvider();
|
||||||
const { mutate, isLoading } = useMutation(
|
const { mutate, isPending } = useMutation({
|
||||||
() =>
|
mutationFn: () =>
|
||||||
dataProvider.createMany("room_directory", {
|
dataProvider.createMany("room_directory", {
|
||||||
ids: selectedIds,
|
ids: selectedIds,
|
||||||
data: {},
|
data: {},
|
||||||
}),
|
}),
|
||||||
{
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
notify("resources.room_directory.action.send_success");
|
||||||
notify("resources.room_directory.action.send_success");
|
unselectAllRooms();
|
||||||
unselectAllRooms();
|
refresh();
|
||||||
refresh();
|
},
|
||||||
},
|
onError: () =>
|
||||||
onError: () =>
|
notify("resources.room_directory.action.send_failure", {
|
||||||
notify("resources.room_directory.action.send_failure", {
|
type: "error",
|
||||||
type: "error",
|
}),
|
||||||
}),
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isLoading}>
|
<Button {...props} label="resources.room_directory.action.create" onClick={mutate} disabled={isPending}>
|
||||||
<RoomDirectoryIcon />
|
<RoomDirectoryIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -102,6 +100,10 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
|||||||
const refresh = useRefresh();
|
const refresh = useRefresh();
|
||||||
const [create, { isLoading }] = useCreate();
|
const [create, { isLoading }] = useCreate();
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
create(
|
create(
|
||||||
"room_directory",
|
"room_directory",
|
||||||
|
@@ -67,6 +67,9 @@ const RoomTitle = () => {
|
|||||||
|
|
||||||
const RoomShowActions = () => {
|
const RoomShowActions = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
||||||
// FIXME: refresh after (un)publish
|
// FIXME: refresh after (un)publish
|
||||||
return (
|
return (
|
||||||
|
@@ -61,6 +61,7 @@ import {
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
import { isASManaged } from "../components/mxid";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
import { DeviceRemoveButton } from "../components/devices";
|
import { DeviceRemoveButton } from "../components/devices";
|
||||||
@@ -103,8 +104,9 @@ const userFilters = [
|
|||||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean }> = props => {
|
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => {
|
||||||
const ownUserIsSelected = props.ownUserIsSelected;
|
const ownUserIsSelected = props.ownUserIsSelected;
|
||||||
|
const asManagedUserIsSelected = props.asManagedUserIsSelected;
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
@@ -112,6 +114,9 @@ const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSele
|
|||||||
if (ownUserIsSelected) {
|
if (ownUserIsSelected) {
|
||||||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
} else if (asManagedUserIsSelected) {
|
||||||
|
notify(<Alert severity="error">{translate("resources.users.helper.modify_managed_user_error")}</Alert>);
|
||||||
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,6 +126,7 @@ const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSele
|
|||||||
const UserBulkActionButtons = () => {
|
const UserBulkActionButtons = () => {
|
||||||
const record = useListContext();
|
const record = useListContext();
|
||||||
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
||||||
|
const [asManagedUserIsSelected, setAsManagedUserIsSelected] = useState(false);
|
||||||
const selectedIds = record.selectedIds;
|
const selectedIds = record.selectedIds;
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
@@ -128,12 +134,13 @@ const UserBulkActionButtons = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||||
|
setAsManagedUserIsSelected(selectedIds.some(id => isASManaged(id)));
|
||||||
}, [selectedIds]);
|
}, [selectedIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle="resources.users.helper.erase"
|
confirmTitle="resources.users.helper.erase"
|
||||||
@@ -184,14 +191,16 @@ const UserEditActions = () => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
if (record && record.id) {
|
if (record && record.id) {
|
||||||
ownUserIsSelected = record.id === ownUserId;
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record?.deactivated && <ServerNoticeButton />}
|
{!record?.deactivated && <ServerNoticeButton />}
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle={translate("resources.users.helper.erase", {
|
confirmTitle={translate("resources.users.helper.erase", {
|
||||||
@@ -235,13 +244,21 @@ export const UserCreate = (props: CreateProps) => (
|
|||||||
|
|
||||||
const UserTitle = () => {
|
const UserTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""
|
||||||
|
if (isASManaged(record?.id)) {
|
||||||
|
username += " 🤖";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate("resources.users.name", {
|
{translate("resources.users.name", {
|
||||||
smart_count: 1,
|
smart_count: 1,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
{username}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -250,8 +267,10 @@ const UserEditToolbar = () => {
|
|||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
if (record && record.id) {
|
if (record && record.id) {
|
||||||
ownUserIsSelected = record.id === ownUserId;
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -259,7 +278,7 @@ const UserEditToolbar = () => {
|
|||||||
<div className={ToolbarClasses.defaultToolbar}>
|
<div className={ToolbarClasses.defaultToolbar}>
|
||||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||||
<SaveButton />
|
<SaveButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
@@ -272,35 +291,53 @@ const UserBooleanInput = props => {
|
|||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
if (record && record.id === ownUserId) {
|
let asManagedUserIsSelected = false;
|
||||||
ownUserIsSelected = true;
|
if (record) {
|
||||||
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BooleanInput {...props} disabled={ownUserIsSelected} />
|
<BooleanInput {...props} disabled={ownUserIsSelected || asManagedUserIsSelected} />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserPasswordInput = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
|
if (record) {
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PasswordInput {...props} helperText="resources.users.helper.modify_managed_user_error" disabled={asManagedUserIsSelected} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UserEdit = (props: EditProps) => {
|
export const UserEdit = (props: EditProps) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
|
||||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
||||||
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||||
<ImageInput source="avatar_file" label="resources.users.fields.avatar" accept="image/*">
|
<ImageInput
|
||||||
<ImageField source="src" title="Avatar" />
|
source="avatar_file"
|
||||||
|
label="resources.users.fields.avatar"
|
||||||
|
accept={{ "image/*": [".png", ".jpg"] }}
|
||||||
|
>
|
||||||
|
<ImageField source="src" title="Avatar" />
|
||||||
</ImageInput>
|
</ImageInput>
|
||||||
<TextInput source="id" disabled />
|
<TextInput source="id" readOnly />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<BooleanInput source="locked" />
|
<UserBooleanInput source="locked" />
|
||||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||||
<BooleanInput source="erased" disabled />
|
<BooleanInput source="erased" disabled />
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
@@ -327,7 +364,7 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
|
|
||||||
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
||||||
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||||
<TextField source="device_id" sortable={false} />
|
<TextField source="device_id" sortable={false} />
|
||||||
<TextField source="display_name" sortable={false} />
|
<TextField source="display_name" sortable={false} />
|
||||||
<TextField source="last_seen_ip" sortable={false} />
|
<TextField source="last_seen_ip" sortable={false} />
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { stringify } from "query-string";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataProvider,
|
DataProvider,
|
||||||
DeleteParams,
|
DeleteParams,
|
||||||
HttpError,
|
HttpError,
|
||||||
Identifier,
|
Identifier,
|
||||||
Options,
|
Options,
|
||||||
|
PaginationPayload,
|
||||||
RaRecord,
|
RaRecord,
|
||||||
|
SortPayload,
|
||||||
UpdateParams,
|
UpdateParams,
|
||||||
fetchUtils,
|
fetchUtils,
|
||||||
withLifecycleCallbacks,
|
withLifecycleCallbacks,
|
||||||
@@ -52,6 +52,10 @@ const mxcUrlToHttp = (mxcUrl: string) => {
|
|||||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterUndefined = (obj: Record<string, any>) => {
|
||||||
|
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
|
||||||
|
};
|
||||||
|
|
||||||
interface Room {
|
interface Room {
|
||||||
room_id: string;
|
room_id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -527,8 +531,8 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
getList: async (resource, params) => {
|
getList: async (resource, params) => {
|
||||||
console.log("getList " + resource);
|
console.log("getList " + resource);
|
||||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||||
const { page, perPage } = params.pagination;
|
const { page, perPage } = params.pagination as PaginationPayload;
|
||||||
const { field, order } = params.sort;
|
const { field, order } = params.sort as SortPayload;
|
||||||
const from = (page - 1) * perPage;
|
const from = (page - 1) * perPage;
|
||||||
const query = {
|
const query = {
|
||||||
from: from,
|
from: from,
|
||||||
@@ -550,7 +554,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const endpoint_url = homeserver + res.path;
|
||||||
const url = `${endpoint_url}?${stringify(query)}`;
|
const url = `${endpoint_url}?${new URLSearchParams(filterUndefined(query)).toString()}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(url);
|
const { json } = await jsonClient(url);
|
||||||
return {
|
return {
|
||||||
@@ -604,7 +608,7 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
|
||||||
const ref = res.reference(params.id);
|
const ref = res.reference(params.id);
|
||||||
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
|
const endpoint_url = `${homeserver}${ref.endpoint}?${new URLSearchParams(filterUndefined(query)).toString()}`;
|
||||||
|
|
||||||
const { json } = await jsonClient(endpoint_url);
|
const { json } = await jsonClient(endpoint_url);
|
||||||
return {
|
return {
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
|
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
Reference in New Issue
Block a user