Skip to content
Open
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
265 changes: 169 additions & 96 deletions packages/website/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,146 +1,219 @@
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 {{limit: number, before: string }} query
* @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)
*
* @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 result = await fetchAuthenticated(`/?${params}`)
return result.value.filter(Boolean)
}

const body = await res.json()
/**
* 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() {
return (await fetchAuthenticated('/user/tags')).value
}

if (body.ok) {
return body.value.filter(Boolean)
} else {
throw new Error(body.error.message)
/**
* @returns {Promise<VersionInfo>} (async) version information for API service
*/
export async function getVersion() {
// the '/version' route doesn't wrap its response in `{ ok, value }` like `fetchRoute` expects
const res = await fetch(API + '/version')
if (!res.ok) {
throw new Error(`HTTP error: [${res.status}] ${res.statusText}`)
}
return res.json()
}

export async function getUserTags() {
const res = await fetch(`${API}/user/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + (await getToken()),
},
})

const body = await res.json()
/**
* @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
}

if (body.ok) {
return body.value
} else {
throw new Error(body.error.message)
/**
* 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}`)
if (body.error && body.error.message) {
throw new Error(body.error.message)
}
throw new Error(
`unexpected response: ok != true, but body is missing error message`
)
}
}
Loading