Add more stuff

This commit is contained in:
Tulir Asokan 2020-10-31 23:54:08 +02:00
parent 9151f4cb6d
commit 0b15a44820
8 changed files with 158 additions and 29 deletions

View File

@ -13,7 +13,20 @@
#
# 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/>.
from aiohttp import web
from ..database import User, AccessToken
routes = web.RouteTableDef()
@routes.get("/whoami")
async def whoami(req: web.Request) -> web.Response:
user: User = req["user"]
token: AccessToken = req["token"]
return web.json_response({
"id": user.id,
"widget_secret": user.widget_secret,
"homeserver_url": user.homeserver_url,
"last_seen": int(token.last_seen_date.timestamp() / 60) * 60,
})

View File

@ -37,7 +37,8 @@ class AccessToken(Base):
@classmethod
async def get(cls, token_id: int) -> Optional['AccessToken']:
q = "SELECT user_id, token_hash, last_seen_ip, last_seen_date FROM pack WHERE token_id=$1"
q = ("SELECT user_id, token_hash, last_seen_ip, last_seen_date "
"FROM access_token WHERE token_id=$1")
row: asyncpg.Record = await cls.db.fetchrow(q, token_id)
if row is None:
return None
@ -48,7 +49,7 @@ class AccessToken(Base):
== datetime.now().replace(second=0, microsecond=0)):
# Same IP and last seen on this minute, skip update
return
q = ("UPDATE access_token SET last_seen_ip=$3, last_seen_date=current_timestamp "
q = ("UPDATE access_token SET last_seen_ip=$2, last_seen_date=current_timestamp "
"WHERE token_id=$1 RETURNING last_seen_date")
self.last_seen_date = await self.db.fetchval(q, self.token_id, ip)
self.last_seen_ip = ip

73
web/src/setup/App.js Normal file
View File

@ -0,0 +1,73 @@
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
// Copyright (C) 2020 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.
import { useEffect, useState } from "../../lib/preact/hooks.js"
import { html } from "../../lib/htm/preact.js"
import LoginView from "./LoginView.js"
import Spinner from "../Spinner.js"
import * as matrix from "./matrix-api.js"
import * as sticker from "./sticker-api.js"
const App = () => {
const [loggedIn, setLoggedIn] = useState(Boolean(localStorage.mxAccessToken))
const [widgetSecret, setWidgetSecret] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
if (!loggedIn) {
return html`
<${LoginView}
onLoggedIn=${() => setLoggedIn(Boolean(localStorage.mxAccessToken))}
/>`
}
useEffect(() => {
if (widgetSecret === null) {
setLoading(true)
const whoamiReceived = data => {
setLoading(false)
setWidgetSecret(data.widget_secret)
}
const reauth = async () => {
const openIDToken = await matrix.requestOpenIDToken(
localStorage.mxHomeserver, localStorage.mxUserID, localStorage.mxAccessToken)
const integrationData = await matrix.requestIntegrationToken(openIDToken)
localStorage.stickerSetupAccessToken = integrationData.token
return await sticker.whoami()
}
const whoamiErrored = err => {
console.error("Setup API whoami returned", err)
if (err.code === "NET.MAUNIUM_TOKEN_EXPIRED" || err.code === "M_UNKNOWN_TOKEN") {
return reauth().then(whoamiReceived)
} else {
throw err
}
}
sticker.whoami().then(whoamiReceived, whoamiErrored).catch(err => {
setLoading(false)
setError(err.message)
})
}
}, [])
if (loading) {
return html`<${Spinner} size=80 green />`
}
return html`${widgetSecret}`
}
export default App

View File

@ -16,13 +16,7 @@
import { useEffect, useLayoutEffect, useRef, useState } from "../../lib/preact/hooks.js"
import { html } from "../../lib/htm/preact.js"
import {
getLoginFlows,
loginMatrix,
requestIntegrationToken,
requestOpenIDToken,
resolveWellKnown,
} from "./matrix-api.js"
import * as matrix from "./matrix-api.js"
import Button from "../Button.js"
import Spinner from "../Spinner.js"
@ -87,11 +81,11 @@ const LoginView = ({ onLoggedIn }) => {
previousServerValue.current = server
setSupportedFlows(null)
setError(null)
resolveWellKnown(server).then(url => {
matrix.resolveWellKnown(server).then(url => {
setServerURL(url)
localStorage.mxServerName = server
localStorage.mxHomeserver = url
return getLoginFlows(url)
return matrix.getLoginFlows(url)
}).then(flows => {
setSupportedFlows(flows)
}).catch(err => {
@ -135,15 +129,13 @@ const LoginView = ({ onLoggedIn }) => {
}
try {
const actualServerURL = serverURLOverride || serverURL
const [accessToken, userID, realURL] = await loginMatrix(actualServerURL, authInfo)
console.log(userID, realURL)
const openIDToken = await requestOpenIDToken(realURL, userID, accessToken)
console.log(openIDToken)
const integrationData = await requestIntegrationToken(openIDToken)
console.log(integrationData)
const [accessToken, userID, realURL] = await matrix.login(actualServerURL, authInfo)
const openIDToken = await matrix.requestOpenIDToken(realURL, userID, accessToken)
const integrationData = await matrix.requestIntegrationToken(openIDToken)
localStorage.mxHomeserver = realURL
localStorage.mxAccessToken = accessToken
localStorage.mxUserID = userID
localStorage.accessToken = integrationData.token
localStorage.stickerSetupAccessToken = integrationData.token
onLoggedIn()
} catch (err) {
setError(err.message)

View File

@ -15,6 +15,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { html, render } from "../../lib/htm/preact.js"
import LoginView from "./LoginView.js"
import App from "./App.js"
render(html`<${LoginView} onLoggedIn=${() => console.log("Logged in")}/>`, document.body)
render(html`<${App} />`, document.body)

View File

@ -13,7 +13,6 @@
//
// 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/>.
import { tryFetch, integrationPrefix } from "./tryGet.js"
export const resolveWellKnown = async (server) => {
@ -41,7 +40,7 @@ export const getLoginFlows = async (address) => {
return flows
}
export const loginMatrix = async (address, authInfo) => {
export const login = async (address, authInfo) => {
const data = await tryFetch(`${address}/_matrix/client/r0/login`, {
method: "POST",
body: JSON.stringify({
@ -67,6 +66,17 @@ export const loginMatrix = async (address, authInfo) => {
return [data.access_token, data.user_id, address]
}
export const whoami = (address, accessToken) => tryFetch(
`${address}/_matrix/client/r0/account/whoami`,
{
headers: { Authorization: `Bearer ${accessToken}` },
},
{
service: address,
requestType: "whoami",
},
)
export const requestOpenIDToken = (address, userID, accessToken) => tryFetch(
`${address}/_matrix/client/r0/user/${userID}/openid/request_token`,
{

View File

@ -0,0 +1,34 @@
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
// Copyright (C) 2020 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.
import { tryFetch as tryFetchDefault, setupPrefix } from "./tryGet.js"
const service = "setup API"
const tryFetch = (url, options, reqInfo) => {
if (!options.headers?.Authorization) {
if (!options.headers) {
options.headers = {}
}
options.headers.Authorization = `Bearer ${localStorage.stickerSetupAccessToken}`
}
return tryFetchDefault(url, options, reqInfo)
}
export const whoami = () => tryFetch(
`${setupPrefix}/whoami`,
{}, { service, requestType: "whoami" },
)

View File

@ -13,8 +13,8 @@
//
// 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/>.
export const integrationPrefix = "../_matrix/integrations/v1"
export const setupPrefix = "api"
export const queryToURL = (url, query) => {
if (!Array.isArray(query)) {
@ -26,15 +26,19 @@ export const queryToURL = (url, query) => {
return url
}
class MatrixError extends Error {
constructor(data, status) {
super(data.error)
this.code = data.errcode
this.httpStatus = status
}
}
export const tryFetch = async (url, options, reqInfo) => {
if (options.query) {
url = queryToURL(url, options.query)
delete options.query
}
options.headers = {
Authorization: `Bearer ${localStorage.accessToken}`,
...options.headers,
}
const reqName = `${reqInfo.service} ${reqInfo.requestType}`
let resp
try {
@ -59,7 +63,9 @@ export const tryFetch = async (url, options, reqInfo) => {
console.error(reqName, "request JSON parse failed:", err)
throw new Error(`Invalid response from ${reqInfo.service}`)
}
if (resp.status >= 400) {
if (data.error && data.errcode) {
throw new MatrixError(data, resp.status)
} else if (resp.status >= 400) {
console.error("Unexpected", reqName, "request status:", resp.status, data)
throw new Error(data.error || data.message || `Invalid response from ${reqInfo.service}`)
}