diff --git a/README.md b/README.md
index 1ab9552..85ee974 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
This project is built using [react-admin](https://marmelab.com/react-admin/).
-It needs at least Synapse v1.41.0 for all functions to work as expected!
+It needs at least Synapse v1.42.0 for all functions to work as expected!
You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://matrix-org.github.io/synapse/develop/admin_api/version_api.html).
diff --git a/src/App.js b/src/App.js
index 018f18c..ee24fac 100644
--- a/src/App.js
+++ b/src/App.js
@@ -8,12 +8,18 @@ import { RoomList, RoomShow } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports";
import LoginPage from "./components/LoginPage";
import UserIcon from "@material-ui/icons/Group";
+import ConfirmationNumberIcon from "@material-ui/icons/ConfirmationNumber";
import EqualizerIcon from "@material-ui/icons/Equalizer";
import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { ImportFeature } from "./components/ImportFeature";
+import {
+ RegistrationTokenCreate,
+ RegistrationTokenEdit,
+ RegistrationTokenList,
+} from "./components/RegistrationTokens";
import { RoomDirectoryList } from "./components/RoomDirectory";
import { Route } from "react-router-dom";
import germanMessages from "./i18n/de";
@@ -66,6 +72,13 @@ const App = () => (
list={RoomDirectoryList}
icon={FolderSharedIcon}
/>
+
diff --git a/src/components/EventReports.js b/src/components/EventReports.js
index c73647f..fc8f621 100644
--- a/src/components/EventReports.js
+++ b/src/components/EventReports.js
@@ -15,6 +15,15 @@ import {
import PageviewIcon from "@material-ui/icons/Pageview";
import ViewListIcon from "@material-ui/icons/ViewList";
+const date_format = {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+};
+
const ReportPagination = props => (
);
@@ -33,14 +42,7 @@ export const ReportShow = props => {
@@ -72,14 +74,7 @@ export const ReportShow = props => {
@@ -116,14 +111,7 @@ export const ReportList = ({ ...props }) => {
diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js
index d0fbd06..095d8e1 100644
--- a/src/components/LoginPage.js
+++ b/src/components/LoginPage.js
@@ -78,11 +78,48 @@ const LoginPage = ({ theme }) => {
const login = useLogin();
const notify = useNotify();
const [loading, setLoading] = useState(false);
+ const [supportPassAuth, setSupportPassAuth] = useState(true);
var locale = useLocale();
const setLocale = useSetLocale();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
+ const [ssoBaseUrl, setSSOBaseUrl] = useState("");
+ const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
+
+ if (loginToken) {
+ const ssoToken = loginToken[1];
+ console.log("SSO token is", ssoToken);
+ // Prevent further requests
+ window.history.replaceState(
+ {},
+ "",
+ window.location.href.replace(loginToken[0], "#").split("#")[0]
+ );
+ const baseUrl = localStorage.getItem("sso_base_url");
+ localStorage.removeItem("sso_base_url");
+ if (baseUrl) {
+ const auth = {
+ base_url: baseUrl,
+ username: null,
+ password: null,
+ loginToken: ssoToken,
+ };
+ console.log("Base URL is:", baseUrl);
+ console.log("SSO Token is:", ssoToken);
+ console.log("Let's try token login...");
+ login(auth).catch(error => {
+ alert(
+ typeof error === "string"
+ ? error
+ : typeof error === "undefined" || !error.message
+ ? "ra.auth.sign_in_error"
+ : error.message
+ );
+ console.error(error);
+ });
+ }
+ }
const renderInput = ({
meta: { touched, error } = {},
@@ -137,6 +174,14 @@ const LoginPage = ({ theme }) => {
});
};
+ const handleSSO = () => {
+ localStorage.setItem("sso_base_url", ssoBaseUrl);
+ const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
+ window.location.href
+ )}`;
+ window.location.href = ssoFullUrl;
+ };
+
const extractHomeServer = username => {
const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
if (!username) return null;
@@ -188,6 +233,31 @@ const LoginPage = ({ theme }) => {
.catch(_ => {
setServerVersion("");
});
+
+ // Set SSO Url
+ const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`;
+ let supportPass = false,
+ supportSSO = false;
+ fetchUtils
+ .fetchJson(authMethodUrl, { method: "GET" })
+ .then(({ json }) => {
+ json.flows.forEach(f => {
+ if (f.type === "m.login.password") {
+ supportPass = true;
+ } else if (f.type === "m.login.sso") {
+ supportSSO = true;
+ }
+ });
+ setSupportPassAuth(supportPass);
+ if (supportSSO) {
+ setSSOBaseUrl(formData.base_url);
+ } else {
+ setSSOBaseUrl("");
+ }
+ })
+ .catch(_ => {
+ setSSOBaseUrl("");
+ });
},
[formData.base_url]
);
@@ -200,7 +270,7 @@ const LoginPage = ({ theme }) => {
name="username"
component={renderInput}
label="ra.auth.username"
- disabled={loading}
+ disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
fullWidth
@@ -212,7 +282,7 @@ const LoginPage = ({ theme }) => {
component={renderInput}
label="ra.auth.password"
type="password"
- disabled={loading}
+ disabled={loading || !supportPassAuth}
resettable
fullWidth
/>
@@ -273,13 +343,24 @@ const LoginPage = ({ theme }) => {
variant="contained"
type="submit"
color="primary"
- disabled={loading}
+ disabled={loading || !supportPassAuth}
className={classes.button}
fullWidth
>
{loading && }
{translate("ra.auth.sign_in")}
+
diff --git a/src/components/RegistrationTokens.js b/src/components/RegistrationTokens.js
new file mode 100644
index 0000000..28213e5
--- /dev/null
+++ b/src/components/RegistrationTokens.js
@@ -0,0 +1,132 @@
+import React from "react";
+import {
+ BooleanInput,
+ Create,
+ Datagrid,
+ DateField,
+ DateTimeInput,
+ Edit,
+ Filter,
+ List,
+ maxValue,
+ number,
+ NumberField,
+ NumberInput,
+ regex,
+ SimpleForm,
+ TextInput,
+ TextField,
+ Toolbar,
+} from "react-admin";
+
+const date_format = {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+};
+
+const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
+const validateUsesAllowed = [number()];
+const validateLength = [number(), maxValue(64)];
+
+const dateParser = v => {
+ const d = new Date(v);
+ if (isNaN(d)) return 0;
+ return d.getTime();
+};
+
+const dateFormatter = v => {
+ if (v === undefined || v === null) return;
+ const d = new Date(v);
+
+ const pad = "00";
+ const year = d.getFullYear().toString();
+ const month = (pad + (d.getMonth() + 1).toString()).slice(-2);
+ const day = (pad + d.getDate().toString()).slice(-2);
+ const hour = (pad + d.getHours().toString()).slice(-2);
+ const minute = (pad + d.getMinutes().toString()).slice(-2);
+
+ // target format yyyy-MM-ddThh:mm
+ return `${year}-${month}-${day}T${hour}:${minute}`;
+};
+
+const RegistrationTokenFilter = props => (
+
+
+
+);
+
+export const RegistrationTokenList = props => {
+ return (
+
}
+ filterDefaultValues={{ valid: true }}
+ pagination={false}
+ perPage={500}
+ >
+
+
+
+
+
+
+
+
+ );
+};
+
+export const RegistrationTokenCreate = props => (
+
+ }>
+
+
+
+
+
+
+);
+
+export const RegistrationTokenEdit = props => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/devices.js b/src/components/devices.js
index ee9292c..5003987 100644
--- a/src/components/devices.js
+++ b/src/components/devices.js
@@ -8,7 +8,7 @@ import {
} from "react-admin";
import ActionDelete from "@material-ui/icons/Delete";
import { makeStyles } from "@material-ui/core/styles";
-import { fade } from "@material-ui/core/styles/colorManipulator";
+import { alpha } from "@material-ui/core/styles/colorManipulator";
import classnames from "classnames";
const useStyles = makeStyles(
@@ -16,7 +16,7 @@ const useStyles = makeStyles(
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
- backgroundColor: fade(theme.palette.error.main, 0.12),
+ backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
diff --git a/src/components/media.js b/src/components/media.js
index a523fa2..a628b79 100644
--- a/src/components/media.js
+++ b/src/components/media.js
@@ -1,6 +1,6 @@
import React, { Fragment, useState } from "react";
import classnames from "classnames";
-import { fade } from "@material-ui/core/styles/colorManipulator";
+import { alpha } from "@material-ui/core/styles/colorManipulator";
import { makeStyles } from "@material-ui/core/styles";
import { Tooltip } from "@material-ui/core";
import {
@@ -33,7 +33,7 @@ const useStyles = makeStyles(
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
- backgroundColor: fade(theme.palette.error.main, 0.12),
+ backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
diff --git a/src/components/rooms.js b/src/components/rooms.js
index fa01c77..309b2c4 100644
--- a/src/components/rooms.js
+++ b/src/components/rooms.js
@@ -41,6 +41,15 @@ import {
RoomDirectorySaveButton,
} from "./RoomDirectory";
+const date_format = {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+};
+
const useStyles = makeStyles(theme => ({
helper_forward_extremities: {
fontFamily: "Roboto, Helvetica, Arial, sans-serif",
@@ -247,14 +256,7 @@ export const RoomShow = props => {
@@ -287,14 +289,7 @@ export const RoomShow = props => {
diff --git a/src/components/users.js b/src/components/users.js
index 83694f9..ecc35f6 100644
--- a/src/components/users.js
+++ b/src/components/users.js
@@ -71,6 +71,15 @@ const useStyles = makeStyles({
},
});
+const date_format = {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+};
+
const UserListActions = ({
currentSort,
className,
@@ -180,14 +189,7 @@ export const UserList = props => {
source="creation_ts"
label="resources.users.fields.creation_ts_ms"
showTime
- options={{
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- }}
+ options={date_format}
/>
@@ -336,18 +338,7 @@ export const UserEdit = props => {
source="deactivated"
helperText="resources.users.helper.deactivate"
/>
-
+
@@ -404,14 +395,7 @@ export const UserEdit = props => {
@@ -439,14 +423,7 @@ export const UserEdit = props => {
{
sort={{ field: "created_ts", order: "DESC" }}
>
-
+
diff --git a/src/i18n/de.js b/src/i18n/de.js
index ced6512..29c4436 100644
--- a/src/i18n/de.js
+++ b/src/i18n/de.js
@@ -10,6 +10,7 @@ const de = {
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",
+ sso_sign_in: "Anmeldung mit SSO",
},
users: {
invalid_user_id: "Lokaler Anteil der Matrix Benutzer-ID ohne Homeserver.",
@@ -351,6 +352,19 @@ const de = {
send_failure: "Beim Entfernen ist ein Fehler aufgetreten.",
},
},
+ registration_tokens: {
+ name: "Registrierungstoken",
+ fields: {
+ token: "Token",
+ valid: "Gültige Token",
+ uses_allowed: "Anzahl",
+ pending: "Ausstehend",
+ completed: "Abgeschlossen",
+ expiry_time: "Ablaufzeit",
+ length: "Länge",
+ },
+ helper: { length: "Länge des Tokens, wenn kein Token vorgegeben wird." },
+ },
},
ra: {
...germanMessages.ra,
@@ -371,7 +385,7 @@ const de = {
},
},
notification: {
- ...germanMessages.ra.notifiaction,
+ ...germanMessages.ra.notification,
logged_out: "Abgemeldet",
},
page: {
diff --git a/src/i18n/en.js b/src/i18n/en.js
index 5ceaed9..fb63005 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -10,6 +10,7 @@ const en = {
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",
+ sso_sign_in: "Sign in with SSO",
},
users: {
invalid_user_id: "Localpart of a Matrix user-id without homeserver.",
@@ -173,10 +174,12 @@ const en = {
},
unencrypted: "Unencrypted",
},
- erase: {
- title: "Delete room",
- content:
- "Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
+ action: {
+ erase: {
+ title: "Delete room",
+ content:
+ "Are you sure you want to delete the room? This cannot be undone. All messages and shared media in the room will be deleted from the server!",
+ },
},
},
reports: {
@@ -348,5 +351,18 @@ const en = {
},
},
},
+ registration_tokens: {
+ name: "Registration tokens",
+ fields: {
+ token: "Token",
+ valid: "Valid token",
+ uses_allowed: "Uses allowed",
+ pending: "Pending",
+ completed: "Completed",
+ expiry_time: "Expiry time",
+ length: "Length",
+ },
+ helper: { length: "Length of the token if no token is given." },
+ },
};
export default en;
diff --git a/src/i18n/zh.js b/src/i18n/zh.js
index 3c36366..7a9b4e7 100644
--- a/src/i18n/zh.js
+++ b/src/i18n/zh.js
@@ -10,10 +10,12 @@ const zh = {
username_error: "请输入完整有效的用户 ID: '@user:domain'",
protocol_error: "URL 需要以'http://'或'https://'作为起始",
url_error: "不是一个有效的 Matrix 服务器地址",
+ sso_sign_in: "使用 SSO 登录",
},
users: {
invalid_user_id:
"必须要是一个有效的 Matrix 用户 ID ,例如 @user_id:homeserver",
+ tabs: { sso: "SSO" },
},
rooms: {
tabs: {
diff --git a/src/synapse/authProvider.js b/src/synapse/authProvider.js
index 470637f..723f155 100644
--- a/src/synapse/authProvider.js
+++ b/src/synapse/authProvider.js
@@ -2,20 +2,31 @@ import { fetchUtils } from "react-admin";
const authProvider = {
// called when the user attempts to log in
- login: ({ base_url, username, password }) => {
+ login: ({ base_url, username, password, loginToken }) => {
// force homeserver for protection in case the form is manipulated
base_url = process.env.REACT_APP_SERVER || base_url;
console.log("login ");
const options = {
method: "POST",
- body: JSON.stringify({
- type: "m.login.password",
- user: username,
- password: password,
- device_id: localStorage.getItem("device_id"),
- initial_device_display_name: "Synapse Admin",
- }),
+ body: JSON.stringify(
+ Object.assign(
+ {
+ device_id: localStorage.getItem("device_id"),
+ initial_device_display_name: "Synapse Admin",
+ },
+ loginToken
+ ? {
+ type: "m.login.token",
+ token: loginToken,
+ }
+ : {
+ type: "m.login.password",
+ user: username,
+ password: password,
+ }
+ )
+ ),
};
// use the base_url from login instead of the well_known entry from the
diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js
index 5b11139..b80b075 100644
--- a/src/synapse/dataProvider.js
+++ b/src/synapse/dataProvider.js
@@ -275,6 +275,25 @@ const resourceMap = {
method: "PUT",
}),
},
+ registration_tokens: {
+ path: "/_synapse/admin/v1/registration_tokens",
+ map: rt => ({
+ ...rt,
+ id: rt.token,
+ }),
+ data: "registration_tokens",
+ total: json => {
+ return json.registration_tokens.length;
+ },
+ create: params => ({
+ endpoint: "/_synapse/admin/v1/registration_tokens/new",
+ body: params,
+ method: "POST",
+ }),
+ delete: params => ({
+ endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`,
+ }),
+ },
};
function filterNullValues(key, value) {
@@ -296,7 +315,8 @@ function getSearchOrder(order) {
const dataProvider = {
getList: (resource, params) => {
console.log("getList " + resource);
- const { user_id, name, guests, deactivated, search_term } = params.filter;
+ const { user_id, name, guests, deactivated, search_term, valid } =
+ params.filter;
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const from = (page - 1) * perPage;
@@ -308,6 +328,7 @@ const dataProvider = {
name: name,
guests: guests,
deactivated: deactivated,
+ valid: valid,
order_by: field,
dir: getSearchOrder(order),
};
diff --git a/yarn.lock b/yarn.lock
index adf2277..2a27af2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5290,9 +5290,9 @@ flush-write-stream@^1.0.0:
readable-stream "^2.3.6"
follow-redirects@^1.0.0:
- version "1.14.7"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
- integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
+ version "1.14.8"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
+ integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
for-in@^1.0.2:
version "1.0.2"
@@ -11457,9 +11457,9 @@ url-loader@4.1.1:
schema-utils "^3.0.0"
url-parse@^1.4.3, url-parse@^1.5.3:
- version "1.5.3"
- resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
- integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
+ version "1.5.7"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
+ integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"