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}/>