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

chore: refactor frontend API client methods & add doc comments #1864

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions packages/api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ export type NFT = {
*/
pin?: { name?: string; meta?: Record<string, string> }
/**
* Name of the JWT token used to create this NFT.
* Optional name of the file(s) uploaded as NFT.
*/
name?: string
/**
* Optional name of the file(s) uploaded as NFT.
* Name of the JWT token used to create this NFT.
*/
scope: string
/**
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 { getMagic } from '../lib/magic.js'
import { logoutMagicSession } from '../lib/magic.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 getMagic().user.logout()
await logoutMagicSession()
delete sessionStorage.hasSeenUserBlockedModal
handleClearUser()
Router.push({ pathname: '/', query: version ? { version } : null })
Expand Down
257 changes: 160 additions & 97 deletions packages/website/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,146 +1,209 @@
import { getMagic } from './magic'
import { getMagicUserToken } from './magic'
import constants from './constants'
import { NFTStorage } from 'nft.storage'

export const API = constants.API
const API = constants.API

const LIFESPAN = 60 * 60 * 2 // 2 hours
/** @type {string | undefined} */
let token
let created = Date.now() / 1000
/**
* TODO(maybe): define a "common types" package, so we can share definitions with the api?
*
* @typedef {object} APITokenInfo an object describing an nft.storage API token
* @property {number} id
* @property {string} name
* @property {string} secret
* @property {number} user_id
* @property {string} inserted_at
* @property {string} updated_at
* @property {string} [deleted_at]
*
* @typedef {'queued' | 'pinning' | 'pinned' | 'failed'} PinStatus
* @typedef {object} Pin an object describing a remote "pin" of an NFT.
* @property {string} cid
* @property {PinStatus} status
* @property {string} created
* @property {string} [name]
* @property {number} [size]
* @property {Record<string, string>} [meta]
*
* @typedef {'queued' | 'active' | 'published' | 'terminated'} DealStatus
* @typedef {object} Deal an object describing a Filecoin deal
* @property {DealStatus} status
* @property {string} datamodelSelector
* @property {string} pieceCid
* @property {string} batchRootCid
* @property {string} [lastChanged]
* @property {number} [chainDealID]
* @property {string} [statusText]
* @property {string} [dealActivation]
* @property {string} [dealExpiration]
* @property {string} [miner]
*
* @typedef {object} NFTResponse an object describing an uploaded NFT, including pinning and deal info
* @property {string} cid - content identifier for the NFT data
* @property {string} type - either "directory" or the value of Blob.type (mime type)
* @property {Array<{ name?: string, type?: string }>} files - files in the directory (only if this NFT is a directory).
* @property {string} [name] - optional name of the file(s) uploaded as NFT.
* @property {string} scope - name of the JWT token used to create this NFT.
* @property {string} created - date this NFT was created in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format: YYYY-MM-DDTHH:MM:SSZ.
* @property {number} size
* @property {Pin} pin
* @property {Deal[]} deals
*
*
* @typedef {object} VersionInfo an object with version info for the nft.storage service
* @property {string} version - semver version number
* @property {string} commit - git commit hash
* @property {string} branch - git branch name
* @property {string} mode - maintenance mode state
*
* @typedef {object} StatsData an object with global stats about the nft.storage service
* @property {number} deals_total
* @property {number} deals_size_total
* @property {number} uploads_past_7_total
* @property {number} uploads_blob_total
* @property {number} uploads_car_total
* @property {number} uploads_nft_total
* @property {number} uploads_remote_total
* @property {number} uploads_multipart_total
*/

export async function getToken() {
const magic = getMagic()
const now = Date.now() / 1000
if (token === undefined || now - created > LIFESPAN - 10) {
token = await magic.user.getIdToken({ lifespan: LIFESPAN })
created = Date.now() / 1000
}
return token
/**
* @returns {Promise<NFTStorage>} an NFTStorage client instance, authenticated with the current user's auth token.
*/
export async function getStorageClient() {
return new NFTStorage({
token: await getMagicUserToken(),
endpoint: new URL(API + '/'),
})
}

/**
* Get tokens
* Get a list of objects describing the user's API tokens.
*
* @returns {Promise<APITokenInfo[]>} (async) a list of APITokenInfo objects for each of the user's API tokens
*/
export async function getTokens() {
const res = await fetch(API + `/internal/tokens`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
})

const body = await res.json()

if (body.ok) {
return body.value
} else {
throw new Error(body.error.message)
}
return (await fetchAuthenticated('/internal/tokens')).value
}

/**
* Delete Token
* Delete one of the user's API tokens with the given name
*
* @param {string} name
*/
export async function deleteToken(name) {
const res = await fetch(API + `/internal/tokens`, {
return fetchAuthenticated('/internal/tokens', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
body: JSON.stringify({ id: name }),
})

const body = await res.json()

if (body.ok) {
return body
} else {
throw new Error(body.error.message)
}
}

/**
* Create Token
* Create an API token with the given name.
*
* @param {string} name
*/
export async function createToken(name) {
const res = await fetch(API + `/internal/tokens`, {
return fetchAuthenticated('/internal/tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
body: JSON.stringify({ name }),
})

const body = await res.json()

if (body.ok) {
return body
} else {
throw new Error(body.error.message)
}
}

/**
* Get NFTs
* Get a list of the user's stored NFTs.
*
* @param {object} query
* @param {number} query.limit - maximum number of NFTs to return
* @param {string} query.before - only return NFTs uploaded before this date (ISO-8601 datetime string)
*
* @param {{limit: number, before: string }} query
* @returns {Promise<NFTResponse[]>}
*/
export async function getNfts({ limit, before }) {
const params = new URLSearchParams({ before, limit: String(limit) })
const res = await fetch(`${API}/?${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
})

const body = await res.json()

if (body.ok) {
return body.value.filter(Boolean)
} else {
throw new Error(body.error.message)
}
const result = await fetchAuthenticated(`/?${params}`)
return result.value.filter(Boolean)
}

/**
* Get the set of tags applied to this user account.
*
* See `packages/api/src/routes/user-tags.js` for tag definitions.
*
* @returns {Promise<Record<string, boolean>>} (async) object whose keys are tag names, with boolean values for tag state.
*/
export async function getUserTags() {
const res = await fetch(`${API}/user/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
})
return (await fetchAuthenticated('/user/tags')).value
}

const body = await res.json()
/**
* @returns {Promise<VersionInfo>} (async) version information for API service
*/
export async function getVersion() {
return (await fetchRoute('/version')).value
}

if (body.ok) {
return body.value
} else {
throw new Error(body.error.message)
/**
* @returns {Promise<StatsData>} (async) global service stats
*/
export async function getStats() {
// @ts-expect-error the stats route is an odd duck... it returns `{ ok, data }` instead of `{ ok, value }`
return (await fetchRoute('/stats')).data
}

/**
* Sends a `fetch` request to an API route, using the current user's authentiation token.
*
* See {@link fetchRoute}.
*
* @param {string} route api route (path + query portion of URL)
* @param {Record<string, any>} fetchOptions options to pass through to `fetch`
* @returns {Promise<{ok: boolean, value: any}>} JSON response body.
*/
async function fetchAuthenticated(route, fetchOptions = {}) {
fetchOptions.headers = {
...fetchOptions.headers,
Authorization: 'Bearer ' + (await getMagicUserToken()),
}
return fetchRoute(route, fetchOptions)
}

export async function getVersion() {
const route = '/version'
const res = await fetch(`${API}${route}`, {
/**
* Sends a `fetch` request to an API route and unpacks the JSON response body.
*
* Note that it does not unpack the `.value` field from the body, so
* you get a response like: `{"ok": true, "value": "thing you care about"}`
*
* Defaults to GET requests, but you can pass in whatever `method` you want to the `fetchOptions` param.
*
* @param {string} route
* @param {Record<string, any>} fetchOptions
* @returns {Promise<{ok: boolean, value: any}>} JSON response body.
*/
async function fetchRoute(route, fetchOptions = {}) {
if (!route.startsWith('/')) {
route = '/' + route
}
const url = API + route
const defaultHeaders = {
'Content-Type': 'application/json',
}

const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
...fetchOptions,
headers: { ...defaultHeaders, ...fetchOptions.headers },
}

if (res.ok) {
return await res.json()
const res = await fetch(url, options)
if (!res.ok) {
throw new Error(`HTTP error: [${res.status}] ${res.statusText}`)
}

const body = await res.json()
if (body.ok) {
return body
} else {
throw new Error(`failed to get version ${res.status}`)
throw new Error(body.error.message)
}
}
Loading