Add support for config from /.well-known/matrix/client (#126)

* Add support for config from /.well-known/matrix/client

* final fixes, refactoring, updated readme
This commit is contained in:
Aine 2024-11-07 00:24:33 +02:00 committed by GitHub
parent 9adc13e722
commit c698f57395
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 160 additions and 41 deletions

View file

@ -90,6 +90,7 @@ with a proper manifest.json generation on build)
* [Add option to control user's experimental features](https://github.com/etkecc/synapse-admin/pull/111)
* [Add random password generation on user create/edit form](https://github.com/etkecc/synapse-admin/pull/123)
* [Add option to set user's rate limits](https://github.com/etkecc/synapse-admin/pull/125)
* [Support configuration via /.well-known/matrix/client](https://github.com/etkecc/synapse-admin/pull/126)
_the list will be updated as new changes are added_
@ -106,7 +107,11 @@ After that open `http://localhost:5173` in your browser, login using the followi
## Configuration
You can use `config.json` file to configure synapse-admin
You can use `config.json` file to configure Synapse Admin instance,
and `/.well-known/matrix/client` file to provide Synapse Admin configuration specifically for your homeserver.
In the latter case, any instance of Synapse Admin will automatically pick up the configuration from the homeserver.
Note that configuration inside the `/.well-known/matrix/client` file should go under the `cc.etke.synapse-admin` key,
and it will override the configuration from the `config.json` file.
The `config.json` can be injected into a Docker container using a bind mount.
@ -131,6 +136,16 @@ Edit `config.json` to restrict either to a single homeserver:
}
```
similar for `/.well-known/matrix/client`:
```json
{
"cc.etke.synapse-admin": {
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
}
}
```
or to a list of homeservers:
```json
@ -139,6 +154,16 @@ or to a list of homeservers:
}
```
similar for `/.well-known/matrix/client`:
```json
{
"cc.etke.synapse-admin": {
"restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
}
}
```
### Protecting appservice managed users
To avoid accidental adjustments of appservice-managed users (e.g., puppets created by a bridge) and breaking the bridge,
@ -152,6 +177,16 @@ Example for [mautrix-telegram](https://github.com/mautrix/telegram)
}
```
similar for `/.well-known/matrix/client`:
```json
{
"cc.etke.synapse-admin": {
"asManagedUsers": ["^@telegram_[a-zA-Z0-9]+:example\\.com$"]
}
}
```
### Adding custom menu items
You can add custom menu items to the main menu by providing a `menu` array in the `config.json`.
@ -168,6 +203,22 @@ You can add custom menu items to the main menu by providing a `menu` array in th
}
```
similar for `/.well-known/matrix/client`:
```json
{
"cc.etke.synapse-admin": {
"menu": [
{
"label": "Contact support",
"icon": "SupportAgent",
"url": "https://github.com/etkecc/synapse-admin/issues"
}
]
}
}
```
Where `icon` is one of the [preloaded icons](./src/components/icons.ts)
### Providing support URL
@ -182,6 +233,16 @@ Where `icon` is one of the [preloaded icons](./src/components/icons.ts)
}
```
similar for `/.well-known/matrix/client`:
```json
{
"cc.etke.synapse-admin": {
"supportURL": "https://example.com/support"
}
}
```
## Usage
### Supported Synapse

View file

@ -1,18 +1,6 @@
import { createContext, useContext } from "react";
interface AppContextType {
restrictBaseUrl: string | string[];
asManagedUsers: string[];
supportURL: string;
menu: MenuItem[];
}
interface MenuItem {
label: string;
icon: string;
url: string;
}
import { Config } from "./components/config";
export const AppContext = createContext({});
export const useAppContext = () => useContext(AppContext) as AppContextType;
export const useAppContext = () => useContext(AppContext) as Config;

63
src/components/config.ts Normal file
View file

@ -0,0 +1,63 @@
import storage from "../storage";
export interface Config {
restrictBaseUrl: string | string[];
asManagedUsers: string[];
supportURL: string;
menu: MenuItem[];
}
export interface MenuItem {
label: string;
icon: string;
url: string;
}
export const WellKnownKey = "cc.etke.synapse-admin";
export const LoadConfig = (context: Config): Config => {
if (context.restrictBaseUrl) {
storage.setItem("restrict_base_url", JSON.stringify(context.restrictBaseUrl));
}
if (context.asManagedUsers) {
storage.setItem("as_managed_users", JSON.stringify(context.asManagedUsers));
}
let menu: MenuItem[] = [];
if (context.menu) {
menu = context.menu;
}
if (context.supportURL) {
const migratedSupportURL = {
label: "Contact support",
icon: "SupportAgent",
url: context.supportURL,
};
console.warn("supportURL config option is deprecated. Please, use the menu option instead. Automatically migrated to the new menu option:", migratedSupportURL);
menu.push(migratedSupportURL as MenuItem);
}
if (menu.length > 0) {
storage.setItem("menu", JSON.stringify(menu));
}
// below we try to calculate "final" config, which will contain values from context and already set values in storage
// because LoadConfig could be called multiple times to get config from different sources
let finalAsManagedUsers: string[] = [];
try {
finalAsManagedUsers = JSON.parse(storage.getItem("as_managed_users") || "");
} catch (e) {}
let finalMenu: MenuItem[] = [];
try {
finalMenu = JSON.parse(storage.getItem("menu") || "");
} catch (e) {}
return {
restrictBaseUrl: storage.getItem("restrict_base_url") || "",
asManagedUsers: finalAsManagedUsers,
supportURL: storage.getItem("support_url") || "",
menu: finalMenu,
} as Config;
}

View file

@ -3,38 +3,42 @@ import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { AppContext, MenuItem } from "./AppContext";
import { Config, WellKnownKey, LoadConfig } from "./components/config";
import { AppContext } from "./AppContext";
import storage from "./storage";
fetch("config.json")
.then(res => res.json())
.then(props => {
if (props.asManagedUsers) {
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
}
// load config.json
let props: Config = {};
try {
const resp = await fetch("config.json");
const configJSON = await resp.json();
console.log("Loaded config.json", configJSON);
props = LoadConfig(configJSON as Config);
} catch (e) {
console.error(e);
}
let menu: MenuItem[] = [];
if (props.menu) {
menu = props.menu;
// if home_server is set, try to load https://home_server/.well-known/matrix/client
const homeserver = storage.getItem("home_server");
if (homeserver) {
try {
const resp = await fetch(`https://${homeserver}/.well-known/matrix/client`);
const configWK = await resp.json();
if (!configWK[WellKnownKey]) {
console.log(`Loaded https://${homeserver}.well-known/matrix/client, but it doesn't contain ${WellKnownKey} key, skipping`, configWK);
} else {
console.log(`Loaded https://${homeserver}.well-known/matrix/client`, configWK);
props = LoadConfig(configWK[WellKnownKey] as Config);
}
if (props.supportURL) {
const migratedSupportURL = {
label: "Contact support",
icon: "SupportAgent",
url: props.supportURL,
};
console.warn("supportURL config option is deprecated. Please, use the menu option instead. Automatically migrated to the new menu option:", migratedSupportURL);
menu.push(migratedSupportURL as MenuItem);
}
if (menu.length > 0) {
storage.setItem("menu", JSON.stringify(menu));
} catch (e) {
console.log(`https://${homeserver}/.well-known/matrix/client not found, skipping`, e);
}
}
return createRoot(document.getElementById("root")).render(
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AppContext.Provider value={props}>
<App />
</AppContext.Provider>
</React.StrictMode>
)
});
);

View file

@ -33,7 +33,7 @@ const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const { restrictBaseUrl } = useAppContext();
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
const allowSingleBaseUrl = typeof restrictBaseUrl === "string" && restrictBaseUrl !== "";
const allowMultipleBaseUrls =
Array.isArray(restrictBaseUrl) &&
restrictBaseUrl.length > 0 &&

View file

@ -5,6 +5,9 @@ import { defineConfig } from "vite";
export default defineConfig({
base: "./",
build: {
target: "esnext",
},
plugins: [
react(),
vitePluginVersionMark({