Compare commits
49 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
28ef08de03 | ||
![]() |
865e53388e | ||
![]() |
3a105bb8c7 | ||
![]() |
edcda7a202 | ||
![]() |
edd69273e2 | ||
![]() |
444bfacbd9 | ||
![]() |
970e0a550f | ||
![]() |
b3ef68d66e | ||
![]() |
31382a42ee | ||
![]() |
1a7748d1ef | ||
![]() |
039b28cc5c | ||
![]() |
57eae3edb3 | ||
![]() |
dadc9416c0 | ||
![]() |
eab2342114 | ||
![]() |
9cf2f83936 | ||
![]() |
d823856873 | ||
![]() |
9b96c7cec8 | ||
![]() |
f211aba873 | ||
![]() |
c0fc2d8937 | ||
![]() |
a88b397748 | ||
![]() |
abc922c956 | ||
![]() |
4f2cd38344 | ||
![]() |
ca71038874 | ||
![]() |
be867b6b0d | ||
![]() |
f2f540b429 | ||
![]() |
7feec4ba07 | ||
![]() |
1d5fef1e53 | ||
![]() |
9c40efde17 | ||
![]() |
53dff66978 | ||
![]() |
3a595247e8 | ||
![]() |
33f5f60e31 | ||
![]() |
9dd2ea57c9 | ||
![]() |
fae7a696de | ||
![]() |
49e8b2d0f5 | ||
![]() |
281d908d3f | ||
![]() |
bacc42fe9c | ||
![]() |
1c26a28ca9 | ||
![]() |
d3a04cd132 | ||
![]() |
e6060a23ac | ||
![]() |
4b7fbf483a | ||
![]() |
bc3c30da92 | ||
![]() |
1896f770d1 | ||
![]() |
99d0b9ad72 | ||
![]() |
944afb9056 | ||
![]() |
23f5a24803 | ||
![]() |
60ae00ac14 | ||
![]() |
26862fa708 | ||
![]() |
853d14c1ce | ||
![]() |
11a5cac709 |
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
28
README.md
28
README.md
@@ -13,6 +13,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
|
||||
* [Configuration](#configuration)
|
||||
* [Restricting available homeserver](#restricting-available-homeserver)
|
||||
* [Protecting appservice managed users](#protecting-appservice-managed-users)
|
||||
* [Adding custom menu items](#adding-custom-menu-items)
|
||||
* [Providing support URL](#providing-support-url)
|
||||
* [Usage](#usage)
|
||||
* [Supported Synapse](#supported-synapse)
|
||||
@@ -66,6 +67,11 @@ The following changes are already implemented:
|
||||
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
||||
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
|
||||
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
|
||||
* [Fix footer causing vertical scrollbar](https://github.com/etkecc/synapse-admin/pull/60)
|
||||
* [Custom Menu Items](https://github.com/etkecc/synapse-admin/pull/79)
|
||||
* [Add user profile to the top menu](https://github.com/etkecc/synapse-admin/pull/80)
|
||||
* [Enable visual customization](https://github.com/etkecc/synapse-admin/pull/81)
|
||||
* [Fix room state events display](https://github.com/etkecc/synapse-admin/pull/100)
|
||||
|
||||
_the list will be updated as new changes are added_
|
||||
|
||||
@@ -128,9 +134,29 @@ Example for [mautrix-telegram](https://github.com/mautrix/telegram)
|
||||
}
|
||||
```
|
||||
|
||||
### Adding custom menu items
|
||||
|
||||
You can add custom menu items to the main menu by providing a `menu` array in the `config.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"menu": [
|
||||
{
|
||||
"label": "Contact support",
|
||||
"icon": "SupportAgent",
|
||||
"url": "https://github.com/etkecc/synapse-admin/issues"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Where `icon` is one of the [preloaded icons](./src/components/icons.ts)
|
||||
|
||||
### Providing support URL
|
||||
|
||||
Synapse-Admin provides a support link in the main menu - `Contact support`. By default, the link points to the GitHub issues page of the project. You can change this link by providing a `supportURL` in the `config.json`.
|
||||
**Deprecated**: use `menu` config option described above. Automatically migrated to the `menu` if the `supportURL` is present.
|
||||
|
||||
~~Synapse-Admin provides a support link in the main menu - `Contact support`. By default, the link points to the GitHub issues page of the project. You can change this link by providing a `supportURL` in the `config.json`.~~
|
||||
|
||||
```json
|
||||
{
|
||||
|
15
index.html
15
index.html
@@ -22,6 +22,11 @@
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.layout {
|
||||
min-height: 90vh !important;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -120,13 +125,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<footer
|
||||
style="position: relative; z-index: 2; height: 2em; margin-top: 0; line-height: 2em; background-color: #eee; border: 0.5px solid #ddd">
|
||||
<a id="copyright" href="https://github.com/etkecc/synapse-admin"
|
||||
style="margin-left: 1em; color: #888; font-family: Roboto, Helvetica, Arial, sans-serif; font-weight: 100; font-size: 0.8em; text-decoration: none;">
|
||||
Synapse-Admin <b><span id="version"></span></b> by Awesome Technologies Innovationslabor GmbH
|
||||
</a>
|
||||
</footer>
|
||||
<span id="js-version" style="display: none;"></span>
|
||||
</body>
|
||||
<script>document.getElementById("version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
|
||||
<script>document.getElementById("js-version").textContent = __SYNAPSE_ADMIN_VERSION__</script>
|
||||
</html>
|
||||
|
61
package.json
61
package.json
@@ -3,7 +3,7 @@
|
||||
"version": "0.10.3",
|
||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||
"type": "module",
|
||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||
"author": "etke.cc (originally by Awesome Technologies Innovationslabor GmbH)",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": ".",
|
||||
"repository": {
|
||||
@@ -11,25 +11,25 @@
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.7.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^22.7.9",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/react": "^18.3.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
||||
"@typescript-eslint/parser": "^8.11.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.1",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-yaml": "^1.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -38,41 +38,42 @@
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.16.1",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-version-mark": "^0.1.0"
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.11.0",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-version-mark": "^0.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^6.1.1",
|
||||
"@mui/material": "^6.1.1",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@mui/icons-material": "^6.1.5",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@mui/utils": "^5.16.6",
|
||||
"@tanstack/react-query": "^5.59.15",
|
||||
"history": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"ra-core": "^5.2.0",
|
||||
"ra-i18n-polyglot": "^5.2.0",
|
||||
"ra-language-english": "^5.2.0",
|
||||
"ra-core": "^5.3.0",
|
||||
"ra-i18n-polyglot": "^5.3.0",
|
||||
"ra-language-english": "^5.3.0",
|
||||
"ra-language-farsi": "^5.0.0",
|
||||
"ra-language-french": "^5.2.0",
|
||||
"ra-language-french": "^5.3.0",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^4.14.2",
|
||||
"react": "^18.3.1",
|
||||
"react-admin": "^5.2.0",
|
||||
"react-admin": "^5.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-is": "^18.3.1",
|
||||
"react-router": "^6.26.2",
|
||||
"react-router-dom": "^6.26.2"
|
||||
"react-router-dom": "^6.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite serve",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
|
||||
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint --ignore-path .gitignore --ignore-pattern testdata/ --ext .ts,.tsx,.yml,.yaml .",
|
||||
"fix": "yarn lint --fix",
|
||||
"test": "yarn jest",
|
||||
"test:watch": "yarn jest --watch"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
fetchMock.enableMocks();
|
||||
|
||||
@@ -9,4 +9,4 @@ describe("App", () => {
|
||||
render(<App />);
|
||||
await screen.findAllByText("Welcome to Synapse-admin");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -23,6 +23,7 @@ import users from "./resources/users";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import Footer from "./components/Footer";
|
||||
|
||||
// TODO: Can we use lazy loading together with browser locale?
|
||||
const messages = {
|
||||
@@ -81,6 +82,7 @@ const App = () => (
|
||||
<Resource name="room_state" />
|
||||
<Resource name="destination_rooms" />
|
||||
</Admin>
|
||||
<Footer />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
|
@@ -4,6 +4,13 @@ interface AppContextType {
|
||||
restrictBaseUrl: string | string[];
|
||||
asManagedUsers: string[];
|
||||
supportURL: string;
|
||||
menu: MenuItem[];
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
icon: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
@@ -1,17 +1,7 @@
|
||||
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
|
||||
import { AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||
import { LoginMethod } from "../pages/LoginPage";
|
||||
import { useState } from "react";
|
||||
|
||||
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
||||
const supportLink = (): string => {
|
||||
try {
|
||||
new URL(localStorage.getItem("support_url") || ""); // Check if the URL is valid
|
||||
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
||||
} catch (e) {
|
||||
return DEFAULT_SUPPORT_LINK;
|
||||
}
|
||||
};
|
||||
import { useEffect, useState, Suspense } from "react";
|
||||
import { Icons, DefaultIcon } from "./icons";
|
||||
|
||||
const AdminUserMenu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -54,17 +44,60 @@ const AdminUserMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />;
|
||||
const AdminAppBar = () => {
|
||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||
<TitlePortal />
|
||||
<InspectorButton />
|
||||
</AppBar>);
|
||||
};
|
||||
|
||||
const AdminMenu = () => (
|
||||
<Menu>
|
||||
<Menu.ResourceItems />
|
||||
<Menu.Item to={supportLink()} target="_blank" primaryText="Contact support" leftIcon={<LiveHelpIcon />} />
|
||||
</Menu>
|
||||
);
|
||||
const AdminMenu = (props) => {
|
||||
const [menu, setMenu] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const menuConfig = localStorage.getItem('menu');
|
||||
if (menuConfig) {
|
||||
try {
|
||||
setMenu(JSON.parse(menuConfig));
|
||||
} catch (e) {
|
||||
console.error('Error parsing menu configuration', e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
<Menu.ResourceItems />
|
||||
{menu.map((item, index) => {
|
||||
const { url, icon, label } = item;
|
||||
const IconComponent = Icons[icon] as React.ComponentType<any> | undefined;
|
||||
|
||||
return (
|
||||
<Suspense key={index}>
|
||||
<Menu.Item
|
||||
to={url}
|
||||
target="_blank"
|
||||
primaryText={label}
|
||||
leftIcon={IconComponent ? <IconComponent /> : <DefaultIcon />}
|
||||
onClick={props.onMenuClick}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdminLayout = ({ children }) => (
|
||||
<Layout appBar={AdminAppBar} menu={AdminMenu}>
|
||||
<Layout appBar={AdminAppBar} menu={AdminMenu} sx={{
|
||||
['& .RaLayout-appFrame']: {
|
||||
minHeight: '90vh',
|
||||
height: '90vh',
|
||||
},
|
||||
['& .RaLayout-content']: {
|
||||
marginBottom: '3rem',
|
||||
},
|
||||
}}>
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
|
40
src/components/Footer.tsx
Normal file
40
src/components/Footer.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Box, Link, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const Footer = () => {
|
||||
const [version, setVersion] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
const version = document.getElementById("js-version")?.textContent;
|
||||
if (version) {
|
||||
setVersion(version);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (<Box
|
||||
component="footer"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
zIndex: 100,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
bgcolor: "#eee",
|
||||
borderTop: '1px solid',
|
||||
borderColor: '#ddd',
|
||||
p: 1,
|
||||
}}>
|
||||
<Typography variant="body2">
|
||||
<Link sx={{ color: "#888", textDecoration: 'none' }} href="https://github.com/etkecc/synapse-admin" target="_blank">
|
||||
Synapse-Admin
|
||||
</Link> <Link href={`https://github.com/etkecc/synapse-admin/releases/tag/`+version} target="_blank">
|
||||
<span style={{ fontWeight: 'bold', color: "#000" }}>{version}</span>
|
||||
</Link> <Link sx={{ color: "#888", textDecoration: 'none' }} href="https://etke.cc/?utm_source=synapse-admin&utm_medium=footer&utm_campaign=synapse-admin" target="_blank">
|
||||
by etke.cc
|
||||
</Link> <Link sx={{ color: "#888", textDecoration: 'none' }} href="https://github.com/awesome-technologies/synapse-admin" target="_blank">
|
||||
(originally developed by Awesome Technologies Innovationslabor GmbH)
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
@@ -121,11 +121,7 @@ const FilePicker = () => {
|
||||
|
||||
const verifyCsv = ({ data, meta, errors }: ParseResult<ImportLine>, { setValues, setStats, setError }) => {
|
||||
/* First, verify the presence of required fields */
|
||||
const missingFields = expectedFields.filter(eF => {
|
||||
const result = meta.fields?.find(mF => eF === mF);
|
||||
if (result === undefined) { return eF; } // missing field
|
||||
return undefined; // field found
|
||||
});
|
||||
const missingFields = expectedFields.filter(eF => !meta.fields?.find(mF => eF === mF));
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
setError(translate("import_users.error.required_field", { field: missingFields[0] }));
|
||||
|
@@ -13,7 +13,7 @@ const LoginFormBox = styled(Box)(({ theme }) => ({
|
||||
backgroundSize: "cover",
|
||||
|
||||
[`& .card`]: {
|
||||
width: "30rem",
|
||||
maxWidth: "30rem",
|
||||
marginTop: "6rem",
|
||||
marginBottom: "6rem",
|
||||
},
|
||||
|
12
src/components/icons.ts
Normal file
12
src/components/icons.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { lazy } from "react";
|
||||
|
||||
export const Icons = {
|
||||
Announcement: lazy(() => import('@mui/icons-material/Announcement')),
|
||||
Engineering: lazy(() => import('@mui/icons-material/Engineering')),
|
||||
HelpCenter: lazy(() => import('@mui/icons-material/HelpCenter')),
|
||||
SupportAgent: lazy(() => import('@mui/icons-material/SupportAgent')),
|
||||
Default: lazy(() => import('@mui/icons-material/OpenInNew')),
|
||||
// Add more icons as needed
|
||||
};
|
||||
|
||||
export const DefaultIcon = Icons.Default;
|
@@ -2,7 +2,7 @@ import { formalGermanMessages } from "@haleos/ra-language-german";
|
||||
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const de: SynapseTranslationMessages = {
|
||||
const fixedGermanMessages = {
|
||||
...formalGermanMessages,
|
||||
ra: {
|
||||
...formalGermanMessages.ra,
|
||||
@@ -10,8 +10,30 @@ const de: SynapseTranslationMessages = {
|
||||
...formalGermanMessages.ra.navigation,
|
||||
no_filtered_results: "Keine Ergebnisse",
|
||||
clear_filters: "Alle Filter entfernen",
|
||||
add_filter: "Filter hinzufügen",
|
||||
},
|
||||
action: {
|
||||
...formalGermanMessages.ra.action,
|
||||
update_application: "Anwendung aktualisieren",
|
||||
},
|
||||
page: {
|
||||
...formalGermanMessages.ra.page,
|
||||
empty: "Leer",
|
||||
access_denied: "Zugriff verweigert",
|
||||
authentication_error: "Authentifizierungsfehler",
|
||||
},
|
||||
message: {
|
||||
...formalGermanMessages.ra.message,
|
||||
access_denied:
|
||||
"Sie haben nicht die richtigen Berechtigungen um auf diese Seite zuzugreifen.",
|
||||
authentication_error:
|
||||
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const de: SynapseTranslationMessages = {
|
||||
...fixedGermanMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Heimserver URL",
|
||||
|
@@ -2,7 +2,7 @@ import russianMessages from "ra-language-russian";
|
||||
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const ru: SynapseTranslationMessages = {
|
||||
const fixedRussianMessages = {
|
||||
...russianMessages,
|
||||
ra: {
|
||||
...russianMessages.ra,
|
||||
@@ -11,7 +11,24 @@ const ru: SynapseTranslationMessages = {
|
||||
no_filtered_results: "Нет результатов",
|
||||
clear_filters: "Все фильтры сбросить",
|
||||
},
|
||||
page: {
|
||||
...russianMessages.ra.page,
|
||||
empty: "Пусто",
|
||||
access_denied: "Доступ запрещен",
|
||||
authentication_error: "Ошибка аутентификации",
|
||||
},
|
||||
message: {
|
||||
...russianMessages.ra.message,
|
||||
access_denied:
|
||||
"У вас нет прав доступа к этой странице.",
|
||||
authentication_error:
|
||||
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const ru: SynapseTranslationMessages = {
|
||||
...fixedRussianMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "Адрес домашнего сервера",
|
||||
|
@@ -2,7 +2,7 @@ import chineseMessages from "@haxqer/ra-language-chinese";
|
||||
|
||||
import { SynapseTranslationMessages } from ".";
|
||||
|
||||
const zh: SynapseTranslationMessages = {
|
||||
const fixedChineseMessages = {
|
||||
...chineseMessages,
|
||||
ra: {
|
||||
...chineseMessages.ra,
|
||||
@@ -11,7 +11,27 @@ const zh: SynapseTranslationMessages = {
|
||||
no_filtered_results: "没有结果",
|
||||
clear_filters: "清除所有过滤器",
|
||||
},
|
||||
action: {
|
||||
...chineseMessages.ra.action,
|
||||
update_application: "Anwendung aktualisieren",
|
||||
},
|
||||
page: {
|
||||
...chineseMessages.ra.page,
|
||||
access_denied: "拒绝访问",
|
||||
authentication_error: "认证错误",
|
||||
},
|
||||
message: {
|
||||
...chineseMessages.ra.message,
|
||||
access_denied:
|
||||
"您没有访问此页面的权限。",
|
||||
authentication_error:
|
||||
"身份验证服务器返回错误,无法验证您的凭据。",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const zh: SynapseTranslationMessages = {
|
||||
...fixedChineseMessages,
|
||||
synapseadmin: {
|
||||
auth: {
|
||||
base_url: "服务器 URL",
|
||||
|
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
import { AppContext } from "./AppContext";
|
||||
import { AppContext, MenuItem } from "./AppContext";
|
||||
import storage from "./storage";
|
||||
|
||||
fetch("config.json")
|
||||
@@ -12,9 +12,24 @@ fetch("config.json")
|
||||
if (props.asManagedUsers) {
|
||||
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||
}
|
||||
if (props.supportURL) {
|
||||
storage.setItem("support_url", props.supportURL);
|
||||
|
||||
let menu: MenuItem[] = [];
|
||||
if (props.menu) {
|
||||
menu = props.menu;
|
||||
}
|
||||
if (props.supportURL) {
|
||||
const migratedSupportURL = {
|
||||
label: "Contact support",
|
||||
icon: "SupportAgent",
|
||||
url: props.supportURL,
|
||||
};
|
||||
console.warn("supportURL config option is deprecated. Please, use the menu option instead. Automatically migrated to the new menu option:", migratedSupportURL);
|
||||
menu.push(migratedSupportURL as MenuItem);
|
||||
}
|
||||
if (menu.length > 0) {
|
||||
storage.setItem("menu", JSON.stringify(menu));
|
||||
}
|
||||
|
||||
return createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<AppContext.Provider value={props}>
|
||||
|
@@ -270,7 +270,7 @@ const LoginPage = () => {
|
||||
))}
|
||||
</Select>
|
||||
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
|
||||
<CardActions className="actions">
|
||||
{loginMethod === "credentials" && <CardActions className="actions">
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
@@ -289,7 +289,18 @@ const LoginPage = () => {
|
||||
>
|
||||
{translate("synapseadmin.auth.sso_sign_in")}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</CardActions>}
|
||||
{loginMethod === "accessToken" && <CardActions className="actions">
|
||||
<Button
|
||||
variant="contained"
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
fullWidth
|
||||
>
|
||||
{translate("ra.auth.sign_in")}
|
||||
</Button>
|
||||
</CardActions>}
|
||||
</Box>
|
||||
</Card>
|
||||
</LoginFormBox>
|
||||
|
@@ -8,6 +8,7 @@ import ErrorIcon from '@mui/icons-material/Error';
|
||||
import {
|
||||
Button,
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
List,
|
||||
ListProps,
|
||||
@@ -123,14 +124,14 @@ export const DestinationList = (props: ListProps) => {
|
||||
pagination={<DestinationPagination />}
|
||||
sort={{ field: "destination", order: "ASC" }}
|
||||
>
|
||||
<Datagrid rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||
<DatagridConfigurable rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||
<FunctionField source="destination" render={destinationFieldRender} />
|
||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||
<TextField source="retry_interval" />
|
||||
<TextField source="last_successful_stream_ordering" />
|
||||
<DestinationReconnectButton />
|
||||
</Datagrid>
|
||||
</DatagridConfigurable>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
Create,
|
||||
CreateProps,
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
DateTimeInput,
|
||||
Edit,
|
||||
@@ -39,13 +40,13 @@ export const RegistrationTokenList = (props: ListProps) => (
|
||||
pagination={false}
|
||||
perPage={500}
|
||||
>
|
||||
<Datagrid rowClick="edit">
|
||||
<DatagridConfigurable rowClick="edit">
|
||||
<TextField source="token" sortable={false} />
|
||||
<NumberField source="uses_allowed" sortable={false} />
|
||||
<NumberField source="pending" sortable={false} />
|
||||
<NumberField source="completed" sortable={false} />
|
||||
<DateField source="expiry_time" showTime options={DATE_FORMAT} sortable={false} />
|
||||
</Datagrid>
|
||||
</DatagridConfigurable>
|
||||
</List>
|
||||
);
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import ViewListIcon from "@mui/icons-material/ViewList";
|
||||
import ReportIcon from "@mui/icons-material/Warning";
|
||||
import {
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
DeleteButton,
|
||||
List,
|
||||
@@ -90,13 +91,13 @@ const ReportShowActions = () => {
|
||||
|
||||
export const ReportList = (props: ListProps) => (
|
||||
<List {...props} pagination={<ReportPagination />} sort={{ field: "received_ts", order: "DESC" }}>
|
||||
<Datagrid rowClick="show" bulkActionButtons={false}>
|
||||
<DatagridConfigurable rowClick="show" bulkActionButtons={false}>
|
||||
<TextField source="id" sortable={false} />
|
||||
<DateField source="received_ts" showTime options={DATE_FORMAT} sortable={true} />
|
||||
<TextField sortable={false} source="user_id" />
|
||||
<TextField sortable={false} source="name" />
|
||||
<TextField sortable={false} source="score" />
|
||||
</Datagrid>
|
||||
</DatagridConfigurable>
|
||||
</List>
|
||||
);
|
||||
|
||||
|
@@ -187,7 +187,7 @@ export const RoomShow = (props: ShowProps) => {
|
||||
<Datagrid style={{ width: "100%" }} bulkActionButtons={false}>
|
||||
<TextField source="type" sortable={false} />
|
||||
<DateField source="origin_server_ts" showTime options={DATE_FORMAT} sortable={false} />
|
||||
<TextField source="content" sortable={false} />
|
||||
<FunctionField source="content" sortable={false} render={record => `${JSON.stringify(record.content, null, 2)}`} />
|
||||
<ReferenceField source="sender" reference="users" sortable={false}>
|
||||
<TextField source="id" />
|
||||
</ReferenceField>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import PermMediaIcon from "@mui/icons-material/PermMedia";
|
||||
import {
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
ExportButton,
|
||||
List,
|
||||
ListProps,
|
||||
@@ -37,12 +38,12 @@ export const UserMediaStatsList = (props: ListProps) => (
|
||||
pagination={<UserMediaStatsPagination />}
|
||||
sort={{ field: "media_length", order: "DESC" }}
|
||||
>
|
||||
<Datagrid rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
||||
<DatagridConfigurable rowClick={id => "/users/" + id + "/media"} bulkActionButtons={false}>
|
||||
<TextField source="user_id" label="resources.users.fields.id" />
|
||||
<TextField source="displayname" label="resources.users.fields.displayname" />
|
||||
<NumberField source="media_count" />
|
||||
<NumberField source="media_length" />
|
||||
</Datagrid>
|
||||
</DatagridConfigurable>
|
||||
</List>
|
||||
);
|
||||
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
ArrayField,
|
||||
Button,
|
||||
Datagrid,
|
||||
DatagridConfigurable,
|
||||
DateField,
|
||||
Create,
|
||||
CreateProps,
|
||||
@@ -156,7 +157,7 @@ export const UserList = (props: ListProps) => (
|
||||
actions={<UserListActions />}
|
||||
pagination={<UserPagination />}
|
||||
>
|
||||
<Datagrid
|
||||
<DatagridConfigurable
|
||||
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||
bulkActionButtons={<UserBulkActionButtons />}
|
||||
>
|
||||
@@ -169,7 +170,7 @@ export const UserList = (props: ListProps) => (
|
||||
<BooleanField source="locked" />
|
||||
<BooleanField source="erased" sortable={false} />
|
||||
<DateField source="creation_ts" label="resources.users.fields.creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||
</Datagrid>
|
||||
</DatagridConfigurable>
|
||||
</List>
|
||||
);
|
||||
|
||||
|
@@ -30,7 +30,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
expect(ret).toEqual({redirectTo: "/"});
|
||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||
expect(fetch).toHaveBeenCalledWith("http://example.com/_matrix/client/v3/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
@@ -61,7 +61,7 @@ describe("authProvider", () => {
|
||||
});
|
||||
|
||||
expect(ret).toEqual({redirectTo: "/"});
|
||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/v3/login", {
|
||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
@@ -83,7 +83,7 @@ describe("authProvider", () => {
|
||||
|
||||
await authProvider.logout(null);
|
||||
|
||||
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
|
||||
expect(fetch).toHaveBeenCalledWith("example.com/_matrix/client/v3/logout", {
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer foo",
|
||||
@@ -123,7 +123,9 @@ describe("authProvider", () => {
|
||||
|
||||
describe("getPermissions", () => {
|
||||
it("should do nothing", async () => {
|
||||
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
|
||||
if (authProvider.getPermissions) {
|
||||
await expect(authProvider.getPermissions(null)).resolves.toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { AuthProvider, HttpError, Options, fetchUtils, useTranslate } from "react-admin";
|
||||
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
|
||||
|
||||
import storage from "../storage";
|
||||
import { MatrixError, displayError } from "../components/error";
|
||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
// called when the user attempts to log in
|
||||
@@ -57,7 +58,7 @@ const authProvider: AuthProvider = {
|
||||
storage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login");
|
||||
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
|
||||
|
||||
let response;
|
||||
|
||||
@@ -74,7 +75,7 @@ const authProvider: AuthProvider = {
|
||||
|
||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||
const json = response.json;
|
||||
storage.setItem("home_server", accessToken ? base_url : json.home_server);
|
||||
storage.setItem("home_server", accessToken ? json.user_id.split(":")[1] : json.home_server);
|
||||
storage.setItem("user_id", json.user_id);
|
||||
storage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||
storage.setItem("device_id", json.device_id);
|
||||
@@ -95,11 +96,48 @@ const authProvider: AuthProvider = {
|
||||
);
|
||||
}
|
||||
},
|
||||
getIdentity: async () => {
|
||||
const access_token = storage.getItem("access_token");
|
||||
const user_id = storage.getItem("user_id");
|
||||
const base_url = storage.getItem("base_url");
|
||||
|
||||
if (typeof access_token !== "string" || typeof user_id !== "string" || typeof base_url !== "string") {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
headers: new Headers({
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const whoami_api_url = base_url + `/_matrix/client/v3/profile/${user_id}`;
|
||||
|
||||
try {
|
||||
let avatar_url = "";
|
||||
const response = await fetchUtils.fetchJson(whoami_api_url, options);
|
||||
if (response.json.avatar_url) {
|
||||
const mediaresp = await fetchAuthenticatedMedia(response.json.avatar_url, "thumbnail");
|
||||
const blob = await mediaresp.blob();
|
||||
avatar_url = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
id: user_id,
|
||||
fullName: response.json.displayname,
|
||||
avatar: avatar_url,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error getting identity", err);
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
// called when the user clicks on the logout button
|
||||
logout: async () => {
|
||||
console.log("logout");
|
||||
|
||||
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/r0/logout";
|
||||
const logout_api_url = storage.getItem("base_url") + "/_matrix/client/v3/logout";
|
||||
const access_token = storage.getItem("access_token");
|
||||
|
||||
const options: Options = {
|
||||
|
@@ -575,13 +575,28 @@ const baseDataProvider: SynapseDataProvider = {
|
||||
|
||||
getMany: async (resource, params) => {
|
||||
console.log("getMany " + resource);
|
||||
const homeserver = storage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set");
|
||||
const base_url = storage.getItem("base_url");
|
||||
const homeserver = storage.getItem("home_server");
|
||||
if (!base_url || !(resource in resourceMap)) throw Error("base_url not set");
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
const responses = await Promise.all(params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)));
|
||||
const endpoint_url = base_url + res.path;
|
||||
const responses = await Promise.all(params.ids.map(id => {
|
||||
// edge case: when user is external / federated, homeserver will return error, as querying external users via
|
||||
// /_synapse/admin/v2/users is not allowed.
|
||||
// That leads to an issue when a user is referenced (e.g., in room state datagrid) - the user cell is just empty.
|
||||
// To avoid that, we fake the response with one specific field (name) which is used in the datagrid.
|
||||
if (homeserver && resource === "users") {
|
||||
if (!(<string>id).endsWith(homeserver)) {
|
||||
const json = {
|
||||
name: id,
|
||||
};
|
||||
return Promise.resolve({ json });
|
||||
}
|
||||
}
|
||||
return jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`);
|
||||
}));
|
||||
return {
|
||||
data: responses.map(({ json }) => res.map(json)),
|
||||
total: responses.length,
|
||||
|
Reference in New Issue
Block a user