diff --git a/README.md b/README.md index b848dfe..66f6d4a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/AppContext.tsx b/src/AppContext.tsx index f005479..72b8bc3 100644 --- a/src/AppContext.tsx +++ b/src/AppContext.tsx @@ -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; diff --git a/src/components/config.ts b/src/components/config.ts new file mode 100644 index 0000000..1056f4f --- /dev/null +++ b/src/components/config.ts @@ -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; + +} diff --git a/src/index.tsx b/src/index.tsx index efa762d..38a892f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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 (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)); +// 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); } + } 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( - ) - }); +); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index e124966..cadb8fb 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -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 && diff --git a/vite.config.ts b/vite.config.ts index 5554787..2390a83 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,9 @@ import { defineConfig } from "vite"; export default defineConfig({ base: "./", + build: { + target: "esnext", + }, plugins: [ react(), vitePluginVersionMark({