Skip to content

Commit

Permalink
personal key auth (#156)
Browse files Browse the repository at this point in the history
add a new function "ClientPKey" to all projects which allows for authentication using personal keys

---------

Co-authored-by: Leo Meyerovich <[email protected]>
  • Loading branch information
mj3cheun and lmeyerov authored Mar 15, 2024
1 parent 3d069ba commit 10fd9ab
Show file tree
Hide file tree
Showing 13 changed files with 569 additions and 136 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

## Dev

## 5.1.0 - 2024-03-15

### Added

* **client-api**: Add API-key-based `ClientPKey` upload client
* **client-api-react**: Add API-key-based `ClientPKey` upload client
* **node-api**: Add API-key-based `ClientPKey` upload client
* **js-upload-api**: Add API-key-based `ClientPKey` upload client

### Refactor

* Factor out base class `AbstractClient` and add type export of `ClientType`

## 5.0.2 - 2024-02-13

### Added
Expand Down
3 changes: 2 additions & 1 deletion projects/client-api-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import * as gAPI from '@graphistry/client-api';
import { ajax, catchError, first, forkJoin, map, of, switchMap, tap } from '@graphistry/client-api'; // avoid explicit rxjs dep
import { bg } from './bg';
import { bindings, panelNames, calls } from './bindings.js';
import { Client as ClientBase, selectionUpdates, subscribeLabels } from '@graphistry/client-api';
import { Client as ClientBase, ClientPKey as ClientPKeyBase, selectionUpdates, subscribeLabels } from '@graphistry/client-api';

export const Client = ClientBase;
export const ClientPKey = ClientPKeyBase;

//https://blog.logrocket.com/how-to-get-previous-props-state-with-react-hooks/
function usePrevious(value) {
Expand Down
44 changes: 41 additions & 3 deletions projects/client-api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import shallowEqual from 'shallowequal';
import { Model } from '@graphistry/falcor-model-rxjs';
import { PostMessageDataSource } from '@graphistry/falcor-socket-datasource';
import { $ref, $atom, $value } from '@graphistry/falcor-json-graph';
import { Client as ClientBase, Dataset as DatasetBase, File as FileBase, EdgeFile as EdgeFileBase, NodeFile as NodeFileBase } from '@graphistry/js-upload-api';
import { Client as ClientBase, ClientPKey as ClientPKeyBase, Dataset as DatasetBase, File as FileBase, EdgeFile as EdgeFileBase, NodeFile as NodeFileBase } from '@graphistry/js-upload-api';


const CLIENT_SUBSCRIPTION_API_VERSION = 1;
Expand All @@ -16,7 +16,7 @@ export const NodeFile = NodeFileBase;

//FIXME not generating jsdoc
/**
* Class wrapping @graphistry/js-upload-api::Client for client->server File and Dataset uploads.
* Class wrapping @graphistry/js-upload-api::Client for client->server File and Dataset uploads using username and password authentication.
* @global
* @extends ClientBase
*/
Expand Down Expand Up @@ -45,14 +45,52 @@ export class Client extends ClientBase {
clientProtocolHostname,
version
) {
console.debug('new client', { username }, { password }, { protocol }, { host }, { clientProtocolHostname }, { version });
// console.debug('new client', { username }, { password }, { protocol }, { host }, { clientProtocolHostname }, { version });
super(
username, password, org,
protocol, host, clientProtocolHostname,
window.fetch.bind(window), version, '@graphistry/client-api');
}
}

/**
* Class wrapping @graphistry/js-upload-api::ClientPKey for client->server File and Dataset uploads using personal key authentication.
* @global
* @extends ClientPKeyBase
*/
export class ClientPKey extends ClientPKeyBase {
/**
* Create a Client
* @constructor
* @param {string} personalKeyId - Graphistry server personal key ID
* @param {string} personalKeySecret - Graphistry server personal key secret
* @param {string} org - Graphistry organization (optional)
* @param {string} [protocol='https'] - 'http' or 'https' for client->server upload communication
* @param {string} [host='hub.graphistry.com'] - Graphistry server hostname
* @param {clientProtocolHostname} clientProtocolHostname - Override URL base path shown in browsers. By default uses protocol/host combo, e.g., https://hub.graphistry.com
*
* For more examples, see @graphistry/node-api and @graphistry/js-upload-api docs
*
* @example **Authenticate against Graphistry Hub**
* ```javascript
* import { Client } from '@graphistry/client-api';
* const client = new Client('my_personal_key_id', 'my_personal_key_secret');
* ```
*/
constructor(
personalKeyId, personalKeySecret, org = undefined,
protocol = 'https', host = 'hub.graphistry.com',
clientProtocolHostname,
version
) {
// console.debug('new client', { personalKeyId }, { personalKeySecret }, { protocol }, { host }, { clientProtocolHostname }, { version });
super(
personalKeyId, personalKeySecret, org,
protocol, host, clientProtocolHostname,
window.fetch.bind(window), version, '@graphistry/client-api');
}
}

import {
ajax,
catchError,
Expand Down
165 changes: 165 additions & 0 deletions projects/js-upload-api/src/AbstractClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
export abstract class AbstractClient {
public readonly protocol: string;
public readonly host: string;
public readonly clientProtocolHostname: string;
public readonly agent: string;
public readonly version?: string;
public readonly org?: string;


protected _getAuthTokenPromise?: Promise<string>; // undefined if not configured

protected _token?: string;

protected fetch: any; // eslint-disable-line @typescript-eslint/no-explicit-any

/**
* @param org The organization to use (optional)
* @param protocol The protocol to use for the server during uploads: 'http' or 'https'.
* @param host The hostname of the server during uploads: defaults to 'hub.graphistry.com'
* @param clientProtocolHostname Base path to use inside the browser and when sharing public URLs: defaults to '{protocol}://{host}'
* @param fetch The fetch implementation to use: defaults to JavaScript fetch function
* @param version The version of the client library
* @param agent The agent name to use when communicating with the server
*/
constructor (
org?: string,
protocol = 'https', host = 'hub.graphistry.com',
clientProtocolHostname?: string,
fetchFn: any = fetch,
version?: string,
agent = '@graphistry/js-upload-api',
) {
this.org = org;
this.protocol = protocol;
this.host = host;
this.fetch = fetchFn;
this.version = version;
this.agent = agent;
this.clientProtocolHostname = clientProtocolHostname || `${protocol}://${host}`;
}

/**
*
* See examples at top of file
*
* Set JWT token if already known
*
* @param token The token to use for authentication.
* @returns The client instance.
*/
public setToken(token: string) {
this._token = token;
return this;
}

/**
*
* @returns Whether the current token is valid
*
*/
public authTokenValid() {
return Boolean(this._token);
}

/**
* @internal
* Internal helper
* @param uri The URI to upload to.
* @param payload The payload to upload.
* @param baseHeaders Optionally override base header object to mix with auth header for the upload.
* @returns The response from the server.
*/
public async post(uri: string, payload: any, baseHeaders: any = undefined) {
console.debug('post', {uri, payload});
const headers = await this.getSecureHeaders(baseHeaders);
console.debug('post', {headers});
const response = await (await this.postToApi(uri, payload, headers)).json();
console.debug('post response', {uri, payload, response});
return response;
}

public static isConfigurationValid(userId: string, secret: string, host: string) {
console.debug('isConfigurationValid', {userId, secret, host: host});
return (userId || '') !== '' && (secret || '') !== '' && (host || '') !== '';
}

protected async getToApi(url: string, headers: any, baseUrl?: string) {
const resolvedFetch = this.fetch;
console.debug('getToApi', {url, headers});
const response = await resolvedFetch((baseUrl ?? this.getBaseUrl()) + url, {
method: 'GET',
headers,
});
console.debug('getToApi', {url, headers});
return response;
}

protected async postToApi(url: string, data: any, headers: any, baseUrl?: string) {
const resolvedFetch = this.fetch;
console.debug('postToApi', {url, data, headers});
const response = await resolvedFetch((baseUrl ?? this.getBaseUrl()) + url, { // change this
method: 'POST',
headers,
body:
//TypedArray
ArrayBuffer.isView(data) && !(data instanceof DataView) ? data
: JSON.stringify(data),
})
console.debug('postToApi', {url, data, headers, response});
return response;
}

protected getBaseHeaders() {
return ({
'Accept': 'application/json',
'Content-Type': 'application/json',
});
}

protected getBaseUrl() {
return `${this.protocol}://${this.host}/`;
}

private async getSecureHeaders(baseHeaders?: any) {
const headers = baseHeaders || this.getBaseHeaders();
const tok = await this.getAuthToken();
console.debug('getSecureHeaders', {headers, tok});
headers.Authorization = `Bearer ${tok}`;
console.debug('getSecureHeaders', {headers});
return headers;
}

public abstract isServerConfigured(): boolean;

public abstract checkStale(
username: string, password: string, protocol: string, host: string, clientProtocolHostname?: string
): boolean;

/**
* Get the authentication token for the current user.
* By default, reuses current token if available.
*
* @param force If true, forces a new token to be generated; defaults to false
*
* @returns The authentication token
*
*/
protected abstract getAuthToken(): Promise<string>;

/**
*
* @param userId
* @param secret
* @param org
* @param protocol
* @param host
* @returns Promise for the authentication token
*
* Helper to fetch a token for a given user
*
*/
public abstract fetchToken(
userId: string, secret: string, org?: string, protocol?: string, host?: string
): Promise<string>
}
Loading

0 comments on commit 10fd9ab

Please sign in to comment.