From 437fd70d6d0eb53e0857a2bbc87fe0fdc357d521 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Thu, 23 Apr 2020 10:44:52 +0200 Subject: [PATCH 1/9] Make creating users a special case in dataProvider Since users are created with PUT instead of POST, this is actually a special case. Change-Id: Ibe430fcac4d81de9723abd650804ffa93f87bf6d --- src/synapse/dataProvider.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 7e60414..7a1500a 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -30,6 +30,11 @@ const resourceMap = { ? parseInt(json.next_token, 10) + perPage : from + json.users.length; }, + create: data => ({ + endpoint: `/_synapse/admin/v2/users/${data.id}`, + body: data, + method: "PUT", + }), delete: id => ({ endpoint: `/_synapse/admin/v1/deactivate/${id}`, body: { erase: true }, @@ -190,11 +195,13 @@ const dataProvider = { if (!homeserver || !(resource in resourceMap)) return Promise.reject(); const res = resourceMap[resource]; + if (!("create" in res)) return Promise.reject(); - const endpoint_url = homeserver + res.path; - return jsonClient(`${endpoint_url}/${params.data.id}`, { - method: "PUT", - body: JSON.stringify(params.data, filterNullValues), + const create = res["create"](params.data); + const endpoint_url = homeserver + create.endpoint; + return jsonClient(endpoint_url, { + method: create.method, + body: JSON.stringify(create.body, filterNullValues), }).then(({ json }) => ({ data: res.map(json), })); From dd022eab04d19ad1b11274c07b7cc622530442a3 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Thu, 30 Apr 2020 18:45:37 +0200 Subject: [PATCH 2/9] Validate URL on input instead of automatic rewrite of http to https Change-Id: I3f3a9c5fb408af1f03ef876456133b331dc4cea3 --- src/components/LoginPage.js | 8 ++++++++ src/i18n/de.js | 2 ++ src/i18n/en.js | 2 ++ src/synapse/authProvider.js | 16 +++------------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 19280b3..9cbcf6e 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -90,6 +90,14 @@ const LoginPage = ({ theme }) => { const errors = {}; if (!values.homeserver) { errors.homeserver = translate("ra.validation.required"); + } else { + if (!values.homeserver.match(/^(http|https):\/\//)) { + errors.homeserver = translate("synapseadmin.auth.protocol_error"); + } else if ( + !values.homeserver.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/) + ) { + errors.homeserver = translate("synapseadmin.auth.url_error"); + } } if (!values.username) { errors.username = translate("ra.validation.required"); diff --git a/src/i18n/de.js b/src/i18n/de.js index 07a83fe..b86dae0 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -6,6 +6,8 @@ export default { auth: { homeserver: "Heimserver", welcome: "Willkommen bei Synapse-admin", + protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", + url_error: "Keine gültige Matrix Server URL", }, users: { invalid_user_id: diff --git a/src/i18n/en.js b/src/i18n/en.js index 7bb739a..8a3974c 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -6,6 +6,8 @@ export default { auth: { homeserver: "Homeserver", welcome: "Welcome to Synapse-admin", + protocol_error: "URL has to start with 'http://' or 'https://'", + url_error: "Not a valid Matrix server URL", }, users: { invalid_user_id: diff --git a/src/synapse/authProvider.js b/src/synapse/authProvider.js index 14ef26c..5068c44 100644 --- a/src/synapse/authProvider.js +++ b/src/synapse/authProvider.js @@ -1,13 +1,5 @@ import { fetchUtils } from "react-admin"; -const ensureHttpsForUrl = url => { - if (/^https:\/\//i.test(url)) { - return url; - } - const domain = url.replace(/http.?:\/\//g, ""); - return "https://" + domain; -}; - const stripTrailingSlash = str => { if (!str) { return; @@ -17,7 +9,7 @@ const stripTrailingSlash = str => { const authProvider = { // called when the user attempts to log in - login: ({ homeserver, username, password }) => { + login: ({ base_url, username, password }) => { console.log("login "); const options = { method: "POST", @@ -28,10 +20,8 @@ const authProvider = { }), }; - const url = window.decodeURIComponent(homeserver); - const trimmed_url = url.trim().replace(/\s/g, ""); - const login_api_url = - ensureHttpsForUrl(trimmed_url) + "/_matrix/client/r0/login"; + 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 normalized_base_url = stripTrailingSlash( From 8a4c0fe0fe973a65fe4bae94d39403ddf0e6a286 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Thu, 30 Apr 2020 20:27:10 +0200 Subject: [PATCH 3/9] Use input components for LoginPage Change-Id: Icaaa579eaeaaafe183fb027e4d3bf206f8f5516a --- src/components/LoginPage.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 9cbcf6e..520a26c 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -6,8 +6,10 @@ import { useLocale, useSetLocale, useTranslate, + PasswordInput, + TextInput, } from "react-admin"; -import { Field, Form } from "react-final-form"; +import { Form } from "react-final-form"; import { Avatar, Button, @@ -155,29 +157,32 @@ const LoginPage = ({ theme }) => {
-
-
-
From 1fb89c9e583c9b19682fd37551ce07f83e864f97 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Sat, 2 May 2020 15:30:11 +0200 Subject: [PATCH 4/9] Add missing german translations Change-Id: I297a730f73a4a4aa47a4ce679bd13ef0af69cc38 --- src/i18n/de.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/i18n/de.js b/src/i18n/de.js index b86dae0..12db9bf 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -63,4 +63,23 @@ export default { }, }, }, + ra: { + ...germanMessages.ra, + auth: { + ...germanMessages.ra.auth, + auth_check_error: "Anmeldung fehlgeschlagen", + }, + input: { + ...germanMessages.ra.input, + password: { + ...germanMessages.ra.input.password, + toggle_hidden: "Anzeigen", + toggle_visible: "Verstecken", + }, + }, + notification: { + ...germanMessages.ra.notifiaction, + logged_out: "Abgemeldet", + }, + }, }; From 2d0ce50444e328eb2a678306294628d7423e1e79 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Sat, 2 May 2020 15:35:15 +0200 Subject: [PATCH 5/9] Save base_url from login input Change-Id: I58447145dfc2df4ab3544b6a165721f900e29b24 --- src/synapse/authProvider.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/synapse/authProvider.js b/src/synapse/authProvider.js index 5068c44..30ce600 100644 --- a/src/synapse/authProvider.js +++ b/src/synapse/authProvider.js @@ -1,12 +1,5 @@ import { fetchUtils } from "react-admin"; -const stripTrailingSlash = str => { - if (!str) { - return; - } - return str.endsWith("/") ? str.slice(0, -1) : str; -}; - const authProvider = { // called when the user attempts to log in login: ({ base_url, username, password }) => { @@ -20,15 +13,16 @@ const authProvider = { }), }; + // use the base_url from login instead of the well_known entry from the + // server, since the admin might want to access the admin API via some + // private address + localStorage.setItem("base_url", base_url); + 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 normalized_base_url = stripTrailingSlash( - json.well_known["m.homeserver"].base_url - ); - localStorage.setItem("base_url", normalized_base_url); - localStorage.setItem("home_server_url", json.home_server); + 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); From 50b770a3122c4ecbeb7b858087679b6d2de03c19 Mon Sep 17 00:00:00 2001 From: Michael Albert Date: Thu, 30 Apr 2020 21:22:35 +0200 Subject: [PATCH 6/9] Extract homeserver URL from fully qualified user id Also lookup the .well-known entry and use it if available. Change-Id: I609046f01860fd5e3ba8cb801006e6098a4ad840 --- src/components/LoginPage.js | 128 +++++++++++++++++++++++------------- src/i18n/de.js | 3 +- src/i18n/en.js | 3 +- 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 520a26c..984300b 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -1,5 +1,7 @@ import React, { useState } from "react"; import { + fetchUtils, + FormDataConsumer, Notification, useLogin, useNotify, @@ -9,7 +11,7 @@ import { PasswordInput, TextInput, } from "react-admin"; -import { Form } from "react-final-form"; +import { Form, useForm } from "react-final-form"; import { Avatar, Button, @@ -36,7 +38,7 @@ const useStyles = makeStyles(theme => ({ backgroundSize: "cover", }, card: { - minWidth: 300, + minWidth: "30em", marginTop: "6em", }, avatar: { @@ -72,7 +74,7 @@ const LoginPage = ({ theme }) => { var locale = useLocale(); const setLocale = useSetLocale(); const translate = useTranslate(); - const homeserver = localStorage.getItem("base_url"); + const base_url = localStorage.getItem("base_url"); const renderInput = ({ meta: { touched, error } = {}, @@ -90,23 +92,21 @@ const LoginPage = ({ theme }) => { const validate = values => { const errors = {}; - if (!values.homeserver) { - errors.homeserver = translate("ra.validation.required"); - } else { - if (!values.homeserver.match(/^(http|https):\/\//)) { - errors.homeserver = translate("synapseadmin.auth.protocol_error"); - } else if ( - !values.homeserver.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/) - ) { - errors.homeserver = translate("synapseadmin.auth.url_error"); - } - } if (!values.username) { errors.username = translate("ra.validation.required"); } if (!values.password) { errors.password = translate("ra.validation.required"); } + if (!values.base_url) { + errors.base_url = translate("ra.validation.required"); + } else { + if (!values.base_url.match(/^(http|https):\/\//)) { + errors.base_url = translate("synapseadmin.auth.protocol_error"); + } else if (!values.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+$/)) { + errors.base_url = translate("synapseadmin.auth.url_error"); + } + } return errors; }; @@ -125,9 +125,75 @@ const LoginPage = ({ theme }) => { }); }; + const extractHomeServer = username => { + const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/; + if (!username) return null; + const res = username.match(usernameRegex); + if (res) return res[1]; + return null; + }; + + const UserData = ({ formData }) => { + const form = useForm(); + + const handleUsernameChange = _ => { + if (formData.base_url) return; + // check if username is a full qualified userId then set base_url accordially + const home_server = extractHomeServer(formData.username); + const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`; + if (home_server) { + // fetch .well-known entry to get base_url + fetchUtils + .fetchJson(wellKnownUrl, { method: "GET" }) + .then(({ json }) => { + form.change("base_url", json["m.homeserver"].base_url); + }) + .catch(_ => { + // if there is no .well-known entry, try the home server name + form.change("base_url", `https://${home_server}`); + }); + } + }; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + }; + return (
( @@ -156,35 +222,9 @@ const LoginPage = ({ theme }) => { English -
- -
-
- -
-
- -
+ + {formDataProps => } + + + ); + + return ( + + + {translate("resources.servernotices.action.send")} + + + + {translate("resources.servernotices.helper.send")} + + } + submitOnEnter={false} + redirect={false} + save={onSend} + > + + + + + ); +}; + +export const ServerNoticeButton = ({ record }) => { + const [open, setOpen] = useState(false); + const notify = useNotify(); + const [create, { loading }] = useCreate("servernotices"); + + const handleDialogOpen = () => setOpen(true); + const handleDialogClose = () => setOpen(false); + + const handleSend = values => { + create( + { payload: { data: { id: record.id, ...values } } }, + { + onSuccess: () => { + notify("resources.servernotices.action.send_success"); + handleDialogClose(); + }, + onFailure: () => + notify("resources.servernotices.action.send_failure", "error"), + } + ); + }; + + return ( + + + + + ); +}; diff --git a/src/components/users.js b/src/components/users.js index 3674127..8e6c75b 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -30,6 +30,7 @@ import { useTranslate, Pagination, } from "react-admin"; +import { ServerNoticeButton } from "./ServerNotices"; const UserPagination = props => ( @@ -108,6 +109,7 @@ const UserEditToolbar = props => { label="resources.users.action.erase" title={translate("resources.users.helper.erase")} /> + ); }; diff --git a/src/i18n/de.js b/src/i18n/de.js index 9871a89..a467c1a 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -63,6 +63,22 @@ export default { user_agent: "User Agent", }, }, + servernotices: { + name: "Serverbenachrichtigungen", + send: "Servernachricht versenden", + fields: { + body: "Nachricht", + }, + action: { + send: "Sende Nachricht", + send_success: "Nachricht erfolgreich versendet.", + send_failure: "Beim Versenden ist ein Fehler aufgetreten.", + }, + helper: { + send: + 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.', + }, + }, }, ra: { ...germanMessages.ra, diff --git a/src/i18n/en.js b/src/i18n/en.js index ff0864a..a7a6141 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -63,5 +63,21 @@ export default { user_agent: "User agent", }, }, + servernotices: { + name: "Server Notices", + send: "Send server notices", + fields: { + body: "Message", + }, + action: { + send: "Send note", + send_success: "Server notice successfully sent.", + send_failure: "An error has occurred.", + }, + helper: { + send: + 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.', + }, + }, }, }; diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 7a1500a..859dc73 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -62,6 +62,20 @@ const resourceMap = { }), data: "connections", }, + servernotices: { + map: n => ({ id: n.event_id }), + create: data => ({ + endpoint: "/_synapse/admin/v1/send_server_notice", + body: { + user_id: data.id, + content: { + msgtype: "m.text", + body: data.body, + }, + }, + method: "POST", + }), + }, }; function filterNullValues(key, value) {