From 79a52141a598f0a48d736c21925ce9cd796f5ed8 Mon Sep 17 00:00:00 2001 From: Yusef Napora Date: Fri, 29 Apr 2022 19:30:17 -0400 Subject: [PATCH 1/2] feat: add /user/meta route to return user info --- packages/api/src/index.js | 2 ++ packages/api/src/routes/user-metadata.js | 16 ++++++++++++++++ packages/api/src/utils/db-client.js | 1 + 3 files changed, 19 insertions(+) create mode 100644 packages/api/src/routes/user-metadata.js diff --git a/packages/api/src/index.js b/packages/api/src/index.js index ce6880f329..e1a075476b 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -23,6 +23,7 @@ import { metaplexUpload } from './routes/metaplex-upload.js' import { blogSubscribe } from './routes/blog-subscribe.js' import { userDIDRegister } from './routes/user-did-register.js' import { userTags } from './routes/user-tags.js' +import { userMetadata } from './routes/user-metadata.js' import { ucanToken } from './routes/ucan-token.js' import { did } from './routes/did.js' @@ -158,6 +159,7 @@ r.add( [postCors] ) r.add('get', '/user/tags', withAuth(withMode(userTags, RO)), [postCors]) +r.add('get', '/user/meta', withAuth(withMode(userMetadata, RO)), [postCors]) // Tokens r.add('get', '/internal/tokens', withAuth(withMode(tokensList, RO)), [postCors]) diff --git a/packages/api/src/routes/user-metadata.js b/packages/api/src/routes/user-metadata.js new file mode 100644 index 0000000000..695a4639c1 --- /dev/null +++ b/packages/api/src/routes/user-metadata.js @@ -0,0 +1,16 @@ +import { checkAuth } from '../utils/auth.js' +import { JSONResponse } from '../utils/json-response.js' + +/** @type {import('../bindings').Handler} */ +export const userMetadata = async (event, ctx) => { + const { user } = checkAuth(ctx) + + const issuer = user.magic_link_id ?? user.did ?? user.github_id + const publicAddress = user.public_address + + const meta = { issuer, publicAddress } + return new JSONResponse({ + ok: true, + value: meta, + }) +} diff --git a/packages/api/src/utils/db-client.js b/packages/api/src/utils/db-client.js index 975c0b85cb..b3d7bc2864 100644 --- a/packages/api/src/utils/db-client.js +++ b/packages/api/src/utils/db-client.js @@ -91,6 +91,7 @@ export class DBClient { magic_link_id, github_id, did, + public_address, keys:auth_key_user_id_fkey(user_id,id,name,secret), tags:user_tag_user_id_fkey(user_id,id,tag,value) ` From 8622dd1535da9a1952d8aabb0197ffc302683ad3 Mon Sep 17 00:00:00 2001 From: Yusef Napora Date: Fri, 29 Apr 2022 19:31:09 -0400 Subject: [PATCH 2/2] feat: login by passing API key in #fragment --- packages/website/components/navbar.js | 4 +- packages/website/lib/api.js | 68 +++++++++++++++++++++++++-- packages/website/lib/magic.js | 14 +++++- packages/website/pages/_app.js | 12 ++--- packages/website/pages/api-docs.js | 4 +- 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/website/components/navbar.js b/packages/website/components/navbar.js index 9074b8c217..7f05ba5641 100644 --- a/packages/website/components/navbar.js +++ b/packages/website/components/navbar.js @@ -7,7 +7,7 @@ import Hamburger from '../icons/hamburger' import Link from 'next/link' import clsx from 'clsx' import countly from '../lib/countly' -import { logoutMagicSession } from '../lib/magic.js' +import { logoutUserSession } from '../lib/api.js' import { useQueryClient } from 'react-query' import Logo from '../components/logo' import { useUser } from 'lib/user.js' @@ -30,7 +30,7 @@ export default function Navbar({ bgColor = 'bg-nsorange', logo, user }) { const version = /** @type {string} */ (query.version) const logout = useCallback(async () => { - await logoutMagicSession() + await logoutUserSession() delete sessionStorage.hasSeenUserBlockedModal handleClearUser() Router.push({ pathname: '/', query: version ? { version } : null }) diff --git a/packages/website/lib/api.js b/packages/website/lib/api.js index 3c4f14d52b..02ef8458f7 100644 --- a/packages/website/lib/api.js +++ b/packages/website/lib/api.js @@ -1,8 +1,9 @@ -import { getMagicUserToken } from './magic' +import { getMagicUserToken, logoutMagicSession } from './magic' import constants from './constants' import { NFTStorage } from 'nft.storage' const API = constants.API +const REQUEST_TOKEN_STORAGE_KEY = 'client-request-token' /** * TODO(maybe): define a "common types" package, so we can share definitions with the api? @@ -67,16 +68,68 @@ const API = constants.API * @property {number} uploads_multipart_total */ +export async function logoutUserSession() { + deleteSavedRequestToken() + return logoutMagicSession() +} + /** * @returns {Promise} an NFTStorage client instance, authenticated with the current user's auth token. */ export async function getStorageClient() { + const token = await getClientRequestToken() + if (!token) { + throw new Error( + `can't get storage client: not logged in / no request token available` + ) + } + return new NFTStorage({ - token: await getMagicUserToken(), + token, endpoint: new URL(API + '/'), }) } +export async function getClientRequestToken() { + let token = tokenFromLocationHash() + if (token) { + saveRequestToken(token) + window.location.hash = '' + return token + } + + token = getSavedRequestToken() + if (token) { + return token + } + + return getMagicUserToken() +} + +/** + * @param {string} token + */ +function saveRequestToken(token) { + localStorage.setItem(REQUEST_TOKEN_STORAGE_KEY, token) +} + +/** + * @returns {string|null} + */ +function getSavedRequestToken() { + return localStorage.getItem(REQUEST_TOKEN_STORAGE_KEY) +} + +function deleteSavedRequestToken() { + localStorage.removeItem(REQUEST_TOKEN_STORAGE_KEY) +} + +function tokenFromLocationHash() { + const fragment = window.location.hash.replace(/^#/, '') + const params = new URLSearchParams(fragment) + return params.get('authToken') +} + /** * Get a list of objects describing the user's API tokens. * @@ -156,6 +209,15 @@ export async function getStats() { return (await fetchRoute('/stats')).data } +export async function getUserMetadata() { + const token = await getClientRequestToken() + if (!token) { + return null + } + + return (await fetchAuthenticated('/user/meta')).value +} + /** * Sends a `fetch` request to an API route, using the current user's authentiation token. * @@ -168,7 +230,7 @@ export async function getStats() { async function fetchAuthenticated(route, fetchOptions = {}) { fetchOptions.headers = { ...fetchOptions.headers, - Authorization: 'Bearer ' + (await getMagicUserToken()), + Authorization: 'Bearer ' + (await getClientRequestToken()), // TODO: handle null here } return fetchRoute(route, fetchOptions) } diff --git a/packages/website/lib/magic.js b/packages/website/lib/magic.js index c808d2fcd8..a3accd10f1 100644 --- a/packages/website/lib/magic.js +++ b/packages/website/lib/magic.js @@ -39,6 +39,13 @@ function getMagic() { } export async function logoutMagicSession() { + // sadly, trying to log out without this check results in + // an error about trying to mutate a database that doesn't exist unless you have a valid session. + // magic.link uses indexdb to store session state, which could be the source. + const loggedIn = await getMagic().user.isLoggedIn() + if (!loggedIn) { + return + } return getMagic().user.logout() } @@ -47,7 +54,7 @@ export async function logoutMagicSession() { * it's still within its expiry time. If the token is nearing expiration, a * new one is requested asynchronously. * - * @returns {Promise} the encoded magic.link token + * @returns {Promise} the encoded magic.link token */ export async function getMagicUserToken() { const magic = getMagic() @@ -58,6 +65,11 @@ export async function getMagicUserToken() { return _magicUserToken } + const loggedIn = await magic.user.isLoggedIn() + if (!loggedIn) { + return null + } + _magicUserToken = await magic.user.getIdToken({ lifespan: MAGIC_USER_TOKEN_LIFESPAN_SEC, }) diff --git a/packages/website/pages/_app.js b/packages/website/pages/_app.js index 5d22995e53..2cf20005a0 100644 --- a/packages/website/pages/_app.js +++ b/packages/website/pages/_app.js @@ -6,9 +6,8 @@ import Layout from '../components/layout.js' import { ReactQueryDevtools } from 'react-query/devtools' import Router, { useRouter } from 'next/router' import countly from '../lib/countly' -import { getUserTags } from '../lib/api' +import { getUserMetadata, getUserTags } from '../lib/api' import { useCallback, useEffect, useState } from 'react' -import { getMagicUserMetadata } from 'lib/magic' import * as Sentry from '@sentry/nextjs' import { UserContext } from 'lib/user' import BlockedUploadsModal from 'components/blockedUploadsModal.js' @@ -30,12 +29,11 @@ export default function App({ Component, pageProps }) { useState(false) const handleIsLoggedIn = useCallback(async () => { - const data = await getMagicUserMetadata() + const data = await getUserMetadata() if (!data) return - if (data) { - // @ts-ignore - Sentry.setUser(user) - } + + // @ts-ignore + Sentry.setUser(user) const tags = await getUserTags() if (tags.HasAccountRestriction && !sessionStorage.hasSeenUserBlockedModal) { sessionStorage.hasSeenUserBlockedModal = true diff --git a/packages/website/pages/api-docs.js b/packages/website/pages/api-docs.js index 11e8f3d151..6b811ea003 100644 --- a/packages/website/pages/api-docs.js +++ b/packages/website/pages/api-docs.js @@ -1,5 +1,5 @@ // @ts-ignore -import { getMagicUserToken } from 'lib/magic' +import { getClientRequestToken } from 'lib/api' import dynamic from 'next/dynamic' const DynamicSwaggerUI = dynamic(import('swagger-ui-react'), { ssr: false }) @@ -11,7 +11,7 @@ const DynamicSwaggerUI = dynamic(import('swagger-ui-react'), { ssr: false }) const requestHandler = async (req) => { let token try { - token = await getMagicUserToken() + token = await getClientRequestToken() // @ts-ignore req.headers.Authorization = 'Bearer ' + token } catch (error) {}