Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

personal key auth #156

Merged
merged 20 commits into from
Mar 15, 2024
Merged
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
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
Loading