Compare commits
65 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2a5b59002e | ||
![]() |
44d801a2f5 | ||
![]() |
5ed5a88225 | ||
![]() |
1ca029fe94 | ||
![]() |
ddf3298b41 | ||
![]() |
e66c321ef9 | ||
![]() |
c215aa2f25 | ||
![]() |
e1afcd2678 | ||
![]() |
acd0d6d848 | ||
![]() |
9491c3ab14 | ||
![]() |
2e52d92fce | ||
![]() |
a360e38b7f | ||
![]() |
b72ace5adc | ||
![]() |
c56bd76c73 | ||
![]() |
28e4ea0e76 | ||
![]() |
b9de3044be | ||
![]() |
7df67b99a6 | ||
![]() |
f17a245839 | ||
![]() |
1e1b1b0e80 | ||
![]() |
0742c64c61 | ||
![]() |
992b3cd266 | ||
![]() |
f9a95882e9 | ||
![]() |
7ca78f9fc5 | ||
![]() |
95950dae86 | ||
![]() |
8e83204b34 | ||
![]() |
328c6b499d | ||
![]() |
af708f5d13 | ||
![]() |
7629e4b090 | ||
![]() |
c596d38d7a | ||
![]() |
c643bdcfce | ||
![]() |
630286a781 | ||
![]() |
71e90c12a7 | ||
![]() |
db6594f11f | ||
![]() |
7844987fe0 | ||
![]() |
9f2293f9c4 | ||
![]() |
fd4bf7ce5c | ||
![]() |
0dc969ce2c | ||
![]() |
f5455ef667 | ||
![]() |
edb8028162 | ||
![]() |
f415d637e9 | ||
![]() |
2c9103c769 | ||
![]() |
1c6f91d3a9 | ||
![]() |
05826e2ae1 | ||
![]() |
f6ebd71ac2 | ||
![]() |
49d0157c81 | ||
![]() |
ccfc5e4478 | ||
![]() |
66953dba3d | ||
![]() |
f4df8c070c | ||
![]() |
c3fa4daddb | ||
![]() |
b6782c64f7 | ||
![]() |
8a65621b35 | ||
![]() |
b8b0d46070 | ||
![]() |
2f73970e47 | ||
![]() |
ceacddf9c6 | ||
![]() |
4e89ad5f56 | ||
![]() |
1d4b4f97da | ||
![]() |
ac36a93203 | ||
![]() |
1dd0b242e2 | ||
![]() |
608d9fa9d8 | ||
![]() |
f5841c6a30 | ||
![]() |
266b13ce0b | ||
![]() |
1be9171b93 | ||
![]() |
037c2acee2 | ||
![]() |
38a0c9a9fc | ||
![]() |
8703cff7bf |
@@ -107,6 +107,9 @@ The following changes are already implemented:
|
|||||||
* 🎞️ [Add "Media" tab for rooms](https://github.com/etkecc/synapse-admin/pull/196)
|
* 🎞️ [Add "Media" tab for rooms](https://github.com/etkecc/synapse-admin/pull/196)
|
||||||
* 📞 [Support E.164-based Matrix IDs (MSC4009)](https://github.com/etkecc/synapse-admin/pull/214)
|
* 📞 [Support E.164-based Matrix IDs (MSC4009)](https://github.com/etkecc/synapse-admin/pull/214)
|
||||||
* 🛑 [Add support for Account Suspension (MSC3823)](https://github.com/etkecc/synapse-admin/pull/195)
|
* 🛑 [Add support for Account Suspension (MSC3823)](https://github.com/etkecc/synapse-admin/pull/195)
|
||||||
|
* 🗑️ [Add "Purge Remote Media" button](https://github.com/etkecc/synapse-admin/pull/237)
|
||||||
|
* [Respect base url (`BASE_PATH` / `vite build --base`) when loading `config.json`](https://github.com/etkecc/synapse-admin/pull/274)
|
||||||
|
* [Add Users' Account Data tab](https://github.com/etkecc/synapse-admin/pull/276)
|
||||||
|
|
||||||
#### exclusive for [etke.cc](https://etke.cc) customers
|
#### exclusive for [etke.cc](https://etke.cc) customers
|
||||||
|
|
||||||
@@ -114,13 +117,14 @@ We at [etke.cc](https://etke.cc) attempting to develop everything open-source, b
|
|||||||
The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers.
|
The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers.
|
||||||
|
|
||||||
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
* 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182)
|
||||||
|
* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
`just run-dev` to start the development stack (depending on your system speed, you may want to re-run this command if
|
`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)
|
user creation fails)
|
||||||
|
|
||||||
This command initializes the development environment (local Synapse server and Postgres DB),
|
This command initializes the development environment (local Synapse server, Element Web client app, and Postgres DB),
|
||||||
and launches the app in a dev mode at `http://localhost:5173`
|
and launches the app in a dev mode at `http://localhost:5173`
|
||||||
|
|
||||||
After that open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) in your browser,
|
After that open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) in your browser,
|
||||||
@@ -130,6 +134,8 @@ login using the following credentials:
|
|||||||
* Password: admin
|
* Password: admin
|
||||||
* Homeserver URL: http://localhost:8008
|
* Homeserver URL: http://localhost:8008
|
||||||
|
|
||||||
|
Element Web runs on http://localhost:8080
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
If you have any questions or need help, feel free to join the [community room](https://matrix.to/#/#synapse-admin:etke.cc) or create an issue on GitHub.
|
If you have any questions or need help, feel free to join the [community room](https://matrix.to/#/#synapse-admin:etke.cc) or create an issue on GitHub.
|
||||||
|
@@ -18,3 +18,17 @@ services:
|
|||||||
POSTGRES_PASSWORD: synapse
|
POSTGRES_PASSWORD: synapse
|
||||||
POSTGRES_DB: synapse
|
POSTGRES_DB: synapse
|
||||||
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"
|
||||||
|
|
||||||
|
element:
|
||||||
|
image: docker.io/vectorim/element-web:latest
|
||||||
|
depends_on:
|
||||||
|
synapse:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./testdata/element/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- /dev/null:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./testdata/element/config.json:/app/config.json:ro
|
||||||
|
|
||||||
|
@@ -9,5 +9,23 @@ const config: JestConfigWithTsJest = {
|
|||||||
coverageReporters: ["html", "text", "text-summary", "cobertura"],
|
coverageReporters: ["html", "text", "text-summary", "cobertura"],
|
||||||
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
extensionsToTreatAsEsm: [".ts", ".tsx"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/src/jest.setup.ts"],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
diagnostics: {
|
||||||
|
ignoreCodes: [1343]
|
||||||
|
},
|
||||||
|
astTransformers: {
|
||||||
|
before: [
|
||||||
|
{
|
||||||
|
path: 'ts-jest-mock-import-meta',
|
||||||
|
options: { metaObjectReplacement: { env: { BASE_URL: "/" } } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
2
justfile
2
justfile
@@ -16,6 +16,8 @@ run-dev:
|
|||||||
@docker-compose -f docker-compose-dev.yml up -d postgres
|
@docker-compose -f docker-compose-dev.yml up -d postgres
|
||||||
@echo "Starting Synapse..."
|
@echo "Starting Synapse..."
|
||||||
@docker-compose -f docker-compose-dev.yml up -d synapse
|
@docker-compose -f docker-compose-dev.yml up -d synapse
|
||||||
|
@echo "Starting Element Web..."
|
||||||
|
@docker-compose -f docker-compose-dev.yml up -d element
|
||||||
@echo "Ensure admin user is registered..."
|
@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
|
@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..."
|
@echo "Starting the app..."
|
||||||
|
39
package.json
39
package.json
@@ -11,20 +11,20 @@
|
|||||||
"url": "https://github.com/etkecc/synapse-admin"
|
"url": "https://github.com/etkecc/synapse-admin"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.14",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.5",
|
||||||
"@types/papaparse": "^5.3.15",
|
"@types/papaparse": "^5.3.15",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.19.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
@@ -39,19 +39,19 @@
|
|||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.17.0",
|
"typescript-eslint": "^8.19.0",
|
||||||
"vite": "^6.0.3",
|
"vite": "^6.0.7",
|
||||||
"vite-plugin-version-mark": "^0.1.4"
|
"vite-plugin-version-mark": "^0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.5",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.13.5",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@haleos/ra-language-german": "^1.0.0",
|
"@haleos/ra-language-german": "^1.0.0",
|
||||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||||
"@mui/icons-material": "^6.1.9",
|
"@mui/icons-material": "^6.3.1",
|
||||||
"@mui/material": "^6.1.10",
|
"@mui/material": "^6.3.1",
|
||||||
"@mui/utils": "^5.16.8",
|
"@mui/utils": "^5.16.13",
|
||||||
"@tanstack/react-query": "^5.62.3",
|
"@tanstack/react-query": "^5.62.15",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -59,16 +59,17 @@
|
|||||||
"ra-i18n-polyglot": "^5.3.4",
|
"ra-i18n-polyglot": "^5.3.4",
|
||||||
"ra-language-english": "^5.3.4",
|
"ra-language-english": "^5.3.4",
|
||||||
"ra-language-farsi": "^5.1.0",
|
"ra-language-farsi": "^5.1.0",
|
||||||
"ra-language-french": "^5.4.1",
|
"ra-language-french": "^5.4.3",
|
||||||
"ra-language-italian": "^3.13.1",
|
"ra-language-italian": "^3.13.1",
|
||||||
"ra-language-russian": "^4.14.2",
|
"ra-language-russian": "^4.14.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-admin": "^5.4.1",
|
"react-admin": "^5.4.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.0",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-router": "^6.26.2",
|
"react-router": "^6.28.1",
|
||||||
"react-router-dom": "^6.28.0"
|
"react-router-dom": "^6.28.1",
|
||||||
|
"ts-jest-mock-import-meta": "^1.2.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
|
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@@ -25,6 +25,7 @@ import authProvider from "./synapse/authProvider";
|
|||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
|
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -65,6 +66,7 @@ export const App = () => (
|
|||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<UserImport />} />
|
<Route path="/import_users" element={<UserImport />} />
|
||||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||||
|
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
|
@@ -5,6 +5,7 @@ import { Icons, DefaultIcon } from "../utils/icons";
|
|||||||
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||||
|
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
|
||||||
|
|
||||||
const AdminUserMenu = () => {
|
const AdminUserMenu = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -50,6 +51,7 @@ const AdminAppBar = () => {
|
|||||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||||
<TitlePortal />
|
<TitlePortal />
|
||||||
<ServerStatusBadge />
|
<ServerStatusBadge />
|
||||||
|
<ServerNotificationsBadge />
|
||||||
<InspectorButton />
|
<InspectorButton />
|
||||||
</AppBar>);
|
</AppBar>);
|
||||||
};
|
};
|
||||||
|
65
src/components/UserAccountData.tsx
Normal file
65
src/components/UserAccountData.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useDataProvider, useRecordContext, useTranslate } from "react-admin";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Typography, Box, Stack, Accordion, AccordionSummary, AccordionDetails } from "@mui/material";
|
||||||
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
|
import { SynapseDataProvider } from "../synapse/dataProvider";
|
||||||
|
|
||||||
|
const UserAccountData = () => {
|
||||||
|
const dataProvider = useDataProvider() as SynapseDataProvider;
|
||||||
|
const record = useRecordContext();
|
||||||
|
const translate = useTranslate();
|
||||||
|
const [globalAccountData, setGlobalAccountData] = useState({});
|
||||||
|
const [roomsAccountData, setRoomsAccountData] = useState({});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAccountData = async () => {
|
||||||
|
const accountData = await dataProvider.getAccountData(record.id);
|
||||||
|
setGlobalAccountData(accountData.account_data.global);
|
||||||
|
setRoomsAccountData(accountData.account_data.rooms);
|
||||||
|
};
|
||||||
|
fetchAccountData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (Object.keys(globalAccountData).length === 0 && Object.keys(roomsAccountData).length === 0) {
|
||||||
|
return <Typography variant="body2">{translate('ra.navigation.no_results', {
|
||||||
|
resource: 'Account Data',
|
||||||
|
_: 'No results found.',
|
||||||
|
})}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={2}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.title')}</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<Box>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.global')}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(globalAccountData, null, 4)}</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ArrowDownwardIcon />}>
|
||||||
|
<Typography variant="h6">{translate('resources.users.account_data.rooms')}</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box sx={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(roomsAccountData, null, 4)}</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserAccountData;
|
@@ -28,3 +28,15 @@ Server Status page. This page contains the following information:
|
|||||||
* Overall server status (up/updating/has issues)
|
* Overall server status (up/updating/has issues)
|
||||||
* Details about the currently running command (if any)
|
* Details about the currently running command (if any)
|
||||||
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
||||||
|
|
||||||
|
### Server Notifications icon
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the application bar the new notifications icon is displayed that shows the number of unread (not removed) notifications
|
||||||
|
|
||||||
|
### Server Notifications page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server.
|
||||||
|
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { Badge, useTheme, Button, Paper, Popper, ClickAwayListener, Box, List, ListItem, ListItemText, Typography, ListSubheader, IconButton, Divider, Tooltip } from "@mui/material";
|
||||||
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { useDataProvider, useStore } from "react-admin";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
import { useAppContext } from "../../Context";
|
||||||
|
import { ServerNotificationsResponse } from "../../synapse/dataProvider";
|
||||||
|
|
||||||
|
const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;
|
||||||
|
|
||||||
|
const useServerNotifications = () => {
|
||||||
|
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", { notifications: [], success: false });
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const dataProvider = useDataProvider();
|
||||||
|
const { notifications, success } = serverNotifications;
|
||||||
|
|
||||||
|
const fetchNotifications = async () => {
|
||||||
|
const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin);
|
||||||
|
setServerNotifications({
|
||||||
|
...notificationsResponse,
|
||||||
|
notifications: notificationsResponse.notifications,
|
||||||
|
success: notificationsResponse.success
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServerNotifications = async () => {
|
||||||
|
const deleteResponse = await dataProvider.deleteServerNotifications(etkeccAdmin);
|
||||||
|
if (deleteResponse.success) {
|
||||||
|
await fetchNotifications();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let serverNotificationsInterval: NodeJS.Timeout;
|
||||||
|
if (etkeccAdmin) {
|
||||||
|
fetchNotifications();
|
||||||
|
setTimeout(() => {
|
||||||
|
// start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests
|
||||||
|
serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||||
|
}, SERVER_NOTIFICATIONS_INTERVAL_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (serverNotificationsInterval) {
|
||||||
|
clearInterval(serverNotificationsInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [etkeccAdmin]);
|
||||||
|
|
||||||
|
return { success, notifications, deleteServerNotifications };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerNotificationsBadge = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { success, notifications, deleteServerNotifications } = useServerNotifications();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Modify menu state to work with Popper
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeeAllNotifications = () => {
|
||||||
|
handleClose();
|
||||||
|
navigate("/server_notifications");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllNotifications = async () => {
|
||||||
|
deleteServerNotifications()
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
|
||||||
|
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}>
|
||||||
|
{notifications && notifications.length > 0 && (
|
||||||
|
<Badge badgeContent={notifications.length} color="error">
|
||||||
|
<NotificationsIcon />
|
||||||
|
</Badge>
|
||||||
|
) || <NotificationsIcon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
placement="bottom-end"
|
||||||
|
style={{ zIndex: 1300 }}
|
||||||
|
>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto",
|
||||||
|
minWidth: "300px",
|
||||||
|
maxWidth: {
|
||||||
|
xs: "100vw", // Full width on mobile
|
||||||
|
sm: "400px" // Fixed width on desktop
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(!notifications || notifications.length === 0) ? (
|
||||||
|
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography>
|
||||||
|
) : (
|
||||||
|
<List sx={{ p: 0 }} dense={true}>
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bold",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Notifications</Typography>
|
||||||
|
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box>
|
||||||
|
</ListSubheader>
|
||||||
|
<Divider />
|
||||||
|
{notifications.map((notification, index) => {
|
||||||
|
return (<Fragment key={notification.event_id ? notification.event_id : index }>
|
||||||
|
<ListItem
|
||||||
|
onClick={() => handleSeeAllNotifications()}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)})}
|
||||||
|
<ListItem>
|
||||||
|
<Button
|
||||||
|
key="clear-all-notifications"
|
||||||
|
onClick={() => handleClearAllNotifications()}
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
sx={{
|
||||||
|
pl: 0,
|
||||||
|
pt: 1,
|
||||||
|
verticalAlign: "middle"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Box, Typography, Paper, Button } from "@mui/material"
|
||||||
|
import { Stack } from "@mui/material"
|
||||||
|
import { useStore } from "react-admin"
|
||||||
|
import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider"
|
||||||
|
import { useAppContext } from "../../Context";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
const DisplayTime = ({ date }: { date: string }) => {
|
||||||
|
const dateFromDateString = new Date(date);
|
||||||
|
return <>{dateFromDateString.toLocaleString()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerNotificationsPage = () => {
|
||||||
|
const { etkeccAdmin } = useAppContext();
|
||||||
|
const [serverNotifications, setServerNotifications] = useStore<ServerNotificationsResponse>("serverNotifications", {
|
||||||
|
notifications: [],
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifications = serverNotifications.notifications;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
<Stack spacing={1} direction="row" alignItems="center">
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
|
||||||
|
<Typography variant="h4">Server Notifications</Typography>
|
||||||
|
<Button variant="text" color="error" onClick={async () => {
|
||||||
|
await dataProvider.deleteServerNotifications(etkeccAdmin);
|
||||||
|
setServerNotifications({
|
||||||
|
notifications: [],
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography>No new notifications.</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification, index) => (
|
||||||
|
<Paper key={notification.event_id ? notification.event_id : index} sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" color="text.secondary">
|
||||||
|
<DisplayTime date={notification.sent_at} />
|
||||||
|
</Typography>
|
||||||
|
<Typography dangerouslySetInnerHTML={{ __html: notification.output }} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerNotificationsPage;
|
@@ -124,6 +124,80 @@ export const DeleteMediaButton = (props: ButtonProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PurgeRemoteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const PurgeRemoteMediaToolbar = (props: ToolbarProps) => (
|
||||||
|
<Toolbar {...props}>
|
||||||
|
<SaveButton label="purge_remote_media.action.send" icon={<DeleteSweepIcon />} />
|
||||||
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
|
<IconCancel />
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>{translate("purge_remote_media.action.send")}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{translate("purge_remote_media.helper.send")}</DialogContentText>
|
||||||
|
<SimpleForm toolbar={<PurgeRemoteMediaToolbar />} onSubmit={onSubmit}>
|
||||||
|
<DateTimeInput
|
||||||
|
source="before_ts"
|
||||||
|
label="purge_remote_media.fields.before_ts"
|
||||||
|
defaultValue={0}
|
||||||
|
parse={dateParser}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PurgeRemoteMediaButton = (props: ButtonProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const notify = useNotify();
|
||||||
|
const dataProvider = useDataProvider<SynapseDataProvider>();
|
||||||
|
const { mutate: purgeRemoteMedia, isPending } = useMutation({
|
||||||
|
mutationFn: (values: DeleteMediaParams) => dataProvider.purgeRemoteMedia(values),
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("purge_remote_media.action.send_success");
|
||||||
|
closeDialog();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
notify("purge_remote_media.action.send_failure", {
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDialog = () => setOpen(true);
|
||||||
|
const closeDialog = () => setOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
label="purge_remote_media.action.send"
|
||||||
|
onClick={openDialog}
|
||||||
|
disabled={isPending}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
// Reset on mouse devices
|
||||||
|
"@media (hover: none)": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteSweepIcon />
|
||||||
|
</Button>
|
||||||
|
<PurgeRemoteMediaDialog open={open} onClose={closeDialog} onSubmit={purgeRemoteMedia} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ProtectMediaButton = (props: ButtonProps) => {
|
export const ProtectMediaButton = (props: ButtonProps) => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
@@ -55,7 +55,7 @@ const de: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits" },
|
tabs: { sso: "SSO", experimental: "Experimentell", limits: "Rate Limits", account_data: "Kontodaten" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Raumdetails",
|
details: "Raumdetails",
|
||||||
@@ -147,6 +147,20 @@ const de: SynapseTranslationMessages = {
|
|||||||
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Externe Medien",
|
||||||
|
fields: {
|
||||||
|
before_ts: "letzter Zugriff vor",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Externe Medien löschen",
|
||||||
|
send_success: "Die Anfrage zum Löschen externer Medien wurde gesendet.",
|
||||||
|
send_failure: "Bei der Anfrage zum Löschen externer Medien ist ein Fehler aufgetreten.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Diese API löscht den externen Medien-Cache von der Festplatte Ihres eigenen Servers. Dazu gehören alle lokalen Thumbnails und Kopien heruntergeladener Medien. Diese API beeinflusst nicht die Medien, die in das eigene Medienarchiv des Servers hochgeladen wurden.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Benutzer",
|
name: "Benutzer",
|
||||||
@@ -215,6 +229,11 @@ const de: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können.",
|
messages_per_second_text: "Die Anzahl der Aktionen, die in einer Sekunde durchgeführt werden können.",
|
||||||
burst_count: "Burst-Anzahl",
|
burst_count: "Burst-Anzahl",
|
||||||
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
|
burst_count_text: "Die Anzahl der Aktionen, die vor der Begrenzung durchgeführt werden können.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Kontodaten",
|
||||||
|
global: "Globale",
|
||||||
|
rooms: "Räume",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -29,6 +29,7 @@ const en: SynapseTranslationMessages = {
|
|||||||
sso: "SSO",
|
sso: "SSO",
|
||||||
experimental: "Experimental",
|
experimental: "Experimental",
|
||||||
limits: "Rate Limits",
|
limits: "Rate Limits",
|
||||||
|
account_data: "Account Data",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -120,6 +121,20 @@ const en: SynapseTranslationMessages = {
|
|||||||
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Remote Media",
|
||||||
|
fields: {
|
||||||
|
before_ts: "last access before",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Purge remote media",
|
||||||
|
send_success: "Purge remote media request has been sent.",
|
||||||
|
send_failure: "An error has occurred with the purge remote media request.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "This API purges the remote media cache from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to the server's own media repository.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "User |||| Users",
|
name: "User |||| Users",
|
||||||
@@ -188,6 +203,11 @@ const en: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "The number of actions that can be performed in a second.",
|
messages_per_second_text: "The number of actions that can be performed in a second.",
|
||||||
burst_count: "Burst count",
|
burst_count: "Burst count",
|
||||||
burst_count_text: "How many actions that can be performed before being limited.",
|
burst_count_text: "How many actions that can be performed before being limited.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Account Data",
|
||||||
|
global: "Global",
|
||||||
|
rooms: "Rooms",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -24,7 +24,7 @@ const fa: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||||
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها" },
|
tabs: { sso: "SSO", experimental: "تجربی", limits: "محدودیت ها", account_data: "داده های کاربر" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -114,6 +114,20 @@ const fa: SynapseTranslationMessages = {
|
|||||||
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانههایی که در مخازن رسانه خارجی آپلود شدهاند تأثیری نخواهد گذاشت.",
|
send: "این API رسانه های محلی را از دیسک سرور خود حذف می کند. این شامل هر تصویر کوچک محلی و کپی از رسانه دانلود شده است. این API بر رسانههایی که در مخازن رسانه خارجی آپلود شدهاند تأثیری نخواهد گذاشت.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "رسانههای از راه دور",
|
||||||
|
fields: {
|
||||||
|
before_ts: "آخرین دسترسی قبل از",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "پاک کردن رسانههای از راه دور",
|
||||||
|
send_success: "درخواست پاک کردن رسانههای از راه دور ارسال شد.",
|
||||||
|
send_failure: "درخواست برای پاک کردن رسانههای از راه دور با خطا مواجه شد.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "این API کش رسانههای از راه دور را از دیسک سرور شما پاک میکند. این شامل هر گونه بندانگشتی محلی و نسخههای رسانههای دانلود شده میشود. این API بر رسانههای آپلود شده به مخزن رسانه سرور تأثیری نخواهد داشت.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "کاربر |||| کاربران",
|
name: "کاربر |||| کاربران",
|
||||||
@@ -181,6 +195,11 @@ const fa: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود.",
|
messages_per_second_text: "تعداد عملیاتی که می تواند در یک ثانیه انجام شود.",
|
||||||
burst_count: "تعداد پیچیدگی",
|
burst_count: "تعداد پیچیدگی",
|
||||||
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
|
burst_count_text: "تعداد عملیاتی که می تواند قبل از محدودیت انجام شود.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "داده های کاربر",
|
||||||
|
global: "عمومی",
|
||||||
|
rooms: "اتاق ها",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -24,7 +24,7 @@ const fr: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
invalid_user_id: "Partie locale d'un identifiant utilisateur Matrix sans le nom du serveur d’accueil.",
|
||||||
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites" },
|
tabs: { sso: "Authentification unique", experimental: "Expérimental", limits: "Limites", account_data: "Données du compte" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -117,6 +117,20 @@ const fr: SynapseTranslationMessages = {
|
|||||||
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
|
send: "Cette API supprime les médias locaux du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléversés dans des dépôts de médias externes.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Médias distants",
|
||||||
|
fields: {
|
||||||
|
before_ts: "dernier accès avant",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Purger les médias distants",
|
||||||
|
send_success: "La demande de purge des médias distants a été envoyée.",
|
||||||
|
send_failure: "Une erreur est survenue lors de la demande de purge des médias distants.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Cette API purge le cache des médias distants du disque de votre propre serveur. Cela inclut toutes les vignettes locales et les copies des médias téléchargés. Cette API n'affectera pas les médias qui ont été téléchargés dans le dépôt de médias du serveur.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Utilisateur |||| Utilisateurs",
|
name: "Utilisateur |||| Utilisateurs",
|
||||||
@@ -183,6 +197,11 @@ const fr: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde.",
|
messages_per_second_text: "Le nombre d'actions que l'utilisateur peut effectuer par seconde.",
|
||||||
burst_count: "Compteur de pics",
|
burst_count: "Compteur de pics",
|
||||||
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
|
burst_count_text: "Le nombre d'actions que l'utilisateur peut effectuer avant d'être limité.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Données du compte",
|
||||||
|
global: "Globales",
|
||||||
|
rooms: "Salons",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
21
src/i18n/index.d.ts
vendored
21
src/i18n/index.d.ts
vendored
@@ -22,7 +22,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: string;
|
invalid_user_id: string;
|
||||||
tabs: { sso: string; experimental: string; limits: string; };
|
tabs: { sso: string; experimental: string; limits: string; account_data: string; };
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
details?: string; // TODO: fa, fr, it, zh
|
details?: string; // TODO: fa, fr, it, zh
|
||||||
@@ -112,6 +112,20 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
send: string;
|
send: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
purge_remote_media: {
|
||||||
|
name: string;
|
||||||
|
fields: {
|
||||||
|
before_ts: string;
|
||||||
|
};
|
||||||
|
action: {
|
||||||
|
send: string;
|
||||||
|
send_success: string;
|
||||||
|
send_failure: string;
|
||||||
|
};
|
||||||
|
helper: {
|
||||||
|
send: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -181,6 +195,11 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
burst_count: string;
|
burst_count: string;
|
||||||
burst_count_text: string;
|
burst_count_text: string;
|
||||||
};
|
};
|
||||||
|
account_data: {
|
||||||
|
title: string;
|
||||||
|
global: string;
|
||||||
|
rooms: string;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
rooms: {
|
rooms: {
|
||||||
name: string;
|
name: string;
|
||||||
|
@@ -24,7 +24,7 @@ const it: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||||
tabs: { sso: "SSO", experimental: "Sperimentale", limits: "Limiti" },
|
tabs: { sso: "SSO", experimental: "Sperimentale", limits: "Limiti", account_data: "Dati del profilo" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -114,6 +114,20 @@ const it: SynapseTranslationMessages = {
|
|||||||
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
|
send: "Questa API cancella i media locali dal disco del tuo server. Questo include anche ogni miniatura e copia del media scaricato. Questa API non inciderà sui media che sono stati caricati nei repository esterni.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Media Remoti",
|
||||||
|
fields: {
|
||||||
|
before_ts: "ultimo accesso prima di",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Elimina media remoti",
|
||||||
|
send_success: "La richiesta per eliminare i media remoti è stata inviata.",
|
||||||
|
send_failure: "Si è verificato un errore con la richiesta di eliminazione dei media remoti.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Questa API elimina la cache dei media remoti dal disco del tuo server. Questo include qualsiasi miniatura locale e copie di media scaricati. Questa API non influirà sui media che sono stati caricati nel repository multimediale del server.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Utente |||| Utenti",
|
name: "Utente |||| Utenti",
|
||||||
@@ -181,6 +195,11 @@ const it: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Il numero di azioni che l'utente può eseguire al secondo.",
|
messages_per_second_text: "Il numero di azioni che l'utente può eseguire al secondo.",
|
||||||
burst_count: "Burst-conteggio",
|
burst_count: "Burst-conteggio",
|
||||||
burst_count_text: "Il numero di azioni che l'utente può eseguire prima di essere limitato.",
|
burst_count_text: "Il numero di azioni che l'utente può eseguire prima di essere limitato.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Dati del profilo",
|
||||||
|
global: "Globale",
|
||||||
|
rooms: "Stanza",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -50,7 +50,7 @@ const ru: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения" },
|
tabs: { sso: "SSO", experimental: "Экспериментальные", limits: "Ограничения", account_data: "Данные пользователя" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
details: "Данные комнаты",
|
details: "Данные комнаты",
|
||||||
@@ -150,6 +150,20 @@ const ru: SynapseTranslationMessages = {
|
|||||||
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
Данный API не затрагивает файлы, загруженные во внешние хранилища.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "Внешние медиа",
|
||||||
|
fields: {
|
||||||
|
before_ts: "последний доступ до",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Очистить внешние медиа",
|
||||||
|
send_success: "Запрос на очистку внешних медиа был отправлен.",
|
||||||
|
send_failure: "Произошла ошибка при запросе очистки внешних медиа.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "Этот API очищает кэш внешних медиа с диска вашего сервера. Это включает любые локальные миниатюры и копии загруженных медиа. Этот API не повлияет на медиа, которые были загружены в собственное медиа-хранилище сервера.",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "Пользователь |||| Пользователи",
|
name: "Пользователь |||| Пользователи",
|
||||||
@@ -218,6 +232,11 @@ const ru: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду.",
|
messages_per_second_text: "Количество действий, которые могут быть выполнены в секунду.",
|
||||||
burst_count: "Burst-счётчик",
|
burst_count: "Burst-счётчик",
|
||||||
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
|
burst_count_text: "Количество действий, которые могут быть выполнены до ограничения.",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "Данные пользователя",
|
||||||
|
global: "Глобальные",
|
||||||
|
rooms: "Комнаты",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -52,7 +52,7 @@ const zh: SynapseTranslationMessages = {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
tabs: { sso: "SSO", experimental: "实验性", limits: "限制" },
|
tabs: { sso: "SSO", experimental: "实验性", limits: "限制", account_data: "账户数据" },
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -142,6 +142,20 @@ const zh: SynapseTranslationMessages = {
|
|||||||
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
purge_remote_media: {
|
||||||
|
name: "远程媒体",
|
||||||
|
fields: {
|
||||||
|
before_ts: "最后访问于之前",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "清除远程媒体",
|
||||||
|
send_success: "远程媒体清除请求已发送。",
|
||||||
|
send_failure: "发生错误,远程媒体清除请求未成功。",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send: "此API清除您服务器磁盘上的远程媒体缓存。这包括任何本地缩略图和下载的媒体副本。此API不会影响已经上传到服务器媒体存储库的媒体。",
|
||||||
|
},
|
||||||
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
name: "用户",
|
name: "用户",
|
||||||
@@ -207,6 +221,11 @@ const zh: SynapseTranslationMessages = {
|
|||||||
messages_per_second_text: "每秒可以执行的操作数。",
|
messages_per_second_text: "每秒可以执行的操作数。",
|
||||||
burst_count: "Burst-计数",
|
burst_count: "Burst-计数",
|
||||||
burst_count_text: "在限制之前可以执行的操作数。",
|
burst_count_text: "在限制之前可以执行的操作数。",
|
||||||
|
},
|
||||||
|
account_data: {
|
||||||
|
title: "账户数据",
|
||||||
|
global: "全局",
|
||||||
|
rooms: "房间",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
@@ -47,7 +47,7 @@ const LoginPage = () => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = new URLSearchParams(window.location.search).get("loginToken")
|
||||||
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,10 +55,11 @@ const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssoToken = loginToken[1];
|
console.log("SSO token is", loginToken);
|
||||||
console.log("SSO token is", ssoToken);
|
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
const previousUrl = new URL(window.location.toString())
|
||||||
|
previousUrl.searchParams.delete("loginToken")
|
||||||
|
window.history.replaceState({}, "", previousUrl.toString());
|
||||||
const baseUrl = localStorage.getItem("sso_base_url");
|
const baseUrl = localStorage.getItem("sso_base_url");
|
||||||
localStorage.removeItem("sso_base_url");
|
localStorage.removeItem("sso_base_url");
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
@@ -66,10 +67,10 @@ const LoginPage = () => {
|
|||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
loginToken: ssoToken,
|
loginToken,
|
||||||
};
|
};
|
||||||
console.log("Base URL is:", baseUrl);
|
console.log("Base URL is:", baseUrl);
|
||||||
console.log("SSO Token is:", ssoToken);
|
console.log("SSO Token is:", loginToken);
|
||||||
console.log("Let's try token login...");
|
console.log("Let's try token login...");
|
||||||
login(auth).catch(error => {
|
login(auth).catch(error => {
|
||||||
alert(
|
alert(
|
||||||
|
@@ -14,13 +14,14 @@ import {
|
|||||||
useListContext,
|
useListContext,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
|
||||||
import { DeleteMediaButton } from "../components/media";
|
import { DeleteMediaButton, PurgeRemoteMediaButton } from "../components/media";
|
||||||
|
|
||||||
const ListActions = () => {
|
const ListActions = () => {
|
||||||
const { isLoading, total } = useListContext();
|
const { isLoading, total } = useListContext();
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
<DeleteMediaButton />
|
<DeleteMediaButton />
|
||||||
|
<PurgeRemoteMediaButton />
|
||||||
<ExportButton disabled={isLoading || total === 0} />
|
<ExportButton disabled={isLoading || total === 0} />
|
||||||
</TopToolbar>
|
</TopToolbar>
|
||||||
);
|
);
|
||||||
|
@@ -10,6 +10,7 @@ import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputCompone
|
|||||||
import ScienceIcon from "@mui/icons-material/Science";
|
import ScienceIcon from "@mui/icons-material/Science";
|
||||||
import LockClockIcon from '@mui/icons-material/LockClock';
|
import LockClockIcon from '@mui/icons-material/LockClock';
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
import DocumentScannerIcon from "@mui/icons-material/DocumentScanner";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Alert, Typography } from "@mui/material";
|
import { Alert, Typography } from "@mui/material";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
SaveButton,
|
SaveButton,
|
||||||
CreateButton,
|
CreateButton,
|
||||||
ExportButton,
|
ExportButton,
|
||||||
|
BulkDeleteButton,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
NumberField,
|
NumberField,
|
||||||
@@ -80,6 +82,7 @@ import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
|
|||||||
import UserRateLimits from "../components/UserRateLimits";
|
import UserRateLimits from "../components/UserRateLimits";
|
||||||
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
||||||
import { MakeAdminBtn } from "./rooms";
|
import { MakeAdminBtn } from "./rooms";
|
||||||
|
import UserAccountData from "../components/UserAccountData";
|
||||||
|
|
||||||
const choices_medium = [
|
const choices_medium = [
|
||||||
{ id: "email", name: "resources.users.email" },
|
{ id: "email", name: "resources.users.email" },
|
||||||
@@ -496,7 +499,7 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
perPage={50}
|
perPage={50}
|
||||||
sort={{ field: "created_ts", order: "DESC" }}
|
sort={{ field: "created_ts", order: "DESC" }}
|
||||||
>
|
>
|
||||||
<Datagrid sx={{ width: "100%" }}>
|
<Datagrid sx={{ width: "100%" }} bulkActionButtons={<BulkDeleteButton/>}>
|
||||||
<MediaIDField source="media_id" />
|
<MediaIDField source="media_id" />
|
||||||
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
<DateField source="created_ts" showTime options={DATE_FORMAT} />
|
||||||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
||||||
@@ -555,6 +558,10 @@ export const UserEdit = (props: EditProps) => {
|
|||||||
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
<FormTab label="synapseadmin.users.tabs.limits" icon={<LockClockIcon />} path="limits">
|
||||||
<UserRateLimits />
|
<UserRateLimits />
|
||||||
</FormTab>
|
</FormTab>
|
||||||
|
|
||||||
|
<FormTab label="synapseadmin.users.tabs.account_data" icon={<DocumentScannerIcon />} path="accountdata">
|
||||||
|
<UserAccountData />
|
||||||
|
</FormTab>
|
||||||
</TabbedForm>
|
</TabbedForm>
|
||||||
</Edit>
|
</Edit>
|
||||||
);
|
);
|
||||||
|
@@ -80,11 +80,7 @@ const authProvider: AuthProvider = {
|
|||||||
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||||
|
|
||||||
// when doing access token auth, config is not fetched, so we need to do it here
|
|
||||||
if (accessToken) {
|
|
||||||
await FetchConfig();
|
await FetchConfig();
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({redirectTo: "/"});
|
return Promise.resolve({redirectTo: "/"});
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@@ -260,6 +260,17 @@ export interface RateLimitsModel {
|
|||||||
burst_count?: number;
|
burst_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccountDataModel {
|
||||||
|
account_data: {
|
||||||
|
global: {
|
||||||
|
[key: string]: object;
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
[key: string]: object;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface UsernameAvailabilityResult {
|
export interface UsernameAvailabilityResult {
|
||||||
available?: boolean;
|
available?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -291,16 +302,31 @@ export interface ServerProcessResponse {
|
|||||||
command?: string;
|
command?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerNotification {
|
||||||
|
event_id: string;
|
||||||
|
output: string;
|
||||||
|
sent_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerNotificationsResponse {
|
||||||
|
success: boolean;
|
||||||
|
notifications: ServerNotification[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SynapseDataProvider extends DataProvider {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
|
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
uploadMedia: (params: UploadMediaParams) => Promise<UploadMediaResult>;
|
||||||
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
updateFeatures: (id: Identifier, features: ExperimentalFeaturesModel) => Promise<void>;
|
||||||
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
|
getRateLimits: (id: Identifier) => Promise<RateLimitsModel>;
|
||||||
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
setRateLimits: (id: Identifier, rateLimits: RateLimitsModel) => Promise<void>;
|
||||||
|
getAccountData: (id: Identifier) => Promise<AccountDataModel>;
|
||||||
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
checkUsernameAvailability: (username: string) => Promise<UsernameAvailabilityResult>;
|
||||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||||
|
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||||
|
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@@ -849,6 +875,23 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
return json as DeleteMediaResult;
|
return json as DeleteMediaResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge remote media by date
|
||||||
|
*
|
||||||
|
* @link https://element-hq.github.io/synapse/latest/admin_api/media_admin_api.html#purge-remote-media-api
|
||||||
|
*
|
||||||
|
* @param before_ts Unix timestamp in milliseconds. Files that were last used before this timestamp will be deleted. It is the timestamp of last access, not the timestamp when the file was created.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
purgeRemoteMedia: async ({ before_ts }) => {
|
||||||
|
const endpoint = `/_synapse/admin/v1/purge_media_cache?before_ts=${before_ts}`;
|
||||||
|
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = base_url + endpoint;
|
||||||
|
const { json } = await jsonClient(endpoint_url, { method: "POST" });
|
||||||
|
return json as DeleteMediaResult;
|
||||||
|
},
|
||||||
|
|
||||||
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
|
||||||
const base_url = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
|
||||||
@@ -880,6 +923,12 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
const { json } = await jsonClient(endpoint_url);
|
const { json } = await jsonClient(endpoint_url);
|
||||||
return json as RateLimitsModel;
|
return json as RateLimitsModel;
|
||||||
},
|
},
|
||||||
|
getAccountData: async (id: Identifier) => {
|
||||||
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/accountdata`;
|
||||||
|
const { json } = await jsonClient(endpoint_url);
|
||||||
|
return json as AccountDataModel;
|
||||||
|
},
|
||||||
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
setRateLimits: async (id: Identifier, rateLimits: RateLimitsModel) => {
|
||||||
const filtered = Object.entries(rateLimits).
|
const filtered = Object.entries(rateLimits).
|
||||||
filter(([key, value]) => value !== null && value !== undefined).
|
filter(([key, value]) => value !== null && value !== undefined).
|
||||||
@@ -977,6 +1026,60 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, ok: false, host: "", results: [] };
|
return { success: false, ok: false, host: "", results: [] };
|
||||||
|
},
|
||||||
|
getServerNotifications: async (serverNotificationsUrl: string): Promise<ServerNotificationsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverNotificationsUrl}/notifications`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error getting server notifications: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false, notifications: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 204) {
|
||||||
|
return { success: true, notifications: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
const result = { success: true, notifications: json } as ServerNotificationsResponse;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, notifications: [] };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting server notifications", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, notifications: [] };
|
||||||
|
},
|
||||||
|
deleteServerNotifications: async (serverNotificationsUrl: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverNotificationsUrl}/notifications`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${localStorage.getItem("access_token")}`
|
||||||
|
},
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status;
|
||||||
|
if (status === 204) {
|
||||||
|
const result = { success: true }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting server notifications", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -22,32 +22,43 @@ let config: Config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FetchConfig = async () => {
|
export const FetchConfig = async () => {
|
||||||
|
// load config.json and honor vite base url (import.meta.env.BASE_URL)
|
||||||
|
// if that url doesn't have a trailing slash - add it
|
||||||
|
let configJSONUrl = "config.json"
|
||||||
|
if (import.meta.env.BASE_URL) {
|
||||||
|
configJSONUrl = `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}config.json`;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("config.json");
|
const resp = await fetch(configJSONUrl);
|
||||||
const configJSON = await resp.json();
|
const configJSON = await resp.json();
|
||||||
console.log("Loaded config.json", configJSON);
|
console.log("Loaded", configJSONUrl, configJSON);
|
||||||
LoadConfig(configJSON);
|
LoadConfig(configJSON);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let protocol = "https";
|
||||||
|
const baseURL = localStorage.getItem("base_url");
|
||||||
|
if (baseURL && baseURL.startsWith("http://")) {
|
||||||
|
protocol = "http";
|
||||||
|
}
|
||||||
|
|
||||||
// if home_server is set, try to load https://home_server/.well-known/matrix/client
|
// if home_server is set, try to load https://home_server/.well-known/matrix/client
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const homeserver = localStorage.getItem("home_server");
|
||||||
if (homeserver) {
|
if (homeserver) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`https://${homeserver}/.well-known/matrix/client`);
|
const resp = await fetch(`${protocol}://${homeserver}/.well-known/matrix/client`);
|
||||||
const configWK = await resp.json();
|
const configWK = await resp.json();
|
||||||
if (!configWK[WellKnownKey]) {
|
if (!configWK[WellKnownKey]) {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
|
console.log(`Loaded ${protocol}://${homeserver}.well-known/matrix/client`, configWK);
|
||||||
LoadConfig(configWK[WellKnownKey]);
|
LoadConfig(configWK[WellKnownKey]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config from context
|
// load config from context
|
||||||
|
@@ -16,7 +16,11 @@ export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id
|
|||||||
* @returns Whether the user is managed by an application service
|
* @returns Whether the user is managed by an application service
|
||||||
*/
|
*/
|
||||||
export const isASManaged = (id: string | Identifier): boolean => {
|
export const isASManaged = (id: string | Identifier): boolean => {
|
||||||
return GetConfig().asManagedUsers.some(regex => regex.test(id as string));
|
const managedUsers = GetConfig().asManagedUsers;
|
||||||
|
if (!managedUsers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return managedUsers.some(regex => regex.test(id as string));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,14 +42,20 @@ export function generateRandomMXID(): string {
|
|||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
*/
|
*/
|
||||||
export function returnMXID(input: string | Identifier): string {
|
export function returnMXID(input: string | Identifier): string {
|
||||||
const homeserver = localStorage.getItem("home_server");
|
const inputStr = input as string;
|
||||||
|
const homeserver = localStorage.getItem("home_server") || "";
|
||||||
|
|
||||||
|
// when homeserver is not (just) a domain name, but a domain:port or even an IPv6 address
|
||||||
|
if (homeserver != "" && inputStr.endsWith(homeserver) && inputStr.startsWith("@")) {
|
||||||
|
return inputStr; // Already a valid MXID
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
if (isMXID(input)) {
|
if (isMXID(input)) {
|
||||||
return input as string; // Already a valid MXID
|
return inputStr; // Already a valid MXID
|
||||||
}
|
}
|
||||||
|
|
||||||
// If input is not a valid MXID, assume it's a localpart and construct the 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;
|
const localpart = typeof input === 'string' && inputStr.startsWith('@') ? inputStr.slice(1) : inputStr;
|
||||||
return `@${localpart}:${homeserver}`;
|
return `@${localpart}:${homeserver}`;
|
||||||
}
|
}
|
||||||
|
9
testdata/element/config.json
vendored
Normal file
9
testdata/element/config.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"default_hs_url": "http://localhost:8008",
|
||||||
|
"default_is_url": "https://vector.im",
|
||||||
|
"integrations_ui_url": "https://scalar.vector.im/",
|
||||||
|
"integrations_rest_url": "https://scalar.vector.im/api",
|
||||||
|
"bug_report_endpoint_url": "https://riot.im/bugreports/submit",
|
||||||
|
"enableLabs": true
|
||||||
|
}
|
||||||
|
|
34
testdata/element/nginx.conf
vendored
Normal file
34
testdata/element/nginx.conf
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
http {
|
||||||
|
client_body_temp_path /tmp/client_body_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
testdata/synapse/homeserver.yaml
vendored
4
testdata/synapse/homeserver.yaml
vendored
@@ -78,7 +78,7 @@ password_config:
|
|||||||
pid_file: /homeserver.pid
|
pid_file: /homeserver.pid
|
||||||
presence:
|
presence:
|
||||||
enabled: true
|
enabled: true
|
||||||
public_baseurl: http://synapse:8008/
|
public_baseurl: http://localhost:8008/
|
||||||
push:
|
push:
|
||||||
include_content: true
|
include_content: true
|
||||||
rc_admin_redaction:
|
rc_admin_redaction:
|
||||||
@@ -149,7 +149,7 @@ saml2_config:
|
|||||||
sp_config: null
|
sp_config: null
|
||||||
user_mapping_provider:
|
user_mapping_provider:
|
||||||
config: null
|
config: null
|
||||||
server_name: synapse
|
server_name: synapse:8008
|
||||||
signing_key_path: /config/synapse.signing.key
|
signing_key_path: /config/synapse.signing.key
|
||||||
spam_checker: []
|
spam_checker: []
|
||||||
sso: null
|
sso: null
|
||||||
|
Reference in New Issue
Block a user