diff --git a/maubot/management/api/client_auth.py b/maubot/management/api/client_auth.py
index 1aa54b4..81bdb6d 100644
--- a/maubot/management/api/client_auth.py
+++ b/maubot/management/api/client_auth.py
@@ -47,7 +47,7 @@ def generate_mac(secret: str, nonce: str, user: str, password: str, admin: bool
@routes.get("/client/auth/servers")
async def get_registerable_servers(_: web.Request) -> web.Response:
- return web.json_response(list(registration_secrets().keys()))
+ return web.json_response({key: value["url"] for key, value in registration_secrets().items()})
AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, password=str)
diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml
index 6fbc6e4..c25967e 100644
--- a/maubot/management/api/spec.yaml
+++ b/maubot/management/api/spec.yaml
@@ -410,13 +410,14 @@ paths:
content:
application/json:
schema:
- type: array
- items:
+ type: object
+ description: Key-value map from server name to homeserver URL
+ additionalProperties:
type: string
+ description: The homeserver URL
example:
- - maunium.net
- - example.com
- - matrix.org
+ maunium.net: https://maunium.net
+ example.com: https://matrix.example.org
401:
$ref: '#/components/responses/Unauthorized'
'/client/auth/{server}/register':
diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js
index fe54646..ddb6ad1 100644
--- a/maubot/management/frontend/src/api.js
+++ b/maubot/management/frontend/src/api.js
@@ -36,8 +36,8 @@ async function defaultDelete(type, id) {
return await resp.json()
}
-async function defaultPut(type, entry, id = undefined) {
- const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}`, {
+async function defaultPut(type, entry, id = undefined, suffix = undefined) {
+ const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}${suffix}`, {
headers: getHeaders(),
body: JSON.stringify(entry),
method: "PUT",
@@ -221,6 +221,17 @@ export function getAvatarURL({ id, avatar_url }) {
export const putClient = client => defaultPut("client", client)
export const deleteClient = id => defaultDelete("client", id)
+export const getClientAuthServers = () => defaultGet("/client/auth/servers")
+
+export async function doClientAuth(server, type, username, password) {
+ const resp = await fetch(`${BASE_PATH}/client/auth/${server}/${type}`, {
+ headers: getHeaders(),
+ body: JSON.stringify({ username, password }),
+ method: "POST",
+ })
+ return await resp.json()
+}
+
export default {
BASE_PATH,
login, ping, getFeatures, remoteGetFeatures,
@@ -230,4 +241,5 @@ export default {
getInstanceDatabase, queryInstanceDatabase,
getPlugins, getPlugin, uploadPlugin, deletePlugin,
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
+ getClientAuthServers, doClientAuth
}
diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js
index b1f71ff..c3048df 100644
--- a/maubot/management/frontend/src/components/PreferenceTable.js
+++ b/maubot/management/frontend/src/components/PreferenceTable.js
@@ -15,6 +15,7 @@
// along with this program. If not, see .
import React from "react"
import Select from "react-select"
+import CreatableSelect from "react-select/creatable"
import Switch from "./Switch"
export const PrefTable = ({ children, wrapperClass }) => {
@@ -56,10 +57,12 @@ export const PrefSwitch = ({ rowName, active, origActive, fullWidth = false, ...
)
-export const PrefSelect = ({ rowName, value, origValue, fullWidth = false, ...args }) => (
+export const PrefSelect = ({ rowName, value, origValue, fullWidth = false, creatable = false, ...args }) => (
-
)
diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js
index efe7024..f9617b0 100644
--- a/maubot/management/frontend/src/pages/dashboard/Client.js
+++ b/maubot/management/frontend/src/pages/dashboard/Client.js
@@ -17,7 +17,7 @@ import React from "react"
import { NavLink, withRouter } from "react-router-dom"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
import { ReactComponent as UploadButton } from "../../res/upload.svg"
-import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable"
+import { PrefTable, PrefSwitch, PrefInput, PrefSelect } from "../../components/PreferenceTable"
import Spinner from "../../components/Spinner"
import api from "../../api"
import BaseMainView from "./BaseMainView"
@@ -48,7 +48,7 @@ class Client extends BaseMainView {
get entryKeys() {
return ["id", "displayname", "homeserver", "avatar_url", "access_token", "sync",
- "autojoin", "enabled", "started"]
+ "autojoin", "enabled", "started"]
}
get initialState() {
@@ -84,6 +84,36 @@ class Client extends BaseMainView {
return client
}
+ get selectedHomeserver() {
+ return this.state.homeserver
+ ? this.homeserverEntry([this.props.ctx.homeserversByURL[this.state.homeserver],
+ this.state.homeserver])
+ : {}
+ }
+
+ homeserverEntry = ([serverName, serverURL]) => serverURL && {
+ id: serverURL,
+ value: serverURL,
+ label: serverName || serverURL,
+ }
+
+ componentWillReceiveProps(nextProps) {
+ super.componentWillReceiveProps(nextProps)
+ this.updateHomeserverOptions()
+ }
+
+ updateHomeserverOptions() {
+ this.homeserverOptions = Object.entries(this.props.ctx.homeserversByName).map(this.homeserverEntry)
+ }
+
+ isValidHomeserver(value) {
+ try {
+ return Boolean(new URL(value))
+ } catch (err) {
+ return false
+ }
+ }
+
avatarUpload = async event => {
const file = event.target.files[0]
this.setState({
@@ -165,9 +195,10 @@ class Client extends BaseMainView {
name={this.isNew ? "id" : ""} className="id"
value={this.state.id} origValue={this.props.entry.id}
placeholder="@fancybot:example.com" onChange={this.inputChange}/>
-
+ this.setState({ homeserver: id })}
+ creatable={true} isValidNewOption={this.isValidHomeserver}/>