Compare commits
9 Commits
v0.10.3-et
...
v0.10.3-et
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52a2f1c936 | ||
|
|
e328380c77 | ||
|
|
a277ded227 | ||
|
|
48d933e028 | ||
|
|
24cf0a60bf | ||
|
|
d5113aad72 | ||
|
|
6957cb1f7d | ||
|
|
66c706532a | ||
|
|
332e98a095 |
16
.github/CONTRIBUTING.md
vendored
16
.github/CONTRIBUTING.md
vendored
@@ -1,16 +0,0 @@
|
|||||||
# Contributing to [etkecc/synapse-admin](https://github.com/etkecc/synapse-admin)
|
|
||||||
|
|
||||||
While etke.cc fork is intended to accept more QoL changes and features,
|
|
||||||
it's good idea to open PR into the upstream repo: [Awesome-Technologies/Synapse-Admin](https://github.com/Awesome-Technologies/synapse-admin).
|
|
||||||
|
|
||||||
1. Use the etkecc/synapse-admin **master** branch as your branch upstream: `git checkout master; git pull; git checkout -b my-new-feature`
|
|
||||||
2. Once your changes are ready, please, open **2** PRs: one from your branch to `Awesome-Technologies/Synapse-Admin` **master**, and another one to `etkecc/synapse-admin` **main**
|
|
||||||
3. Once PR is accepted in the `etkecc/synapse-admin`, update `README.md` file (either directly in the `main` branch, or via another PR) to add link to the merged PR in the [Fork differences](https://github.com/etkecc/synapse-admin#fork-differences) section
|
|
||||||
|
|
||||||
### Why?
|
|
||||||
|
|
||||||
The upstream project may not want to accept all the changes, so to ensure they are not lost, we will gladly add them to the etke.cc fork.
|
|
||||||
Unfortunately, it's challenging to keep changes separated, so to avoid messing upstream and fork changes (e.g., CI changes that should not be pushed to the upstream, as they intended for this fork specifically), there are 2 branches:
|
|
||||||
|
|
||||||
* `master` - read-only copy of upstream's master branch to easily sync changes, and use it as base for new PRs
|
|
||||||
* `main` - fork-own branch with all changes
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -191,3 +191,6 @@ sketch
|
|||||||
# .pnp.*
|
# .pnp.*
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
|
||||||
|
|
||||||
|
/testdata/synapse.data
|
||||||
|
/testdata/postgres.data
|
||||||
|
|||||||
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
|
|
||||||
112
README.md
112
README.md
@@ -1,26 +1,19 @@
|
|||||||
[](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
# Synapse Admin UI [](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
|
||||||
[](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
|
|
||||||
[](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
|
|
||||||
[](https://awesome-technologies.github.io/synapse-admin/)
|
|
||||||
[](https://hub.docker.com/r/awesometechnologies/synapse-admin)
|
|
||||||
[](https://github.com/Awesome-Technologies/synapse-admin/releases)
|
|
||||||
|
|
||||||
# Synapse admin ui
|
|
||||||
|
|
||||||
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||||
|
|
||||||
## Fork differences
|
## Fork differences
|
||||||
|
|
||||||
|
With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
|
||||||
|
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||||
|
user-friendly interface for managing Synapse homeservers.
|
||||||
|
|
||||||
### Available via CDN
|
### Available via CDN
|
||||||
|
|
||||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
The following changes are already implemented:
|
The following changes are already implemented:
|
||||||
|
|
||||||
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||||
@@ -36,9 +29,25 @@ The following changes are already implemented:
|
|||||||
* [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (CI only)
|
* [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)
|
* [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)
|
* [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)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
|
||||||
|
user creation fails)
|
||||||
|
|
||||||
|
After that open `http://localhost:5173` in your browser, login using the following credentials:
|
||||||
|
|
||||||
|
* Login: admin
|
||||||
|
* Password: admin
|
||||||
|
* Homeserver URL: http://localhost:8008
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Supported Synapse
|
### Supported Synapse
|
||||||
@@ -118,37 +127,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.
|
||||||
@@ -186,6 +164,54 @@ services:
|
|||||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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$"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
20
docker-compose-dev.yml
Normal file
20
docker-compose-dev.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
synapse:
|
||||||
|
image: ghcr.io/element-hq/synapse:latest
|
||||||
|
entrypoint: python
|
||||||
|
command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
|
||||||
|
ports:
|
||||||
|
- "8008:8008"
|
||||||
|
volumes:
|
||||||
|
- ./testdata/synapse:/config
|
||||||
|
- ./testdata/synapse.data:/media-store
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:alpine
|
||||||
|
volumes:
|
||||||
|
- ./testdata/postgres.data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: synapse
|
||||||
|
POSTGRES_PASSWORD: synapse
|
||||||
|
POSTGRES_DB: synapse
|
||||||
|
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
||||||
27
justfile
27
justfile
@@ -6,8 +6,33 @@ default:
|
|||||||
build: __install
|
build: __install
|
||||||
@yarn run build --base=./
|
@yarn run build --base=./
|
||||||
|
|
||||||
|
# run the app in a development mode
|
||||||
|
run:
|
||||||
|
@yarn start --host 0.0.0.0
|
||||||
|
|
||||||
|
# run dev stack and start the app in a development mode
|
||||||
|
run-dev:
|
||||||
|
@echo "Starting the database..."
|
||||||
|
@docker-compose -f docker-compose-dev.yml up -d postgres
|
||||||
|
@echo "Starting Synapse..."
|
||||||
|
@docker-compose -f docker-compose-dev.yml up -d synapse
|
||||||
|
@echo "Ensure admin user is registered..."
|
||||||
|
@docker-compose -f docker-compose-dev.yml exec synapse register_new_matrix_user --admin -u admin -p admin -c /config/homeserver.yaml http://localhost:8008 || true
|
||||||
|
@echo "Starting the app..."
|
||||||
|
@yarn start --host 0.0.0.0
|
||||||
|
|
||||||
|
# stop the dev stack
|
||||||
|
stop-dev:
|
||||||
|
@docker-compose -f docker-compose-dev.yml stop
|
||||||
|
|
||||||
|
|
||||||
|
register-user localpart password *admin:
|
||||||
|
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# run the app in a production mode
|
# run the app in a production mode
|
||||||
run: build
|
run-prod: build
|
||||||
@python -m http.server -d dist 1313
|
@python -m http.server -d dist 1313
|
||||||
|
|
||||||
# install the project
|
# install the project
|
||||||
|
|||||||
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";
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ const App = () => (
|
|||||||
authProvider={authProvider}
|
authProvider={authProvider}
|
||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
i18nProvider={i18nProvider}
|
i18nProvider={i18nProvider}
|
||||||
darkTheme={{ palette: { mode: "dark" } }}
|
|
||||||
>
|
>
|
||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<ImportFeature />} />
|
<Route path="/import_users" element={<ImportFeature />} />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createContext, useContext } from "react";
|
|||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
restrictBaseUrl: string | string[];
|
restrictBaseUrl: string | string[];
|
||||||
|
asManagedUsers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
export const AppContext = createContext({});
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { DataProvider, useTranslate } from "ra-core";
|
import { DataProvider, useTranslate } from "ra-core";
|
||||||
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
||||||
|
|
||||||
import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
|
import { generateRandomMxId, generateRandomPassword, returnMXID } from "../synapse/synapse";
|
||||||
|
|
||||||
const LOGGING = true;
|
const LOGGING = true;
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ const FilePicker = () => {
|
|||||||
|
|
||||||
const [conflictMode, setConflictMode] = useState("stop");
|
const [conflictMode, setConflictMode] = useState("stop");
|
||||||
const [passwordMode, setPasswordMode] = useState(true);
|
const [passwordMode, setPasswordMode] = useState(true);
|
||||||
const [useridMode, setUseridMode] = useState("ignore");
|
const [useridMode, setUseridMode] = useState("update");
|
||||||
|
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
@@ -121,7 +121,11 @@ const FilePicker = () => {
|
|||||||
|
|
||||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||||
/* First, verify the presence of required fields */
|
/* First, verify the presence of required fields */
|
||||||
const missingFields = expectedFields.filter(eF => meta.fields?.find(mF => eF === mF));
|
const missingFields = expectedFields.filter(eF => {
|
||||||
|
const result = meta.fields?.find(mF => eF === mF);
|
||||||
|
if (result === undefined) { return eF; } // missing field
|
||||||
|
return undefined; // field found
|
||||||
|
});
|
||||||
|
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||||
@@ -262,12 +266,15 @@ const FilePicker = () => {
|
|||||||
const userRecord = { ...entry };
|
const userRecord = { ...entry };
|
||||||
// No need to do a bunch of cryptographic random number getting if
|
// No need to do a bunch of cryptographic random number getting if
|
||||||
// we are using neither a generated password nor a generated user id.
|
// we are using neither a generated password nor a generated user id.
|
||||||
if (useridMode === "ignore" || userRecord.id === undefined) {
|
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") {
|
||||||
userRecord.id = generateRandomMxId();
|
userRecord.id = generateRandomMxId();
|
||||||
}
|
}
|
||||||
if (passwordMode === false || entry.password === undefined) {
|
if (passwordMode === false || entry.password === undefined || entry.password === "") {
|
||||||
userRecord.password = generateRandomPassword();
|
userRecord.password = generateRandomPassword();
|
||||||
}
|
}
|
||||||
|
// we want to ensure that the ID is always full MXID, otherwise randomly-generated MXIDs will be in the full
|
||||||
|
// form, but the ones from the CSV will be localpart-only.
|
||||||
|
userRecord.id = returnMXID(userRecord.id);
|
||||||
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
|
/* TODO record update stats (especially admin no -> yes, deactivated x -> !x, ... */
|
||||||
|
|
||||||
/* For these modes we will consider the ID that's in the record.
|
/* For these modes we will consider the ID that's in the record.
|
||||||
|
|||||||
@@ -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,13 +103,12 @@ 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();
|
||||||
@@ -116,12 +118,11 @@ export const ServerNoticeBulkButton = () => {
|
|||||||
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,9 +83,8 @@ 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();
|
||||||
@@ -98,8 +94,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
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": {
|
||||||
|
|||||||
15
src/components/mxid.tsx
Normal file
15
src/components/mxid.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is managed by an application service
|
||||||
|
* @param id The user ID to check
|
||||||
|
* @returns Whether the user is managed by an application service
|
||||||
|
*/
|
||||||
|
export const isASManaged = (id: string) => {
|
||||||
|
const managedUsersString = localStorage.getItem("as_managed_users");
|
||||||
|
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,11 +150,13 @@ 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",
|
||||||
|
erase_avatar: "Avatar löschen"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
@@ -143,9 +143,11 @@ 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",
|
||||||
|
erase_avatar: "Erase avatar"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
@@ -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,9 +141,11 @@ 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",
|
||||||
|
erase_avatar: "Effacer l'avatar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
@@ -142,6 +142,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
};
|
};
|
||||||
action: {
|
action: {
|
||||||
erase: string;
|
erase: string;
|
||||||
|
erase_avatar: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
@@ -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,9 +160,11 @@ const ru: SynapseTranslationMessages = {
|
|||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
|
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Удалить данные пользователя",
|
erase: "Удалить данные пользователя",
|
||||||
|
erase_avatar: "Удалить аватар",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
@@ -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,9 +144,11 @@ const zh: SynapseTranslationMessages = {
|
|||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
|
modify_managed_user_error: "不允许修改系统管理的用户。",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "抹除用户信息",
|
erase: "抹除用户信息",
|
||||||
|
erase_avatar: "抹掉头像",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ 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));
|
||||||
|
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 () => {
|
||||||
|
await act(async () => {
|
||||||
render(
|
render(
|
||||||
<AdminContext i18nProvider={i18nProvider}>
|
<AdminContext i18nProvider={i18nProvider}>
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
</AdminContext>
|
</AdminContext>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
||||||
screen.getByRole("combobox", { name: "" });
|
screen.getByRole("combobox", { name: "" });
|
||||||
|
|||||||
@@ -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,13 +70,12 @@ 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();
|
||||||
@@ -86,11 +85,10 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
|||||||
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 (
|
||||||
|
|||||||
@@ -55,10 +55,13 @@ import {
|
|||||||
ToolbarClasses,
|
ToolbarClasses,
|
||||||
Identifier,
|
Identifier,
|
||||||
RaRecord,
|
RaRecord,
|
||||||
|
ImageInput,
|
||||||
|
ImageField,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
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";
|
||||||
@@ -101,26 +104,29 @@ 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();
|
||||||
|
|
||||||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
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();
|
||||||
|
} else if (asManagedUserIsSelected) {
|
||||||
|
notify(<Alert severity="error">{translate("resources.users.helper.modify_managed_user_error")}</Alert>);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div onClickCapture={handleDeleteClick}>
|
return <div onClickCapture={handleDeleteClick}>{props.children}</div>;
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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));
|
||||||
}, [ selectedIds ]);
|
setAsManagedUserIsSelected(selectedIds.some(id => isASManaged(id)));
|
||||||
|
}, [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"
|
||||||
@@ -141,6 +148,7 @@ const UserBulkActionButtons = () => {
|
|||||||
/>
|
/>
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
||||||
@@ -183,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", {
|
||||||
@@ -204,9 +214,12 @@ const UserEditActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = (props: CreateProps) => (
|
||||||
<Create { ...props} redirect={(resource, id, data) => {
|
<Create
|
||||||
|
{...props}
|
||||||
|
redirect={(resource, id, data) => {
|
||||||
return `users/${id}`;
|
return `users/${id}`;
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" validate={maxLength(256)} />
|
<TextInput source="displayname" validate={maxLength(256)} />
|
||||||
@@ -232,12 +245,16 @@ export const UserCreate = (props: CreateProps) => (
|
|||||||
const UserTitle = () => {
|
const UserTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -246,48 +263,77 @@ 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 (
|
||||||
|
<>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserBooleanInput = (props) => {
|
const UserBooleanInput = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
const isOwnUser = false;
|
|
||||||
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 <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
|
return (
|
||||||
}
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
|
<BooleanInput {...props} disabled={ownUserIsSelected || asManagedUserIsSelected} />
|
||||||
|
</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", float: "right" }} />
|
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
||||||
<TextInput source="id" disabled />
|
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||||
|
<ImageInput
|
||||||
|
source="avatar_file"
|
||||||
|
label="resources.users.fields.avatar"
|
||||||
|
accept={{ "image/*": [".png", ".jpg"] }}
|
||||||
|
>
|
||||||
|
<ImageField source="src" title="Avatar" />
|
||||||
|
</ImageInput>
|
||||||
|
<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} />
|
||||||
@@ -314,7 +360,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="">
|
||||||
<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} />
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ describe("authProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 401", async () => {
|
it("should reject if error.status is 401", async () => {
|
||||||
await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
|
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject if error.status is 403", async () => {
|
it("should reject if error.status is 403", async () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe("dataProvider", () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
name: "user_id1",
|
name: "@user_id1:provider",
|
||||||
password_hash: "password_hash1",
|
password_hash: "password_hash1",
|
||||||
is_guest: 0,
|
is_guest: 0,
|
||||||
admin: 0,
|
admin: 0,
|
||||||
@@ -27,7 +27,7 @@ describe("dataProvider", () => {
|
|||||||
displayname: "User One",
|
displayname: "User One",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user_id2",
|
name: "@user_id2:provider",
|
||||||
password_hash: "password_hash2",
|
password_hash: "password_hash2",
|
||||||
is_guest: 0,
|
is_guest: 0,
|
||||||
admin: 1,
|
admin: 1,
|
||||||
@@ -47,7 +47,7 @@ describe("dataProvider", () => {
|
|||||||
filter: { author_id: 12 },
|
filter: { author_id: 12 },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(users.data[0].id).toEqual("user_id1");
|
expect(users.data[0].id).toEqual("@user_id1:provider");
|
||||||
expect(users.total).toEqual(200);
|
expect(users.total).toEqual(200);
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ describe("dataProvider", () => {
|
|||||||
it("fetches one user", async () => {
|
it("fetches one user", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetchMock.mockResponseOnce(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
name: "user_id1",
|
name: "@user_id1:provider",
|
||||||
password: "user_password",
|
password: "user_password",
|
||||||
displayname: "User",
|
displayname: "User",
|
||||||
threepids: [
|
threepids: [
|
||||||
@@ -74,9 +74,9 @@ describe("dataProvider", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await dataProvider.getOne("users", { id: "user_id1" });
|
const user = await dataProvider.getOne("users", { id: "@user_id1:provider" });
|
||||||
|
|
||||||
expect(user.data.id).toEqual("user_id1");
|
expect(user.data.id).toEqual("@user_id1:provider");
|
||||||
expect(user.data.displayname).toEqual("User");
|
expect(user.data.displayname).toEqual("User");
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
import { stringify } from "query-string";
|
import {
|
||||||
|
DataProvider,
|
||||||
import { DataProvider, DeleteParams, HttpError, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
DeleteParams,
|
||||||
|
HttpError,
|
||||||
|
Identifier,
|
||||||
|
Options,
|
||||||
|
PaginationPayload,
|
||||||
|
RaRecord,
|
||||||
|
SortPayload,
|
||||||
|
UpdateParams,
|
||||||
|
fetchUtils,
|
||||||
|
withLifecycleCallbacks,
|
||||||
|
} from "react-admin";
|
||||||
|
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
import { returnMXID } from "./synapse";
|
||||||
import { MatrixError, displayError } from "../components/error";
|
import { MatrixError, displayError } from "../components/error";
|
||||||
|
|
||||||
// Adds the access token to all requests
|
// Adds the access token to all requests
|
||||||
const jsonClient = async (url: string, options: Options = {}) => {
|
const jsonClient = async (url: string, options: Options = {}) => {
|
||||||
const token = storage.getItem("access_token");
|
const token = storage.getItem("access_token");
|
||||||
console.log("httpClient " + url);
|
console.log("httpClient " + url);
|
||||||
if (token != null) {
|
if (token !== null) {
|
||||||
options.user = {
|
options.user = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
token: `Bearer ${token}`,
|
token: `Bearer ${token}`,
|
||||||
@@ -22,7 +33,9 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
|||||||
const error = err as HttpError;
|
const error = err as HttpError;
|
||||||
const errorStatus = error.status;
|
const errorStatus = error.status;
|
||||||
const errorBody = error.body as MatrixError;
|
const errorBody = error.body as MatrixError;
|
||||||
const errMsg = !!errorBody?.errcode ? displayError(errorBody.errcode, errorStatus, errorBody.error) : displayError("M_INVALID", errorStatus, error.message);
|
const errMsg = !!errorBody?.errcode
|
||||||
|
? displayError(errorBody.errcode, errorStatus, errorBody.error)
|
||||||
|
: displayError("M_INVALID", errorStatus, error.message);
|
||||||
|
|
||||||
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
return Promise.reject(new HttpError(errMsg, errorStatus, errorBody));
|
||||||
}
|
}
|
||||||
@@ -39,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;
|
||||||
@@ -225,8 +242,19 @@ export interface DeleteMediaResult {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadMediaParams {
|
||||||
|
file: File;
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadMediaResult {
|
||||||
|
content_uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@@ -234,7 +262,7 @@ const resourceMap = {
|
|||||||
path: "/_synapse/admin/v2/users",
|
path: "/_synapse/admin/v2/users",
|
||||||
map: (u: User) => ({
|
map: (u: User) => ({
|
||||||
...u,
|
...u,
|
||||||
id: u.name,
|
id: returnMXID(u.name),
|
||||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
||||||
is_guest: !!u.is_guest,
|
is_guest: !!u.is_guest,
|
||||||
admin: !!u.admin,
|
admin: !!u.admin,
|
||||||
@@ -245,12 +273,12 @@ const resourceMap = {
|
|||||||
data: "users",
|
data: "users",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
create: (data: RaRecord) => ({
|
create: (data: RaRecord) => ({
|
||||||
endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${storage.getItem("home_server")}`,
|
endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(returnMXID(data.id))}`,
|
||||||
body: data,
|
body: data,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
}),
|
}),
|
||||||
delete: (params: DeleteParams) => ({
|
delete: (params: DeleteParams) => ({
|
||||||
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
|
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(returnMXID(params.id))}`,
|
||||||
body: { erase: true },
|
body: { erase: true },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
@@ -349,7 +377,7 @@ const resourceMap = {
|
|||||||
id: um.media_id,
|
id: um.media_id,
|
||||||
}),
|
}),
|
||||||
reference: (id: Identifier) => ({
|
reference: (id: Identifier) => ({
|
||||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`,
|
||||||
}),
|
}),
|
||||||
data: "media",
|
data: "media",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
@@ -384,7 +412,7 @@ const resourceMap = {
|
|||||||
create: (data: RaServerNotice) => ({
|
create: (data: RaServerNotice) => ({
|
||||||
endpoint: "/_synapse/admin/v1/send_server_notice",
|
endpoint: "/_synapse/admin/v1/send_server_notice",
|
||||||
body: {
|
body: {
|
||||||
user_id: data.id,
|
user_id: returnMXID(data.id),
|
||||||
content: {
|
content: {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: data.body,
|
body: data.body,
|
||||||
@@ -397,7 +425,7 @@ const resourceMap = {
|
|||||||
path: "/_synapse/admin/v1/statistics/users/media",
|
path: "/_synapse/admin/v1/statistics/users/media",
|
||||||
map: (usms: UserMediaStatistic) => ({
|
map: (usms: UserMediaStatistic) => ({
|
||||||
...usms,
|
...usms,
|
||||||
id: usms.user_id,
|
id: returnMXID(usms.user_id),
|
||||||
}),
|
}),
|
||||||
data: "users",
|
data: "users",
|
||||||
total: json => json.total,
|
total: json => json.total,
|
||||||
@@ -499,12 +527,12 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProvider: SynapseDataProvider = {
|
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,
|
||||||
@@ -526,7 +554,7 @@ const dataProvider: 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 {
|
||||||
@@ -580,7 +608,7 @@ const dataProvider: 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 {
|
||||||
@@ -741,6 +769,46 @@ const dataProvider: SynapseDataProvider = {
|
|||||||
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||||
return json as DeleteMediaResult;
|
return json as DeleteMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
||||||
|
const base_url = storage.getItem("base_url");
|
||||||
|
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
||||||
|
|
||||||
|
const { json } = await jsonClient(`${uploadMediaURL}?filename=${filename}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: file,
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": content_type,
|
||||||
|
}) as Headers,
|
||||||
|
});
|
||||||
|
return json as UploadMediaResult;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dataProvider = withLifecycleCallbacks(baseDataProvider, [
|
||||||
|
{
|
||||||
|
resource: "users",
|
||||||
|
beforeUpdate: async (params: UpdateParams<any>, dataProvider: DataProvider) => {
|
||||||
|
const avatarFile = params.data.avatar_file?.rawFile;
|
||||||
|
const avatarErase = params.data.avatar_erase;
|
||||||
|
|
||||||
|
if (avatarErase) {
|
||||||
|
params.data.avatar_url = "";
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarFile instanceof File) {
|
||||||
|
const reponse = await dataProvider.uploadMedia({
|
||||||
|
file: avatarFile,
|
||||||
|
filename: params.data.avatar_file.title,
|
||||||
|
content_type: params.data.avatar_file.rawFile.type,
|
||||||
|
});
|
||||||
|
params.data.avatar_url = reponse.content_uri;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export default dataProvider;
|
export default dataProvider;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fetchUtils } from "react-admin";
|
import { Identifier, fetchUtils } from "react-admin";
|
||||||
|
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
|
||||||
@@ -72,6 +72,26 @@ export function generateRandomMxId(): string {
|
|||||||
return `@${localpart}:${homeserver}`;
|
return `@${localpart}:${homeserver}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the full MXID from an arbitrary input
|
||||||
|
* @param input the input string
|
||||||
|
* @returns full MXID as string
|
||||||
|
*/
|
||||||
|
export function returnMXID(input: string | Identifier): string {
|
||||||
|
const homeserver = storage.getItem("home_server");
|
||||||
|
|
||||||
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
|
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
||||||
|
if (typeof input === 'string' && mxidPattern.test(input)) {
|
||||||
|
return input; // Already a valid MXID
|
||||||
|
}
|
||||||
|
|
||||||
|
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
||||||
|
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
|
||||||
|
return `@${localpart}:${homeserver}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random user password
|
* Generate a random user password
|
||||||
* @returns a new random password as string
|
* @returns a new random password as string
|
||||||
|
|||||||
191
testdata/synapse/homeserver.yaml
vendored
Normal file
191
testdata/synapse/homeserver.yaml
vendored
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
account_threepid_delegates:
|
||||||
|
msisdn: ''
|
||||||
|
alias_creation_rules:
|
||||||
|
- action: allow
|
||||||
|
alias: '*'
|
||||||
|
room_id: '*'
|
||||||
|
user_id: '*'
|
||||||
|
allow_guest_access: false
|
||||||
|
allow_public_rooms_over_federation: true
|
||||||
|
allow_public_rooms_without_auth: true
|
||||||
|
app_service_config_files: []
|
||||||
|
autocreate_auto_join_rooms: true
|
||||||
|
background_updates: null
|
||||||
|
caches:
|
||||||
|
global_factor: 0.5
|
||||||
|
per_cache_factors: null
|
||||||
|
cas_config: null
|
||||||
|
database:
|
||||||
|
args:
|
||||||
|
cp_max: 10
|
||||||
|
cp_min: 5
|
||||||
|
database: synapse
|
||||||
|
host: postgres
|
||||||
|
password: synapse
|
||||||
|
port: 5432
|
||||||
|
user: synapse
|
||||||
|
name: psycopg2
|
||||||
|
txn_limit: 0
|
||||||
|
default_room_version: '10'
|
||||||
|
disable_msisdn_registration: true
|
||||||
|
email:
|
||||||
|
enable_media_repo: true
|
||||||
|
enable_metrics: false
|
||||||
|
enable_registration: false
|
||||||
|
enable_registration_captcha: false
|
||||||
|
enable_registration_without_verification: false
|
||||||
|
enable_room_list_search: true
|
||||||
|
encryption_enabled_by_default_for_room_type: 'off'
|
||||||
|
event_cache_size: 100K
|
||||||
|
federation_rr_transactions_per_room_per_second: 50
|
||||||
|
form_secret: sLKKoFMsQUZgLAW0vU1PQQ8ca1POGMDheurGtKW0uJ20iGqtxR9O7JQ6Knvs44Wi
|
||||||
|
include_profile_data_on_invite: true
|
||||||
|
instance_map: {}
|
||||||
|
limit_profile_requests_to_users_who_share_rooms: false
|
||||||
|
limit_remote_rooms: null
|
||||||
|
listeners:
|
||||||
|
- bind_addresses:
|
||||||
|
- '::'
|
||||||
|
port: 8008
|
||||||
|
resources:
|
||||||
|
- compress: false
|
||||||
|
names:
|
||||||
|
- client
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
log_config: /config/synapse.log.config
|
||||||
|
macaroon_secret_key: Lg8DxGGfy95J367eVJZHLxmqP9XtN4FKdKxWpPvBS3mhviq9at8sw7KHRPkGmyqE
|
||||||
|
manhole_settings: null
|
||||||
|
max_spider_size: 10M
|
||||||
|
max_upload_size: 1024M
|
||||||
|
media_retention:
|
||||||
|
local_media_lifetime: 30d
|
||||||
|
remote_media_lifetime: 7d
|
||||||
|
media_storage_providers: []
|
||||||
|
media_store_path: /media-store
|
||||||
|
metrics_flags: null
|
||||||
|
modules: []
|
||||||
|
oembed: null
|
||||||
|
oidc_providers: null
|
||||||
|
old_signing_keys: null
|
||||||
|
opentracing: null
|
||||||
|
password_config:
|
||||||
|
enabled: true
|
||||||
|
localdb_enabled: true
|
||||||
|
pepper: zfvnYqxe3GTkdJ9BlfZiAqy2zMsjOg02uBTEiWLp2hjQGqlDw33pTSTplE6HoWlF
|
||||||
|
policy: null
|
||||||
|
pid_file: /homeserver.pid
|
||||||
|
presence:
|
||||||
|
enabled: true
|
||||||
|
public_baseurl: http://synapse:8008/
|
||||||
|
push:
|
||||||
|
include_content: true
|
||||||
|
rc_admin_redaction:
|
||||||
|
burst_count: 50
|
||||||
|
per_second: 1
|
||||||
|
rc_federation:
|
||||||
|
concurrent: 3
|
||||||
|
reject_limit: 50
|
||||||
|
sleep_delay: 500
|
||||||
|
sleep_limit: 10
|
||||||
|
window_size: 1000
|
||||||
|
rc_invites:
|
||||||
|
per_issuer:
|
||||||
|
burst_count: 10
|
||||||
|
per_second: 0.3
|
||||||
|
per_room:
|
||||||
|
burst_count: 10
|
||||||
|
per_second: 0.3
|
||||||
|
per_user:
|
||||||
|
burst_count: 5
|
||||||
|
per_second: 0.003
|
||||||
|
rc_joins:
|
||||||
|
local:
|
||||||
|
burst_count: 10
|
||||||
|
per_second: 0.1
|
||||||
|
remote:
|
||||||
|
burst_count: 10
|
||||||
|
per_second: 0.01
|
||||||
|
rc_login:
|
||||||
|
account:
|
||||||
|
burst_count: 3
|
||||||
|
per_second: 0.17
|
||||||
|
address:
|
||||||
|
burst_count: 3
|
||||||
|
per_second: 0.17
|
||||||
|
failed_attempts:
|
||||||
|
burst_count: 3
|
||||||
|
per_second: 0.17
|
||||||
|
rc_message:
|
||||||
|
burst_count: 10
|
||||||
|
per_second: 0.2
|
||||||
|
rc_registration:
|
||||||
|
burst_count: 3
|
||||||
|
per_second: 0.17
|
||||||
|
recaptcha_private_key: ''
|
||||||
|
recaptcha_public_key: ''
|
||||||
|
redaction_retention_period: 5m
|
||||||
|
redis:
|
||||||
|
enabled: false
|
||||||
|
host: null
|
||||||
|
password: null
|
||||||
|
port: 6379
|
||||||
|
registration_requires_token: false
|
||||||
|
registration_shared_secret: jBUKJozByo8s3bvKtYFpB350ZAnxGlzXsDpAZkgOFJuQfKAFHhqbc2dw8D54u4T9
|
||||||
|
report_stats: false
|
||||||
|
require_auth_for_profile_requests: false
|
||||||
|
retention:
|
||||||
|
enabled: true
|
||||||
|
purge_jobs:
|
||||||
|
- interval: 12h
|
||||||
|
room_list_publication_rules:
|
||||||
|
- action: allow
|
||||||
|
alias: '*'
|
||||||
|
room_id: '*'
|
||||||
|
user_id: '*'
|
||||||
|
room_prejoin_state: null
|
||||||
|
saml2_config:
|
||||||
|
sp_config: null
|
||||||
|
user_mapping_provider:
|
||||||
|
config: null
|
||||||
|
server_name: synapse
|
||||||
|
signing_key_path: /config/synapse.signing.key
|
||||||
|
spam_checker: []
|
||||||
|
sso: null
|
||||||
|
stats: null
|
||||||
|
stream_writers: {}
|
||||||
|
templates: null
|
||||||
|
tls_certificate_path: null
|
||||||
|
tls_private_key_path: null
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: matrix.org
|
||||||
|
turn_allow_guests: false
|
||||||
|
ui_auth: null
|
||||||
|
url_preview_accept_language:
|
||||||
|
- en-US
|
||||||
|
- en
|
||||||
|
url_preview_enabled: true
|
||||||
|
url_preview_ip_range_blacklist:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
|
- 100.64.0.0/10
|
||||||
|
- 192.0.0.0/24
|
||||||
|
- 169.254.0.0/16
|
||||||
|
- 192.88.99.0/24
|
||||||
|
- 198.18.0.0/15
|
||||||
|
- 192.0.2.0/24
|
||||||
|
- 198.51.100.0/24
|
||||||
|
- 203.0.113.0/24
|
||||||
|
- 224.0.0.0/4
|
||||||
|
- ::1/128
|
||||||
|
- fe80::/10
|
||||||
|
- fc00::/7
|
||||||
|
- 2001:db8::/32
|
||||||
|
- ff00::/8
|
||||||
|
- fec0::/10
|
||||||
|
user_directory: null
|
||||||
|
user_ips_max_age: 5m
|
||||||
|
|
||||||
28
testdata/synapse/synapse.log.config
vendored
Normal file
28
testdata/synapse/synapse.log.config
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
version: 1
|
||||||
|
formatters:
|
||||||
|
precise:
|
||||||
|
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||||
|
filters:
|
||||||
|
context:
|
||||||
|
(): synapse.util.logcontext.LoggingContextFilter
|
||||||
|
request: ""
|
||||||
|
handlers:
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: precise
|
||||||
|
filters: [context]
|
||||||
|
loggers:
|
||||||
|
synapse:
|
||||||
|
level: INFO
|
||||||
|
shared_secret_authenticator:
|
||||||
|
level: INFO
|
||||||
|
rest_auth_provider:
|
||||||
|
level: INFO
|
||||||
|
synapse.storage.SQL:
|
||||||
|
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||||
|
# information such as access tokens.
|
||||||
|
level: INFO
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [console]
|
||||||
|
|
||||||
1
testdata/synapse/synapse.signing.key
vendored
Normal file
1
testdata/synapse/synapse.signing.key
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ed25519 a_FswB rsh+VxdR4YUv6rFM6393VmSEJJxzaDrdwlVwLe2rcRo
|
||||||
@@ -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