mirror of
https://github.com/etkecc/synapse-admin.git
synced 2024-12-03 15:09:32 +03:00
Rework configuration process
Dynamically loads `config.json` on startup. Fixes #167, #284, #449, #486 Change-Id: I9efb1079c0c88e6e0272c5fda734a367aa8f84a3
This commit is contained in:
parent
ef3836313c
commit
4b1277f653
11 changed files with 254 additions and 46 deletions
5
.env
5
.env
|
@ -1,5 +0,0 @@
|
|||
# This setting allows to fix the homeserver.
|
||||
# If you set this setting, the user will not be able to select
|
||||
# the server and have to use synapse-admin with this server.
|
||||
|
||||
#REACT_APP_SERVER=https://yourmatrixserver.example.com
|
|
@ -1,13 +1,12 @@
|
|||
# Builder
|
||||
FROM node:lts as builder
|
||||
LABEL org.opencontainers.image.url=https://github.com/Awesome-Technologies/synapse-admin org.opencontainers.image.source=https://github.com/Awesome-Technologies/synapse-admin
|
||||
ARG REACT_APP_SERVER
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src
|
||||
RUN yarn --network-timeout=300000 install --immutable
|
||||
RUN REACT_APP_SERVER=$REACT_APP_SERVER yarn build
|
||||
RUN yarn build
|
||||
|
||||
|
||||
# App
|
||||
|
|
42
README.md
42
README.md
|
@ -64,11 +64,6 @@ You have three options:
|
|||
- download dependencies: `yarn install`
|
||||
- start web server: `yarn start`
|
||||
|
||||
You can fix the homeserver, so that the user can no longer define it himself.
|
||||
Either you define it at startup (e.g. `REACT_APP_SERVER=https://yourmatrixserver.example.com yarn start`)
|
||||
or by editing it in the [.env](.env) file. See also the
|
||||
[documentation](https://create-react-app.dev/docs/adding-custom-environment-variables/).
|
||||
|
||||
#### Steps for 3)
|
||||
|
||||
- run the Docker container from the public docker registry: `docker run -p 8080:80 awesometechnologies/synapse-admin` or use the [docker-compose.yml](docker-compose.yml): `docker-compose up -d`
|
||||
|
@ -76,8 +71,6 @@ or by editing it in the [.env](.env) file. See also the
|
|||
> note: if you're building on an architecture other than amd64 (for example a raspberry pi), make sure to define a maximum ram for node. otherwise the build will fail.
|
||||
|
||||
```yml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
|
@ -88,7 +81,6 @@ or by editing it in the [.env](.env) file. See also the
|
|||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# # see #266, PUBLIC_URL must be without surrounding quotation marks
|
||||
# - PUBLIC_URL=/synapse-admin
|
||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
@ -96,6 +88,40 @@ or by editing it in the [.env](.env) file. See also the
|
|||
|
||||
- browse to http://localhost:8080
|
||||
|
||||
### Restricting available homeserver
|
||||
|
||||
You can restrict the homeserver(s), so that the user can no longer define it himself.
|
||||
|
||||
Edit `config.json` to restrict either to a single homeserver:
|
||||
|
||||
```json
|
||||
{
|
||||
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
or to a list of homeservers:
|
||||
|
||||
```json
|
||||
{
|
||||
"restrictBaseUrl": [
|
||||
"https://your-first-matrix-server.example.com",
|
||||
"https://your-second-matrix-server.example.com"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `config.json` can be injected into a Docker container using a bind mount.
|
||||
|
||||
```yml
|
||||
services:
|
||||
synapse-admin:
|
||||
...
|
||||
volumes:
|
||||
./config.json:/app/config.json
|
||||
...
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Screenshots](./screenshots.jpg)
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
synapse-admin:
|
||||
container_name: synapse-admin
|
||||
hostname: synapse-admin
|
||||
image: awesometechnologies/synapse-admin:latest
|
||||
# build:
|
||||
# context: .
|
||||
# context: .
|
||||
|
||||
# to use the docker-compose as standalone without a local repo clone,
|
||||
# replace the context definition with this:
|
||||
|
@ -18,9 +16,6 @@ services:
|
|||
# - NODE_OPTIONS="--max_old_space_size=1024"
|
||||
# default is .
|
||||
# - PUBLIC_URL=/synapse-admin
|
||||
# You can use a fixed homeserver, so that the user can no longer
|
||||
# define it himself
|
||||
# - REACT_APP_SERVER="https://matrix.example.com"
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
|
1
public/config.json
Normal file
1
public/config.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -24,10 +24,110 @@
|
|||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Synapse-Admin</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
|
||||
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
color: #283593;
|
||||
font-size: 11px;
|
||||
text-indent: -99999em;
|
||||
margin: 55px auto;
|
||||
position: relative;
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
box-shadow: inset 0 0 0 1em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
width: 5.2em;
|
||||
height: 10.2em;
|
||||
background: #fafafa;
|
||||
border-radius: 10.2em 0 0 10.2em;
|
||||
top: -0.1em;
|
||||
left: -0.1em;
|
||||
-webkit-transform-origin: 5.2em 5.1em;
|
||||
transform-origin: 5.2em 5.1em;
|
||||
-webkit-animation: load2 2s infinite ease 1.5s;
|
||||
animation: load2 2s infinite ease 1.5s;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
width: 5.2em;
|
||||
height: 10.2em;
|
||||
background: #fafafa;
|
||||
border-radius: 0 10.2em 10.2em 0;
|
||||
top: -0.1em;
|
||||
left: 5.1em;
|
||||
-webkit-transform-origin: 0px 5.1em;
|
||||
transform-origin: 0px 5.1em;
|
||||
-webkit-animation: load2 2s infinite ease;
|
||||
animation: load2 2s infinite ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes load2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root">
|
||||
<div class="loader-container">
|
||||
<div class="loader">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
@ -46,4 +146,4 @@
|
|||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
5
src/AppContext.jsx
Normal file
5
src/AppContext.jsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createContext, useContext } from "react";
|
||||
|
||||
export const AppContext = createContext({});
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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" });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue