diff --git a/Dockerfile b/Dockerfile index 37e0b6c2..dcbd6edd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ FROM node:16.13.0-slim as base WORKDIR /opt/graphistry-js +RUN sed -i 's/deb.debian.org/archive.debian.org/g' /etc/apt/sources.list && \ + sed -i 's/security.debian.org/archive.debian.org/g' /etc/apt/sources.list && \ + sed -i '/buster-updates/d' /etc/apt/sources.list + RUN --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \ --mount=target=/var/cache/apt,type=cache,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean \ diff --git a/README.md b/README.md index 1699ca92..4cf75e97 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,38 @@ import { Graphistry } from '@graphistry/client-api-react';` // + variants for di See [@graphistry/client-api-react project](projects/client-api-react/README.md), [interactive storybook docs](https://graphistry.github.io/graphistry-js/), and [Create React App project sample](projects/cra-test/README.md) +### Authentication with Client API + +For secure authentication, create a `Client` instance and pass it to your components: + +```javascript +import { Client, Dataset, EdgeFile, NodeFile } from '@graphistry/client-api'; +import { Graphistry } from '@graphistry/client-api-react'; + +// Create authenticated client +const client = new Client( + 'my_username', + 'my_password', + '', // org (optional) + 'https', // protocol + 'hub.graphistry.com' // host +); + +// Use with React component +return ( + +); +``` + +This approach provides: +- JWT-based authentication (vs deprecated API keys) +- Automatic token management and refresh +- Secure server-to-server communication +

## @graphistry/node-api @@ -140,9 +172,11 @@ To support server-acceleration and fast interactions, Graphistry decouples uploa - You can configure your Graphistry server to run as http, https, or both - Uploads require authentication + - **Recommended**: The `Client` class provides modern JWT-based authentication for both browser and Node.js environments - The `node-api` client already uses the new JWT-based protocol ("API 3") - - Deprecated: The clientside JavaScript convenience APIs still use the deprecrated "API 1" protocol (key-based), which lacks JWT-based authentication and authorization + - **Deprecated**: The clientside JavaScript convenience APIs still use the deprecated "API 1" protocol (key-based), which lacks JWT-based authentication and authorization - We recommend clients instead use `fetch` or other HTTP callers to directly invoke the REST API: See how the `node-api` performs it - The client JavaScript APIs will updated to the new JWT method alongside recent CORS and SSO updates; contact staff if you desire assistance + - **Deprecated**: Legacy API key authentication is still supported but will be phased out - Sessions are based on unguessable web keys: sharing a secret ID means sharing read access -- Datasets are immutable and thus their integrity is safe for sharing, while session state (e.g., filters) are writable: share a copy when in doubt +- Datasets are immutable and thus their integrity is safe for sharing, while session state (e.g., filters) are writable: share a copy when in doubt \ No newline at end of file diff --git a/projects/client-api-react/src/index.js b/projects/client-api-react/src/index.js index 9b1e7ead..2cf5b14a 100644 --- a/projects/client-api-react/src/index.js +++ b/projects/client-api-react/src/index.js @@ -104,7 +104,8 @@ const propTypes = { onLabelsUpdate: PropTypes.func, selectionUpdateOptions: PropTypes.object, - queryParamExtra: PropTypes.object + queryParamExtra: PropTypes.object, + client: PropTypes.object }; const defaultProps = { @@ -465,13 +466,16 @@ const Graphistry = forwardRef((props, ref) => { onUpdateObservableG, onSelectionUpdate, onLabelsUpdate, - selectionUpdateOptions + selectionUpdateOptions, + client } = props; const [loading, setLoading] = useState(!!props.loading); const [dataset, setDataset] = useState(props.dataset); const [loadingMessage, setLoadingMessage] = useState(props.loadingMessage || ''); + const authToken = props.client && props.client.authTokenValid() ? props.client._token : null; + const [g, setG] = useState(null); const [gObs, setGObs] = useState(null); const [gSub, setGSub] = useState(null); @@ -479,6 +483,7 @@ const Graphistry = forwardRef((props, ref) => { const prevSub = usePrevious(gSub); const [axesMap] = useState(new WeakMap()); + const iframeElementRef = useRef(null); const [isFirstRun, setFirstRun] = useState(true); handleUpdates({ g, isFirstRun, axesMap, props }); @@ -523,9 +528,10 @@ const Graphistry = forwardRef((props, ref) => { return { g, + client, ...exportedCalls }; - }, [g]); + }, [g, client, dataset]); useEffect(() => { if (g && onSelectionUpdate) { @@ -556,6 +562,28 @@ const Graphistry = forwardRef((props, ref) => { } }, [g, onLabelsUpdate]) + // jwt token storage + postMessage to iframe + useEffect(() => { + if (authToken && iframeElementRef.current) { + try { + localStorage.setItem('graphistry_auth_token', authToken); + console.info('Stored JWT token for iframe authentication'); + + const iframe = iframeElementRef.current; + if (iframe.contentWindow) { + const targetOrigin = new URL(graphistryHost).origin; + iframe.contentWindow.postMessage({ + type: 'auth-token-available', + agent: 'graphistryjs', + token: authToken + }, targetOrigin); + } + } catch (error) { + console.warn('Failed to store or send auth token:', error); + } + } + }, [authToken, graphistryHost]); + const playNormalized = typeof play === 'boolean' ? play : (play | 0) * 1000; const optionalParams = (type ? `&type=${type}` : ``) + (controls ? `&controls=${controls}` : ``) + @@ -586,6 +614,11 @@ const Graphistry = forwardRef((props, ref) => { tolerateLoadErrors }); + const combinedIframeRef = useCallback((iframeElement) => { + iframeRef(iframeElement); + iframeElementRef.current = iframeElement; + }, [iframeRef]); + const children = [ { children.push(