Compare commits
38 Commits
v0.10.3-et
...
v0.10.3-et
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8680dbc268 | ||
![]() |
ea2b84c5dc | ||
![]() |
45c7027d3c | ||
![]() |
62017d4f4e | ||
![]() |
0d7dcdc284 | ||
![]() |
0eb3b77bc5 | ||
![]() |
e2fba4bbdd | ||
![]() |
6425a6bfc4 | ||
![]() |
42925e8a7c | ||
![]() |
75e89fe628 | ||
![]() |
f9c806d292 | ||
![]() |
3f5022d515 | ||
![]() |
0748f98d47 | ||
![]() |
3c8fd351a1 | ||
![]() |
40e6d80c35 | ||
![]() |
243cc40da4 | ||
![]() |
3bcc51d12c | ||
![]() |
2afd7d6737 | ||
![]() |
2357d63120 | ||
![]() |
e28d07ebd3 | ||
![]() |
33f960579c | ||
![]() |
6e14bd7959 | ||
![]() |
bdbc0df95b | ||
![]() |
5e10d94e5f | ||
![]() |
a934942bf6 | ||
![]() |
c440e88806 | ||
![]() |
45b7ec005b | ||
![]() |
c748523dbc | ||
![]() |
34eea8dff4 | ||
![]() |
87408c0e6d | ||
![]() |
5ad787075c | ||
![]() |
01ae5a411f | ||
![]() |
cde60a2aba | ||
![]() |
3f5808c67b | ||
![]() |
2c697b40dd | ||
![]() |
9453490bca | ||
![]() |
0baf6ad94d | ||
![]() |
df911c9e97 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
/testdata
|
11
Dockerfile.build
Normal file
11
Dockerfile.build
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:lts AS builder
|
||||
ARG BASE_PATH=./
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
RUN yarn config set enableTelemetry 0 && \
|
||||
yarn install --immutable --network-timeout=300000 && \
|
||||
yarn build --base=$BASE_PATH
|
||||
|
||||
FROM ghcr.io/static-web-server/static-web-server:2
|
||||
ENV SERVER_ROOT=/app
|
||||
COPY --from=builder /src/dist /app
|
14
README.md
14
README.md
@@ -261,6 +261,7 @@ You have three options:
|
||||
hostname: synapse-admin
|
||||
build:
|
||||
context: https://github.com/etkecc/synapse-admin.git
|
||||
dockerfile: Dockerfile.build
|
||||
args:
|
||||
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
@@ -289,7 +290,7 @@ Example for Traefik:
|
||||
```yml
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:mimolette
|
||||
image: traefik:v3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -302,11 +303,12 @@ services:
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.synapse-admin.rule=Host(`example.com`)&&PathPrefix(`/admin`)"
|
||||
- "traefik.http.routers.synapse-admin.middlewares=admin,admin_path"
|
||||
- "traefik.http.middlewares.admin.redirectregex.regex=^(.*)/admin/?"
|
||||
- "traefik.http.middlewares.admin.redirectregex.replacement=$${1}/admin/"
|
||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||
- "traefik.http.routers.admin.rule=Host(`example.com`) && PathPrefix(`/admin`)"
|
||||
- "traefik.http.services.admin.loadbalancer.server.port=80"
|
||||
- "traefik.http.middlewares.admin-slashless-redirect.redirectregex.regex=(/admin)$$"
|
||||
- "traefik.http.middlewares.admin-slashless-redirect.redirectregex.replacement=$${1}/"
|
||||
- "traefik.http.middlewares.admin-strip-prefix.stripprefix.prefixes=/admin"
|
||||
- "traefik.http.routers.admin.middlewares=admin-slashless-redirect,admin-strip-prefix"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
@@ -5,6 +5,7 @@ services:
|
||||
image: ghcr.io/etkecc/synapse-admin:latest
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile.build
|
||||
|
||||
# to use the docker-compose as standalone without a local repo clone,
|
||||
# replace the context definition with this:
|
||||
|
@@ -2,7 +2,7 @@ import type { JestConfigWithTsJest } from "ts-jest";
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
testEnvironment: "jest-fixed-jsdom",
|
||||
collectCoverage: true,
|
||||
coveragePathIgnorePatterns: ["node_modules", "dist"],
|
||||
coverageDirectory: "<rootDir>/coverage/",
|
||||
|
41
package.json
41
package.json
@@ -11,24 +11,24 @@
|
||||
"url": "https://github.com/etkecc/synapse-admin"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.10",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/react": "^18.3.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-yaml": "^1.0.3",
|
||||
"jest": "^29.7.0",
|
||||
@@ -38,9 +38,9 @@
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.19.0",
|
||||
"vite": "^6.0.7",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-version-mark": "^0.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -49,26 +49,25 @@
|
||||
"@haleos/ra-language-german": "^1.0.0",
|
||||
"@haxqer/ra-language-chinese": "^4.16.2",
|
||||
"@mui/icons-material": "^6.3.1",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/utils": "^5.16.13",
|
||||
"@tanstack/react-query": "^5.62.15",
|
||||
"@mui/material": "^6.4.0",
|
||||
"@mui/utils": "^5.16.14",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
"history": "^5.3.0",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"lodash": "^4.17.21",
|
||||
"papaparse": "^5.4.1",
|
||||
"ra-core": "^5.4.1",
|
||||
"ra-i18n-polyglot": "^5.3.4",
|
||||
"ra-language-english": "^5.3.4",
|
||||
"papaparse": "^5.5.1",
|
||||
"ra-core": "^5.4.4",
|
||||
"ra-i18n-polyglot": "^5.4.4",
|
||||
"ra-language-english": "^5.4.4",
|
||||
"ra-language-farsi": "^5.1.0",
|
||||
"ra-language-french": "^5.4.3",
|
||||
"ra-language-french": "^5.5.2",
|
||||
"ra-language-italian": "^3.13.1",
|
||||
"ra-language-russian": "^4.14.2",
|
||||
"react": "^18.3.1",
|
||||
"react-admin": "^5.4.3",
|
||||
"react-admin": "^5.5.2",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-is": "^18.3.1",
|
||||
"react-router": "^6.28.1",
|
||||
"react-router-dom": "^6.28.1",
|
||||
"ts-jest-mock-import-meta": "^1.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
@@ -1,12 +1,32 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
fetchMock.enableMocks();
|
||||
|
||||
jest.mock("./synapse/authProvider", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
logout: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
}));
|
||||
|
||||
import App from "./App";
|
||||
|
||||
describe("App", () => {
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
fetchMock.resetMocks();
|
||||
// Mock any fetch call to return empty JSON immediately
|
||||
fetchMock.mockResponseOnce(JSON.stringify({}));
|
||||
});
|
||||
|
||||
it("renders", async () => {
|
||||
render(<App />);
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
await screen.findAllByText("Welcome to Synapse Admin");
|
||||
});
|
||||
});
|
||||
|
@@ -35,6 +35,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { dateParser } from "../utils/date";
|
||||
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
|
||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||
|
||||
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
|
||||
const translate = useTranslate();
|
||||
@@ -481,7 +482,7 @@ export const MediaIDField = ({ source }) => {
|
||||
|
||||
let uploadName = mediaID;
|
||||
if (get(record, "upload_name")) {
|
||||
uploadName = decodeURIComponent(get(record, "upload_name")?.toString());
|
||||
uploadName = decodeURLComponent(get(record, "upload_name")?.toString());
|
||||
}
|
||||
|
||||
let mxcURL = mediaID;
|
||||
@@ -504,7 +505,10 @@ export const ReportMediaContent = ({ source }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadName = decodeURIComponent(get(record, "event_json.content.body")?.toString());
|
||||
let uploadName = "";
|
||||
if (get(record, "event_json.content.body")) {
|
||||
uploadName = decodeURLComponent(get(record, "event_json.content.body")?.toString());
|
||||
}
|
||||
|
||||
return <ViewMediaButton mxcURL={mxcURL} label={mxcURL} uploadName={uploadName} mimetype={record.media_type}/>;
|
||||
};
|
||||
|
@@ -15,6 +15,7 @@ const fixedGermanMessages = {
|
||||
action: {
|
||||
...formalGermanMessages.ra.action,
|
||||
update_application: "Anwendung aktualisieren",
|
||||
select_all_button: "Alle auswählen",
|
||||
},
|
||||
page: {
|
||||
...formalGermanMessages.ra.page,
|
||||
@@ -28,6 +29,7 @@ const fixedGermanMessages = {
|
||||
"Sie haben nicht die erforderlichen Berechtigungen um auf diese Seite zuzugreifen.",
|
||||
authentication_error:
|
||||
"Der Authentifizierungsserver hat einen Fehler zurückgegeben und Ihre Anmeldedaten konnten nicht überprüft werden.",
|
||||
select_all_limit_reached: "Es gibt zu viele Elemente, um sie alle auszuwählen. Es wurden nur die ersten %{max} Elemente ausgewählt.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -11,6 +11,10 @@ const fixedRussianMessages = {
|
||||
no_filtered_results: "Нет результатов",
|
||||
clear_filters: "Все фильтры сбросить",
|
||||
},
|
||||
action: {
|
||||
...russianMessages.ra.action,
|
||||
select_all_button: "Выбрать все",
|
||||
},
|
||||
page: {
|
||||
...russianMessages.ra.page,
|
||||
empty: "Пусто",
|
||||
@@ -23,6 +27,7 @@ const fixedRussianMessages = {
|
||||
"У вас нет прав доступа к этой странице.",
|
||||
authentication_error:
|
||||
"Сервер аутентификации вернул ошибку и не смог проверить ваши учетные данные.",
|
||||
select_all_limit_reached: "Слишком много элементов для выбора. Были выбраны только первые %{max} элементов.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@ const fixedChineseMessages = {
|
||||
},
|
||||
action: {
|
||||
...chineseMessages.ra.action,
|
||||
update_application: "Anwendung aktualisieren",
|
||||
update_application: "更新应用",
|
||||
select_all_button: "全部选择",
|
||||
},
|
||||
page: {
|
||||
...chineseMessages.ra.page,
|
||||
@@ -26,6 +27,7 @@ const fixedChineseMessages = {
|
||||
"您没有访问此页面的权限。",
|
||||
authentication_error:
|
||||
"身份验证服务器返回错误,无法验证您的凭据。",
|
||||
select_all_limit_reached: "选择的元素太多。只选择了前 %{max} 个元素。",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { AdminContext } from "react-admin";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import LoginPage from "./LoginPage";
|
||||
import { AppContext } from "../Context";
|
||||
@@ -14,9 +15,11 @@ describe("LoginForm", () => {
|
||||
it("renders with no restriction to homeserver", async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
<BrowserRouter>
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
</BrowserRouter>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,13 +36,15 @@ describe("LoginForm", () => {
|
||||
|
||||
it("renders with single restricted homeserver", () => {
|
||||
render(
|
||||
<AppContext.Provider
|
||||
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<AppContext.Provider
|
||||
value={{ restrictBaseUrl: "https://matrix.example.com", asManagedUsers: [], menu: [] }}
|
||||
>
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
</AdminContext>
|
||||
</AppContext.Provider>
|
||||
</AdminContext>
|
||||
</AppContext.Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
screen.getByText(englishMessages.synapseadmin.auth.welcome);
|
||||
@@ -56,14 +61,16 @@ describe("LoginForm", () => {
|
||||
it("renders with multiple restricted homeservers", async () => {
|
||||
render(
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
||||
asManagedUsers: [],
|
||||
value={{
|
||||
restrictBaseUrl: ["https://matrix.example.com", "https://matrix.example.org"],
|
||||
asManagedUsers: [],
|
||||
menu: [],
|
||||
}}
|
||||
>
|
||||
<AdminContext i18nProvider={i18nProvider}>
|
||||
<LoginPage />
|
||||
<BrowserRouter>
|
||||
<LoginPage />
|
||||
</BrowserRouter>
|
||||
</AdminContext>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
@@ -83,6 +83,7 @@ import UserRateLimits from "../components/UserRateLimits";
|
||||
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
|
||||
import { MakeAdminBtn } from "./rooms";
|
||||
import UserAccountData from "../components/UserAccountData";
|
||||
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||
|
||||
const choices_medium = [
|
||||
{ id: "email", name: "resources.users.email" },
|
||||
@@ -505,7 +506,7 @@ export const UserEdit = (props: EditProps) => {
|
||||
<DateField source="last_access_ts" showTime options={DATE_FORMAT} />
|
||||
<NumberField source="media_length" />
|
||||
<TextField source="media_type" sx={{ display: "block", width: 200, wordBreak: "break-word" }} />
|
||||
<FunctionField source="upload_name" render={record => decodeURIComponent(record.upload_name)} />
|
||||
<FunctionField source="upload_name" render={record => record.upload_name ? decodeURLComponent(record.upload_name) : ""} />
|
||||
<TextField source="quarantined_by" />
|
||||
<QuarantineMediaButton label="resources.quarantine_media.action.name" />
|
||||
<ProtectMediaButton label="resources.users_media.fields.safe_from_quarantine" />
|
||||
@@ -527,7 +528,9 @@ export const UserEdit = (props: EditProps) => {
|
||||
<ReferenceField reference="rooms" source="id" label="resources.rooms.fields.joined_members" link={false} sortable={false}>
|
||||
<TextField source="joined_members" sortable={false} />
|
||||
</ReferenceField>
|
||||
<ReferenceField reference="rooms" source="id" label={false} link={false} sortable={false}>
|
||||
<MakeAdminBtn />
|
||||
</ReferenceField>
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</FormTab>
|
||||
|
@@ -3,6 +3,7 @@ import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
|
||||
import { MatrixError, displayError } from "../utils/error";
|
||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||
import { FetchConfig, ClearConfig } from "../utils/config";
|
||||
import decodeURLComponent from "../utils/decodeURLComponent";
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
// called when the user attempts to log in
|
||||
@@ -57,7 +58,7 @@ const authProvider: AuthProvider = {
|
||||
base_url = base_url.replace(/\/+$/g, "");
|
||||
localStorage.setItem("base_url", base_url);
|
||||
|
||||
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||
const decoded_base_url = decodeURLComponent(base_url);
|
||||
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
|
||||
|
||||
let response;
|
||||
|
15
src/utils/decodeURLComponent.ts
Normal file
15
src/utils/decodeURLComponent.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Decode a URI component, and if it fails, return the original string.
|
||||
* @param str The string to decode.
|
||||
* @returns The decoded string, or the original string if decoding fails.
|
||||
* @example decodeURIComponent("Hello%20World") // "Hello World"
|
||||
*/
|
||||
const decodeURLComponent = (str: any): any => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export default decodeURLComponent;
|
Reference in New Issue
Block a user