Proxy avatar requests through server and improve css

This commit is contained in:
Tulir Asokan 2018-11-10 12:57:06 +02:00
parent 2aac4fbee1
commit 53d2264351
14 changed files with 92 additions and 90 deletions

View File

@ -13,6 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional
from time import time from time import time
import json import json
@ -39,13 +40,31 @@ def create_token(user: UserID) -> str:
}) })
@routes.post("/auth/ping") def get_token(request: web.Request) -> str:
async def ping(request: web.Request) -> web.Response:
token = request.headers.get("Authorization", "") token = request.headers.get("Authorization", "")
if not token or not token.startswith("Bearer "): 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 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: if not data:
return resp.invalid_token return resp.invalid_token
user = data.get("user_id", None) user = data.get("user_id", None)

View File

@ -83,6 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response:
return resp.bad_client_access_token return resp.bad_client_access_token
except MatrixRequestError: except MatrixRequestError:
return resp.bad_client_access_details return resp.bad_client_access_details
except MatrixConnectionError:
return resp.bad_client_connection_details
except ValueError as e: except ValueError as e:
return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):]) return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
await client.update_avatar_url(data.get("avatar_url", None)) 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_uri": await client.client.upload_media(
content, request.headers.get("Content-Type", None)), 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))

View File

@ -19,7 +19,7 @@ import logging
from aiohttp import web from aiohttp import web
from .responses import resp from .responses import resp
from .auth import is_valid_token from .auth import check_token
Handler = Callable[[web.Request], Awaitable[web.Response]] 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: async def auth(request: web.Request, handler: Handler) -> web.Response:
if "/auth/" in request.path: if "/auth/" in request.path:
return await handler(request) return await handler(request)
token = request.headers.get("Authorization", "") return check_token(request) or await handler(request)
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)
log = logging.getLogger("maubot.server") log = logging.getLogger("maubot.server")

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const BASE_PATH = "/_matrix/maubot/v1" export const BASE_PATH = "/_matrix/maubot/v1"
export async function login(username, password) { export async function login(username, password) {
const resp = await fetch(`${BASE_PATH}/auth/login`, { const resp = await fetch(`${BASE_PATH}/auth/login`, {
@ -105,6 +105,10 @@ export async function uploadAvatar(id, data, mime) {
return await resp.json() return await resp.json()
} }
export function getAvatarURL(id) {
return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}`
}
export async function putClient(client) { export async function putClient(client) {
const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { const resp = await fetch(`${BASE_PATH}/client/${client.id}`, {
headers: getHeaders(), headers: getHeaders(),
@ -128,8 +132,9 @@ export async function deleteClient(id) {
} }
export default { export default {
BASE_PATH,
login, ping, login, ping,
getInstances, getInstance, getInstances, getInstance,
getPlugins, getPlugin, uploadPlugin, getPlugins, getPlugin, uploadPlugin,
getClients, getClient, uploadAvatar, putClient, deleteClient, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
} }

View File

@ -37,9 +37,12 @@ class Switch extends Component {
} }
} }
toggleKeyboard = evt => (evt.key === " " || evt.key === "Enter") && this.toggle()
render() { render() {
return ( return (
<div className="switch" data-active={this.state.active} onClick={this.toggle}> <div className="switch" data-active={this.state.active} onClick={this.toggle}
tabIndex="0" onKeyPress={this.toggleKeyboard}>
<div className="box"> <div className="box">
<span className="text"> <span className="text">
<span className="on">{this.props.onText || "On"}</span> <span className="on">{this.props.onText || "On"}</span>

View File

@ -44,7 +44,7 @@ class Main extends Component {
localStorage.username = username localStorage.username = username
this.setState({ authed: true }) this.setState({ authed: true })
} else { } else {
localStorage.accessToken = undefined delete localStorage.accessToken
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)

View File

@ -21,14 +21,6 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab
import Spinner from "../../components/Spinner" import Spinner from "../../components/Spinner"
import api from "../../api" 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 ClientListEntry = ({ client }) => {
const classes = ["client", "entry"] const classes = ["client", "entry"]
if (!client.enabled) { if (!client.enabled) {
@ -38,7 +30,7 @@ const ClientListEntry = ({ client }) => {
} }
return ( return (
<NavLink className={classes.join(" ")} to={`/client/${client.id}`}> <NavLink className={classes.join(" ")} to={`/client/${client.id}`}>
<img className="avatar" src={getAvatarURL(client)} alt={client.id.substr(1, 1)}/> <img className="avatar" src={api.getAvatarURL(client.id)} alt=""/>
<span className="displayname">{client.displayname || client.id}</span> <span className="displayname">{client.displayname || client.id}</span>
<ChevronRight/> <ChevronRight/>
</NavLink> </NavLink>
@ -153,8 +145,8 @@ class Client extends Component {
startOrStop = async () => { startOrStop = async () => {
this.setState({ startingOrStopping: true }) this.setState({ startingOrStopping: true })
const resp = await api.putClient({ const resp = await api.putClient({
id: this.state.id, id: this.props.client.id,
started: !this.state.started, started: !this.props.client.started,
}) })
if (resp.id) { if (resp.id) {
this.props.onChange(resp) this.props.onChange(resp)
@ -172,11 +164,11 @@ class Client extends Component {
return !Boolean(this.props.client) return !Boolean(this.props.client)
} }
renderSidebar = () => ( renderSidebar = () => !this.isNew && (
<div className="sidebar"> <div className="sidebar">
<div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"} <div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"}
${this.state.uploadingAvatar ? "uploading" : ""}`}> ${this.state.uploadingAvatar ? "uploading" : ""}`}>
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/> <img className="avatar" src={api.getAvatarURL(this.state.id)} alt="Avatar"/>
<UploadButton className="upload"/> <UploadButton className="upload"/>
<input className="file-selector" type="file" accept="image/png, image/jpeg" <input className="file-selector" type="file" accept="image/png, image/jpeg"
onChange={this.avatarUpload} disabled={this.state.uploadingAvatar} onChange={this.avatarUpload} disabled={this.state.uploadingAvatar}
@ -230,20 +222,7 @@ class Client extends Component {
</PrefTable> </PrefTable>
) )
renderInstances = () => ( renderPrefButtons = () => <>
<div className="instances">
<h3>Instances</h3>
{this.state.instances.map(instance => (
<Link className="instance" key={instance.id} to={`/instance/${instance.id}`}>
{instance.id}
</Link>
))}
</div>
)
renderInfoContainer = () => (
<div className="info">
{this.renderPreferences()}
<div className="buttons"> <div className="buttons">
{!this.isNew && ( {!this.isNew && (
<button className="delete" onClick={this.delete} disabled={this.loading}> <button className="delete" onClick={this.delete} disabled={this.loading}>
@ -255,14 +234,27 @@ class Client extends Component {
</button> </button>
</div> </div>
<div className="error">{this.state.error}</div> <div className="error">{this.state.error}</div>
{this.renderInstances()} </>
renderInstances = () => !this.isNew && (
<div className="instances">
<h3>{this.state.instances.length > 0 ? "Instances" : "No instances :("}</h3>
{this.state.instances.map(instance => (
<Link className="instance" key={instance.id} to={`/instance/${instance.id}`}>
{instance.id}
</Link>
))}
</div> </div>
) )
render() { render() {
return <div className="client"> return <div className="client">
{!this.isNew && this.renderSidebar()} {this.renderSidebar()}
{this.renderInfoContainer()} <div className="info">
{this.renderPreferences()}
{this.renderPrefButtons()}
{this.renderInstances()}
</div>
</div> </div>
} }
} }

View File

@ -34,34 +34,6 @@ body
right: 0 right: 0
background-color: $background-dark background-color: $background-dark
> *
background-color: $background
.maubot-loading .maubot-loading
margin-top: 10rem margin-top: 10rem
width: 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

View File

@ -22,7 +22,7 @@
background-color: $background background-color: $background
border: none border: none
border-radius: .25rem border-radius: .25rem
color: $inverted-text-color color: $text-color
box-sizing: border-box box-sizing: border-box
font-size: 1rem font-size: 1rem

View File

@ -37,7 +37,7 @@
transition: .5s transition: .5s
text-align: center text-align: center
color: $inverted-text-color color: $text-color
border-radius: .15rem 0 0 .15rem border-radius: .15rem 0 0 .15rem
background-color: $primary background-color: $primary
@ -50,7 +50,7 @@
text-align: center text-align: center
vertical-align: middle vertical-align: middle
color: $inverted-text-color color: $text-color
font-size: 1rem font-size: 1rem
user-select: none user-select: none
@ -67,14 +67,9 @@
transform: translateX(100%) transform: translateX(100%)
border-radius: 0 .15rem .15rem 0 border-radius: 0 .15rem .15rem 0
background-color: $primary
.on .on
display: inline display: inline
.off .off
display: none display: none

View File

@ -32,5 +32,4 @@
border: 1px solid $primary border: 1px solid $primary
&:hover &:hover
color: $inverted-text-color
background-color: $primary background-color: $primary

View File

@ -21,6 +21,7 @@
max-width: 60rem max-width: 60rem
margin: auto margin: auto
box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5)
background-color: $background
> a.title > a.title
grid-area: title grid-area: title

View File

@ -29,9 +29,8 @@
div.title div.title
h2 h2
margin: 0
display: inline-block display: inline-block
margin: 0 0 .25rem 0
font-size: 1.25rem font-size: 1.25rem
a a

View File

@ -13,16 +13,25 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from aiohttp import web
import logging import logging
import asyncio import asyncio
from aiohttp import web
from aiohttp.abc import AbstractAccessLogger
from mautrix.api import PathBuilder, Method from mautrix.api import PathBuilder, Method
from .config import Config from .config import Config
from .__meta__ import __version__ 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: class MaubotServer:
log: logging.Logger = logging.getLogger("maubot.server") log: logging.Logger = logging.getLogger("maubot.server")
@ -39,7 +48,7 @@ class MaubotServer:
as_path = PathBuilder(config["server.appservice_base_path"]) as_path = PathBuilder(config["server.appservice_base_path"])
self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) 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: def add_route(self, method: Method, path: PathBuilder, handler) -> None:
self.app.router.add_route(method.value, str(path), handler) self.app.router.add_route(method.value, str(path), handler)