Compare commits
5 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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
|
||||
|
31
README.md
31
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,23 @@ 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)
|
||||
|
||||
_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
|
||||
|
@@ -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.
|
||||
|
@@ -147,6 +147,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: {
|
||||
|
@@ -155,6 +155,7 @@ const ru: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "Удалить данные пользователя",
|
||||
erase_avatar: "Удалить аватар",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -139,6 +139,7 @@ const zh: SynapseTranslationMessages = {
|
||||
},
|
||||
action: {
|
||||
erase: "抹除用户信息",
|
||||
erase_avatar: "抹掉头像",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
|
@@ -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,19 +128,20 @@ const UserBulkActionButtons = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||
}, [ selectedIds ]);
|
||||
}, [selectedIds]);
|
||||
|
||||
|
||||
return <>
|
||||
<ServerNoticeBulkButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BulkDeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle="resources.users.helper.erase"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</UserPreventSelfDelete>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<ServerNoticeBulkButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<BulkDeleteButton
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle="resources.users.helper.erase"
|
||||
mutationMode="pessimistic"
|
||||
/>
|
||||
</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) => {
|
||||
return `users/${id}`;
|
||||
}}>
|
||||
<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,29 +254,34 @@ const UserEditToolbar = () => {
|
||||
ownUserIsSelected = record.id === ownUserId;
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={ToolbarClasses.defaultToolbar}>
|
||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||
return (
|
||||
<>
|
||||
<div className={ToolbarClasses.defaultToolbar}>
|
||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||
<SaveButton />
|
||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
||||
<DeleteButton />
|
||||
</UserPreventSelfDelete>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</>
|
||||
</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();
|
||||
@@ -281,7 +290,11 @@ export const UserEdit = (props: EditProps) => {
|
||||
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
|
||||
<TabbedForm toolbar={<UserEditToolbar />}>
|
||||
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
|
||||
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
|
||||
<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/*">
|
||||
<ImageField source="src" title="Avatar" />
|
||||
</ImageInput>
|
||||
<TextInput source="id" disabled />
|
||||
<TextInput source="displayname" />
|
||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||||
|
@@ -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,
|
||||
RaRecord,
|
||||
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));
|
||||
}
|
||||
@@ -225,8 +238,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 +258,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 +269,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 +373,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 +408,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 +421,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,7 +523,7 @@ 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;
|
||||
@@ -741,6 +765,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
|
@@ -3215,9 +3215,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:^2.4.3":
|
||||
version: 2.5.0
|
||||
resolution: "dompurify@npm:2.5.0"
|
||||
checksum: 10c0/637dcf3430f3fedf66b58f84fd59ea9b3615a19a6db5efe444c635b2473a77a345b31d7328b56dbc80f692791915ffd6049d69041ff013e33692fdb8b0d84e48
|
||||
version: 2.5.6
|
||||
resolution: "dompurify@npm:2.5.6"
|
||||
checksum: 10c0/ee7e7d17982b1017a20982a2d57a0463d7fbb67f7b92a13ecf772e5e6acf0a529a19e3e31d725b05d5a2524d40e0aeb7ebc4be0aff396a6345bd6f2749fe560d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user