Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
aa43327
IDP-45: add the favicon
D-GopalKrishna Oct 9, 2026
0ed0e67
IDP-45: dual login (EMBER-DANDI and OSB) from a Login dialog
D-GopalKrishna Oct 9, 2026
d8cf116
IDP-45: My dandisets link; Protocols page's Data upload button
D-GopalKrishna Oct 9, 2026
5b224d9
IDP-45: upload to EMBER-DANDI and import from it
D-GopalKrishna Oct 10, 2026
49eb19a
IDP-45: clearer names for the OSB URL settings; no bucket name in docs
D-GopalKrishna Oct 10, 2026
e67f9f4
IDP-45: share a copy with MAABCD, signed with a Keycloak user's key
D-GopalKrishna Oct 10, 2026
7316be9
IDP-45: keep the OSB login state current
D-GopalKrishna Oct 10, 2026
20b7e4c
IDP-45: tidy comments design-doc references
D-GopalKrishna Oct 10, 2026
9d64778
IDP-45: publish the researcher's dandiset for a DOI at the end of a run
D-GopalKrishna Oct 10, 2026
e2daf9b
IDP-45: show OSB as logged in only while there is a token
D-GopalKrishna Oct 10, 2026
f470d14
IDP-45: keep disabled primary buttons visible
D-GopalKrishna Oct 10, 2026
25a0bf7
IDP-45: Retry carries on where a run stopped; the dialog keeps its run
D-GopalKrishna Oct 10, 2026
0697910
IDP-45: clearer comments; drop an unused helper and a lint suppression
D-GopalKrishna Oct 10, 2026
c97cb6c
IDP-51: call OSB's /dandi upload endpoints; remove the old DANDI uplo…
D-GopalKrishna Oct 14, 2026
f8e68aa
IDP-51: publish button to publish and get a DOI for the uploaded data
D-GopalKrishna Oct 14, 2026
a947d87
IDP-51: rename the publish check by its role; document protocols and …
D-GopalKrishna Oct 22, 2026
a3c923a
IDP-52: publish once EMBER has checked every file
D-GopalKrishna Oct 22, 2026
a42476e
IDP-51: popup detection, bucket-mode login, tighter EMBER proxy paths
D-GopalKrishna Oct 26, 2026
0fb039a
IDP-51: check each recorded workspace instead of listing the first page
D-GopalKrishna Oct 26, 2026
eac97ff
IDP-51: fix EMBER api link to const; read every page of the user's da…
D-GopalKrishna Oct 26, 2026
c24a990
IDP-51: share with MAABCD as its own step, with Retry
D-GopalKrishna Oct 26, 2026
e8555e2
IDP-52: keep the run across a page reload; Retry on the failed step's…
D-GopalKrishna Oct 26, 2026
aab0205
IDP-52: choose the upload backend at build time; independent flow for…
D-GopalKrishna Oct 26, 2026
19f0080
IDP-52: log clearly when the chosen upload backend isn't set up
D-GopalKrishna Oct 26, 2026
404f908
IDP-52: 404 page for unknown URLs; fix EMBER-DANDI left in bucket mode
D-GopalKrishna Oct 27, 2026
4e924fd
IDP-52: stopped runs name the step and why; Retry on the row; plainer…
D-GopalKrishna Oct 27, 2026
8f99029
IDP-52: fix the path/run folder when the imports wait fails for OSB …
D-GopalKrishna Oct 27, 2026
80a2430
IDP-52: an ended OSB session stops the run in place; it reopens after…
D-GopalKrishna Oct 27, 2026
5129b47
IDP-52: fix silent fail of OSB's sign-in when can't be reached
D-GopalKrishna Oct 27, 2026
a640075
IDP-52: fix the dandiset picker making a call per dandiset and per wo…
D-GopalKrishna Oct 27, 2026
fac9c49
IDP-52: read a version's status, not just the draft's
D-GopalKrishna Oct 27, 2026
f3e6a25
fixing conditional destination for ember
ddelpiano Oct 27, 2026
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
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,21 @@ Unit tests (no browser, no backend):
yarn test:unit
```

Uploads (Scenario 1 of the MAABCD–OSB integration) go from the browser straight to a public bucket
open for anonymous writes, named by `VITE_UPLOAD_BUCKET_URL` (e.g. `https://storage.googleapis.com/maabcd`),
as `uploads/<protocolId>/<userSub>/<uploadId>/<filename>`. The bucket's CORS must allow PUT from this app's origin.
Uploads go to one of two places, chosen at build time by `VITE_UPLOAD_BACKEND`: `bucket`, or
EMBER-DANDI for any other value, including when it isn't set (the default). If the chosen backend
lacks what it needs (`VITE_EMBER_CLIENT_ID` for EMBER-DANDI, `VITE_UPLOAD_BUCKET_URL` for the
bucket), the rest of the site works, and signing in to EMBER-DANDI or uploading says uploads aren't
set up; the console names the missing variable. Either way, OSB imports the file into the
researcher's workspace from there.

- **`ember`** (default): EMBER-DANDI, into the researcher's own dandiset and the protocol's
MAABCD dandiset (`maabcdAggregateDandisetId` in `src/data/protocols.json`). Needs both logins
(EMBER-DANDI and OSB), `VITE_EMBER_CLIENT_ID`,
an EMBER-DANDI OAuth application (public client, PKCE) with `<this app's origin>/ember-callback`
as a redirect URI, and, for the MAABCD copy, the
`DANDI_API_KEY` attribute on the OSB user named by `MAABCD_EMBER_USERNAME` (`container.ts`).
- **`bucket`**: a public bucket open for anonymous writes, named by `VITE_UPLOAD_BUCKET_URL`.
Its CORS must allow PUT from this app's origin. Needs only the OSB login.

---

Expand Down
13 changes: 11 additions & 2 deletions applications/idp-arc/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,26 @@ COPY frontend/package.json ./
COPY frontend/yarn.lock ./
RUN yarn install --frozen-lockfile
COPY frontend/ ./
# Baked into the bundle by Vite at build time; the Codefresh pipeline passes it as a build argument.
# Baked into the bundle by Vite at build time; the Codefresh pipeline passes them as build arguments.
# `bucket` sends uploads to VITE_UPLOAD_BUCKET_URL instead of EMBER-DANDI (the default).
ARG VITE_UPLOAD_BACKEND
ENV VITE_UPLOAD_BACKEND=${VITE_UPLOAD_BACKEND}
ARG VITE_UPLOAD_BUCKET_URL
ENV VITE_UPLOAD_BUCKET_URL=${VITE_UPLOAD_BUCKET_URL}
# EMBER-DANDI OAuth application (public client, PKCE) registered for this IDP origin.
ARG VITE_EMBER_CLIENT_ID
ENV VITE_EMBER_CLIENT_ID=${VITE_EMBER_CLIENT_ID}
RUN yarn build

FROM nginxinc/nginx-unprivileged:stable-alpine

USER root
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build --chown=101:101 /app/dist/ /usr/share/nginx/html/
COPY --chown=101:101 deploy/resources/default.conf /etc/nginx/conf.d/default.conf
# The image's entrypoint renders templates into conf.d at start; the local-resolvers flag gives the
# template the pod's DNS servers so the EMBER proxy can resolve at request time (see default.conf).
COPY --chown=101:101 deploy/resources/default.conf /etc/nginx/templates/default.conf.template
ENV NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1
USER 101

EXPOSE 8080
28 changes: 28 additions & 0 deletions applications/idp-arc/deploy/resources/default.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Installed as an envsubst template (Dockerfile): ${NGINX_LOCAL_RESOLVERS} becomes the pod's
# nameservers from /etc/resolv.conf at container start. Other $names are nginx variables.
server {
listen 8080;

Expand All @@ -8,6 +10,32 @@ server {
try_files $uri $uri/ /index.html;
}

# EMBER's POST /oauth/token/ reply has no CORS header, so the browser can't read the login token
# from it directly; through this same-origin proxy it can. (File bytes go straight to S3, not here.)
# Only what the app calls: the token exchange and the REST API. Anything else under it is refused.
# The upstream is a variable so nginx resolves EMBER per request (honouring the DNS TTL) instead of
# once at startup: a failed lookup then only breaks EMBER calls, not nginx starting, and EMBER's IPs
# can change under us. A variable proxy_pass doesn't strip the location prefix, hence the rewrite.
resolver ${NGINX_LOCAL_RESOLVERS} ipv6=off;
resolver_timeout 5s;
set $ember https://api-dandi.emberarchive.org;

location /ember-proxy/oauth/token/ {
rewrite ^/ember-proxy(/.*)$ $1 break;
proxy_pass $ember;
proxy_ssl_server_name on;
proxy_set_header Host api-dandi.emberarchive.org;
}
location /ember-proxy/api/ {
rewrite ^/ember-proxy(/.*)$ $1 break;
proxy_pass $ember;
proxy_ssl_server_name on;
proxy_set_header Host api-dandi.emberarchive.org;
}
location /ember-proxy/ {
return 404;
}

location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ {
try_files $uri =404;
expires 7d;
Expand Down
1 change: 1 addition & 0 deletions applications/idp-arc/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>MAABCD</title>
</head>
<body>
Expand Down
30 changes: 30 additions & 0 deletions applications/idp-arc/frontend/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
111 changes: 86 additions & 25 deletions applications/idp-arc/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,58 @@ import { useTranslation } from 'react-i18next'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { ThemeProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import Typography from '@mui/material/Typography'

import { AppContext } from './AppContext'
import { authClient } from './app/container'
import { authClient, EMBER_CALLBACK_PATH } from './app/container'
import { theme } from './theme/components'
import type { AuthState } from './core/types'
import Home from './pages/LandingPage'
import Workspaces from './pages/Workspaces'
import ProtocolsPage from './pages/ProtocolsPage'
import AboutPage from './pages/AboutPage'
import EmberCallback from './pages/EmberCallback'
import NotFound from './pages/NotFound'
import { OSB_LOGIN_POPUP_NAME } from './components/loginReturn'

/**
* This window is OSB's (Keycloak) login popup, sent back here once the user has signed in. It only
* finishes the sign-in, hands it to the main tab and closes; it never shows the app (it would
* otherwise reopen the Login dialog: a popup starts with a copy of its opener's sessionStorage).
* Told by its window name (OSB_LOGIN_POPUP_NAME). EMBER-DANDI's popup is handled on its callback route.
*/
const IN_OSB_LOGIN_POPUP = typeof window !== 'undefined' && window.name === OSB_LOGIN_POPUP_NAME
&& window.location.pathname !== EMBER_CALLBACK_PATH

/** What the OSB login popup shows while it finishes the sign-in and closes itself. */
function OsbLoginPopupScreen({ authState }: { authState: AuthState }) {
const { t } = useTranslation('landingPage')
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 2, p: 4, textAlign: 'center' }}>
{authState === 'loading' || authState === 'authenticated' ? (
<>
<CircularProgress size={28} />
<Typography variant="body2" sx={{ opacity: 0.7 }}>{t('loginDialog.finishingSignIn')}</Typography>
</>
) : (
<Typography variant="body1">{t('loginDialog.closeWindow')}</Typography>
)}
</Box>
</ThemeProvider>
)
}

const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/workspaces', element: <Workspaces /> },
{ path: '/protocols', element: <ProtocolsPage /> },
{ path: '/about', element: <AboutPage /> },
{ path: EMBER_CALLBACK_PATH, element: <EmberCallback /> },
{ path: '*', element: <NotFound /> },
])

function App() {
Expand All @@ -29,31 +66,53 @@ function App() {

useEffect(() => {
if (initialized.current) return
initialized.current = true

authClient
.init()
.then((authenticated: boolean) => {
// If running inside the login popup, signal the parent and close.
if (authenticated && window.opener) {
window.opener.postMessage({ type: 'keycloak-login-complete' }, window.location.origin)
window.close()
return
}
setAuthState(authenticated ? 'authenticated' : 'unauthenticated')
if (authenticated && authClient.tokenParsed) {
setTokenParsed(authClient.tokenParsed)
}
})
.catch((err: Error) => {
// Clear stale auth fragment so a page refresh doesn't retry a failed token exchange.
if (window.location.hash) {
window.history.replaceState(null, '', window.location.pathname + window.location.search)
}
console.error('Keycloak init failed', err)
setAuthError(t('errors.authFailed'))
setAuthState('unauthenticated')
})
// Keycloak has no business on the EMBER-DANDI callback route: its check-sso can fall back to a
// real top-level redirect, which would abort the EMBER token exchange running there. It
// starts once the callback page has navigated back (router.subscribe: App renders the
// RouterProvider, so it isn't inside it and can't useLocation).
const tryInit = () => {
if (initialized.current) return
if (router.state.location.pathname === EMBER_CALLBACK_PATH) return
initialized.current = true
unsubscribe()
startKeycloak()
}
const unsubscribe = router.subscribe(tryInit)
tryInit()
return () => unsubscribe()

function startKeycloak() {
authClient
.init()
.then((authenticated: boolean) => {
// If running inside the login popup, signal the parent and close.
if (authenticated && IN_OSB_LOGIN_POPUP && window.opener) {
window.opener.postMessage({ type: 'keycloak-login-complete' }, window.location.origin)
window.close()
return
}
setAuthState(authenticated ? 'authenticated' : 'unauthenticated')
// The OSB session can end later (its refresh refused): show it as signed out then, so
// the Login dialog offers OSB's login again instead of "Logged in".
authClient.onSessionEnded(() => {
setAuthState('unauthenticated')
setTokenParsed(null)
})
if (authenticated && authClient.tokenParsed) {
setTokenParsed(authClient.tokenParsed)
}
})
.catch((err: Error) => {
// Clear stale auth fragment so a page refresh doesn't retry a failed token exchange.
if (window.location.hash) {
window.history.replaceState(null, '', window.location.pathname + window.location.search)
}
console.error('Keycloak init failed', err)
setAuthError(t('errors.authFailed'))
setAuthState('unauthenticated')
})
}
}, [t])

// Reload after popup login so check-sso picks up the new Keycloak session.
Expand All @@ -77,6 +136,8 @@ function App() {
[authState, tokenParsed, authError, username],
)

if (IN_OSB_LOGIN_POPUP) return <OsbLoginPopupScreen authState={authState} />

return (
<AppContext.Provider value={appContextValue}>
<ThemeProvider theme={theme}>
Expand Down
Loading
Loading