Compare commits

..

No commits in common. "master" and "0.9.2" have entirely different histories.

14 changed files with 118 additions and 666 deletions

View File

@ -1,5 +1,4 @@
name: Create docker image(s) and push to docker hub and ghcr.io name: Create docker image(s) and push to docker hub
# see https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub-and-github-packages
on: on:
push: push:
@ -14,50 +13,39 @@ on:
jobs: jobs:
docker: docker:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-tags: true
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Calculate docker image tag
- name: Login to GHCR id: set-tag
uses: docker/login-action@v3 run: |
with: case "${GITHUB_REF}" in
registry: ghcr.io refs/heads/master|refs/heads/main)
username: ${{ github.actor }} tag=latest
password: ${{ secrets.GITHUB_TOKEN }} ;;
refs/tags/*)
- name: Extract metadata (tags, labels) for Docker tag=${GITHUB_REF#refs/tags/}
id: meta ;;
uses: docker/metadata-action@v5 *)
with: tag=${GITHUB_SHA}
images: | ;;
awesometechnologies/synapse-admin esac
ghcr.io/${{ github.repository }} echo "::set-output name=tag::$tag"
- name: Build and Push Tag - name: Build and Push Tag
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: "awesometechnologies/synapse-admin:${{ steps.set-tag.outputs.tag }}"
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@ -11,8 +11,6 @@ jobs:
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"

View File

@ -14,8 +14,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-tags: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "18" node-version: "18"

View File

@ -1,6 +1,6 @@
# Builder # Builder
FROM node:lts as builder FROM node:lts as builder
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
ARG REACT_APP_SERVER ARG REACT_APP_SERVER
WORKDIR /src WORKDIR /src

View File

@ -30,7 +30,6 @@
"ra-language-german": "^3.13.4", "ra-language-german": "^3.13.4",
"ra-language-italian": "^3.13.1", "ra-language-italian": "^3.13.1",
"ra-language-farsi": "^4.2.0", "ra-language-farsi": "^4.2.0",
"ra-language-russian": "^4.14.2",
"react": "^18.0.0", "react": "^18.0.0",
"react-admin": "^4.16.15", "react-admin": "^4.16.15",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",

View File

@ -14,6 +14,7 @@ import userMediaStats from "./components/statistics";
import reports from "./components/EventReports"; import reports from "./components/EventReports";
import roomDirectory from "./components/RoomDirectory"; import roomDirectory from "./components/RoomDirectory";
import destinations from "./components/destinations"; import destinations from "./components/destinations";
import registrationToken from "./components/RegistrationTokens";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/LoginPage";
import { ImportFeature } from "./components/ImportFeature"; import { ImportFeature } from "./components/ImportFeature";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
@ -22,7 +23,6 @@ import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr"; import frenchMessages from "./i18n/fr";
import chineseMessages from "./i18n/zh"; import chineseMessages from "./i18n/zh";
import italianMessages from "./i18n/it"; import italianMessages from "./i18n/it";
import russianMessages from "./i18n/ru";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {
@ -31,7 +31,6 @@ const messages = {
fr: frenchMessages, fr: frenchMessages,
it: italianMessages, it: italianMessages,
zh: chineseMessages, zh: chineseMessages,
ru: russianMessages
}; };
const i18nProvider = polyglotI18nProvider( const i18nProvider = polyglotI18nProvider(
locale => (messages[locale] ? messages[locale] : messages.en), locale => (messages[locale] ? messages[locale] : messages.en),
@ -56,6 +55,7 @@ const App = () => (
<Resource {...reports} /> <Resource {...reports} />
<Resource {...roomDirectory} /> <Resource {...roomDirectory} />
<Resource {...destinations} /> <Resource {...destinations} />
<Resource {...registrationToken} />
<Resource name="connections" /> <Resource name="connections" />
<Resource name="devices" /> <Resource name="devices" />
<Resource name="room_members" /> <Resource name="room_members" />

View File

@ -21,13 +21,13 @@ import {
CircularProgress, CircularProgress,
MenuItem, MenuItem,
Select, Select,
TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import { import {
getServerVersion, getServerVersion,
getSupportedFeatures,
getSupportedLoginFlows, getSupportedLoginFlows,
getWellKnownUrl, getWellKnownUrl,
isValidBaseUrl, isValidBaseUrl,
@ -37,7 +37,7 @@ import {
const FormBox = styled(Box)(({ theme }) => ({ const FormBox = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
minHeight: "calc(100vh - 1rem)", minHeight: "calc(100vh - 1em)",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
background: "url(./images/floating-cogs.svg)", background: "url(./images/floating-cogs.svg)",
@ -46,12 +46,12 @@ const FormBox = styled(Box)(({ theme }) => ({
backgroundSize: "cover", backgroundSize: "cover",
[`& .card`]: { [`& .card`]: {
width: "30rem", minWidth: "30em",
marginTop: "6rem", marginTop: "6em",
marginBottom: "6rem", marginBottom: "6em",
}, },
[`& .avatar`]: { [`& .avatar`]: {
margin: "1rem", margin: "1em",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
}, },
@ -60,31 +60,24 @@ const FormBox = styled(Box)(({ theme }) => ({
}, },
[`& .hint`]: { [`& .hint`]: {
marginTop: "1em", marginTop: "1em",
marginBottom: "1em",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
color: theme.palette.grey[600], color: theme.palette.grey[600],
}, },
[`& .form`]: { [`& .form`]: {
padding: "0 1rem 1rem 1rem", padding: "0 1em 1em 1em",
}, },
[`& .select`]: { [`& .input`]: {
marginBottom: "2rem", marginTop: "1em",
}, },
[`& .actions`]: { [`& .actions`]: {
padding: "0 1rem 1rem 1rem", padding: "0 1em 1em 1em",
}, },
[`& .serverVersion`]: { [`& .serverVersion`]: {
color: theme.palette.grey[500], color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif", fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginLeft: "0.5rem", marginBottom: "1em",
}, marginLeft: "0.5em",
[`& .matrixVersions`]: {
color: theme.palette.grey[500],
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
fontSize: "0.8rem",
marginBottom: "1rem",
marginLeft: "0.5rem",
}, },
})); }));
@ -134,6 +127,20 @@ const LoginPage = () => {
} }
} }
const renderInput = ({
meta: { touched, error } = {},
input: { ...inputProps },
...props
}) => (
<TextField
error={!!(touched && error)}
helperText={touched && error}
{...inputProps}
{...props}
fullWidth
/>
);
const validateBaseUrl = value => { const validateBaseUrl = value => {
if (!value.match(/^(http|https):\/\//)) { if (!value.match(/^(http|https):\/\//)) {
return translate("synapseadmin.auth.protocol_error"); return translate("synapseadmin.auth.protocol_error");
@ -172,7 +179,6 @@ const LoginPage = () => {
const UserData = ({ formData }) => { const UserData = ({ formData }) => {
const form = useFormContext(); const form = useFormContext();
const [serverVersion, setServerVersion] = useState(""); const [serverVersion, setServerVersion] = useState("");
const [matrixVersions, setMatrixVersions] = useState("");
const handleUsernameChange = _ => { const handleUsernameChange = _ => {
if (formData.base_url || cfg_base_url) return; if (formData.base_url || cfg_base_url) return;
@ -194,14 +200,6 @@ const LoginPage = () => {
) )
.catch(() => setServerVersion("")); .catch(() => setServerVersion(""));
getSupportedFeatures(formData.base_url)
.then(features =>
setMatrixVersions(
`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`
)
)
.catch(() => setMatrixVersions(""));
// Set SSO Url // Set SSO Url
getSupportedLoginFlows(formData.base_url) getSupportedLoginFlows(formData.base_url)
.then(loginFlows => { .then(loginFlows => {
@ -221,6 +219,7 @@ const LoginPage = () => {
<TextInput <TextInput
autoFocus autoFocus
name="username" name="username"
component={renderInput}
label="ra.auth.username" label="ra.auth.username"
disabled={loading || !supportPassAuth} disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange} onBlur={handleUsernameChange}
@ -233,6 +232,7 @@ const LoginPage = () => {
<Box> <Box>
<PasswordInput <PasswordInput
name="password" name="password"
component={renderInput}
label="ra.auth.password" label="ra.auth.password"
type="password" type="password"
disabled={loading || !supportPassAuth} disabled={loading || !supportPassAuth}
@ -245,6 +245,7 @@ const LoginPage = () => {
<Box> <Box>
<TextInput <TextInput
name="base_url" name="base_url"
component={renderInput}
label="synapseadmin.auth.base_url" label="synapseadmin.auth.base_url"
disabled={cfg_base_url || loading} disabled={cfg_base_url || loading}
resettable resettable
@ -254,7 +255,6 @@ const LoginPage = () => {
/> />
</Box> </Box>
<Typography className="serverVersion">{serverVersion}</Typography> <Typography className="serverVersion">{serverVersion}</Typography>
<Typography className="matrixVersions">{matrixVersions}</Typography>
</> </>
); );
}; };
@ -285,7 +285,7 @@ const LoginPage = () => {
}} }}
fullWidth fullWidth
disabled={loading} disabled={loading}
className="select" className="input"
> >
<MenuItem value="de">Deutsch</MenuItem> <MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
@ -293,7 +293,6 @@ const LoginPage = () => {
<MenuItem value="it">Italiano</MenuItem> <MenuItem value="it">Italiano</MenuItem>
<MenuItem value="zh">简体中文</MenuItem> <MenuItem value="zh">简体中文</MenuItem>
<MenuItem value="fa">Persian(فارسی)</MenuItem> <MenuItem value="fa">Persian(فارسی)</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
</Select> </Select>
<FormDataConsumer> <FormDataConsumer>
{formDataProps => <UserData {...formDataProps} />} {formDataProps => <UserData {...formDataProps} />}

View File

@ -7,7 +7,6 @@ const de = {
base_url: "Heimserver URL", base_url: "Heimserver URL",
welcome: "Willkommen bei Synapse-admin", welcome: "Willkommen bei Synapse-admin",
server_version: "Synapse Version", server_version: "Synapse Version",
supports_specs: "unterstützt Matrix-Specs",
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'", username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL", url_error: "Keine gültige Matrix Server URL",

View File

@ -7,7 +7,6 @@ const en = {
base_url: "Homeserver URL", base_url: "Homeserver URL",
welcome: "Welcome to Synapse-admin", welcome: "Welcome to Synapse-admin",
server_version: "Synapse version", server_version: "Synapse version",
supports_specs: "supports Matrix specs",
username_error: "Please enter fully qualified user ID: '@user:domain'", username_error: "Please enter fully qualified user ID: '@user:domain'",
protocol_error: "URL has to start with 'http://' or 'https://'", protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL", url_error: "Not a valid Matrix server URL",

View File

@ -1,390 +0,0 @@
import russianMessages from "ra-language-russian";
const ru = {
...russianMessages,
synapseadmin: {
auth: {
base_url: "Домашняя страница",
welcome: "Добро пожаловать в Synapse-admin",
server_version: "Версия Synapse",
supports_specs: "поддерживает спецификации Matrix",
username_error: "Введите полный идентификатор пользователя: '@user:domain'",
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
url_error: "Некорректный сервер Matrix",
sso_sign_in: "Присоединиться с помощью SSO",
},
users: {
invalid_user_id: "Локальная часть идентификатора пользователя Matrix без домашнего сервера.",
tabs: { sso: "SSO" },
},
rooms: {
tabs: {
basic: "Основное",
members: "Участники",
detail: "Подробности",
permission: "Права",
},
},
reports: { tabs: { basic: "Основное", detail: "Подробности" } },
},
import_users: {
error: {
at_entry: "При входе %{entry}: %{message}",
error: "Ошибка",
required_field: "Обязательное поле '%{field}' не представленно",
invalid_value:
"Недопустимое значение в строке %{row}. '%{field}' поле может быть только 'true' или 'false'",
unreasonably_big:
"Отказался загружать неоправданно большой файл %{size} Мбайт",
already_in_progress: "Импорт уже запущен",
id_exits: "Идентификатор %{id} уже существует",
},
title: "Импорт пользователей из CSV",
goToPdf: "В PDF",
cards: {
importstats: {
header: "Импорт пользователей",
users_total:
"%{smart_count} пользователь в CSV файл |||| %{smart_count} пользователей в CSV файл",
guest_count: "%{smart_count} гость |||| %{smart_count} гостей",
admin_count: "%{smart_count} администратор |||| %{smart_count} администраторов",
},
conflicts: {
header: "Решение конфликтов",
mode: {
stop: "Остановиться, если конфликт произошел",
skip: "Вывести ошибку и пропустить конфликт",
},
},
ids: {
header: "Идентификаторы",
all_ids_present: "Идентификаторы представлены для каждой записи",
count_ids_present:
"%{smart_count} запись с идентификатором |||| %{smart_count} записей с идентификатором",
mode: {
ignore: "Игнорировать идентификаторы в CSV и создавать новые",
update: "Обновлять существующие записи",
},
},
passwords: {
header: "Пароли",
all_passwords_present: "Пароли представлены для каждой записи",
count_passwords_present:
"%{smart_count} запись с паролем |||| %{smart_count} записей с паролем",
use_passwords: "Использовать пароли из CSV",
},
upload: {
header: "Загрузка CSV файла",
explanation:
"Здесь вы можете загрузить файл со значениями, разделенными запятыми, который будет обработан для создания или обновления пользователей. Файл должен содержать поля 'id' и 'displayname'. Вы можете загрузить пример здесь:",
},
startImport: {
simulate_only: "Не выполнять реальных действий",
run_import: "Импорт",
},
results: {
header: "Импорт результатов",
total:
"Всего %{smart_count} запись |||| Всего %{smart_count} записей",
successful: "%{smart_count} записей успешно импортировано",
skipped: "%{smart_count} записей пропущено",
download_skipped: "Загрузить пропущенные записи",
with_error:
"%{smart_count} запись с ошибками ||| %{smart_count} записей с ошибками",
simulated_only: "Результат не будет сохранен",
},
},
},
resources: {
users: {
name: "Пользователей |||| Пользователи",
email: "Почта",
msisdn: "Телефон",
threepid: "Почта / Телефон",
fields: {
avatar: "Аватар",
id: "Идентификатор пользователя",
name: "Имя",
is_guest: "Гость",
admin: "Администратор",
deactivated: "Деактивирован",
guests: "Показать гостей",
show_deactivated: "Показать деактивированных пользователей",
user_id: "Найти пользователя",
displayname: "Отображаемое имя",
password: "Пароль",
avatar_url: "Ссылка на аватар",
avatar_src: "Аватар",
medium: "Тип",
threepids: "Иной идентификатор",
address: "Адрес",
creation_ts_ms: "Время создания",
consent_version: "Версия соглашения",
auth_provider: "Поставщик",
user_type: "Тип пользователя",
},
helper: {
password: "Изменение пароля приведет к выходу пользователя из всех сеансов.",
deactivate: "Вы должны ввести пароль для повторной активации учетной записи.",
erase: "Пометить пользователя как удаленного в связи с защитой персональных данных",
},
action: {
erase: "Удаление пользовательских данных",
},
},
rooms: {
name: "Комнат |||| Комнаты",
fields: {
room_id: "Идентификатор комнаты",
name: "Название",
canonical_alias: "Псевдоним",
joined_members: "Участники",
joined_local_members: "Внутренние участники",
joined_local_devices: "Используемые устройства",
state_events: "События изменения состояния",
version: "Версия",
is_encrypted: "Зашифрованно",
encryption: "Шифрование",
federatable: "Федерация",
public: "Видимость в списке комнат",
creator: "Создатель",
join_rules: "Правила присоединения",
guest_access: "Гостевой доступ",
history_visibility: "Видимость истории",
topic: "Тема",
avatar: "Аватар",
},
helper: {
forward_extremities:
"Перенаправленные заключения, это такие события, которые не имеют потомков в рамках графа, отвечающего за их репрезентацию. Чем больше пользователей находится в комнате, тем больше операций требуется выполнить Synapse для разрешения коллиций, которые возникают при их проверке наступления событий (это дорогостоящая операция). Хотя в Synapse есть код, предотвращающий одновременное присутствие слишком большого количества таких объектов в комнате, ошибки иногда могут привести к их повторному появлению. Если в комнате содержится >10 перенаправленных заключений, имеет смысл выяснить, какая комната стала причиной их появления и, возможно, удалить их, используя SQL-запросы, упомянутые issue #1760.",
},
enums: {
join_rules: {
public: "Публичный",
knock: "По запросу",
invite: "По приглашению",
private: "Приватный",
},
guest_access: {
can_join: "Гости могут присоединиться",
forbidden: "Гости не могут присоединиться",
},
history_visibility: {
invited: "С момента приглашения",
joined: "С момента присоединения",
shared: "С момента разрешения",
world_readable: "Всегда",
},
unencrypted: "Не зашифрованно",
},
action: {
erase: {
title: "Удалить комнату",
content:
"Вы уверены, что хотите удалить комнату? Это невозможно отменить. Все сообщения и медиафайлы, находящиеся в общем доступе в комнате, будут удалены с сервера!",
},
},
},
reports: {
name: "Жалоб |||| Жалобы",
fields: {
id: "идентификатор",
received_ts: "время жалобы",
user_id: "коментатор",
name: "название комнаты",
score: "оценка",
reason: "причина",
event_id: "идентификатор события",
event_json: {
origin: "сервер",
origin_server_ts: "время отправки",
type: "тип события",
content: {
msgtype: "тип содержания",
body: "содержание",
format: "формат",
formatted_body: "форматированное содержание",
algorithm: "алгоритм",
},
},
},
action: {
erase: {
title: "Удалить жалобу",
content:
"Вы уверены, что хотите удалить жалобу? Это невозможно отменить.",
},
},
},
connections: {
name: "Подключения",
fields: {
last_seen: "Дата",
ip: "IP адрес",
user_agent: "User agent",
},
},
devices: {
name: "Устройств |||| Устройства",
fields: {
device_id: "Идентификатор устройства",
display_name: "Название устройства",
last_seen_ts: "Метка времени",
last_seen_ip: "IP адрес",
},
action: {
erase: {
title: "Удаление %{id}",
content: 'Вы уверены, что хотите удалить устройство "%{name}"?',
success: "Устройство успешно удалено.",
failure: "Произошла ошибка.",
},
},
},
users_media: {
name: "Медиа",
fields: {
media_id: "Идентификатор медиа",
media_length: "Размер файла (в байтах)",
media_type: "Тип",
upload_name: "Имя файла",
quarantined_by: "Отправлен в карантин",
safe_from_quarantine: "Защищен от карантина",
created_ts: "Создан",
last_access_ts: "Последнее обращение",
},
},
delete_media: {
name: "Медиа",
fields: {
before_ts: "последнее обращение",
size_gt: "Больше чем (в байтах)",
keep_profiles: "Сохранить аватары",
},
action: {
send: "Удалить медиафайлы",
send_success: "Запрос отправлен.",
send_failure: "Произошла ошибка.",
},
helper: {
send: "Этот метод удаляет медиафайлы с диска сервера. Это включает в себя любые миниатюры и копии загруженных файлов. Этот метод не повлияет на файлы, загруженные во внешние хранилища.",
},
},
protect_media: {
action: {
create: "Не защищено, установить защиту",
delete: "Защищено, удалить защиту",
none: "В карантине",
send_success: "Успешно изменен статус защиты.",
send_failure: "Произошла ошибка.",
},
},
quarantine_media: {
action: {
name: "Карантин",
create: "Добавить в карантин",
delete: "В карантине, вывести из карантина",
none: "Защищено от карантина",
send_success: "Успешно изменен статус карантина.",
send_failure: "Произошла ошибка.",
},
},
pushers: {
name: "Уведомление |||| Уведомления",
fields: {
app: "Приложение",
app_display_name: "Отображаемое имя приложения",
app_id: "Идентификатор приложения",
device_display_name: "Отображаемое имя устройства",
kind: "Тип",
lang: "Язык",
profile_tag: "Тег профиля",
pushkey: "Токен доступа",
data: { url: "URL" },
},
},
servernotices: {
name: "Уведомления",
send: "Отправить уведомление",
fields: {
body: "Сообщение",
},
action: {
send: "Отправить",
send_success: "Уведомление успешно отправлено.",
send_failure: "Произошла ошибка.",
},
helper: {
send: 'Отправляет уведомление выбранным пользователям. Функция "Server Notices" должна быть активирована на сервере.',
},
},
user_media_statistics: {
name: "Медиа",
fields: {
media_count: "Количество медиафайлов",
media_length: "Длина медиафайлов",
},
},
forward_extremities: {
name: "Перенаправленные заключения",
fields: {
id: "Идентификатор события",
received_ts: "Время",
depth: "Глубина",
state_group: "Группа состояния",
},
},
room_state: {
name: "События изменения состояния",
fields: {
type: "Тип",
content: "Содержание",
origin_server_ts: "время отправления",
sender: "Отправитель",
},
},
room_directory: {
name: "Публичных комнат |||| Публичные комнаты",
fields: {
world_readable: "Видимо для гостей",
guest_can_join: "Гости могут присоединиться",
},
action: {
title:
"Удалить комнату |||| Удалить %{smart_count} комнат",
content:
"Вы уверены, что хотите удалить комнату? |||| Вы уверены, что хотите удалить %{smart_count} комнат?",
erase: "Удалить комнату",
create: "Опубликовать комнату",
send_success: "Комната успешно опубликована.",
send_failure: "Произошла ошибка.",
},
},
destinations: {
name: "Федераций |||| Федерация",
fields: {
destination: "Назначение",
failure_ts: "Время сбоя",
retry_last_ts: "Время последней попытки",
retry_interval: "Интервал повторения",
last_successful_stream_ordering: "Последнее успешное соединение",
stream_ordering: "Соединение",
},
action: { reconnect: "Переподключиться" },
},
},
registration_tokens: {
name: "Токены регистрации",
fields: {
token: "Токен",
valid: "Допустимый токен",
uses_allowed: "Разрешено",
pending: "Ожидается",
completed: "Использован",
expiry_time: "Время истечения срока действия",
length: "Длина",
},
helper: { length: "Длина токена, если токен не указан." },
},
};
export default ru;

View File

@ -2,7 +2,7 @@ import { fetchUtils } from "react-admin";
const authProvider = { const authProvider = {
// called when the user attempts to log in // called when the user attempts to log in
login: async ({ base_url, username, password, loginToken }) => { login: ({ base_url, username, password, loginToken }) => {
// force homeserver for protection in case the form is manipulated // force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url; base_url = process.env.REACT_APP_SERVER || base_url;
@ -38,14 +38,15 @@ const authProvider = {
const decoded_base_url = window.decodeURIComponent(base_url); const decoded_base_url = window.decodeURIComponent(base_url);
const login_api_url = decoded_base_url + "/_matrix/client/r0/login"; const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
const { json } = await fetchUtils.fetchJson(login_api_url, options); return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
localStorage.setItem("home_server", json.home_server); localStorage.setItem("home_server", json.home_server);
localStorage.setItem("user_id", json.user_id); localStorage.setItem("user_id", json.user_id);
localStorage.setItem("access_token", json.access_token); localStorage.setItem("access_token", json.access_token);
localStorage.setItem("device_id", json.device_id); localStorage.setItem("device_id", json.device_id);
});
}, },
// called when the user clicks on the logout button // called when the user clicks on the logout button
logout: async () => { logout: () => {
console.log("logout"); console.log("logout");
const logout_api_url = const logout_api_url =
@ -61,9 +62,11 @@ const authProvider = {
}; };
if (typeof access_token === "string") { if (typeof access_token === "string") {
await fetchUtils.fetchJson(logout_api_url, options); fetchUtils.fetchJson(logout_api_url, options).then(({ json }) => {
localStorage.removeItem("access_token"); localStorage.removeItem("access_token");
});
} }
return Promise.resolve();
}, },
// called when the API returns an error // called when the API returns an error
checkError: ({ status }) => { checkError: ({ status }) => {

View File

@ -1,135 +0,0 @@
import authProvider from "./authProvider";
describe("authProvider", () => {
beforeEach(() => {
fetch.resetMocks();
localStorage.clear();
});
describe("login", () => {
it("should successfully login with username and password", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
const ret = await authProvider.login({
base_url: "http://example.com",
username: "@user:example.com",
password: "secret",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith(
"http://example.com/_matrix/client/r0/login",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","user":"@user:example.com","password":"secret"}',
headers: new Headers({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("http://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
});
it("should successfully login with token", async () => {
fetch.once(
JSON.stringify({
home_server: "example.com",
user_id: "@user:example.com",
access_token: "foobar",
device_id: "some_device",
})
);
const ret = await authProvider.login({
base_url: "https://example.com/",
loginToken: "login_token",
});
expect(ret).toBe(undefined);
expect(fetch).toBeCalledWith(
"https://example.com/_matrix/client/r0/login",
{
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
headers: new Headers({
Accept: ["application/json"],
"Content-Type": ["application/json"],
}),
method: "POST",
}
);
expect(localStorage.getItem("base_url")).toEqual("https://example.com");
expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
expect(localStorage.getItem("access_token")).toEqual("foobar");
expect(localStorage.getItem("device_id")).toEqual("some_device");
});
describe("logout", () => {
it("should remove the access_token from localStorage", async () => {
localStorage.setItem("base_url", "example.com");
localStorage.setItem("access_token", "foo");
fetch.mockResponse(JSON.stringify({}));
await authProvider.logout();
expect(fetch).toBeCalledWith("example.com/_matrix/client/r0/logout", {
headers: new Headers({
Accept: ["application/json"],
Authorization: ["Bearer foo"],
}),
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
expect(localStorage.getItem("access_token")).toBeNull();
});
});
describe("checkError", () => {
it("should resolve if error.status is not 401 or 403", async () => {
await expect(
authProvider.checkError({ status: 200 })
).resolves.toBeUndefined();
});
it("should reject if error.status is 401", async () => {
await expect(
authProvider.checkError({ status: 401 })
).rejects.toBeUndefined();
});
it("should reject if error.status is 403", async () => {
await expect(
authProvider.checkError({ status: 403 })
).rejects.toBeUndefined();
});
});
describe("checkAuth", () => {
it("should reject when not logged in", async () => {
await expect(authProvider.checkAuth({})).rejects.toBeUndefined();
});
it("should resolve when logged in", async () => {
localStorage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});
});
describe("getPermissions", () => {
it("should do nothing", async () => {
await expect(authProvider.getPermissions()).resolves.toBeUndefined();
});
});
});

View File

@ -348,7 +348,7 @@ function getSearchOrder(order) {
} }
const dataProvider = { const dataProvider = {
getList: async (resource, params) => { getList: (resource, params) => {
console.log("getList " + resource); console.log("getList " + resource);
const { const {
user_id, user_id,
@ -383,14 +383,13 @@ const dataProvider = {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const url = `${endpoint_url}?${stringify(query)}`; const url = `${endpoint_url}?${stringify(query)}`;
const { json } = await jsonClient(url); return jsonClient(url).then(({ json }) => ({
return {
data: json[res.data].map(res.map), data: json[res.data].map(res.map),
total: res.total(json, from, perPage), total: res.total(json, from, perPage),
}; }));
}, },
getOne: async (resource, params) => { getOne: (resource, params) => {
console.log("getOne " + resource); console.log("getOne " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -398,13 +397,14 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const { json } = await jsonClient( return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`).then(
`${endpoint_url}/${encodeURIComponent(params.id)}` ({ json }) => ({
data: res.map(json),
})
); );
return { data: res.map(json) };
}, },
getMany: async (resource, params) => { getMany: (resource, params) => {
console.log("getMany " + resource); console.log("getMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -412,18 +412,17 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const responses = await Promise.all( return Promise.all(
params.ids.map(id => params.ids.map(id =>
jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`) jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`)
) )
); ).then(responses => ({
return {
data: responses.map(({ json }) => res.map(json)), data: responses.map(({ json }) => res.map(json)),
total: responses.length, total: responses.length,
}; }));
}, },
getManyReference: async (resource, params) => { getManyReference: (resource, params) => {
console.log("getManyReference " + resource); console.log("getManyReference " + resource);
const { page, perPage } = params.pagination; const { page, perPage } = params.pagination;
const { field, order } = params.sort; const { field, order } = params.sort;
@ -443,14 +442,13 @@ const dataProvider = {
const ref = res["reference"](params.id); const ref = res["reference"](params.id);
const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`; const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`;
const { json } = await jsonClient(endpoint_url); return jsonClient(endpoint_url).then(({ headers, json }) => ({
return {
data: json[res.data].map(res.map), data: json[res.data].map(res.map),
total: res.total(json, from, perPage), total: res.total(json, from, perPage),
}; }));
}, },
update: async (resource, params) => { update: (resource, params) => {
console.log("update " + resource); console.log("update " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -458,17 +456,15 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const { json } = await jsonClient( return jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, {
`${endpoint_url}/${encodeURIComponent(params.id)}`,
{
method: "PUT", method: "PUT",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.data, filterNullValues),
} }).then(({ json }) => ({
); data: res.map(json),
return { data: res.map(json) }; }));
}, },
updateMany: async (resource, params) => { updateMany: (resource, params) => {
console.log("updateMany " + resource); console.log("updateMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -476,7 +472,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const responses = await Promise.all( return Promise.all(
params.ids.map( params.ids.map(
id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`),
{ {
@ -484,11 +480,12 @@ const dataProvider = {
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.data, filterNullValues),
} }
) )
); ).then(responses => ({
return { data: responses.map(({ json }) => json) }; data: responses.map(({ json }) => json),
}));
}, },
create: async (resource, params) => { create: (resource, params) => {
console.log("create " + resource); console.log("create " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -498,14 +495,15 @@ const dataProvider = {
const create = res["create"](params.data); const create = res["create"](params.data);
const endpoint_url = homeserver + create.endpoint; const endpoint_url = homeserver + create.endpoint;
const { json } = await jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: create.method, method: create.method,
body: JSON.stringify(create.body, filterNullValues), body: JSON.stringify(create.body, filterNullValues),
}); }).then(({ json }) => ({
return { data: res.map(json) }; data: res.map(json),
}));
}, },
createMany: async (resource, params) => { createMany: (resource, params) => {
console.log("createMany " + resource); console.log("createMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -513,7 +511,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if (!("create" in res)) return Promise.reject(); if (!("create" in res)) return Promise.reject();
const responses = await Promise.all( return Promise.all(
params.ids.map(id => { params.ids.map(id => {
params.data.id = id; params.data.id = id;
const cre = res["create"](params.data); const cre = res["create"](params.data);
@ -523,11 +521,12 @@ const dataProvider = {
body: JSON.stringify(cre.body, filterNullValues), body: JSON.stringify(cre.body, filterNullValues),
}); });
}) })
); ).then(responses => ({
return { data: responses.map(({ json }) => json) }; data: responses.map(({ json }) => json),
}));
}, },
delete: async (resource, params) => { delete: (resource, params) => {
console.log("delete " + resource); console.log("delete " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -537,22 +536,24 @@ const dataProvider = {
if ("delete" in res) { if ("delete" in res) {
const del = res["delete"](params); const del = res["delete"](params);
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
const { json } = await jsonClient(endpoint_url, { return jsonClient(endpoint_url, {
method: "method" in del ? del.method : "DELETE", method: "method" in del ? del.method : "DELETE",
body: "body" in del ? JSON.stringify(del.body) : null, body: "body" in del ? JSON.stringify(del.body) : null,
}); }).then(({ json }) => ({
return { data: json }; data: json,
}));
} else { } else {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const { json } = await jsonClient(`${endpoint_url}/${params.id}`, { return jsonClient(`${endpoint_url}/${params.id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify(params.previousData, filterNullValues), body: JSON.stringify(params.previousData, filterNullValues),
}); }).then(({ json }) => ({
return { data: json }; data: json,
}));
} }
}, },
deleteMany: async (resource, params) => { deleteMany: (resource, params) => {
console.log("deleteMany " + resource); console.log("deleteMany " + resource);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -560,7 +561,7 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
if ("delete" in res) { if ("delete" in res) {
const responses = await Promise.all( return Promise.all(
params.ids.map(id => { params.ids.map(id => {
const del = res["delete"]({ ...params, id: id }); const del = res["delete"]({ ...params, id: id });
const endpoint_url = homeserver + del.endpoint; const endpoint_url = homeserver + del.endpoint;
@ -569,21 +570,21 @@ const dataProvider = {
body: "body" in del ? JSON.stringify(del.body) : null, body: "body" in del ? JSON.stringify(del.body) : null,
}); });
}) })
); ).then(responses => ({
return {
data: responses.map(({ json }) => json), data: responses.map(({ json }) => json),
}; }));
} else { } else {
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
const responses = await Promise.all( return Promise.all(
params.ids.map(id => params.ids.map(id =>
jsonClient(`${endpoint_url}/${id}`, { jsonClient(`${endpoint_url}/${id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(params.data, filterNullValues),
}) })
) )
); ).then(responses => ({
return { data: responses.map(({ json }) => json) }; data: responses.map(({ json }) => json),
}));
} }
}, },
}; };

View File

@ -36,13 +36,6 @@ export const getServerVersion = async baseUrl => {
return response.json.server_version; return response.json.server_version;
}; };
/** Get supported Matrix features */
export const getSupportedFeatures = async baseUrl => {
const versionUrl = `${baseUrl}/_matrix/client/versions`;
const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" });
return response.json;
};
/** /**
* Get supported login flows * Get supported login flows
* @param baseUrl the base URL of the homeserver * @param baseUrl the base URL of the homeserver