diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index 34303d1..e754809 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional from time import time import json @@ -39,13 +40,31 @@ def create_token(user: UserID) -> str: }) -@routes.post("/auth/ping") -async def ping(request: web.Request) -> web.Response: +def get_token(request: web.Request) -> str: token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): + token = request.query.get("access_token", None) + else: + token = token[len("Bearer "):] + return token + + +def check_token(request: web.Request) -> Optional[web.Response]: + token = get_token(request) + if not token: + return resp.no_token + elif not is_valid_token(token): + return resp.invalid_token + return None + + +@routes.post("/auth/ping") +async def ping(request: web.Request) -> web.Response: + token = get_token(request) + if not token: return resp.no_token - data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):]) + data = verify_token(get_config()["server.unshared_secret"], token) if not data: return resp.invalid_token user = data.get("user_id", None) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 5bee0af..2975581 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -83,6 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response: return resp.bad_client_access_token except MatrixRequestError: return resp.bad_client_access_details + except MatrixConnectionError: + return resp.bad_client_connection_details except ValueError as e: return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):]) await client.update_avatar_url(data.get("avatar_url", None)) @@ -142,3 +144,14 @@ async def upload_avatar(request: web.Request) -> web.Response: "content_uri": await client.client.upload_media( content, request.headers.get("Content-Type", None)), }) + + +@routes.get("/client/{id}/avatar") +async def download_avatar(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + if not client.avatar_url or client.avatar_url == "disable": + return web.Response() + return web.Response(body=await client.client.download_media(client.avatar_url)) diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 2fefbe8..79bc26b 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -19,7 +19,7 @@ import logging from aiohttp import web from .responses import resp -from .auth import is_valid_token +from .auth import check_token Handler = Callable[[web.Request], Awaitable[web.Response]] @@ -28,12 +28,7 @@ Handler = Callable[[web.Request], Awaitable[web.Response]] async def auth(request: web.Request, handler: Handler) -> web.Response: if "/auth/" in request.path: return await handler(request) - token = request.headers.get("Authorization", "") - if not token or not token.startswith("Bearer "): - return resp.no_token - if not is_valid_token(token[len("Bearer "):]): - return resp.invalid_token - return await handler(request) + return check_token(request) or await handler(request) log = logging.getLogger("maubot.server") diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index e9fc926..6342b9d 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const BASE_PATH = "/_matrix/maubot/v1" +export const BASE_PATH = "/_matrix/maubot/v1" export async function login(username, password) { const resp = await fetch(`${BASE_PATH}/auth/login`, { @@ -105,6 +105,10 @@ export async function uploadAvatar(id, data, mime) { return await resp.json() } +export function getAvatarURL(id) { + return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` +} + export async function putClient(client) { const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { headers: getHeaders(), @@ -128,8 +132,9 @@ export async function deleteClient(id) { } export default { + BASE_PATH, login, ping, getInstances, getInstance, getPlugins, getPlugin, uploadPlugin, - getClients, getClient, uploadAvatar, putClient, deleteClient, + getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js index 975b47c..6ef43fd 100644 --- a/maubot/management/frontend/src/components/Switch.js +++ b/maubot/management/frontend/src/components/Switch.js @@ -37,9 +37,12 @@ class Switch extends Component { } } + toggleKeyboard = evt => (evt.key === " " || evt.key === "Enter") && this.toggle() + render() { return ( -
+
{this.props.onText || "On"} diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index efb9ac3..b655085 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -44,7 +44,7 @@ class Main extends Component { localStorage.username = username this.setState({ authed: true }) } else { - localStorage.accessToken = undefined + delete localStorage.accessToken } } catch (err) { console.error(err) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 1350a31..ef414a0 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -21,14 +21,6 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab import Spinner from "../../components/Spinner" import api from "../../api" -function getAvatarURL(client) { - if (!client.avatar_url) { - return "" - } - const id = client.avatar_url.substr("mxc://".length) - return `${client.homeserver}/_matrix/media/r0/download/${id}` -} - const ClientListEntry = ({ client }) => { const classes = ["client", "entry"] if (!client.enabled) { @@ -38,7 +30,7 @@ const ClientListEntry = ({ client }) => { } return ( - {client.id.substr(1, + {client.displayname || client.id} @@ -153,8 +145,8 @@ class Client extends Component { startOrStop = async () => { this.setState({ startingOrStopping: true }) const resp = await api.putClient({ - id: this.state.id, - started: !this.state.started, + id: this.props.client.id, + started: !this.props.client.started, }) if (resp.id) { this.props.onChange(resp) @@ -172,11 +164,11 @@ class Client extends Component { return !Boolean(this.props.client) } - renderSidebar = () => ( + renderSidebar = () => !this.isNew && (
- Avatar + Avatar ) - renderInstances = () => ( + renderPrefButtons = () => <> +
+ {!this.isNew && ( + + )} + +
+
{this.state.error}
+ + + renderInstances = () => !this.isNew && (
-

Instances

+

{this.state.instances.length > 0 ? "Instances" : "No instances :("}

{this.state.instances.map(instance => ( {instance.id} @@ -241,28 +247,14 @@ class Client extends Component {
) - renderInfoContainer = () => ( -
- {this.renderPreferences()} -
- {!this.isNew && ( - - )} - -
-
{this.state.error}
- {this.renderInstances()} -
- ) - render() { return
- {!this.isNew && this.renderSidebar()} - {this.renderInfoContainer()} + {this.renderSidebar()} +
+ {this.renderPreferences()} + {this.renderPrefButtons()} + {this.renderInstances()} +
} } diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index ba0406b..c30dd67 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -34,34 +34,6 @@ body right: 0 background-color: $background-dark - > * - background-color: $background - .maubot-loading margin-top: 10rem width: 10rem - -//.lindeb - > header - position: absolute - top: 0 - height: $header-height - left: 0 - right: 0 - - > main - position: absolute - top: $header-height - bottom: 0 - left: 0 - right: 0 - - text-align: center - - > .lindeb-content - text-align: left - display: inline-block - width: 100% - max-width: $max-width - box-sizing: border-box - padding: 0 1rem diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 6270aba..99a6e5e 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -22,7 +22,7 @@ background-color: $background border: none border-radius: .25rem - color: $inverted-text-color + color: $text-color box-sizing: border-box font-size: 1rem diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass index 570320c..ac18689 100644 --- a/maubot/management/frontend/src/style/lib/switch.sass +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -37,7 +37,7 @@ transition: .5s text-align: center - color: $inverted-text-color + color: $text-color border-radius: .15rem 0 0 .15rem background-color: $primary @@ -50,7 +50,7 @@ text-align: center vertical-align: middle - color: $inverted-text-color + color: $text-color font-size: 1rem user-select: none @@ -67,14 +67,9 @@ transform: translateX(100%) border-radius: 0 .15rem .15rem 0 - background-color: $primary .on display: inline .off display: none - - - - diff --git a/maubot/management/frontend/src/style/pages/client/instances.sass b/maubot/management/frontend/src/style/pages/client/instances.sass index 7d5fb3c..24c5407 100644 --- a/maubot/management/frontend/src/style/pages/client/instances.sass +++ b/maubot/management/frontend/src/style/pages/client/instances.sass @@ -32,5 +32,4 @@ border: 1px solid $primary &:hover - color: $inverted-text-color background-color: $primary diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 8ec9563..9eb8ecb 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -21,6 +21,7 @@ max-width: 60rem margin: auto box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) + background-color: $background > a.title grid-area: title diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 8015e1a..c086714 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -29,9 +29,8 @@ div.title h2 - margin: 0 display: inline-block - + margin: 0 0 .25rem 0 font-size: 1.25rem a diff --git a/maubot/server.py b/maubot/server.py index 501677a..5788d8a 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -13,16 +13,25 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web import logging import asyncio +from aiohttp import web +from aiohttp.abc import AbstractAccessLogger + from mautrix.api import PathBuilder, Method from .config import Config from .__meta__ import __version__ +class AccessLogger(AbstractAccessLogger): + def log(self, request: web.Request, response: web.Response, time: int): + self.logger.info(f'{request.remote} "{request.method} {request.path} ' + f'{response.status} {response.body_length} ' + f'in {round(time, 4)}s"') + + class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") @@ -39,7 +48,7 @@ class MaubotServer: as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) - self.runner = web.AppRunner(self.app) + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler)