Compare commits
6 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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.*
|
||||
|
||||
# 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",
|
||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
32
README.md
32
README.md
@@ -1,26 +1,19 @@
|
||||
[](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
|
||||
# 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/).
|
||||
|
||||
## 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
|
||||
|
||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
||||
|
||||
### 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:
|
||||
|
||||
* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
|
||||
@@ -36,9 +29,24 @@ The following changes are already implemented:
|
||||
* [Put the version into manifest.json](https://github.com/Awesome-Technologies/synapse-admin/issues/507) (CI only)
|
||||
* [Federation page improvements](https://github.com/Awesome-Technologies/synapse-admin/pull/583) (using theme colors)
|
||||
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
|
||||
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
|
||||
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
|
||||
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
|
||||
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
|
||||
|
||||
_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
|
||||
|
||||
### Supported Synapse
|
||||
|
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
|
||||
@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: build
|
||||
run-prod: build
|
||||
@python -m http.server -d dist 1313
|
||||
|
||||
# install the project
|
||||
|
34
package.json
34
package.json
@@ -10,21 +10,20 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.7.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@@ -37,11 +36,11 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"prettier": "^3.3.3",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.2.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.3.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-version-mark": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -49,27 +48,26 @@
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^5.16.4",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@mui/icons-material": "^6.1.1",
|
||||
"@mui/material": "^6.1.1",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"query-string": "^7.1.3",
|
||||
"ra-core": "^4.16.20",
|
||||
"ra-i18n-polyglot": "^4.16.20",
|
||||
"ra-language-english": "^4.16.20",
|
||||
"ra-language-farsi": "^4.2.0",
|
||||
"ra-language-french": "^4.16.20",
|
||||
"ra-core": "^5.2.0",
|
||||
"ra-i18n-polyglot": "^5.2.0",
|
||||
"ra-language-english": "^5.2.0",
|
||||
"ra-language-farsi": "^5.0.0",
|
||||
"ra-language-french": "^5.2.0",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^4.14.2",
|
||||
"react": "^18.3.1",
|
||||
"react-admin": "^4.16.20",
|
||||
"react-admin": "^5.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-is": "^18.3.1",
|
||||
"react-query": "^3.39.3",
|
||||
"react-router": "^6.25.1",
|
||||
"react-router-dom": "^6.25.1"
|
||||
"react-router": "^6.26.2",
|
||||
"react-router-dom": "^6.26.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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";
|
||||
|
||||
|
@@ -53,7 +53,6 @@ const App = () => (
|
||||
authProvider={authProvider}
|
||||
dataProvider={dataProvider}
|
||||
i18nProvider={i18nProvider}
|
||||
darkTheme={{ palette: { mode: "dark" } }}
|
||||
>
|
||||
<CustomRoutes>
|
||||
<Route path="/import_users" element={<ImportFeature />} />
|
||||
|
@@ -74,7 +74,6 @@ const DeleteRoomButton: React.FC<DeleteRoomButtonProps> = (props) => {
|
||||
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
|
||||
<SimpleForm toolbar={false}>
|
||||
<BooleanInput
|
||||
fullWidth
|
||||
source="block"
|
||||
value={block}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setBlock(event.target.checked)}
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
import { DataProvider, useTranslate } from "ra-core";
|
||||
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
|
||||
|
||||
import { generateRandomMxId, generateRandomPassword } from "../synapse/synapse";
|
||||
import { generateRandomMxId, generateRandomPassword, returnMXID } from "../synapse/synapse";
|
||||
|
||||
const LOGGING = true;
|
||||
|
||||
@@ -74,7 +74,7 @@ const FilePicker = () => {
|
||||
|
||||
const [conflictMode, setConflictMode] = useState("stop");
|
||||
const [passwordMode, setPasswordMode] = useState(true);
|
||||
const [useridMode, setUseridMode] = useState("ignore");
|
||||
const [useridMode, setUseridMode] = useState("update");
|
||||
|
||||
const translate = useTranslate();
|
||||
const notify = useNotify();
|
||||
@@ -121,7 +121,11 @@ const FilePicker = () => {
|
||||
|
||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||
/* 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) {
|
||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||
@@ -262,12 +266,15 @@ const FilePicker = () => {
|
||||
const userRecord = { ...entry };
|
||||
// No need to do a bunch of cryptographic random number getting if
|
||||
// we are using neither a generated password nor a generated user id.
|
||||
if (useridMode === "ignore" || userRecord.id === undefined) {
|
||||
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") {
|
||||
userRecord.id = generateRandomMxId();
|
||||
}
|
||||
if (passwordMode === false || entry.password === undefined) {
|
||||
if (passwordMode === false || entry.password === undefined || entry.password === "") {
|
||||
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, ... */
|
||||
|
||||
/* For these modes we will consider the ID that's in the record.
|
||||
|
@@ -20,7 +20,7 @@ import {
|
||||
useTranslate,
|
||||
useUnselectAll,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
@@ -43,7 +43,6 @@ const ServerNoticeDialog = ({ open, onClose, onSubmit }) => {
|
||||
<TextInput
|
||||
source="body"
|
||||
label="resources.servernotices.fields.body"
|
||||
fullWidth
|
||||
multiline
|
||||
rows="4"
|
||||
resettable
|
||||
@@ -64,6 +63,10 @@ export const ServerNoticeButton = () => {
|
||||
const handleDialogOpen = () => setOpen(true);
|
||||
const handleDialogClose = () => setOpen(false);
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSend = (values: Partial<RaRecord>) => {
|
||||
create(
|
||||
"servernotices",
|
||||
@@ -100,13 +103,12 @@ export const ServerNoticeBulkButton = () => {
|
||||
const unselectAllUsers = useUnselectAll("users");
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
const { mutate: sendNotices, isLoading } = useMutation(
|
||||
data =>
|
||||
const { mutate: sendNotices, isPending } = useMutation({
|
||||
mutationFn: (data) =>
|
||||
dataProvider.createMany("servernotices", {
|
||||
ids: selectedIds,
|
||||
data: data,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.servernotices.action.send_success");
|
||||
unselectAllUsers();
|
||||
@@ -116,12 +118,11 @@ export const ServerNoticeBulkButton = () => {
|
||||
notify("resources.servernotices.action.send_failure", {
|
||||
type: "error",
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isLoading}>
|
||||
<Button label="resources.servernotices.send" onClick={openDialog} disabled={isPending}>
|
||||
<MessageIcon />
|
||||
</Button>
|
||||
<ServerNoticeDialog open={open} onClose={closeDialog} onSubmit={sendNotices} />
|
||||
|
@@ -28,7 +28,7 @@ import {
|
||||
useRefresh,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { dateParser } from "./date";
|
||||
@@ -55,14 +55,12 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
<DialogContentText>{translate("delete_media.helper.send")}</DialogContentText>
|
||||
<SimpleForm toolbar={<DeleteMediaToolbar />} onSubmit={onSubmit}>
|
||||
<DateTimeInput
|
||||
fullWidth
|
||||
source="before_ts"
|
||||
label="delete_media.fields.before_ts"
|
||||
defaultValue={0}
|
||||
parse={dateParser}
|
||||
/>
|
||||
<NumberInput
|
||||
fullWidth
|
||||
source="size_gt"
|
||||
label="delete_media.fields.size_gt"
|
||||
defaultValue={0}
|
||||
@@ -70,7 +68,6 @@ const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
step={1024}
|
||||
/>
|
||||
<BooleanInput
|
||||
fullWidth
|
||||
source="keep_profiles"
|
||||
label="delete_media.fields.keep_profiles"
|
||||
defaultValue={true}
|
||||
@@ -86,9 +83,8 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const notify = useNotify();
|
||||
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||
const { mutate: deleteMedia, isLoading } = useMutation(
|
||||
(values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||
{
|
||||
const { mutate: deleteMedia, isPending } = useMutation({
|
||||
mutationFn: (values: DeleteMediaParams) => dataProvider.deleteMedia(values),
|
||||
onSuccess: () => {
|
||||
notify("delete_media.action.send_success");
|
||||
closeDialog();
|
||||
@@ -98,8 +94,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const openDialog = () => setOpen(true);
|
||||
const closeDialog = () => setOpen(false);
|
||||
@@ -110,7 +105,7 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
||||
{...props}
|
||||
label="delete_media.action.send"
|
||||
onClick={openDialog}
|
||||
disabled={isLoading}
|
||||
disabled={isPending}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
"&:hover": {
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const de: SynapseTranslationMessages = {
|
||||
...formalGermanMessages,
|
||||
ra: {
|
||||
...formalGermanMessages.ra,
|
||||
navigation: {
|
||||
...formalGermanMessages.ra.navigation,
|
||||
no_filtered_results: "Keine Ergebnisse",
|
||||
clear_filters: "Alle Filter entfernen",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Heimserver URL",
|
||||
@@ -147,6 +155,7 @@ const de: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Lösche Benutzerdaten",
|
||||
erase_avatar: "Avatar löschen"
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -146,6 +146,7 @@ const en: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Erase user data",
|
||||
erase_avatar: "Erase avatar"
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -144,6 +144,7 @@ const fr: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Effacer les données de l'utilisateur",
|
||||
erase_avatar: "Effacer l'avatar",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
1
src/i18n/index.d.ts
vendored
1
src/i18n/index.d.ts
vendored
@@ -142,6 +142,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
||||
};
|
||||
action: {
|
||||
erase: string;
|
||||
erase_avatar: string;
|
||||
};
|
||||
};
|
||||
rooms: {
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const ru: SynapseTranslationMessages = {
|
||||
...russianMessages,
|
||||
ra: {
|
||||
...russianMessages.ra,
|
||||
navigation: {
|
||||
...russianMessages.ra.navigation,
|
||||
no_filtered_results: "Нет результатов",
|
||||
clear_filters: "Все фильтры сбросить",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Адрес домашнего сервера",
|
||||
@@ -155,6 +163,7 @@ const ru: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Удалить данные пользователя",
|
||||
erase_avatar: "Удалить аватар",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -4,6 +4,14 @@ import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const zh: SynapseTranslationMessages = {
|
||||
...chineseMessages,
|
||||
ra: {
|
||||
...chineseMessages.ra,
|
||||
navigation: {
|
||||
...chineseMessages.ra.navigation,
|
||||
no_filtered_results: "没有结果",
|
||||
clear_filters: "清除所有过滤器",
|
||||
},
|
||||
},
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "服务器 URL",
|
||||
@@ -139,6 +147,7 @@ const zh: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "抹除用户信息",
|
||||
erase_avatar: "抹掉头像",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -8,14 +8,17 @@ import { AppContext } from "../AppContext";
|
||||
import englishMessages from "../i18n/en";
|
||||
|
||||
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
|
||||
import { act } from "@testing-library/react";
|
||||
|
||||
describe("LoginForm", () => {
|
||||
it("renders with no restriction to homeserver", () => {
|
||||
it("renders with no restriction to homeserver", async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
);
|
||||
});
|
||||
|
||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
||||
screen.getByRole("combobox", { name: "" });
|
||||
|
@@ -222,7 +222,6 @@ const LoginPage = () => {
|
||||
disabled={loading || !supportPassAuth}
|
||||
onBlur={handleUsernameChange}
|
||||
resettable
|
||||
fullWidth
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
@@ -234,7 +233,6 @@ const LoginPage = () => {
|
||||
autoComplete="current-password"
|
||||
disabled={loading || !supportPassAuth}
|
||||
resettable
|
||||
fullWidth
|
||||
validate={required()}
|
||||
/>
|
||||
</Box>
|
||||
@@ -247,7 +245,6 @@ const LoginPage = () => {
|
||||
disabled={loading}
|
||||
readOnly={allowSingleBaseUrl}
|
||||
resettable={allowAnyBaseUrl}
|
||||
fullWidth
|
||||
validate={[required(), validateBaseUrl]}
|
||||
>
|
||||
{allowMultipleBaseUrls &&
|
||||
@@ -280,9 +277,9 @@ const LoginPage = () => {
|
||||
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
||||
<Box className="form">
|
||||
<Select
|
||||
fullWidth
|
||||
value={locale}
|
||||
onChange={e => setLocale(e.target.value)}
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
className="select"
|
||||
>
|
||||
|
@@ -25,7 +25,7 @@ import {
|
||||
useRefresh,
|
||||
useUnselectAll,
|
||||
} from "react-admin";
|
||||
import { useMutation } from "react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import AvatarField from "../components/AvatarField";
|
||||
|
||||
@@ -70,13 +70,12 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||
const refresh = useRefresh();
|
||||
const unselectAllRooms = useUnselectAll("rooms");
|
||||
const dataProvider = useDataProvider();
|
||||
const { mutate, isLoading } = useMutation(
|
||||
() =>
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: () =>
|
||||
dataProvider.createMany("room_directory", {
|
||||
ids: selectedIds,
|
||||
data: {},
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify("resources.room_directory.action.send_success");
|
||||
unselectAllRooms();
|
||||
@@ -86,11 +85,10 @@ export const RoomDirectoryBulkPublishButton = (props: ButtonProps) => {
|
||||
notify("resources.room_directory.action.send_failure", {
|
||||
type: "error",
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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 />
|
||||
</Button>
|
||||
);
|
||||
@@ -102,6 +100,10 @@ export const RoomDirectoryPublishButton = (props: ButtonProps) => {
|
||||
const refresh = useRefresh();
|
||||
const [create, { isLoading }] = useCreate();
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
create(
|
||||
"room_directory",
|
||||
|
@@ -67,6 +67,9 @@ const RoomTitle = () => {
|
||||
|
||||
const RoomShowActions = () => {
|
||||
const record = useRecordContext();
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
const publishButton = record?.public ? <RoomDirectoryUnpublishButton /> : <RoomDirectoryPublishButton />;
|
||||
// FIXME: refresh after (un)publish
|
||||
return (
|
||||
|
@@ -55,6 +55,8 @@ import {
|
||||
ToolbarClasses,
|
||||
Identifier,
|
||||
RaRecord,
|
||||
ImageInput,
|
||||
ImageField,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
@@ -101,26 +103,24 @@ const userFilters = [
|
||||
<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 }> = props => {
|
||||
const ownUserIsSelected = props.ownUserIsSelected;
|
||||
const notify = useNotify();
|
||||
const translate = useTranslate();
|
||||
|
||||
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
return <div onClickCapture={handleDeleteClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
return <div onClickCapture={handleDeleteClick}>{props.children}</div>;
|
||||
};
|
||||
|
||||
const UserBulkActionButtons = () => {
|
||||
const record = useListContext();
|
||||
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
|
||||
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
||||
const selectedIds = record.selectedIds;
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const notify = useNotify();
|
||||
@@ -128,10 +128,10 @@ const UserBulkActionButtons = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||
}, [ selectedIds ]);
|
||||
}, [selectedIds]);
|
||||
|
||||
|
||||
return <>
|
||||
return (
|
||||
<>
|
||||
<ServerNoticeBulkButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BulkDeleteButton
|
||||
@@ -141,6 +141,7 @@ const UserBulkActionButtons = () => {
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
||||
@@ -204,9 +205,12 @@ const UserEditActions = () => {
|
||||
};
|
||||
|
||||
export const UserCreate = (props: CreateProps) => (
|
||||
<Create { ...props} redirect={(resource, id, data) => {
|
||||
<Create
|
||||
{...props}
|
||||
redirect={(resource, id, data) => {
|
||||
return `users/${id}`;
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<SimpleForm>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
<TextInput source="displayname" validate={maxLength(256)} />
|
||||
@@ -237,7 +241,7 @@ const UserTitle = () => {
|
||||
{translate("resources.users.name", {
|
||||
smart_count: 1,
|
||||
})}{" "}
|
||||
{record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
||||
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -250,7 +254,8 @@ const UserEditToolbar = () => {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return <>
|
||||
return (
|
||||
<>
|
||||
<div className={ToolbarClasses.defaultToolbar}>
|
||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||
<SaveButton />
|
||||
@@ -260,29 +265,41 @@ const UserEditToolbar = () => {
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserBooleanInput = (props) => {
|
||||
const UserBooleanInput = props => {
|
||||
const record = useRecordContext();
|
||||
const ownUserId = localStorage.getItem("user_id");
|
||||
const isOwnUser = false;
|
||||
let ownUserIsSelected = false;
|
||||
if (record && (record.id === ownUserId)) {
|
||||
if (record && record.id === ownUserId) {
|
||||
ownUserIsSelected = true;
|
||||
}
|
||||
|
||||
return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
|
||||
}
|
||||
return (
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BooleanInput {...props} disabled={ownUserIsSelected} />
|
||||
</UserPreventSelfDelete>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserEdit = (props: EditProps) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />} mutationMode="pessimistic">
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
||||
<TextInput source="id" disabled />
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
|
||||
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
|
||||
<ImageInput
|
||||
source="avatar_file"
|
||||
label="resources.users.fields.avatar"
|
||||
accept={{ "image/*": [".png", ".jpg"] }}
|
||||
>
|
||||
<ImageField source="src" title="Avatar" />
|
||||
</ImageInput>
|
||||
<TextInput source="id" readOnly />
|
||||
<TextInput source="displayname" />
|
||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||
|
@@ -101,7 +101,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
@@ -18,7 +18,7 @@ describe("dataProvider", () => {
|
||||
JSON.stringify({
|
||||
users: [
|
||||
{
|
||||
name: "user_id1",
|
||||
name: "@user_id1:provider",
|
||||
password_hash: "password_hash1",
|
||||
is_guest: 0,
|
||||
admin: 0,
|
||||
@@ -27,7 +27,7 @@ describe("dataProvider", () => {
|
||||
displayname: "User One",
|
||||
},
|
||||
{
|
||||
name: "user_id2",
|
||||
name: "@user_id2:provider",
|
||||
password_hash: "password_hash2",
|
||||
is_guest: 0,
|
||||
admin: 1,
|
||||
@@ -47,7 +47,7 @@ describe("dataProvider", () => {
|
||||
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(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -55,7 +55,7 @@ describe("dataProvider", () => {
|
||||
it("fetches one user", async () => {
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
name: "user_id1",
|
||||
name: "@user_id1:provider",
|
||||
password: "user_password",
|
||||
displayname: "User",
|
||||
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(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@@ -1,15 +1,26 @@
|
||||
import { stringify } from "query-string";
|
||||
|
||||
import { DataProvider, DeleteParams, HttpError, Identifier, Options, RaRecord, fetchUtils } from "react-admin";
|
||||
import {
|
||||
DataProvider,
|
||||
DeleteParams,
|
||||
HttpError,
|
||||
Identifier,
|
||||
Options,
|
||||
PaginationPayload,
|
||||
RaRecord,
|
||||
SortPayload,
|
||||
UpdateParams,
|
||||
fetchUtils,
|
||||
withLifecycleCallbacks,
|
||||
} from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
import { returnMXID } from "./synapse";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
|
||||
// Adds the access token to all requests
|
||||
const jsonClient = async (url: string, options: Options = {}) => {
|
||||
const token = storage.getItem("access_token");
|
||||
console.log("httpClient " + url);
|
||||
if (token != null) {
|
||||
if (token !== null) {
|
||||
options.user = {
|
||||
authenticated: true,
|
||||
token: `Bearer ${token}`,
|
||||
@@ -22,7 +33,9 @@ const jsonClient = async (url: string, options: Options = {}) => {
|
||||
const error = err as HttpError;
|
||||
const errorStatus = error.status;
|
||||
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));
|
||||
}
|
||||
@@ -39,6 +52,10 @@ const mxcUrlToHttp = (mxcUrl: string) => {
|
||||
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 {
|
||||
room_id: string;
|
||||
name?: string;
|
||||
@@ -225,8 +242,19 @@ export interface DeleteMediaResult {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UploadMediaParams {
|
||||
file: File;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
}
|
||||
|
||||
export interface UploadMediaResult {
|
||||
content_uri: string;
|
||||
}
|
||||
|
||||
export interface SynapseDataProvider extends DataProvider {
|
||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||
}
|
||||
|
||||
const resourceMap = {
|
||||
@@ -234,7 +262,7 @@ const resourceMap = {
|
||||
path: "/_synapse/admin/v2/users",
|
||||
map: (u: User) => ({
|
||||
...u,
|
||||
id: u.name,
|
||||
id: returnMXID(u.name),
|
||||
avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined,
|
||||
is_guest: !!u.is_guest,
|
||||
admin: !!u.admin,
|
||||
@@ -245,12 +273,12 @@ const resourceMap = {
|
||||
data: "users",
|
||||
total: json => json.total,
|
||||
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,
|
||||
method: "PUT",
|
||||
}),
|
||||
delete: (params: DeleteParams) => ({
|
||||
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`,
|
||||
endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(returnMXID(params.id))}`,
|
||||
body: { erase: true },
|
||||
method: "POST",
|
||||
}),
|
||||
@@ -349,7 +377,7 @@ const resourceMap = {
|
||||
id: um.media_id,
|
||||
}),
|
||||
reference: (id: Identifier) => ({
|
||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`,
|
||||
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`,
|
||||
}),
|
||||
data: "media",
|
||||
total: json => json.total,
|
||||
@@ -384,7 +412,7 @@ const resourceMap = {
|
||||
create: (data: RaServerNotice) => ({
|
||||
endpoint: "/_synapse/admin/v1/send_server_notice",
|
||||
body: {
|
||||
user_id: data.id,
|
||||
user_id: returnMXID(data.id),
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: data.body,
|
||||
@@ -397,7 +425,7 @@ const resourceMap = {
|
||||
path: "/_synapse/admin/v1/statistics/users/media",
|
||||
map: (usms: UserMediaStatistic) => ({
|
||||
...usms,
|
||||
id: usms.user_id,
|
||||
id: returnMXID(usms.user_id),
|
||||
}),
|
||||
data: "users",
|
||||
total: json => json.total,
|
||||
@@ -499,12 +527,12 @@ function getSearchOrder(order: "ASC" | "DESC") {
|
||||
}
|
||||
}
|
||||
|
||||
const dataProvider: SynapseDataProvider = {
|
||||
const baseDataProvider: SynapseDataProvider = {
|
||||
getList: async (resource, params) => {
|
||||
console.log("getList " + resource);
|
||||
const { user_id, name, guests, deactivated, locked, search_term, destination, valid } = params.filter;
|
||||
const { page, perPage } = params.pagination;
|
||||
const { field, order } = params.sort;
|
||||
const { page, perPage } = params.pagination as PaginationPayload;
|
||||
const { field, order } = params.sort as SortPayload;
|
||||
const from = (page - 1) * perPage;
|
||||
const query = {
|
||||
from: from,
|
||||
@@ -526,7 +554,7 @@ const dataProvider: SynapseDataProvider = {
|
||||
const res = resourceMap[resource];
|
||||
|
||||
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);
|
||||
return {
|
||||
@@ -580,7 +608,7 @@ const dataProvider: SynapseDataProvider = {
|
||||
const res = resourceMap[resource];
|
||||
|
||||
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);
|
||||
return {
|
||||
@@ -741,6 +769,46 @@ const dataProvider: SynapseDataProvider = {
|
||||
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||
return json as DeleteMediaResult;
|
||||
},
|
||||
|
||||
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
||||
const base_url = storage.getItem("base_url");
|
||||
const 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;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { fetchUtils } from "react-admin";
|
||||
import { Identifier, fetchUtils } from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
|
||||
@@ -72,6 +72,26 @@ export function generateRandomMxId(): string {
|
||||
return `@${localpart}:${homeserver}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full MXID from an arbitrary input
|
||||
* @param input the input string
|
||||
* @returns full MXID as string
|
||||
*/
|
||||
export function returnMXID(input: string | Identifier): string {
|
||||
const homeserver = storage.getItem("home_server");
|
||||
|
||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||
const mxidPattern = /^@[^@:]+:[^@:]+$/;
|
||||
if (typeof input === 'string' && mxidPattern.test(input)) {
|
||||
return input; // Already a valid MXID
|
||||
}
|
||||
|
||||
// If input is not a valid MXID, assume it's a localpart and construct the MXID
|
||||
const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
|
||||
return `@${localpart}:${homeserver}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a random user password
|
||||
* @returns a new random password as string
|
||||
|
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": true /* Enable all strict type-checking options. */,
|
||||
"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. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
|
Reference in New Issue
Block a user