Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Graphistry
client={client}
dataset="Miserables"
// other props...
/>
);
```

This approach provides:
- JWT-based authentication (vs deprecated API keys)
- Automatic token management and refresh
- Secure server-to-server communication

<br><br>

## @graphistry/node-api
Expand Down Expand Up @@ -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
41 changes: 37 additions & 4 deletions projects/client-api-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ const propTypes = {
onLabelsUpdate: PropTypes.func,
selectionUpdateOptions: PropTypes.object,

queryParamExtra: PropTypes.object
queryParamExtra: PropTypes.object,
client: PropTypes.object
};

const defaultProps = {
Expand Down Expand Up @@ -465,20 +466,24 @@ 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);
const [gErr, setGErr] = useState(undefined);
const prevSub = usePrevious(gSub);

const [axesMap] = useState(new WeakMap());
const iframeElementRef = useRef(null);

const [isFirstRun, setFirstRun] = useState(true);
handleUpdates({ g, isFirstRun, axesMap, props });
Expand Down Expand Up @@ -523,9 +528,10 @@ const Graphistry = forwardRef((props, ref) => {

return {
g,
client,
...exportedCalls
};
}, [g]);
}, [g, client, dataset]);

useEffect(() => {
if (g && onSelectionUpdate) {
Expand Down Expand Up @@ -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}` : ``) +
Expand Down Expand Up @@ -586,6 +614,11 @@ const Graphistry = forwardRef((props, ref) => {
tolerateLoadErrors
});

const combinedIframeRef = useCallback((iframeElement) => {
iframeRef(iframeElement);
iframeElementRef.current = iframeElement;
}, [iframeRef]);

const children = [
<ETLUploader
key={`g_etl_${props.key}`}
Expand Down Expand Up @@ -618,7 +651,7 @@ const Graphistry = forwardRef((props, ref) => {
children.push(
<iframe
key={`g_iframe_${url}_${props.key}`}
ref={iframeRef}
ref={combinedIframeRef}
scrolling='no'
style={iframeStyle}
className={iframeClassName}
Expand Down
150 changes: 148 additions & 2 deletions projects/client-api-react/src/stories/Graphistry.stories.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import '../../assets/index.css';

import { Graphistry } from '../index';
import { Graphistry, Client } from '../index';

import {
//graphistry
Expand Down Expand Up @@ -482,3 +482,149 @@ export const Ticks = {
</>);
},
};

// test for auth, can remove later
export const ClientVerificationTest = () => {
const graphistryRef = useRef();
const [verificationLog, setVerificationLog] = useState([]);
const [client, setClient] = useState(null);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [org, setOrg] = useState('');
const [isConnecting, setIsConnecting] = useState(false);

const addLog = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString();
setVerificationLog(prev => [...prev, `[${timestamp}] ${type.toUpperCase()}: ${message}`]);
console.log(`[AUTH TEST] ${message}`);
};

const createTestClient = async () => {
if (!username || !password) {
addLog('❌ Need username/password to create client', 'error');
return;
}

try {
setIsConnecting(true);
addLog('Creating Client instance...', 'info');

const testClient = new Client(username, password, org, 'https', 'hub.graphistry.com');
addLog('✅ Client instance created', 'success');

addLog('Waiting for authentication...', 'info');
await testClient._getAuthTokenPromise;

addLog('✅ Client authenticated successfully', 'success');
addLog(`Token received: ${testClient._token.substring(0, 20)}...`, 'info');
setClient(testClient);

} catch (err) {
addLog(`❌ Client creation failed: ${err.message}`, 'error');
} finally {
setIsConnecting(false);
}
};

return (
<div style={{ display: 'flex', height: '600px' }}>
{/* control */}
<div style={{ width: '350px', padding: '10px', borderRight: '1px solid #ddd', overflowY: 'auto' }}>
<h3>JWT Auth Test</h3>

{/* auth */}
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#f8f9fa', border: '1px solid #e9ecef' }}>
<h4 style={{ marginTop: 0 }}>Create Client</h4>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ width: '100%', marginBottom: '5px', padding: '5px', boxSizing: 'border-box' }}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ width: '100%', marginBottom: '5px', padding: '5px', boxSizing: 'border-box' }}
/>
<input
type="text"
placeholder="Org (optional)"
value={org}
onChange={(e) => setOrg(e.target.value)}
style={{ width: '100%', marginBottom: '10px', padding: '5px', boxSizing: 'border-box' }}
/>
<button
onClick={createTestClient}
disabled={isConnecting || !username || !password}
style={{
width: '100%',
padding: '8px',
backgroundColor: client ? '#28a745' : '#007bff',
color: 'white',
border: 'none',
cursor: (isConnecting || !username || !password) ? 'not-allowed' : 'pointer'
}}
>
{isConnecting ? 'Authenticating...' : client ? '✅ Authenticated' : 'Authenticate'}
</button>
</div>

{/* log */}
<div style={{ backgroundColor: '#f8f9fa', border: '1px solid #e9ecef', flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '5px', borderBottom: '1px solid #e9ecef', backgroundColor: '#e9ecef', fontWeight: 'bold', fontSize: '12px' }}>
Auth Log
<button
onClick={() => setVerificationLog([])}
style={{ float: 'right', fontSize: '10px', padding: '2px 6px', cursor: 'pointer' }}
>
Clear
</button>
</div>
<div style={{ padding: '5px', fontSize: '10px', fontFamily: 'monospace', flex: 1, overflowY: 'auto' }}>
{verificationLog.length === 0 ? (
<div style={{ color: '#666' }}>Enter credentials and click "Authenticate"</div>
) : (
verificationLog.map((log, index) => (
<div
key={index}
style={{
marginBottom: '2px',
color: log.includes('SUCCESS') ? '#155724' : log.includes('ERROR') ? '#721c24' : log.includes('WARNING') ? '#856404' : '#495057'
}}
>
{log}
</div>
))
)}
</div>
</div>
</div>

{/* viz */}
<div style={{ flex: 1 }}>
<Graphistry
ref={graphistryRef}
client={client}
dataset="Miserables"
graphistryHost="https://hub.graphistry.com"
onClientAPIConnected={(g) => {
addLog('🔗 Graphistry iframe connected', 'success');

// check if token was stored
const storedToken = localStorage.getItem('graphistry_auth_token');
if (storedToken) {
addLog('✅ JWT token stored in localStorage', 'success');
addLog('✅ Token sent to iframe via postMessage', 'success');
} else {
addLog('⚠️ No JWT token in localStorage', 'warning');
}
}}
/>
</div>
</div>
);
};