Compare commits
7 Commits
v0.10.3-et
...
v0.10.3-et
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d021021df | ||
|
|
19302466ef | ||
|
|
0594259ae4 | ||
|
|
ba485bbb18 | ||
|
|
9fc005032c | ||
|
|
f5d6f24b30 | ||
|
|
a42efe7eda |
@@ -7,7 +7,7 @@ This project is built using [react-admin](https://marmelab.com/react-admin/).
|
|||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Fork differences](#fork-differences)
|
* [Fork differences](#fork-differences)
|
||||||
* [Available via CDN](#available-via-cdn)
|
* [Availability](#availability)
|
||||||
* [Changes](#changes)
|
* [Changes](#changes)
|
||||||
* [Development](#development)
|
* [Development](#development)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
@@ -33,9 +33,11 @@ With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologie
|
|||||||
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
|
||||||
user-friendly interface for managing Synapse homeservers.
|
user-friendly interface for managing Synapse homeservers.
|
||||||
|
|
||||||
### Available via CDN
|
### Availability
|
||||||
|
|
||||||
On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.
|
* As a core/default component on [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=synapse-admin)
|
||||||
|
* Via CDN on [admin.etke.cc](https://admin.etke.cc)
|
||||||
|
* As a component in [Matrix-Docker-Ansible-Deploy Playbook](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-synapse-admin.md)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ The following changes are already implemented:
|
|||||||
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
|
* [Provide options to delete media and redact events on user erase](https://github.com/etkecc/synapse-admin/pull/49)
|
||||||
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
* [Authenticated Media support](https://github.com/etkecc/synapse-admin/pull/51)
|
||||||
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
|
* [Better media preview/download](https://github.com/etkecc/synapse-admin/pull/53)
|
||||||
|
* [Login with access token](https://github.com/etkecc/synapse-admin/pull/58)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
|
|||||||
8
justfile
8
justfile
@@ -25,11 +25,15 @@ run-dev:
|
|||||||
stop-dev:
|
stop-dev:
|
||||||
@docker-compose -f docker-compose-dev.yml stop
|
@docker-compose -f docker-compose-dev.yml stop
|
||||||
|
|
||||||
|
# register a user in the dev stack
|
||||||
register-user localpart password *admin:
|
register-user localpart password *admin:
|
||||||
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
|
docker-compose exec synapse register_new_matrix_user {{ if admin == "1" {"--admin"} else {"--no-admin"} }} -u {{ localpart }} -p {{ password }} -c /config/homeserver.yaml http://localhost:8008
|
||||||
|
|
||||||
|
# run yarn {fix,lint,test} commands
|
||||||
|
test:
|
||||||
|
@-yarn run fix
|
||||||
|
@-yarn run lint
|
||||||
|
@-yarn run test
|
||||||
|
|
||||||
# run the app in a production mode
|
# run the app in a production mode
|
||||||
run-prod: build
|
run-prod: build
|
||||||
|
|||||||
@@ -1,16 +1,60 @@
|
|||||||
import { Layout, Menu } from 'react-admin';
|
import { AppBar, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
|
||||||
import LiveHelpIcon from '@mui/icons-material/LiveHelp';
|
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
|
||||||
|
import { LoginMethod } from "../pages/LoginPage";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
const DEFAULT_SUPPORT_LINK = "https://github.com/etkecc/synapse-admin/issues";
|
||||||
const supportLink = (): string => {
|
const supportLink = (): string => {
|
||||||
try {
|
try {
|
||||||
new URL(localStorage.getItem("support_url") || ''); // Check if the URL is valid
|
new URL(localStorage.getItem("support_url") || ""); // Check if the URL is valid
|
||||||
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
return localStorage.getItem("support_url") || DEFAULT_SUPPORT_LINK;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return DEFAULT_SUPPORT_LINK;
|
return DEFAULT_SUPPORT_LINK;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AdminUserMenu = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const logout = useLogout();
|
||||||
|
const checkLoginType = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const loginType: LoginMethod = (localStorage.getItem("login_type") || "credentials") as LoginMethod;
|
||||||
|
if (loginType === "accessToken") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setOpen(false);
|
||||||
|
logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
|
localStorage.removeItem("login_type");
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserMenu>
|
||||||
|
<div onClickCapture={checkLoginType}>
|
||||||
|
<Logout />
|
||||||
|
</div>
|
||||||
|
<Confirm
|
||||||
|
isOpen={open}
|
||||||
|
title="synapseadmin.auth.logout_acces_token_dialog.title"
|
||||||
|
content="synapseadmin.auth.logout_acces_token_dialog.content"
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleDialogClose}
|
||||||
|
confirm="synapseadmin.auth.logout_acces_token_dialog.confirm"
|
||||||
|
cancel="synapseadmin.auth.logout_acces_token_dialog.cancel"
|
||||||
|
/>
|
||||||
|
</UserMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminAppBar = () => <AppBar userMenu={<AdminUserMenu />} />;
|
||||||
|
|
||||||
const AdminMenu = () => (
|
const AdminMenu = () => (
|
||||||
<Menu>
|
<Menu>
|
||||||
@@ -20,7 +64,7 @@ const AdminMenu = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AdminLayout = ({ children }) => (
|
export const AdminLayout = ({ children }) => (
|
||||||
<Layout menu={AdminMenu}>
|
<Layout appBar={AdminAppBar} menu={AdminMenu}>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
import { Avatar, AvatarProps } from "@mui/material";
|
import { Avatar, AvatarProps } from "@mui/material";
|
||||||
import { useRecordContext } from "react-admin";
|
import { FieldProps, useRecordContext } from "react-admin";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
|
||||||
|
|
||||||
const AvatarField = ({ source, ...rest }: AvatarProps & { source: string, label?: string }) => {
|
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
|
||||||
const { alt, classes, sizes, sx, variant } = rest;
|
const { alt, classes, sizes, sx, variant } = rest;
|
||||||
|
|
||||||
const record = useRecordContext(rest);
|
const record = useRecordContext(rest);
|
||||||
const mxcURL = get(record, source)?.toString();
|
const mxcURL = get(record, source)?.toString();
|
||||||
|
|
||||||
|
|||||||
58
src/components/LoginFormBox.tsx
Normal file
58
src/components/LoginFormBox.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
const LoginFormBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minHeight: "calc(100vh - 1rem)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
background: "url(./images/floating-cogs.svg)",
|
||||||
|
backgroundColor: "#f9f9f9",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
|
||||||
|
[`& .card`]: {
|
||||||
|
width: "30rem",
|
||||||
|
marginTop: "6rem",
|
||||||
|
marginBottom: "6rem",
|
||||||
|
},
|
||||||
|
[`& .avatar`]: {
|
||||||
|
margin: "1rem",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
[`& .icon`]: {
|
||||||
|
backgroundColor: theme.palette.grey[500],
|
||||||
|
},
|
||||||
|
[`& .hint`]: {
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
[`& .form`]: {
|
||||||
|
padding: "0 1rem 1rem 1rem",
|
||||||
|
},
|
||||||
|
[`& .select`]: {
|
||||||
|
marginBottom: "2rem",
|
||||||
|
},
|
||||||
|
[`& .actions`]: {
|
||||||
|
padding: "0 1rem 1rem 1rem",
|
||||||
|
},
|
||||||
|
[`& .serverVersion`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
marginLeft: "0.5rem",
|
||||||
|
},
|
||||||
|
[`& .matrixVersions`]: {
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
marginLeft: "0.5rem",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default LoginFormBox;
|
||||||
@@ -22,6 +22,14 @@ const de: SynapseTranslationMessages = {
|
|||||||
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",
|
||||||
sso_sign_in: "Anmeldung mit SSO",
|
sso_sign_in: "Anmeldung mit SSO",
|
||||||
|
credentials: "Anmeldedaten",
|
||||||
|
access_token: "Zugriffstoken",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Sie verwenden ein bestehendes Matrix-Zugriffstoken.",
|
||||||
|
content: "Möchten Sie diese Sitzung (die anderswo, z.B. in einem Matrix-Client, verwendet werden könnte) beenden oder sich nur vom Admin-Panel abmelden?",
|
||||||
|
confirm: "Sitzung beenden",
|
||||||
|
cancel: "Nur vom Admin-Panel abmelden",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ const en: SynapseTranslationMessages = {
|
|||||||
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",
|
||||||
sso_sign_in: "Sign in with SSO",
|
sso_sign_in: "Sign in with SSO",
|
||||||
|
credentials: "Credentials",
|
||||||
|
access_token: "Access token",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "You are using an existing Matrix access token.",
|
||||||
|
content: "Do you want to destroy this session (that could be used elsewhere, e.g. in a Matrix client) or just logout from the admin panel?",
|
||||||
|
confirm: "Destroy session",
|
||||||
|
cancel: "Just logout from admin panel",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ const fa: SynapseTranslationMessages = {
|
|||||||
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
protocol_error: "URL باید با 'http://' یا 'https://' شروع شود",
|
||||||
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
url_error: "آدرس وارد شده یک سرور معتبر نیست",
|
||||||
sso_sign_in: "با SSO وارد شوید",
|
sso_sign_in: "با SSO وارد شوید",
|
||||||
|
credentials: "اعتبارنامه",
|
||||||
|
access_token: "توکن دسترسی",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "شما در حال استفاده از یک نشانه دسترسی ماتریکس موجود هستید.",
|
||||||
|
content: "آیا میخواهید این جلسه (که میتواند در جای دیگر، مانند یک کلاینت ماتریکس استفاده شود) را نابود کنید یا فقط از پنل مدیریت خارج شوید؟",
|
||||||
|
confirm: "نابودی جلسه",
|
||||||
|
cancel: "فقط خروج از پنل مدیریت",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
invalid_user_id: "بخش محلی یک شناسه کاربری ماتریکس بدون سرور خانگی.",
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ const fr: SynapseTranslationMessages = {
|
|||||||
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
protocol_error: "L'URL doit commencer par « http:// » ou « https:// »",
|
||||||
url_error: "L'URL du serveur Matrix n'est pas valide",
|
url_error: "L'URL du serveur Matrix n'est pas valide",
|
||||||
sso_sign_in: "Se connecter avec l’authentification unique",
|
sso_sign_in: "Se connecter avec l’authentification unique",
|
||||||
|
credentials: "Identifiants",
|
||||||
|
access_token: "Jeton d'accès",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Vous utilisez un jeton d'accès Matrix existant.",
|
||||||
|
content: "Voulez-vous détruire cette session (qui pourrait être utilisée ailleurs, par exemple dans un client Matrix) ou simplement vous déconnecter du panneau d'administration?",
|
||||||
|
confirm: "Détruire la session",
|
||||||
|
cancel: "Se déconnecter simplement du panneau d'administration",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
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.",
|
||||||
|
|||||||
8
src/i18n/index.d.ts
vendored
8
src/i18n/index.d.ts
vendored
@@ -11,6 +11,14 @@ interface SynapseTranslationMessages extends TranslationMessages {
|
|||||||
protocol_error: string;
|
protocol_error: string;
|
||||||
url_error: string;
|
url_error: string;
|
||||||
sso_sign_in: string;
|
sso_sign_in: string;
|
||||||
|
credentials: string;
|
||||||
|
access_token: string;
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
confirm: string;
|
||||||
|
cancel: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: string;
|
invalid_user_id: string;
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ const it: SynapseTranslationMessages = {
|
|||||||
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
protocol_error: "L'URL deve iniziare per 'http://' o 'https://'",
|
||||||
url_error: "URL del server Matrix non valido",
|
url_error: "URL del server Matrix non valido",
|
||||||
sso_sign_in: "Accedi con SSO",
|
sso_sign_in: "Accedi con SSO",
|
||||||
|
credentials: "Credenziali",
|
||||||
|
access_token: "Token di accesso",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Stai utilizzando un token di accesso Matrix esistente.",
|
||||||
|
content: "Vuoi distruggere questa sessione (che potrebbe essere utilizzata altrove, ad esempio in un client Matrix) o semplicemente disconnetterti dal pannello di amministrazione?",
|
||||||
|
confirm: "Distruggi sessione",
|
||||||
|
cancel: "Disconnetti solo dal pannello di amministrazione",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "ID utente non valido su questo homeserver.",
|
invalid_user_id: "ID utente non valido su questo homeserver.",
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ const ru: SynapseTranslationMessages = {
|
|||||||
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
protocol_error: "Адрес должен начинаться с 'http://' или 'https://'",
|
||||||
url_error: "Неверный адрес сервера Matrix",
|
url_error: "Неверный адрес сервера Matrix",
|
||||||
sso_sign_in: "Вход через SSO",
|
sso_sign_in: "Вход через SSO",
|
||||||
|
credentials: "Учетные данные",
|
||||||
|
access_token: "Токен доступа",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "Вы используете существующий токен доступа Matrix.",
|
||||||
|
content: "Вы хотите завершить эту сессию (которая может быть использована в другом месте, например, в клиенте Matrix) или просто выйти из панели администрирования?",
|
||||||
|
confirm: "Завершить сессию",
|
||||||
|
cancel: "Просто выйти из панели администрирования",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
invalid_user_id: "Локальная часть ID пользователя Matrix без адреса домашнего сервера.",
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ const zh: SynapseTranslationMessages = {
|
|||||||
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
protocol_error: "URL 需要以'http://'或'https://'作为起始",
|
||||||
url_error: "不是一个有效的 Matrix 服务器地址",
|
url_error: "不是一个有效的 Matrix 服务器地址",
|
||||||
sso_sign_in: "使用 SSO 登录",
|
sso_sign_in: "使用 SSO 登录",
|
||||||
|
credentials: "凭证",
|
||||||
|
access_token: "访问令牌",
|
||||||
|
logout_acces_token_dialog: {
|
||||||
|
title: "您正在使用现有的 Matrix 访问令牌。",
|
||||||
|
content: "您想销毁此会话(可能在其他地方使用,例如在 Matrix 客户端中)还是仅从管理面板退出?",
|
||||||
|
confirm: "销毁会话",
|
||||||
|
cancel: "仅从管理面板退出",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
invalid_user_id: "必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import storage from "./storage";
|
|||||||
fetch("config.json")
|
fetch("config.json")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props => {
|
.then(props => {
|
||||||
|
if (props.asManagedUsers) {
|
||||||
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||||
|
}
|
||||||
|
if (props.supportURL) {
|
||||||
storage.setItem("support_url", props.supportURL);
|
storage.setItem("support_url", props.supportURL);
|
||||||
|
}
|
||||||
return createRoot(document.getElementById("root")).render(
|
return createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppContext.Provider value={props}>
|
<AppContext.Provider value={props}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Typography } from "@mui/material";
|
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormDataConsumer,
|
FormDataConsumer,
|
||||||
@@ -17,7 +16,7 @@ import {
|
|||||||
useLocales,
|
useLocales,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import LoginFormBox from "../components/LoginFormBox";
|
||||||
import { useAppContext } from "../AppContext";
|
import { useAppContext } from "../AppContext";
|
||||||
import {
|
import {
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
@@ -29,66 +28,18 @@ import {
|
|||||||
} from "../synapse/synapse";
|
} from "../synapse/synapse";
|
||||||
import storage from "../storage";
|
import storage from "../storage";
|
||||||
|
|
||||||
const FormBox = styled(Box)(({ theme }) => ({
|
export type LoginMethod = "credentials" | "accessToken";
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
minHeight: "calc(100vh - 1rem)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
background: "url(./images/floating-cogs.svg)",
|
|
||||||
backgroundColor: "#f9f9f9",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
|
|
||||||
[`& .card`]: {
|
|
||||||
width: "30rem",
|
|
||||||
marginTop: "6rem",
|
|
||||||
marginBottom: "6rem",
|
|
||||||
},
|
|
||||||
[`& .avatar`]: {
|
|
||||||
margin: "1rem",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
[`& .icon`]: {
|
|
||||||
backgroundColor: theme.palette.grey[500],
|
|
||||||
},
|
|
||||||
[`& .hint`]: {
|
|
||||||
marginTop: "1em",
|
|
||||||
marginBottom: "1em",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: theme.palette.grey[600],
|
|
||||||
},
|
|
||||||
[`& .form`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .select`]: {
|
|
||||||
marginBottom: "2rem",
|
|
||||||
},
|
|
||||||
[`& .actions`]: {
|
|
||||||
padding: "0 1rem 1rem 1rem",
|
|
||||||
},
|
|
||||||
[`& .serverVersion`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
[`& .matrixVersions`]: {
|
|
||||||
color: theme.palette.grey[500],
|
|
||||||
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
marginLeft: "0.5rem",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const { restrictBaseUrl } = useAppContext();
|
const { restrictBaseUrl } = useAppContext();
|
||||||
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
|
||||||
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
|
const allowMultipleBaseUrls =
|
||||||
|
Array.isArray(restrictBaseUrl) &&
|
||||||
|
restrictBaseUrl.length > 0 &&
|
||||||
|
restrictBaseUrl[0] !== "" &&
|
||||||
|
restrictBaseUrl[0] !== null;
|
||||||
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
||||||
@@ -98,8 +49,13 @@ const LoginPage = () => {
|
|||||||
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
|
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
|
||||||
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
||||||
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
||||||
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loginToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (loginToken) {
|
|
||||||
const ssoToken = loginToken[1];
|
const ssoToken = loginToken[1];
|
||||||
console.log("SSO token is", ssoToken);
|
console.log("SSO token is", ssoToken);
|
||||||
// Prevent further requests
|
// Prevent further requests
|
||||||
@@ -127,7 +83,7 @@ const LoginPage = () => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}, [loginToken]);
|
||||||
|
|
||||||
const validateBaseUrl = value => {
|
const validateBaseUrl = value => {
|
||||||
if (!value.match(/^(http|https):\/\//)) {
|
if (!value.match(/^(http|https):\/\//)) {
|
||||||
@@ -212,6 +168,18 @@ const LoginPage = () => {
|
|||||||
}, [formData.base_url, form]);
|
}, [formData.base_url, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
value={loginMethod}
|
||||||
|
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
|
||||||
|
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
|
||||||
|
</Tabs>
|
||||||
|
{loginMethod === "credentials" ? (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -236,6 +204,18 @@ const LoginPage = () => {
|
|||||||
validate={required()}
|
validate={required()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
source="accessToken"
|
||||||
|
label="synapseadmin.auth.access_token"
|
||||||
|
disabled={loading}
|
||||||
|
resettable
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<TextInput
|
<TextInput
|
||||||
source="base_url"
|
source="base_url"
|
||||||
@@ -263,7 +243,7 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
||||||
<FormBox>
|
<LoginFormBox>
|
||||||
<Card className="card">
|
<Card className="card">
|
||||||
<Box className="avatar">
|
<Box className="avatar">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -312,7 +292,7 @@ const LoginPage = () => {
|
|||||||
</CardActions>
|
</CardActions>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</FormBox>
|
</LoginFormBox>
|
||||||
<Notification />
|
<Notification />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AutorenewIcon from "@mui/icons-material/Autorenew";
|
|||||||
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
import DestinationsIcon from "@mui/icons-material/CloudQueue";
|
||||||
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
import FolderSharedIcon from "@mui/icons-material/FolderShared";
|
||||||
import ViewListIcon from "@mui/icons-material/ViewList";
|
import ViewListIcon from "@mui/icons-material/ViewList";
|
||||||
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Datagrid,
|
Datagrid,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TabbedShowLayout,
|
TabbedShowLayout,
|
||||||
TextField,
|
TextField,
|
||||||
|
FunctionField,
|
||||||
TopToolbar,
|
TopToolbar,
|
||||||
useRecordContext,
|
useRecordContext,
|
||||||
useDelete,
|
useDelete,
|
||||||
@@ -35,13 +37,6 @@ import { get } from "lodash";
|
|||||||
|
|
||||||
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
const DestinationPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />;
|
||||||
|
|
||||||
const destinationRowSx = (record: RaRecord) => ({
|
|
||||||
backgroundColor: record.retry_last_ts > 0 ? "warning.light" : "primary.contrastText",
|
|
||||||
"& .MuiButtonBase-root": {
|
|
||||||
color: "primary.dark",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
const destinationFilters = [<SearchInput source="destination" alwaysOn />];
|
||||||
|
|
||||||
export const DestinationReconnectButton = () => {
|
export const DestinationReconnectButton = () => {
|
||||||
@@ -105,7 +100,22 @@ const RetryDateField = (props: DateFieldProps) => {
|
|||||||
return <DateField {...props} />;
|
return <DateField {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const destinationFieldRender = (record: RaRecord) => {
|
||||||
|
if (record.retry_last_ts > 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ErrorIcon fontSize="inherit" color="error" sx={{verticalAlign: "middle"}}/>
|
||||||
|
|
||||||
|
{record.destination}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <> {record.destination} </>;
|
||||||
|
}
|
||||||
|
|
||||||
export const DestinationList = (props: ListProps) => {
|
export const DestinationList = (props: ListProps) => {
|
||||||
|
const record = useRecordContext(props);
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@@ -113,8 +123,8 @@ export const DestinationList = (props: ListProps) => {
|
|||||||
pagination={<DestinationPagination />}
|
pagination={<DestinationPagination />}
|
||||||
sort={{ field: "destination", order: "ASC" }}
|
sort={{ field: "destination", order: "ASC" }}
|
||||||
>
|
>
|
||||||
<Datagrid rowSx={destinationRowSx} rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
<Datagrid rowClick={id => `${id}/show/rooms`} bulkActionButtons={false}>
|
||||||
<TextField source="destination" />
|
<FunctionField source="destination" render={destinationFieldRender} />
|
||||||
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
<DateField source="failure_ts" showTime options={DATE_FORMAT} />
|
||||||
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
<RetryDateField source="retry_last_ts" showTime options={DATE_FORMAT} />
|
||||||
<TextField source="retry_interval" />
|
<TextField source="retry_interval" />
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ import {
|
|||||||
NumberField,
|
NumberField,
|
||||||
useListContext,
|
useListContext,
|
||||||
useNotify,
|
useNotify,
|
||||||
ToolbarClasses,
|
|
||||||
Identifier,
|
Identifier,
|
||||||
|
ToolbarClasses,
|
||||||
RaRecord,
|
RaRecord,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
ImageField,
|
ImageField,
|
||||||
@@ -147,10 +147,6 @@ const UserBulkActionButtons = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
|
|
||||||
return `/users/${id}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserList = (props: ListProps) => (
|
export const UserList = (props: ListProps) => (
|
||||||
<List
|
<List
|
||||||
{...props}
|
{...props}
|
||||||
@@ -160,8 +156,11 @@ export const UserList = (props: ListProps) => (
|
|||||||
actions={<UserListActions />}
|
actions={<UserListActions />}
|
||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick={usersRowClick} bulkActionButtons={<UserBulkActionButtons />}>
|
<Datagrid
|
||||||
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} />
|
rowClick={(id: Identifier, resource: string) => `/${resource}/${id}`}
|
||||||
|
bulkActionButtons={<UserBulkActionButtons />}
|
||||||
|
>
|
||||||
|
<AvatarField source="avatar_src" sx={{ height: "40px", width: "40px" }} sortBy="avatar_url" />
|
||||||
<TextField source="id" sortBy="name" />
|
<TextField source="id" sortBy="name" />
|
||||||
<TextField source="displayname" />
|
<TextField source="displayname" />
|
||||||
<BooleanField source="is_guest" />
|
<BooleanField source="is_guest" />
|
||||||
@@ -212,9 +211,7 @@ const UserEditActions = () => {
|
|||||||
export const UserCreate = (props: CreateProps) => (
|
export const UserCreate = (props: CreateProps) => (
|
||||||
<Create
|
<Create
|
||||||
{...props}
|
{...props}
|
||||||
redirect={(resource, id, data) => {
|
redirect={(resource: string | undefined, id: Identifier | undefined) => `${resource}/${id}`}
|
||||||
return `users/${id}`;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SimpleForm>
|
<SimpleForm>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ describe("authProvider", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "http://example.com",
|
base_url: "http://example.com",
|
||||||
username: "@user:example.com",
|
username: "@user:example.com",
|
||||||
password: "secret",
|
password: "secret",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toEqual({redirectTo: "/"});
|
||||||
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
expect(fetch).toBeCalledWith("http://example.com/_matrix/client/r0/login", {
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.password","identifier":{"type":"m.id.user","user":"@user:example.com"},"password":"secret"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
@@ -55,12 +55,12 @@ describe("authProvider", () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ret: undefined = await authProvider.login({
|
const ret = await authProvider.login({
|
||||||
base_url: "https://example.com/",
|
base_url: "https://example.com/",
|
||||||
loginToken: "login_token",
|
loginToken: "login_token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ret).toBe(undefined);
|
expect(ret).toEqual({redirectTo: "/"});
|
||||||
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
expect(fetch).toHaveBeenCalledWith("https://example.com/_matrix/client/r0/login", {
|
||||||
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
body: '{"device_id":null,"initial_device_display_name":"Synapse Admin","type":"m.login.token","token":"login_token"}',
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ const authProvider: AuthProvider = {
|
|||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
loginToken,
|
loginToken,
|
||||||
|
accessToken,
|
||||||
}: {
|
}: {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
loginToken: string;
|
loginToken: string;
|
||||||
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options: Options = {
|
let options: Options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(
|
body: JSON.stringify(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@@ -55,11 +57,30 @@ const authProvider: AuthProvider = {
|
|||||||
storage.setItem("base_url", base_url);
|
storage.setItem("base_url", base_url);
|
||||||
|
|
||||||
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";
|
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/r0/login");
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (accessToken) {
|
||||||
|
// this a login with an already obtained access token, let's just validate it
|
||||||
|
options = {
|
||||||
|
headers: new Headers({
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
response = await fetchUtils.fetchJson(login_api_url, options);
|
response = await fetchUtils.fetchJson(login_api_url, options);
|
||||||
|
const json = response.json;
|
||||||
|
storage.setItem("home_server", accessToken ? base_url : json.home_server);
|
||||||
|
storage.setItem("user_id", json.user_id);
|
||||||
|
storage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||||
|
storage.setItem("device_id", json.device_id);
|
||||||
|
storage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||||
|
|
||||||
|
return Promise.resolve({redirectTo: "/"});
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
const error = err as HttpError;
|
const error = err as HttpError;
|
||||||
const errorStatus = error.status;
|
const errorStatus = error.status;
|
||||||
@@ -73,12 +94,6 @@ const authProvider: AuthProvider = {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = response.json;
|
|
||||||
storage.setItem("home_server", json.home_server);
|
|
||||||
storage.setItem("user_id", json.user_id);
|
|
||||||
storage.setItem("access_token", json.access_token);
|
|
||||||
storage.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: async () => {
|
||||||
@@ -102,6 +117,7 @@ const authProvider: AuthProvider = {
|
|||||||
console.log("Error logging out", err);
|
console.log("Error logging out", err);
|
||||||
} finally {
|
} finally {
|
||||||
storage.removeItem("access_token");
|
storage.removeItem("access_token");
|
||||||
|
storage.removeItem("login_type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user