Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: login by passing API token in URL #hash #1875

Draft
wants to merge 2 commits into
base: chore/refactor-frontend-api-methods
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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])
Expand Down
16 changes: 16 additions & 0 deletions packages/api/src/routes/user-metadata.js
Original file line number Diff line number Diff line change
@@ -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,
})
}
1 change: 1 addition & 0 deletions packages/api/src/utils/db-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
`
Expand Down
4 changes: 2 additions & 2 deletions packages/website/components/navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 })
Expand Down
68 changes: 65 additions & 3 deletions packages/website/lib/api.js
Original file line number Diff line number Diff line change
@@ -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?
Expand Down Expand Up @@ -67,16 +68,68 @@ const API = constants.API
* @property {number} uploads_multipart_total
*/

export async function logoutUserSession() {
deleteSavedRequestToken()
return logoutMagicSession()
}

/**
* @returns {Promise<NFTStorage>} 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.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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)
}
Expand Down
14 changes: 13 additions & 1 deletion packages/website/lib/magic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand All @@ -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<string>} the encoded magic.link token
* @returns {Promise<string|null>} the encoded magic.link token
*/
export async function getMagicUserToken() {
const magic = getMagic()
Expand All @@ -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,
})
Expand Down
12 changes: 5 additions & 7 deletions packages/website/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/website/pages/api-docs.js
Original file line number Diff line number Diff line change
@@ -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 })
Expand All @@ -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) {}
Expand Down