diff --git a/package.json b/package.json index 0b12331..3436528 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Admin GUI for the Matrix.org server Synapse", "author": "Awesome Technologies Innovationslabor GmbH", "license": "Apache-2.0", + "homepage": ".", "repository": { "type": "git", "url": "https://github.com/Awesome-Technologies/synapse-admin" diff --git a/src/App.js b/src/App.js index c646d2d..c8cc6aa 100644 --- a/src/App.js +++ b/src/App.js @@ -48,6 +48,7 @@ const App = () => ( icon={RoomIcon} /> + ); diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 19280b3..6834625 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -1,13 +1,17 @@ import React, { useState } from "react"; import { + fetchUtils, + FormDataConsumer, Notification, useLogin, useNotify, useLocale, useSetLocale, useTranslate, + PasswordInput, + TextInput, } from "react-admin"; -import { Field, Form } from "react-final-form"; +import { Form, useForm } from "react-final-form"; import { Avatar, Button, @@ -34,7 +38,7 @@ const useStyles = makeStyles(theme => ({ backgroundSize: "cover", }, card: { - minWidth: 300, + minWidth: "30em", marginTop: "6em", }, avatar: { @@ -70,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 } = {}, @@ -88,15 +92,23 @@ const LoginPage = ({ theme }) => { const validate = values => { const errors = {}; - if (!values.homeserver) { - errors.homeserver = translate("ra.validation.required"); - } 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\-.]+(:\d{1,5})?$/) + ) { + errors.base_url = translate("synapseadmin.auth.url_error"); + } + } return errors; }; @@ -115,9 +127,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 (
( @@ -146,32 +224,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/rooms.js b/src/components/rooms.js index 238cfdd..e872374 100644 --- a/src/components/rooms.js +++ b/src/components/rooms.js @@ -43,7 +43,7 @@ const validateDisplayName = fieldval => : undefined; function approximateAliasLength(alias, homeserver) { - /* TODO maybe handle punycode in homeserver URL */ + /* TODO maybe handle punycode in homeserver name */ var te; @@ -69,7 +69,7 @@ const validateAlias = fieldval => { if (fieldval === undefined) { return undefined; } - const homeserver = localStorage.getItem("home_server_url"); + const homeserver = localStorage.getItem("home_server"); if (approximateAliasLength(fieldval, homeserver) > 255) { return "synapseadmin.rooms.alias_too_long"; diff --git a/src/components/users.js b/src/components/users.js index 2fceae5..f2289ef 100644 --- a/src/components/users.js +++ b/src/components/users.js @@ -32,6 +32,7 @@ import { Pagination, } from "react-admin"; import SaveQrButton from "./SaveQrButton"; +import { ServerNoticeButton } from "./ServerNotices"; const UserPagination = props => ( @@ -82,7 +83,7 @@ export const UserList = props => ( ); function generateRandomUser() { - const homeserver = localStorage.getItem("home_server_url"); + const homeserver = localStorage.getItem("home_server"); const user_id = "@" + Array(8) @@ -175,6 +176,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 9d0dac5..14abc0a 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -4,8 +4,11 @@ export default { ...germanMessages, synapseadmin: { auth: { - homeserver: "Heimserver", + base_url: "Heimserver URL", welcome: "Willkommen bei Synapse-admin", + 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", }, action: { save_and_show: "Speichern und QR Code erzeugen", @@ -77,6 +80,41 @@ 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, + 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", + }, }, ra: { ...germanMessages.ra, diff --git a/src/i18n/en.js b/src/i18n/en.js index 137b181..f1e6f51 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -4,8 +4,11 @@ export default { ...englishMessages, synapseadmin: { auth: { - homeserver: "Homeserver", + base_url: "Homeserver URL", welcome: "Welcome to Synapse-admin", + 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", }, action: { save_and_show: "Create QR code", @@ -78,5 +81,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/authProvider.js b/src/synapse/authProvider.js index 14ef26c..30ce600 100644 --- a/src/synapse/authProvider.js +++ b/src/synapse/authProvider.js @@ -1,23 +1,8 @@ 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; - } - return str.endsWith("/") ? str.slice(0, -1) : 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,17 +13,16 @@ 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"; + // 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); diff --git a/src/synapse/dataProvider.js b/src/synapse/dataProvider.js index 00d14be..de29f08 100644 --- a/src/synapse/dataProvider.js +++ b/src/synapse/dataProvider.js @@ -27,6 +27,11 @@ const resourceMap = { }), data: "users", total: json => json.total, + create: data => ({ + endpoint: `/_synapse/admin/v2/users/${data.id}`, + body: data, + method: "PUT", + }), delete: id => ({ endpoint: `/_synapse/admin/v1/deactivate/${id}`, body: { erase: true }, @@ -41,23 +46,19 @@ const resourceMap = { }), data: "rooms", total: json => json.total_rooms, - create: params => { - let invitees = params.data.invitees; - return { - method: "POST", - endpoint: "/_matrix/client/r0/createRoom", - body: { - name: params.data.name, - room_alias_name: params.data.canonical_alias, - visibility: params.data.public ? "public" : "private", - invite: - Array.isArray(invitees) && invitees.length > 0 - ? invitees - : undefined, - }, - map: r => ({ id: r.room_id }), - }; - }, + create: data => ({ + endpoint: "/_matrix/client/r0/createRoom", + body: { + name: data.name, + room_alias_name: data.canonical_alias, + visibility: data.public ? "public" : "private", + invite: + Array.isArray(data.invitees) && data.invitees.length > 0 + ? data.invitees + : undefined, + }, + method: "POST", + }) }, connections: { path: "/_synapse/admin/v1/whois", @@ -67,6 +68,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) { @@ -200,25 +215,16 @@ const dataProvider = { if (!homeserver || !(resource in resourceMap)) return Promise.reject(); const res = resourceMap[resource]; + if (!("create" in res)) return Promise.reject(); - if ("create" in res) { - const create = res["create"](params); - const endpoint_url = homeserver + create.endpoint; - return jsonClient(endpoint_url, { - method: create.method, - body: JSON.stringify(create.body, filterNullValues), - }).then(({ json }) => ({ - data: create.map(json), - })); - } else { - const endpoint_url = homeserver + res.path; - return jsonClient(`${endpoint_url}/${params.data.id}`, { - method: "PUT", - body: JSON.stringify(params.data, filterNullValues), - }).then(({ json }) => ({ - data: res.map(json), - })); - } + 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), + })); }, delete: (resource, params) => {