Rework configuration process

Dynamically loads `config.json` on startup.

Fixes #167, #284, #449, #486

Change-Id: I9efb1079c0c88e6e0272c5fda734a367aa8f84a3
This commit is contained in:
Manuel Stahl
2024-02-05 17:32:32 +01:00
committed by Manuel Stahl
parent ef3836313c
commit 4b1277f653
11 changed files with 254 additions and 46 deletions

5
src/AppContext.jsx Normal file
View File

@@ -0,0 +1,5 @@
import { createContext, useContext } from "react";
export const AppContext = createContext({});
export const useAppContext = () => useContext(AppContext);

View File

@@ -26,6 +26,8 @@ import {
} from "@mui/material";
import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock";
import { useAppContext } from "../AppContext";
import {
getServerVersion,
getSupportedFeatures,
@@ -92,13 +94,18 @@ const FormBox = styled(Box)(({ theme }) => ({
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const { restrictBaseUrl } = useAppContext();
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true);
const [locale, setLocale] = useLocaleState();
const locales = useLocales();
const translate = useTranslate();
const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER;
const base_url = allowSingleBaseUrl
? restrictBaseUrl
: localStorage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
@@ -177,15 +184,24 @@ const LoginPage = () => {
const [matrixVersions, setMatrixVersions] = useState("");
const handleUsernameChange = _ => {
if (formData.base_url || cfg_base_url) return;
if (formData.base_url || allowSingleBaseUrl) return;
// check if username is a full qualified userId then set base_url accordingly
const domain = splitMxid(formData.username)?.domain;
if (domain) {
getWellKnownUrl(domain).then(url => form.setValue("base_url", url));
getWellKnownUrl(domain).then(url => {
if (
allowAnyBaseUrl ||
(allowMultipleBaseUrls && restrictBaseUrl.includes(url))
)
form.setValue("base_url", url);
});
}
};
useEffect(() => {
if (formData.base_url === "" && allowMultipleBaseUrls) {
form.setValue("base_url", restrictBaseUrl[0]);
}
if (!isValidBaseUrl(formData.base_url)) return;
getServerVersion(formData.base_url)
@@ -215,7 +231,7 @@ const LoginPage = () => {
setSSOBaseUrl(supportSSO ? formData.base_url : "");
})
.catch(() => setSSOBaseUrl(""));
}, [formData.base_url]);
}, [formData.base_url, form]);
return (
<>
@@ -224,11 +240,11 @@ const LoginPage = () => {
autoFocus
name="username"
label="ra.auth.username"
autoComplete="username"
disabled={loading || !supportPassAuth}
onBlur={handleUsernameChange}
resettable
fullWidth
className="input"
validate={required()}
/>
</Box>
@@ -237,10 +253,10 @@ const LoginPage = () => {
name="password"
label="ra.auth.password"
type="password"
autoComplete="current-password"
disabled={loading || !supportPassAuth}
resettable
fullWidth
className="input"
validate={required()}
/>
</Box>
@@ -248,12 +264,21 @@ const LoginPage = () => {
<TextInput
name="base_url"
label="synapseadmin.auth.base_url"
disabled={cfg_base_url || loading}
resettable
select={allowMultipleBaseUrls}
autoComplete="url"
disabled={loading}
readOnly={allowSingleBaseUrl}
resettable={allowAnyBaseUrl}
fullWidth
className="input"
validate={[required(), validateBaseUrl]}
/>
>
{allowMultipleBaseUrls &&
restrictBaseUrl.map(url => (
<MenuItem key={url} value={url}>
{url}
</MenuItem>
))}
</TextInput>
</Box>
<Typography className="serverVersion">{serverVersion}</Typography>
<Typography className="matrixVersions">{matrixVersions}</Typography>
@@ -263,7 +288,7 @@ const LoginPage = () => {
return (
<Form
defaultValues={{ base_url: cfg_base_url || base_url }}
defaultValues={{ base_url: base_url }}
onSubmit={handleSubmit}
mode="onTouched"
>

View File

@@ -1,14 +1,71 @@
import React from "react";
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage";
import { AppContext } from "../AppContext";
describe("LoginForm", () => {
it("renders", () => {
it("renders with no restriction to homeserver", () => {
render(
<AdminContext>
<LoginPage />
</AdminContext>
);
screen.getByText("synapseadmin.auth.welcome");
screen.getByRole("combobox", { name: "" });
screen.getByRole("textbox", { name: "ra.auth.username" });
screen.getByText("ra.auth.password");
const baseUrlInput = screen.getByRole("textbox", {
name: "synapseadmin.auth.base_url",
});
expect(baseUrlInput.className.split(" ")).not.toContain("Mui-readOnly");
screen.getByRole("button", { name: "ra.auth.sign_in" });
});
it("renders with single restricted homeserver", () => {
render(
<AppContext.Provider
value={{ restrictBaseUrl: "https://matrix.example.com" }}
>
<AdminContext>
<LoginPage />
</AdminContext>
</AppContext.Provider>
);
screen.getByText("synapseadmin.auth.welcome");
screen.getByRole("combobox", { name: "" });
screen.getByRole("textbox", { name: "ra.auth.username" });
screen.getByText("ra.auth.password");
const baseUrlInput = screen.getByRole("textbox", {
name: "synapseadmin.auth.base_url",
});
expect(baseUrlInput.className.split(" ")).toContain("Mui-readOnly");
screen.getByRole("button", { name: "ra.auth.sign_in" });
});
it("renders with multiple restricted homeservers", async () => {
render(
<AppContext.Provider
value={{
restrictBaseUrl: [
"https://matrix.example.com",
"https://matrix.example.org",
],
}}
>
<AdminContext>
<LoginPage />
</AdminContext>
</AppContext.Provider>
);
screen.getByText("synapseadmin.auth.welcome");
screen.getByRole("combobox", { name: "" });
screen.getByRole("textbox", { name: "ra.auth.username" });
screen.getByText("ra.auth.password");
screen.getByRole("combobox", { name: "synapseadmin.auth.base_url" });
screen.getByRole("button", { name: "ra.auth.sign_in" });
});
});

View File

@@ -1,9 +1,17 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import App from "./App";
import { AppContext } from "./AppContext";
fetch("config.json")
.then(res => res.json())
.then(props =>
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AppContext.Provider value={props}>
<App />
</AppContext.Provider>
</React.StrictMode>
)
);

View File

@@ -3,9 +3,6 @@ import { fetchUtils } from "react-admin";
const authProvider = {
// called when the user attempts to log in
login: async ({ 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",