Skip to content

Commit

Permalink
support embargoed dandisets
Browse files Browse the repository at this point in the history
  • Loading branch information
magland committed Sep 27, 2023
1 parent a119c76 commit c19b83c
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 8 deletions.
73 changes: 73 additions & 0 deletions gui/src/ApiKeysWindow/ApiKeysWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FunctionComponent, useCallback, useEffect, useReducer } from "react"

type ApiKeysWindowProps = {
onClose: () => void
}

type KeysState = {
dandiApiKey: string
dandiStagingApiKey: string
}

const defaultKeysState: KeysState = {
dandiApiKey: '',
dandiStagingApiKey: ''
}

type KeysAction = {
type: 'setDandiApiKey' | 'setDandiStagingApiKey'
value: string
}

const keysReducer = (state: KeysState, action: KeysAction): KeysState => {
switch (action.type) {
case 'setDandiApiKey':
return {...state, dandiApiKey: action.value}
case 'setDandiStagingApiKey':
return {...state, dandiStagingApiKey: action.value}
default:
throw new Error('invalid action type')
}
}

const ApiKeysWindow: FunctionComponent<ApiKeysWindowProps> = ({onClose}) => {
const [keys, keysDispatch] = useReducer(keysReducer, defaultKeysState)
useEffect(() => {
// initialize from local storage
const dandiApiKey = localStorage.getItem('dandiApiKey') || ''
const dandiStagingApiKey = localStorage.getItem('dandiStagingApiKey') || ''
keysDispatch({type: 'setDandiApiKey', value: dandiApiKey})
keysDispatch({type: 'setDandiStagingApiKey', value: dandiStagingApiKey})
}, [])
const handleSave = useCallback(() => {
localStorage.setItem('dandiApiKey', keys.dandiApiKey)
localStorage.setItem('dandiStagingApiKey', keys.dandiStagingApiKey)
onClose()
}, [keys, onClose])
return (
<div style={{padding: 30}}>
<h3>Set API Keys</h3>
<hr />
<table className="table-1" style={{maxWidth: 300}}>
<tbody>
<tr>
<td>DANDI API Key: </td>
<td><input type="password" value={keys.dandiApiKey} onChange={e => keysDispatch({type: 'setDandiApiKey', value: e.target.value})} /></td>
</tr>
<tr>
<td>DANDI Staging API Key: </td>
<td><input type="password" value={keys.dandiStagingApiKey} onChange={e => keysDispatch({type: 'setDandiStagingApiKey', value: e.target.value})} /></td>
</tr>
</tbody>
</table>
<hr />
<div>
<button onClick={handleSave}>Save</button>
&nbsp;
<button onClick={onClose}>Cancel</button>
</div>
</div>
)
}

export default ApiKeysWindow
21 changes: 20 additions & 1 deletion gui/src/ApplicationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Login, Logout } from "@mui/icons-material";
import { Key, Login, Logout } from "@mui/icons-material";
import { AppBar, Toolbar } from "@mui/material";
import { FunctionComponent, useCallback, useMemo, useState } from "react";
import Hyperlink from "./components/Hyperlink";
Expand All @@ -7,6 +7,8 @@ import GitHubLoginWindow from "./GitHub/GitHubLoginWindow";
import { useGithubAuth } from "./GithubAuth/useGithubAuth";
import UserIdComponent from "./UserIdComponent";
import useRoute from "./useRoute";
import SmallIconButton from "./components/SmallIconButton";
import ApiKeysWindow from "./ApiKeysWindow/ApiKeysWindow";

type Props = {
// none
Expand All @@ -30,6 +32,7 @@ const ApplicationBar: FunctionComponent<Props> = () => {
}, [setRoute])

const {visible: githubAccessWindowVisible, handleOpen: openGitHubAccessWindow, handleClose: closeGitHubAccessWindow} = useModalDialog()
const {visible: apiKeysWindowVisible, handleOpen: openApiKeysWindow, handleClose: closeApiKeysWindow} = useModalDialog()

// light greenish background color for app bar
// const barColor = '#e0ffe0'
Expand All @@ -50,6 +53,14 @@ const ApplicationBar: FunctionComponent<Props> = () => {
<div onClick={onHome} style={{cursor: 'pointer', color: titleColor}}>&nbsp;&nbsp;&nbsp;Neurosift</div>
<div style={{color: bannerColor, position: 'relative', top: -2}}>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{star} This viewer is in alpha and is under <Hyperlink color={bannerColor} href="https://github.com/flatironinstitute/neurosift" target="_blank">active development</Hyperlink> {star}</div>
<span style={{marginLeft: 'auto'}} />
<span style={{color: 'yellow'}}>
<SmallIconButton
icon={<Key />}
onClick={openApiKeysWindow}
title={`Set DANDI API key`}
/>
</span>
&nbsp;&nbsp;
{
signedIn && (
<span style={{fontFamily: 'courier', color: 'lightgray', cursor: 'pointer'}} title={`Signed in as ${userId}`} onClick={openGitHubAccessWindow}><UserIdComponent userId={userId} />&nbsp;&nbsp;</span>
Expand Down Expand Up @@ -80,6 +91,14 @@ const ApplicationBar: FunctionComponent<Props> = () => {
onClose={() => closeGitHubAccessWindow()}
/>
</ModalWindow>
<ModalWindow
open={apiKeysWindowVisible}
// onClose={closeApiKeysWindow}
>
<ApiKeysWindow
onClose={() => closeApiKeysWindow()}
/>
</ModalWindow>
</span>
)
}
Expand Down
55 changes: 48 additions & 7 deletions gui/src/pages/NwbPage/NwbPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import NwbTabWidget from "./NwbTabWidget"
import { getRemoteH5File, globalRemoteH5FileStats, RemoteH5File } from "./RemoteH5File/RemoteH5File"
import { SelectedItemViewsContext, selectedItemViewsReducer } from "./SelectedItemViewsContext"
import { fetchJson } from "./viewPlugins/ImageSeries/ImageSeriesItemView"
import getAuthorizationHeaderForUrl from "./getAuthorizationHeaderForUrl"

type Props = {
width: number
Expand Down Expand Up @@ -74,16 +75,17 @@ const NwbPageChild: FunctionComponent<Props> = ({width, height}) => {
useEffect(() => {
let canceled = false
const load = async () => {
const metaUrl = await getMetaUrl(url || defaultUrl)
const urlResolved = await getResolvedUrl(url || defaultUrl)
const metaUrl = await getMetaUrl(urlResolved)
if (canceled) return
if ((!metaUrl) && (url) && (rtcshareClient)) {
console.info(`Requesting meta for ${url}`)
if ((!metaUrl) && (urlResolved) && (rtcshareClient)) {
console.info(`Requesting meta for ${urlResolved}`)
rtcshareClient.serviceQuery('neurosift-nwb-request', {
type: 'request-meta-nwb',
nwbUrl: url
nwbUrl: urlResolved
})
}
const f = await getRemoteH5File(url || defaultUrl, metaUrl)
const f = await getRemoteH5File(urlResolved, metaUrl)
if (canceled) return
setNwbFile(f)
}
Expand All @@ -105,7 +107,7 @@ const NwbPageChild: FunctionComponent<Props> = ({width, height}) => {
)
}

export const headRequest = async (url: string) => {
export const headRequest = async (url: string, headers?: any) => {
// Cannot use HEAD, because it is not allowed by CORS on DANDI AWS bucket
// let headResponse
// try {
Expand All @@ -123,7 +125,10 @@ export const headRequest = async (url: string) => {
// Instead, use aborted GET.
const controller = new AbortController();
const signal = controller.signal;
const response = await fetch(url, { signal })
const response = await fetch(url, {
signal,
headers
})
controller.abort();
return response
}
Expand Down Expand Up @@ -183,4 +188,40 @@ const getMetaUrl = async (url: string) => {
// return undefined
}

const getResolvedUrl = async (url: string) => {
if (isDandiAssetUrl(url)) {
const authorizationHeader = getAuthorizationHeaderForUrl(url)
const headers = authorizationHeader ? {Authorization: authorizationHeader} : undefined
const redirectUrl = await getRedirectUrl(url, headers)
if (redirectUrl) {
return redirectUrl
}
}
return url
}

const getRedirectUrl = async (url: string, headers: any) => {
// This is tricky. Normally we would do a HEAD request with a redirect: 'manual' option.
// and then look at the Location response header.
// However, we run into mysterious cors problems
// So instead, we do a HEAD request with no redirect option, and then look at the response.url
const response = await headRequest(url, headers)
if (response.url) return response.url

// if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
// return response.headers.get('Location')
// }

return null // No redirect
}

const isDandiAssetUrl = (url: string) => {
if (url.startsWith('https://api-staging.dandiarchive.org/')) {
return true
}
if (url.startsWith('https://api.dandiarchive.org/')) {
return true
}
}

export default NwbPage
14 changes: 14 additions & 0 deletions gui/src/pages/NwbPage/getAuthorizationHeaderForUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const getAuthorizationHeaderForUrl = (url?: string) => {
if (!url) return ''
let key = ''
if (url.startsWith('https://api-staging.dandiarchive.org/')) {
key = localStorage.getItem('dandiStagingApiKey') || ''
}
else if (url.startsWith('https://api.dandiarchive.org/')) {
key = localStorage.getItem('dandiApiKey') || ''
}
if (key) return 'token ' + key
else return ''
}

export default getAuthorizationHeaderForUrl

0 comments on commit c19b83c

Please sign in to comment.