diff --git a/public/images/floating-cogs.svg b/public/images/floating-cogs.svg new file mode 100644 index 0000000..4281b59 --- /dev/null +++ b/public/images/floating-cogs.svg @@ -0,0 +1 @@ + diff --git a/src/App.js b/src/App.js index 2dc16d6..7b71d24 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,9 @@ import React from "react"; import { Admin, Resource, resolveBrowserLocale } from "react-admin"; import polyglotI18nProvider from "ra-i18n-polyglot"; +import authProvider from "./synapse/authProvider"; import dataProvider from "./dataProvider"; +import LoginPage from "./components/LoginPage"; import germanMessages from "./i18n/de"; import englishMessages from "./i18n/en"; @@ -16,7 +18,12 @@ const i18nProvider = polyglotI18nProvider( ); const App = () => ( - + ); diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js new file mode 100644 index 0000000..49293db --- /dev/null +++ b/src/components/LoginPage.js @@ -0,0 +1,198 @@ +import React, { useState } from "react"; +import { + Notification, + useLogin, + useNotify, + useLocale, + useSetLocale, + useTranslate, +} from "react-admin"; +import { Field, Form } from "react-final-form"; +import { + Avatar, + Button, + Card, + CardActions, + CircularProgress, + MenuItem, + Select, + TextField, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import LockIcon from "@material-ui/icons/Lock"; + +const useStyles = makeStyles(theme => ({ + main: { + display: "flex", + flexDirection: "column", + minHeight: "100vh", + alignItems: "center", + justifyContent: "flex-start", + background: "url(./images/floating-cogs.svg)", + backgroundColor: "#f9f9f9", + backgroundRepeat: "no-repeat", + backgroundSize: "cover", + }, + card: { + minWidth: 300, + marginTop: "6em", + }, + avatar: { + margin: "1em", + display: "flex", + justifyContent: "center", + }, + icon: { + backgroundColor: theme.palette.secondary.main, + }, + hint: { + marginTop: "1em", + display: "flex", + justifyContent: "center", + color: theme.palette.grey[500], + }, + form: { + padding: "0 1em 1em 1em", + }, + input: { + marginTop: "1em", + }, + actions: { + padding: "0 1em 1em 1em", + }, +})); + +const LoginPage = ({ theme }) => { + const classes = useStyles({ theme }); + const login = useLogin(); + const notify = useNotify(); + const [loading, setLoading] = useState(false); + var locale = useLocale(); + const setLocale = useSetLocale(); + const translate = useTranslate(); + const homeserver = localStorage.getItem("home_server"); + + const renderInput = ({ + meta: { touched, error } = {}, + input: { ...inputProps }, + ...props + }) => ( + + ); + + 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"); + } + return errors; + }; + + const handleSubmit = auth => { + setLoading(true); + login(auth).catch(error => { + setLoading(false); + notify( + typeof error === "string" + ? error + : typeof error === "undefined" || !error.message + ? "ra.auth.sign_in_error" + : error.message, + "warning" + ); + }); + }; + + return ( +
( + +
+ +
+ + + +
+
+ {translate("synapseadmin.auth.welcome")} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+ +
+
+ )} + /> + ); +}; + +export default LoginPage; diff --git a/src/components/LoginPage.test.js b/src/components/LoginPage.test.js new file mode 100644 index 0000000..f9d74e1 --- /dev/null +++ b/src/components/LoginPage.test.js @@ -0,0 +1,14 @@ +import React from "react"; +import { TestContext } from "react-admin"; +import { shallow } from "enzyme"; +import LoginPage from "./LoginPage"; + +describe("LoginForm", () => { + it("renders", () => { + shallow( + + + + ); + }); +}); diff --git a/src/i18n/de.js b/src/i18n/de.js index 1de8dd9..7ba3c22 100644 --- a/src/i18n/de.js +++ b/src/i18n/de.js @@ -2,4 +2,10 @@ import germanMessages from "ra-language-german"; export default { ...germanMessages, + synapseadmin: { + auth: { + homeserver: "Heimserver", + welcome: "Willkommen bei Synapse-admin", + }, + }, }; diff --git a/src/i18n/en.js b/src/i18n/en.js index fcee097..187af84 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -2,4 +2,10 @@ import englishMessages from "ra-language-english"; export default { ...englishMessages, + synapseadmin: { + auth: { + homeserver: "Homeserver", + welcome: "Welcome to Synapse-admin", + }, + }, }; diff --git a/src/synapse/authProvider.js b/src/synapse/authProvider.js new file mode 100644 index 0000000..a25a6e8 --- /dev/null +++ b/src/synapse/authProvider.js @@ -0,0 +1,57 @@ +import { fetchUtils } from "react-admin"; + +const authProvider = { + // called when the user attempts to log in + login: ({ homeserver, username, password }) => { + console.log("login "); + const options = { + method: "POST", + body: JSON.stringify({ + type: "m.login.password", + user: username, + password: password, + }), + }; + + // add 'https://' to homeserver url if its missing + let newUrl = window.decodeURIComponent(homeserver); + newUrl = newUrl.trim().replace(/\s/g, ""); + if (!/^https?:\/\//i.test(newUrl)) { + homeserver = `https://${newUrl}`; + } + + const url = homeserver + "/_matrix/client/r0/login"; + return fetchUtils.fetchJson(url, options).then(({ json }) => { + 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: () => { + console.log("logout "); + localStorage.removeItem("access_token"); + return Promise.resolve(); + }, + // called when the API returns an error + checkError: ({ status }) => { + console.log("checkError " + status); + if (status === 401 || status === 403) { + return Promise.reject(); + } + return Promise.resolve(); + }, + // called when the user navigates to a new location, to check for authentication + checkAuth: () => { + const access_token = localStorage.getItem("access_token"); + console.log("checkAuth " + access_token); + return typeof access_token == "string" + ? Promise.resolve() + : Promise.reject(); + }, + // called when the user navigates to a new location, to check for permissions / roles + getPermissions: () => Promise.resolve(), +}; + +export default authProvider;