Compare commits

...

13 Commits

Author SHA1 Message Date
311db77306
Disable registration tokens 2024-04-20 22:59:35 +03:00
7c08f846a5
Update translate 2024-04-20 22:52:08 +03:00
f2f096d8a5
Update russian language 2024-04-20 22:34:55 +03:00
16191d9cc8
Update russian language 2024-04-20 22:08:36 +03:00
703129f88b
Add russian language 2024-04-19 13:14:04 +03:00
Manuel Stahl
c9364f631b Fetch tags in github workflows
Tags are required to construct the version information.

Change-Id: Ic1af3e8f50eafafcc8a0c3ca37f362d6bd05e116
2024-04-18 21:13:06 +02:00
Manuel Stahl
08dc5f6271 Push docker images also to ghcr.io
Fixes #350.

Change-Id: Ifdb7e4e7fda46efd0ed9e760587033f52ff4a130
2024-04-18 17:44:06 +02:00
Manuel Stahl
c9cb9aa9e0 Show Matrix specs supported by the homeserver
Change-Id: I01c110fb4b3de4de49b34f290c91c8bf424521fe
2024-04-18 10:01:52 +02:00
Manuel Stahl
25020c2d5b Remove unused function "renderInput"
Seems to be obsolete since react-admin v4.

Change-Id: I9f1d528a43510efd61befd23a05d1c8ebf40ddfd
2024-04-18 10:01:52 +02:00
Manuel Stahl
1acffdb618 Make functions in dataProvider async
Change-Id: Iab36ba6379340e47e7d58b1b2d882cd7cc111f41
2024-04-18 10:01:52 +02:00
Manuel Stahl
0b4f3a60c0 Make login and logout in authProvider async
Change-Id: I6bfb1c7a5a3c5a43f9fa622e87d9d487a95a0b6e
2024-04-18 10:01:52 +02:00
Manuel Stahl
33d29e01b1 Add authProvider test
Change-Id: Ia5acce659a386437687e38ae03d578e3bccb9324
2024-04-18 10:01:52 +02:00
Gavin Mogan
a2e47cb793
Add source urls to docker so tools can find sourcecode (#506)
For tools like renovate or dependabot, they like to put changelog notes in PRs updating deps. Having the labels allows the tools to link it back to sourcecode and share commits/release notes
2024-04-17 20:32:41 +02:00
14 changed files with 666 additions and 118 deletions

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# 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
WORKDIR /src

View File

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

View File

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

View File

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

View File

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

View File

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

390
src/i18n/ru.js Normal file
View File

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

View File

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

View File

@ -36,6 +36,13 @@ export const getServerVersion = async baseUrl => {
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
* @param baseUrl the base URL of the homeserver