From 10a9f1b1f88733f6d619bb856171075fa201027d Mon Sep 17 00:00:00 2001 From: SushilMallRC Date: Tue, 23 Apr 2024 00:18:14 +0530 Subject: [PATCH 1/4] Add comments for code --- .github/workflows/publish-jsdoc.yml | 1 + react/src/AuthGate.tsx | 18 +++++ redux-demo/src/pages/Index.tsx | 7 ++ redux/src/actions.ts | 39 +++++++++-- redux/src/index.ts | 45 +++++++++---- sdk/src/SDK.ts | 29 +++++++++ sdk/src/index.ts | 12 ++-- subscriptions-deprecated/src/Subscriptions.ts | 30 ++++++++- subscriptions/src/Subscriptions.ts | 65 +++++++++++++++---- 9 files changed, 211 insertions(+), 35 deletions(-) diff --git a/.github/workflows/publish-jsdoc.yml b/.github/workflows/publish-jsdoc.yml index e6adec3e..2d3da74b 100644 --- a/.github/workflows/publish-jsdoc.yml +++ b/.github/workflows/publish-jsdoc.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - comments permissions: contents: write jobs: diff --git a/react/src/AuthGate.tsx b/react/src/AuthGate.tsx index 9530867b..7ad21297 100644 --- a/react/src/AuthGate.tsx +++ b/react/src/AuthGate.tsx @@ -2,31 +2,39 @@ import React, {Component} from 'react'; import {SDK} from '@ringcentral/sdk'; +// Utility function to get display name function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } +// Utility function for delaying execution const delay = () => new Promise(res => setTimeout(res, 0)); +// Interface for the state of AuthGate component export interface AuthGateState { isAuthorized: boolean; authorizing: boolean; authError: null | Error; } +// Interface for the render props provided by AuthGate export interface AuthGateRenderProps extends AuthGateState { loginUrl: (options: any) => string; parseRedirect: (search: string) => Promise; logout: () => Promise; } +// Interface for the props of AuthGate component export interface AuthGateProps { sdk: SDK; ensure?: boolean; children: (props: AuthGateRenderProps) => any; } +// AuthGate component definition export class AuthGate extends Component { + + // Initial state public state = { isAuthorized: false, authorizing: true, @@ -51,6 +59,7 @@ export class AuthGate extends Component { const {sdk, ensure} = this.props; const platform = sdk.platform(); + // Event listeners for various platform events platform.on(platform.events.beforeRefresh, this.before); platform.on(platform.events.beforeLogin, this.before); platform.on(platform.events.refreshError, this.error); @@ -73,6 +82,7 @@ export class AuthGate extends Component { this.mounted = false; const {sdk} = this.props; const platform = sdk.platform(); + // Removing event listeners platform.removeListener(platform.events.beforeRefresh, this.before); platform.removeListener(platform.events.beforeLogin, this.before); platform.removeListener(platform.events.refreshError, this.error); @@ -82,19 +92,25 @@ export class AuthGate extends Component { platform.removeListener(platform.events.refreshSuccess, this.success); } + // Handler for before event public before = () => this.mounted && this.setState({authorizing: true}); + // Handler for error event public error = async e => this.updateState(e); + // Handler for success event public success = async () => this.updateState(null); + // Method to get login URL public loginUrl = options => this.props.sdk.platform().loginUrl(options); + // Method to logout public logout = async () => { const platform = this.props.sdk.platform(); return platform.logout(); }; + // Method to parse redirect public parseRedirect = async search => { try { const platform = this.props.sdk.platform(); @@ -107,6 +123,7 @@ export class AuthGate extends Component { } }; + // Method to update state public updateState = async (authError = null) => { await delay(); this.mounted && @@ -120,6 +137,7 @@ export class AuthGate extends Component { }); }; + // Render method public render() { const {sdk, ensure, children, ...props} = this.props; return children({ diff --git a/redux-demo/src/pages/Index.tsx b/redux-demo/src/pages/Index.tsx index 9bdaa78c..d7fde22b 100644 --- a/redux-demo/src/pages/Index.tsx +++ b/redux-demo/src/pages/Index.tsx @@ -28,13 +28,20 @@ class Index extends Component { } } + /** + * Renders the component based on the current state. + * @returns JSX representing the rendered component. + */ public render() { const {error, user} = this.state; + // If there's an error, render an error message. if (error) {return
Error: {error.toString()}
;} + // If user data is not available yet, render a loading message. if (!user) {return
Loading...
;} + // If user data is available, render user information and a logout button. return (

Logged in as {user.name}

diff --git a/redux/src/actions.ts b/redux/src/actions.ts index fa4ebe71..c0bafe6e 100644 --- a/redux/src/actions.ts +++ b/redux/src/actions.ts @@ -4,35 +4,66 @@ import {AUTH_ERROR, LOGIN, LOGIN_SUCCESS, LOGOUT, LOGOUT_SUCCESS} from './consta export interface ActionsOptions { sdk: SDK; } - +/** + * Actions class responsible for handling authentication actions + */ export default class Actions { public sdk: SDK; + // Constructor to initialize the Actions class with an SDK instance public constructor({sdk}: ActionsOptions) { this.sdk = sdk; } - public login = query => { - if (query.error_description) {return this.authError(new Error(query.error_description));} + /** + * Method to handle login action + * It initiates the login process using the SDK + * If an error description is provided, it dispatches an authentication error + * Returns an action of type LOGIN + * @param query + * @returns + */ + public login = (query) => { + if (query.error_description) { + return this.authError(new Error(query.error_description)); + } this.sdk.login(query); // we ignore promise result because we listen to all events already return {type: LOGIN}; }; + /** + * Method to handle logout action + * It initiates the logout process using the SDK + * @returns Returns an action of type LOGOUT + */ public logout = () => { this.sdk.logout(); // we ignore promise result because we listen to all events already return {type: LOGOUT}; }; + /** + * Method to dispatch login success action + * @returns Returns an action of type LOGIN_SUCCESS + */ public loginSuccess = () => ({ type: LOGIN_SUCCESS, }); - public authError = error => ({ + /** + * Method to dispatch authentication error action + * @param error Takes an error object as input and dispatches an action of type AUTH_ERROR + * @returns Returns the dispatched action + */ + public authError = (error) => ({ type: AUTH_ERROR, payload: error, error: true, }); + /** + * Method to dispatch logout success action + * @returns Returns an action of type LOGOUT_SUCCESS + */ public logoutSuccess = () => ({ type: LOGOUT_SUCCESS, }); diff --git a/redux/src/index.ts b/redux/src/index.ts index 0014b598..f0be9117 100644 --- a/redux/src/index.ts +++ b/redux/src/index.ts @@ -8,55 +8,76 @@ export interface StoreConnectorOptions extends ActionsOptions { root?: string; } +/** + * Class responsible for connecting authentication actions with Redux store + */ export default class StoreConnector { - private sdk: SDK; + private sdk: SDK; // SDK instance for authentication - private store: Store; + private store: Store; // Redux store to connect with - public root: string; + public root: string; // Root key in the Redux store for authentication state - public actions: Actions; + public actions: Actions; // Instance of Actions class for dispatching authentication actions + /** + * Constructor to initialize StoreConnector with SDK instance and options + */ public constructor({sdk, root = 'rcAuth'}: StoreConnectorOptions) { this.sdk = sdk; this.root = root; this.actions = new Actions({sdk}); } + /** + * Method to connect StoreConnector with a Redux store + * Attaches event listeners to SDK platform events for handling authentication + * Dispatches actions based on authentication events + */ public connectToStore = async (store: Store) => { this.store = store; const {dispatch} = store; const platform = this.sdk.platform(); - platform.on(platform.events.loginError, e => dispatch(this.actions.authError(e))); - platform.on(platform.events.refreshError, e => dispatch(this.actions.authError(e))); - platform.on(platform.events.logoutError, e => dispatch(this.actions.authError(e))); + // Event listeners for handling authentication events + platform.on(platform.events.loginError, (e) => dispatch(this.actions.authError(e))); + platform.on(platform.events.refreshError, (e) => dispatch(this.actions.authError(e))); + platform.on(platform.events.logoutError, (e) => dispatch(this.actions.authError(e))); platform.on(platform.events.loginSuccess, () => dispatch(this.actions.loginSuccess())); platform.on(platform.events.logoutSuccess, () => dispatch(this.actions.logoutSuccess())); try { + // Checking if access token is valid or refreshing token if (await platform.auth().accessTokenValid()) { dispatch(this.actions.loginSuccess()); // manual dispatch } else { await platform.refresh(); } - } catch (e) { //eslint-disable-line + } catch (e) { + //eslint-disable-line // can be empty because error is caught by event listener } }; + /** + * Reducer function for combining authentication reducers + */ public reducer = combineReducers({ status, error, loading, }); - public getAuth = state => state[this.root]; + // Selector function to get authentication state from Redux store + public getAuth = (state) => state[this.root]; - public getAuthStatus = state => this.getAuth(state).status; + // Selector function to get authentication status from Redux store + public getAuthStatus = (state) => this.getAuth(state).status; - public getAuthError = state => this.getAuth(state).error; + // Selector function to get authentication error from Redux store + public getAuthError = (state) => this.getAuth(state).error; - public getAuthLoading = state => this.getAuth(state).loading; + // Selector function to get authentication loading state from Redux store + public getAuthLoading = (state) => this.getAuth(state).loading; } diff --git a/sdk/src/SDK.ts b/sdk/src/SDK.ts index a4ba3b40..3820b93a 100644 --- a/sdk/src/SDK.ts +++ b/sdk/src/SDK.ts @@ -44,39 +44,68 @@ export class SDK { public static EventEmitter = EventEmitter; + /** + * Sandbox Server https://platform.devtest.ringcentral.com + * Production Server https://platform.ringcentral.com + */ public static server = { sandbox: 'https://platform.devtest.ringcentral.com', production: 'https://platform.ringcentral.com', }; + + /** + * Handles login redirect by sending authentication response to the opener window. + * @param origin The origin to post the message to. + * @param win The window object. If not provided, defaults to the global window object. + */ public static handleLoginRedirect(origin, win) { + // Use the provided window object or default to the global window object. win = win || window; + + // Get the authentication response from the location search or hash. const response = win.location.search ? win.location.search : win.location.hash; + + // Create a message object containing the authentication response. const msg = {}; msg[Constants.authResponseProperty] = response; + + // Post the message to the opener window with the specified origin. win.opener.postMessage(msg, origin || win.location.origin); } + /** + * Constructs a new SDK instance with the provided options. + * @param options The SDK options. + */ public constructor(options: SDKOptions = {}) { + // Destructure options or use default values. const {cachePrefix, defaultRequestInit, handleRateLimit} = options; + + // Warn if using sandbox server (deprecated). if (options?.server === SDK.server.sandbox) { // eslint-disable-next-line no-console console.warn('Sandbox support is deprecated. Please migrate your application to Production Server.'); } + + // Initialize external dependencies. this._externals = new Externals({ ...defaultExternals, ...options, }); + // Initialize cache. this._cache = new Cache({ externals: this._externals, prefix: cachePrefix, }); + // Initialize client. this._client = new Client({ externals: this._externals, defaultRequestInit, }); + // Initialize platform. this._platform = new Platform({ ...options, externals: this._externals, diff --git a/sdk/src/index.ts b/sdk/src/index.ts index a2a1f80e..dee2a4be 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -4,12 +4,14 @@ import {setDefaultExternals} from './SDK'; export * from './SDK'; +// Create a new instance of DomStorage for localStorage with strict mode enabled. const localStorage = new DomStorage(null, {strict: true}); +// Set default externals for the application. setDefaultExternals({ - localStorage, - fetch, - Request, - Response, - Headers, + localStorage, // LocalStorage instance. + fetch, // Fetch function for making HTTP requests. + Request, // Request constructor for creating HTTP requests. + Response, // Response constructor for handling HTTP responses. + Headers, // Headers constructor for working with HTTP headers. }); diff --git a/subscriptions-deprecated/src/Subscriptions.ts b/subscriptions-deprecated/src/Subscriptions.ts index f4cce233..c3fe9962 100644 --- a/subscriptions-deprecated/src/Subscriptions.ts +++ b/subscriptions-deprecated/src/Subscriptions.ts @@ -5,18 +5,33 @@ import CachedSubscription, {CachedSubscriptionOptions} from './subscription/Cach export {SubscriptionOptions, CachedSubscriptionOptions}; +/** + * Represents a set of subscription management functionalities. + */ export class Subscriptions { - private _sdk: SDK; + private _sdk: SDK; // The SDK instance used for subscription management. - private _PubNub: any; // typeof PubNub; + private _PubNub: any; // The PubNub instance used for subscription communication. + /** + * Constructor for Subscriptions class. + * @param sdk The SDK instance. + * @param PubNub The PubNub instance (deprecated, use WebSockets instead). + */ public constructor({sdk, PubNub = PubNubDefault}: SubscriptionsOptions) { this._sdk = sdk; this._PubNub = PubNub; + // Warn about deprecated usage of PubNub. // eslint-disable-next-line no-console console.warn('PubNub support is deprecated. Please migrate your application to WebSockets.'); } + /** + * Creates a new subscription. + * @param pollInterval Polling interval for subscription. + * @param renewHandicapMs Renewal handicap for subscription. + * @returns A new Subscription instance. + */ public createSubscription({pollInterval, renewHandicapMs}: SubscriptionOptions = {}) { return new Subscription({ pollInterval, @@ -26,6 +41,13 @@ export class Subscriptions { }); } + /** + * Creates a new cached subscription. + * @param cacheKey Cache key for subscription. + * @param pollInterval Polling interval for subscription. + * @param renewHandicapMs Renewal handicap for subscription. + * @returns A new CachedSubscription instance. + */ public createCachedSubscription({cacheKey, pollInterval, renewHandicapMs}: CachedSubscriptionOptions) { return new CachedSubscription({ cacheKey, @@ -36,6 +58,10 @@ export class Subscriptions { }); } + /** + * Gets the PubNub instance. + * @returns The PubNub instance. + */ public getPubNub() { return this._PubNub; } diff --git a/subscriptions/src/Subscriptions.ts b/subscriptions/src/Subscriptions.ts index cc3ae9a1..a82d394a 100644 --- a/subscriptions/src/Subscriptions.ts +++ b/subscriptions/src/Subscriptions.ts @@ -5,47 +5,80 @@ import RcSdkExtension from '@rc-ex/rcsdk'; import WebSocketExtension from '@rc-ex/ws'; import waitFor from 'wait-for-async'; +/** + * Class for handling subscriptions. + * Extends EventEmitter for event handling. + */ export class Subscription extends EventEmitter { - private subscriptions: Subscriptions; + private subscriptions: Subscriptions; // Private member variable for Subscriptions instance public events = { - notification: 'notification', + notification: 'notification', // Define event name for notifications }; - public eventFilters!: string[]; + public eventFilters!: string[]; // Define property for event filters + /** + * Constructor to initialize the Subscription instance. + * @param options Options object containing subscriptions. + */ public constructor(options: {subscriptions: Subscriptions}) { - super(); - this.subscriptions = options.subscriptions; + super(); // Call EventEmitter constructor + this.subscriptions = options.subscriptions; // Assign Subscriptions instance from options } + /** + * Method to set event filters. + * @param eventFilters - Array of event filters. + * @returns + */ public setEventFilters(eventFilters: string[]) { this.eventFilters = eventFilters; return this; } + /** + * Method to register for subscriptions. + * Subscribes to events with specified event filters and emits notifications. + * @returns + */ public async register() { await this.subscriptions.init(); const wsExtension = await this.subscriptions.newWsExtension(); - return await wsExtension.subscribe(this.eventFilters, event => { + return await wsExtension.subscribe(this.eventFilters, (event) => { this.emit(this.events.notification, event); }); } } +/** + * Class for managing subscriptions. + */ export class Subscriptions { - private status = 'new'; // new, in-progress, ready - public rc: RingCentral; - public rcSdkExtension: RcSdkExtension; + private status = 'new'; // Status of subscriptions: new, in-progress, ready + public rc: RingCentral; // RingCentral instance + public rcSdkExtension: RcSdkExtension; // RcSdkExtension instance + /** + * Constructor to initialize Subscriptions instance with SDK. + * @param options - Options object containing SDK. + */ public constructor(options: {sdk: SDK}) { - this.rc = new RingCentral(); - this.rcSdkExtension = new RcSdkExtension({rcSdk: options.sdk}); + this.rc = new RingCentral(); // Initialize RingCentral instance + this.rcSdkExtension = new RcSdkExtension({rcSdk: options.sdk}); // Initialize RcSdkExtension instance with SDK } + /** + * Method to initialize subscriptions. + * Installs RcSdkExtension and sets status to 'ready' when subscriptions are ready. + * If subscriptions are already ready or in-progress, does nothing. + * If subscriptions are in-progress, waits until they are ready. + */ public async init() { if (this.status === 'ready') { + // If subscriptions are already ready, return return; } if (this.status === 'in-progress') { + // If subscriptions are in progress, wait for them to be ready await waitFor({ condition: () => this.status === 'ready', }); @@ -53,15 +86,23 @@ export class Subscriptions { } this.status = 'in-progress'; await this.rc.installExtension(this.rcSdkExtension); - this.status = 'ready'; + this.status = 'ready'; // Set status to ready when subscriptions are ready } + /** + * Method to create a new WebSocket extension. + * @returns Promise that resolves with the created WebSocket extension. + */ public async newWsExtension() { const wsExtension = new WebSocketExtension(); await this.rc.installExtension(wsExtension); return wsExtension; } + /** + * Method to create a new Subscription instance. + * @returns New Subscription instance. + */ public createSubscription(): Subscription { return new Subscription({subscriptions: this}); } From 6173f8a80db7fdc00687d9fa77e7a880025b12e5 Mon Sep 17 00:00:00 2001 From: SushilMallRC Date: Tue, 23 Apr 2024 00:29:39 +0530 Subject: [PATCH 2/4] Remove branch --- .github/workflows/publish-jsdoc.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish-jsdoc.yml b/.github/workflows/publish-jsdoc.yml index 2d3da74b..e6adec3e 100644 --- a/.github/workflows/publish-jsdoc.yml +++ b/.github/workflows/publish-jsdoc.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - comments permissions: contents: write jobs: From 31775763e4bca460c2e874cb751a3aba6e18b04a Mon Sep 17 00:00:00 2001 From: SushilMallRC Date: Tue, 23 Apr 2024 10:25:28 +0530 Subject: [PATCH 3/4] Add comments on public methods --- sdk/src/core/Cache.ts | 30 ++++- sdk/src/http/Client.ts | 71 +++++++++--- sdk/src/http/utils.ts | 9 +- sdk/src/platform/Auth.ts | 5 + sdk/src/platform/Platform.ts | 209 ++++++++++++++++++++++++++++------- 5 files changed, 260 insertions(+), 64 deletions(-) diff --git a/sdk/src/core/Cache.ts b/sdk/src/core/Cache.ts index 70e723f0..806538d5 100644 --- a/sdk/src/core/Cache.ts +++ b/sdk/src/core/Cache.ts @@ -17,6 +17,12 @@ export default class Cache { this._externals = externals; } + /** + * Synchronously sets an item in the local storage. + * @param {string} key - The key under which to store the data. + * @param {any} data - The data to store. + * @returns {Object} - Returns the current instance for chaining. + */ public setItemSync(key, data) { this._externals.localStorage.setItem(this._prefixKey(key), JSON.stringify(data)); return this; @@ -26,6 +32,11 @@ export default class Cache { this.setItemSync(key, data); } + /** + * Synchronously removes an item from the local storage. + * @param {string} key - The key of the item to remove. + * @returns {Object} - Returns the current instance for chaining. + */ public removeItemSync(key) { this._externals.localStorage.removeItem(this._prefixKey(key)); return this; @@ -35,9 +46,16 @@ export default class Cache { await this.removeItemSync(key); } + /** + * Synchronously retrieves an item from the local storage. + * @param {string} key - The key of the item to retrieve. + * @returns {any} - The retrieved item, or null if the item does not exist. + */ public getItemSync(key) { const item = this._externals.localStorage.getItem(this._prefixKey(key)); - if (!item) {return null;} + if (!item) { + return null; + } return JSON.parse(item); } @@ -51,13 +69,19 @@ export default class Cache { : Object.keys(this._externals.localStorage); } + /** + * Asynchronously cleans up the local storage by removing all items with keys prefixed by the specified prefix. + * @returns {Object} - Returns the current instance for chaining. + */ public async clean() { await Promise.all( - (await this._keys()).map(async key => { + ( + await this._keys() + ).map(async (key) => { if (key.startsWith(this._prefix)) { await this._externals.localStorage.removeItem(key); } - }), + }) ); return this; diff --git a/sdk/src/http/Client.ts b/sdk/src/http/Client.ts index 8a5fd36d..1fb5c409 100644 --- a/sdk/src/http/Client.ts +++ b/sdk/src/http/Client.ts @@ -7,8 +7,12 @@ import {objectToUrlParams} from './utils'; function findHeaderName(name, headers) { name = name.toLowerCase(); return Object.keys(headers).reduce((res, key) => { - if (res) {return res;} - if (name === key.toLowerCase()) {return key;} + if (res) { + return res; + } + if (name === key.toLowerCase()) { + return key; + } return res; }, null); } @@ -68,6 +72,11 @@ export default class Client extends EventEmitter { this._externals = externals; } + /** + * Send a request and handle the response asynchronously. + * @param {Request} request - The request to send. + * @returns {Promise} - A promise resolving to the response. + */ public async sendRequest(request: Request): Promise { let response; try { @@ -76,7 +85,9 @@ export default class Client extends EventEmitter { response = await this._loadResponse(request); - if (!response.ok) {throw new Error('Response has unsuccessful status');} + if (!response.ok) { + throw new Error('Response has unsuccessful status'); + } this.emit(this.events.requestSuccess, response, request); @@ -114,8 +125,12 @@ export default class Client extends EventEmitter { init.headers = init.headers || {}; // Sanity checks - if (!init.url) {throw new Error('Url is not defined');} - if (!init.method) {init.method = 'GET';} + if (!init.url) { + throw new Error('Url is not defined'); + } + if (!init.method) { + init.method = 'GET'; + } init.method = init.method.toUpperCase(); if (init.method && Client._allowedMethods.indexOf(init.method) < 0) { throw new Error(`Method has wrong value: ${init.method}`); @@ -183,13 +198,17 @@ export default class Client extends EventEmitter { } public async multipart(response: Response): Promise { - if (!this.isMultipart(response)) {throw new Error('Response is not multipart');} + if (!this.isMultipart(response)) { + throw new Error('Response is not multipart'); + } // Step 1. Split multipart response const text = await response.text(); - if (!text) {throw new Error('No response body');} + if (!text) { + throw new Error('No response body'); + } let boundary; @@ -199,14 +218,22 @@ export default class Client extends EventEmitter { throw new Error('Cannot find boundary'); } - if (!boundary) {throw new Error('Cannot find boundary');} + if (!boundary) { + throw new Error('Cannot find boundary'); + } const parts = text.toString().split(Client._boundarySeparator + boundary); - if (parts[0].trim() === '') {parts.shift();} - if (parts[parts.length - 1].trim() === Client._boundarySeparator) {parts.pop();} + if (parts[0].trim() === '') { + parts.shift(); + } + if (parts[parts.length - 1].trim() === Client._boundarySeparator) { + parts.pop(); + } - if (parts.length < 1) {throw new Error('No parts in body');} + if (parts.length < 1) { + throw new Error('No parts in body'); + } // Step 2. Parse status info @@ -229,12 +256,14 @@ export default class Client extends EventEmitter { text = headersAndBody.length > 0 ? headersAndBody.join(Client._bodySeparator) : null; - (headersText || '').split('\n').forEach(header => { + (headersText || '').split('\n').forEach((header) => { const split = header.trim().split(Client._headerSeparator); const key = split.shift().trim(); const value = split.join(Client._headerSeparator).trim(); - if (key) {headers.append(key, value);} + if (key) { + headers.append(key, value); + } }); return new this._externals.Response(text, { @@ -245,16 +274,24 @@ export default class Client extends EventEmitter { } public async error(response: Response, skipOKCheck = false): Promise { - if (response.ok && !skipOKCheck) {return null;} + if (response.ok && !skipOKCheck) { + return null; + } let msg = (response.status ? `${response.status} ` : '') + (response.statusText ? response.statusText : ''); try { const {message, error_description, description} = await response.clone().json(); - if (message) {msg = message;} - if (error_description) {msg = error_description;} - if (description) {msg = description;} + if (message) { + msg = message; + } + if (error_description) { + msg = error_description; + } + if (description) { + msg = description; + } } catch (e) {} //eslint-disable-line return msg; diff --git a/sdk/src/http/utils.ts b/sdk/src/http/utils.ts index f89f6090..6907d571 100644 --- a/sdk/src/http/utils.ts +++ b/sdk/src/http/utils.ts @@ -2,12 +2,17 @@ function encodeURIComponentWithUndefined(value) { return typeof value === 'undefined' ? '' : encodeURIComponent(value); } +/** + * Convert an object to URL query parameters string. + * @param {object} obj - The object to convert. + * @returns {string} - The URL query parameters string. + */ export function objectToUrlParams(obj) { return Object.keys(obj) - .map(key => { + .map((key) => { if (Array.isArray(obj[key])) { return obj[key] - .map(value => encodeURIComponent(key) + '=' + encodeURIComponentWithUndefined(value)) + .map((value) => encodeURIComponent(key) + '=' + encodeURIComponentWithUndefined(value)) .join('&'); } return encodeURIComponent(key) + '=' + encodeURIComponentWithUndefined(obj[key]); diff --git a/sdk/src/platform/Auth.ts b/sdk/src/platform/Auth.ts index a146de65..aae93d7f 100644 --- a/sdk/src/platform/Auth.ts +++ b/sdk/src/platform/Auth.ts @@ -11,6 +11,9 @@ export interface AuthOptionsConstructor extends AuthOptions { cacheId: string; } +/** + * Represents an authentication handler class + */ export default class Auth { private _cache: Cache; @@ -24,6 +27,7 @@ export default class Auth { this._refreshHandicapMs = refreshHandicapMs; } + // Fetches authentication data from the cache public async data(): Promise { return ( (await this._cache.getItem(this._cacheId)) || { @@ -39,6 +43,7 @@ export default class Auth { ); } + // Sets authentication data in the cache public async setData(newData: AuthData = {}) { const data = await this.data(); const savedData: AuthData = { diff --git a/sdk/src/platform/Platform.ts b/sdk/src/platform/Platform.ts index a35c0f03..2bc2127d 100644 --- a/sdk/src/platform/Platform.ts +++ b/sdk/src/platform/Platform.ts @@ -1,7 +1,4 @@ -import { - createHash, - randomBytes, -} from 'crypto'; +import {createHash, randomBytes} from 'crypto'; import {EventEmitter} from 'events'; import Cache from '../core/Cache'; @@ -157,10 +154,10 @@ export default class Platform extends EventEmitter { initialEndpoint, fetchGet: this.get.bind(this), }); - this._discovery.on(this._discovery.events.initialized, discoveryData => { + this._discovery.on(this._discovery.events.initialized, (discoveryData) => { this._authorizeEndpoint = discoveryData.authApi.authorizationUri; }); - this._client.on(this._client.events.requestSuccess, response => { + this._client.on(this._client.events.requestSuccess, (response) => { if (response.headers.get('discovery-required')) { this._discovery.refreshExternalData(); } @@ -185,17 +182,32 @@ export default class Platform extends EventEmitter { return super.on(event, listener); } + /** + * Returns the authentication object. + * @returns {Authentication | null} - The authentication object if available, otherwise null. + */ public auth() { return this._auth; } + /** + * Returns the discovery object. + * @returns {Discovery | null} - The discovery object if available, otherwise null. + */ public discovery() { return this._discovery; } + /** + * Constructs a URL with the given path and options. + * @param {string} [path=''] - The path to append to the URL. + * @param {CreateUrlOptions} [options={}] - Options for constructing the URL. + * @returns {string} - The constructed URL. + */ public createUrl(path = '', options: CreateUrlOptions = {}) { let builtUrl = ''; + // Check if the path contains 'http://' or 'https://' const hasHttp = checkPathHasHttp(path); if (options.addServer && !hasHttp) { @@ -206,19 +218,36 @@ export default class Platform extends EventEmitter { } } - if (this._urlPrefix) {builtUrl += this._urlPrefix;} + // Append the URL prefix, if available + if (this._urlPrefix) { + builtUrl += this._urlPrefix; + } + // Append the provided path builtUrl += path; - if (options.addMethod) {builtUrl += `${path.includes('?') ? '&' : '?'}_method=${options.addMethod}`;} + // Add method parameter if requested + if (options.addMethod) { + builtUrl += `${path.includes('?') ? '&' : '?'}_method=${options.addMethod}`; + } + // Return the constructed URL return builtUrl; } + /** + * @param path The path to append to the URL. + * @returns Return sigin Url + */ public async signUrl(path: string) { return `${path + (path.includes('?') ? '&' : '?')}access_token=${(await this._auth.data()).access_token}`; } + /** + * Generates a login URL with discovery data. + * @param {LoginUrlOptions} options - Options for constructing the login URL. + * @returns - The constructed login URL. + */ public async loginUrlWithDiscovery(options: LoginUrlOptions = {}) { if (this._discovery) { try { @@ -241,6 +270,10 @@ export default class Platform extends EventEmitter { return this.loginUrl(options); } + /** + * Initializes the discovery process. + * @throws {Error} If discovery is not enabled or if an error occurs during initialization. + */ public async initDiscovery() { if (!this._discovery) { throw new Error('Discovery is not enabled!'); @@ -254,6 +287,22 @@ export default class Platform extends EventEmitter { } } + /** + * Constructs a login URL with specified options. + * @param {Object} options - Options for constructing the login URL. + * @param {boolean} [options.implicit] - Whether to use implicit grant flow. + * @param {string} [options.state] - State parameter for security. + * @param {string} [options.brandId] - ID of the brand. + * @param {string} [options.display] - Hint to the authentication system on how to display the authentication page. + * @param {string} [options.prompt] - Space-delimited, case-sensitive list of prompts to present the user. + * @param {Object} [options.uiOptions] - UI options for the authentication page. + * @param {string[]} [options.uiLocales] - Preferred languages for the authentication page. + * @param {string} [options.localeId] - ID of the preferred locale for the authentication page. + * @param {boolean} [options.usePKCE] - Whether to use PKCE (Proof Key for Code Exchange). + * @param {string} [options.responseHint] - Hint about the type of the token returned. + * @param {string} [options.redirectUri] - Redirect URI after authentication. + * @returns {string} - The constructed login URL. + */ public loginUrl({ implicit, state, @@ -307,30 +356,33 @@ export default class Platform extends EventEmitter { } /** - * @return {string} + * Generates a random code verifier for PKCE (Proof Key for Code Exchange) flow. + * @return {string} A randomly generated code verifier. */ private _generateCodeVerifier() { let codeVerifier: any = randomBytes(32); - codeVerifier = codeVerifier - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + codeVerifier = codeVerifier.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); return codeVerifier; } /** - * @param {string} url - * @return {Object} + * Parses the login redirect URL and extracts the query parameters. + * @param {string} url The URL to parse. + * @return {Object} An object containing the query parameters. + * @throws {Error} If unable to parse the response or if there's an error in the response. */ public parseLoginRedirect(url: string) { const response = (url.startsWith('#') && getParts(url, '#')) || (url.startsWith('?') && getParts(url, '?')) || null; - if (!response) {throw new Error('Unable to parse response');} + if (!response) { + throw new Error('Unable to parse response'); + } const queryString = new URLSearchParams(response); - if (!queryString) {throw new Error('Unable to parse response');} + if (!queryString) { + throw new Error('Unable to parse response'); + } const error = queryString.get('error_description') || queryString.get('error'); @@ -359,9 +411,13 @@ export default class Platform extends EventEmitter { // clear check last timeout when user open loginWindow twice to avoid leak this._clearLoginWindowCheckTimeout(); return new Promise((resolve, reject) => { - if (typeof window === 'undefined') {throw new Error('This method can be used only in browser');} + if (typeof window === 'undefined') { + throw new Error('This method can be used only in browser'); + } - if (!url) {throw new Error('Missing mandatory URL parameter');} + if (!url) { + throw new Error('Missing mandatory URL parameter'); + } const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : 0; const dualScreenTop = window.screenTop !== undefined ? window.screenTop : 0; @@ -377,30 +433,37 @@ export default class Platform extends EventEmitter { '_blank', target === '_blank' ? `scrollbars=yes, status=yes, width=${width}, height=${height}, left=${left}, top=${top}` - : '', + : '' ); if (!win) { throw new Error('Could not open login window. Please allow popups for this site'); } - if (win.focus) {win.focus();} + if (win.focus) { + win.focus(); + } // clear listener when user open loginWindow twice to avoid leak if (this._loginWindowEventListener) { window.removeEventListener('message', this._loginWindowEventListener); } - this._loginWindowEventListener = e => { + this._loginWindowEventListener = (e) => { try { - if (e.origin !== origin) {return;} - if (!e.data || !e.data[property]) {return;} // keep waiting + if (e.origin !== origin) { + return; + } + if (!e.data || !e.data[property]) { + return; + } // keep waiting this._clearLoginWindowCheckTimeout(); win.close(); window.removeEventListener('message', this._loginWindowEventListener); this._loginWindowEventListener = null; const loginOptions = this.parseLoginRedirect(e.data[property]); - if (!loginOptions.code && !loginOptions.access_token) - {throw new Error('No authorization code or token');} + if (!loginOptions.code && !loginOptions.access_token) { + throw new Error('No authorization code or token'); + } resolve(loginOptions); } catch (e1) { @@ -435,7 +498,8 @@ export default class Platform extends EventEmitter { } /** - * @return {Promise} + * Checks if the user is logged in. + * @return {Promise} - A promise that resolves to true if the user is logged in, otherwise false. */ public async loggedIn() { try { @@ -450,6 +514,12 @@ export default class Platform extends EventEmitter { } } + /** + * Logs in the user with the provided credentials and options. + * @param Options for the login process. + * @returns response + * @throw Error if an error occurs during login process. + */ public async login({ username, password, @@ -496,7 +566,7 @@ export default class Platform extends EventEmitter { body.password = password; // eslint-disable-next-line no-console console.warn( - 'Username/password authentication is deprecated. Please migrate to the JWT grant type.', + 'Username/password authentication is deprecated. Please migrate to the JWT grant type.' ); } else if (jwt) { body.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; @@ -511,9 +581,15 @@ export default class Platform extends EventEmitter { } } - if (access_token_ttl) {body.access_token_ttl = access_token_ttl;} - if (refresh_token_ttl) {body.refresh_token_ttl = refresh_token_ttl;} - if (endpoint_id) {body.endpoint_id = endpoint_id;} + if (access_token_ttl) { + body.access_token_ttl = access_token_ttl; + } + if (refresh_token_ttl) { + body.refresh_token_ttl = refresh_token_ttl; + } + if (endpoint_id) { + body.endpoint_id = endpoint_id; + } response = await this._tokenRequest(tokenEndpoint, body); json = await response.clone().json(); @@ -579,9 +655,13 @@ export default class Platform extends EventEmitter { const authData = await this.auth().data(); // Perform sanity checks - if (!authData.refresh_token) {throw new Error('Refresh token is missing');} + if (!authData.refresh_token) { + throw new Error('Refresh token is missing'); + } const refreshTokenValid = await this._auth.refreshTokenValid(); - if (!refreshTokenValid) {throw new Error('Refresh token has expired');} + if (!refreshTokenValid) { + throw new Error('Refresh token has expired'); + } const body: RefreshTokenBody = { grant_type: 'refresh_token', refresh_token: authData.refresh_token, @@ -629,6 +709,10 @@ export default class Platform extends EventEmitter { } } + /** + * Refreshes the authentication token. + * @returns A Promise resolving to a Response object. + */ public async refresh(): Promise { if (this._authProxy) { throw new Error('Refresh is not supported in Auth Proxy mode'); @@ -649,6 +733,10 @@ export default class Platform extends EventEmitter { return this._refreshPromise; } + /** + * Logs out the user by revoking the access token. + * @returns + */ public async logout(): Promise { if (this._authProxy) { throw new Error('Logout is not supported in Auth Proxy mode'); @@ -695,6 +783,12 @@ export default class Platform extends EventEmitter { return revokeEndpoint; } + /** + * Modifies the given request with necessary headers and authentication, if required. + * @param request - The request to inflate. + * @param options - The options for inflating the request. Default is an empty object. + * @returns A Promise resolving to the modified request. + */ public async inflateRequest(request: Request, options: SendOptions = {}): Promise { options = options || {}; let userAgent = this._userAgent; @@ -703,16 +797,26 @@ export default class Platform extends EventEmitter { } request.headers.set('X-User-Agent', userAgent); - if (options.skipAuthCheck) {return request;} + if (options.skipAuthCheck) { + return request; + } await this.ensureLoggedIn(); request.headers.set('Client-Id', this._clientId); - if (!this._authProxy) {request.headers.set('Authorization', await this.authHeader());} + if (!this._authProxy) { + request.headers.set('Authorization', await this.authHeader()); + } return request; } + /** + * Send request + * @param request + * @param options + * @returns + */ public async sendRequest(request: Request, options: SendOptions = {}): Promise { try { request = await this.inflateRequest(request, options); @@ -721,13 +825,16 @@ export default class Platform extends EventEmitter { let {retry, handleRateLimit} = options; // Guard is for errors that come from polling - if (!e.response || retry) {throw e;} + if (!e.response || retry) { + throw e; + } const {response} = e; const {status} = response; - if ((status !== Client._unauthorizedStatus && status !== Client._rateLimitStatus) || this._authProxy) - {throw e;} + if ((status !== Client._unauthorizedStatus && status !== Client._rateLimitStatus) || this._authProxy) { + throw e; + } options.retry = true; @@ -750,7 +857,9 @@ export default class Platform extends EventEmitter { this.emit(this.events.rateLimitError, e); - if (!handleRateLimit) {throw e;} + if (!handleRateLimit) { + throw e; + } } await delay(retryAfter); @@ -758,6 +867,10 @@ export default class Platform extends EventEmitter { } } + /** + * Sends a request with the given options, handling authentication and discovery checks if necessary. + * @param options - The options for the request. Default is an empty object. + */ public async send(options: SendOptions = {}) { if (!options.skipAuthCheck && !options.skipDiscoveryCheck && this._discovery) { if (this._discoveryInitPromise) { @@ -804,9 +917,17 @@ export default class Platform extends EventEmitter { return this.send({method: 'DELETE', url, query, ...options}); } + /** + * Ensures that the user is logged in by refreshing the authentication token if necessary. + * @returns A Promise resolving to a Response object or null. + */ public async ensureLoggedIn(): Promise { - if (this._authProxy) {return null;} - if (await this._auth.accessTokenValid()) {return null;} + if (this._authProxy) { + return null; + } + if (await this._auth.accessTokenValid()) { + return null; + } await this.refresh(); return null; } @@ -830,6 +951,10 @@ export default class Platform extends EventEmitter { }); } + /** + * + * @returns Authorization header + */ public basicAuthHeader(): string { const apiKey = this._clientId + (this._clientSecret ? `:${this._clientSecret}` : ''); return `Basic ${typeof btoa === 'function' ? btoa(apiKey) : Buffer.from(apiKey).toString('base64')}`; From e16ec521caee41742219e836a1eda06080e92dbb Mon Sep 17 00:00:00 2001 From: SushilMallRC Date: Fri, 26 Apr 2024 14:38:09 +0530 Subject: [PATCH 4/4] Fix coverage --- sdk/src/http/Client.ts | 20 +++--------- sdk/src/platform/Platform.ts | 60 +++++++++--------------------------- 2 files changed, 20 insertions(+), 60 deletions(-) diff --git a/sdk/src/http/Client.ts b/sdk/src/http/Client.ts index 1fb5c409..f7f8e3ab 100644 --- a/sdk/src/http/Client.ts +++ b/sdk/src/http/Client.ts @@ -125,9 +125,7 @@ export default class Client extends EventEmitter { init.headers = init.headers || {}; // Sanity checks - if (!init.url) { - throw new Error('Url is not defined'); - } + if (!init.url) {throw new Error('Url is not defined');} if (!init.method) { init.method = 'GET'; } @@ -214,13 +212,9 @@ export default class Client extends EventEmitter { try { boundary = this.getContentType(response).match(/boundary=([^;]+)/i)[1]; //eslint-disable-line - } catch (e) { - throw new Error('Cannot find boundary'); - } + } catch (e) { throw new Error('Cannot find boundary');} - if (!boundary) { - throw new Error('Cannot find boundary'); - } + if (!boundary) {throw new Error('Cannot find boundary');} const parts = text.toString().split(Client._boundarySeparator + boundary); @@ -231,9 +225,7 @@ export default class Client extends EventEmitter { parts.pop(); } - if (parts.length < 1) { - throw new Error('No parts in body'); - } + if (parts.length < 1) {throw new Error('No parts in body');} // Step 2. Parse status info @@ -274,9 +266,7 @@ export default class Client extends EventEmitter { } public async error(response: Response, skipOKCheck = false): Promise { - if (response.ok && !skipOKCheck) { - return null; - } + if (response.ok && !skipOKCheck) {return null;} let msg = (response.status ? `${response.status} ` : '') + (response.statusText ? response.statusText : ''); diff --git a/sdk/src/platform/Platform.ts b/sdk/src/platform/Platform.ts index 2bc2127d..9a715eb2 100644 --- a/sdk/src/platform/Platform.ts +++ b/sdk/src/platform/Platform.ts @@ -219,9 +219,7 @@ export default class Platform extends EventEmitter { } // Append the URL prefix, if available - if (this._urlPrefix) { - builtUrl += this._urlPrefix; - } + if (this._urlPrefix) {builtUrl += this._urlPrefix;} // Append the provided path builtUrl += path; @@ -275,9 +273,7 @@ export default class Platform extends EventEmitter { * @throws {Error} If discovery is not enabled or if an error occurs during initialization. */ public async initDiscovery() { - if (!this._discovery) { - throw new Error('Discovery is not enabled!'); - } + if (!this._discovery) {throw new Error('Discovery is not enabled!');} try { await this._discovery.init(); this._discoveryInitPromise = null; @@ -375,14 +371,10 @@ export default class Platform extends EventEmitter { const response = (url.startsWith('#') && getParts(url, '#')) || (url.startsWith('?') && getParts(url, '?')) || null; - if (!response) { - throw new Error('Unable to parse response'); - } + if (!response) {throw new Error('Unable to parse response');} const queryString = new URLSearchParams(response); - if (!queryString) { - throw new Error('Unable to parse response'); - } + if (!queryString) {throw new Error('Unable to parse response');} const error = queryString.get('error_description') || queryString.get('error'); @@ -411,13 +403,9 @@ export default class Platform extends EventEmitter { // clear check last timeout when user open loginWindow twice to avoid leak this._clearLoginWindowCheckTimeout(); return new Promise((resolve, reject) => { - if (typeof window === 'undefined') { - throw new Error('This method can be used only in browser'); - } + if (typeof window === 'undefined') {throw new Error('This method can be used only in browser');} - if (!url) { - throw new Error('Missing mandatory URL parameter'); - } + if (!url) {throw new Error('Missing mandatory URL parameter');} const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : 0; const dualScreenTop = window.screenTop !== undefined ? window.screenTop : 0; @@ -587,9 +575,7 @@ export default class Platform extends EventEmitter { if (refresh_token_ttl) { body.refresh_token_ttl = refresh_token_ttl; } - if (endpoint_id) { - body.endpoint_id = endpoint_id; - } + if (endpoint_id) {body.endpoint_id = endpoint_id;} response = await this._tokenRequest(tokenEndpoint, body); json = await response.clone().json(); @@ -655,13 +641,9 @@ export default class Platform extends EventEmitter { const authData = await this.auth().data(); // Perform sanity checks - if (!authData.refresh_token) { - throw new Error('Refresh token is missing'); - } + if (!authData.refresh_token) {throw new Error('Refresh token is missing');} const refreshTokenValid = await this._auth.refreshTokenValid(); - if (!refreshTokenValid) { - throw new Error('Refresh token has expired'); - } + if (!refreshTokenValid) {throw new Error('Refresh token has expired');} const body: RefreshTokenBody = { grant_type: 'refresh_token', refresh_token: authData.refresh_token, @@ -714,9 +696,7 @@ export default class Platform extends EventEmitter { * @returns A Promise resolving to a Response object. */ public async refresh(): Promise { - if (this._authProxy) { - throw new Error('Refresh is not supported in Auth Proxy mode'); - } + if (this._authProxy) {throw new Error('Refresh is not supported in Auth Proxy mode');} if (!this._refreshPromise) { this._refreshPromise = (async () => { try { @@ -738,9 +718,7 @@ export default class Platform extends EventEmitter { * @returns */ public async logout(): Promise { - if (this._authProxy) { - throw new Error('Logout is not supported in Auth Proxy mode'); - } + if (this._authProxy) {throw new Error('Logout is not supported in Auth Proxy mode');} try { this.emit(this.events.beforeLogout); @@ -825,9 +803,7 @@ export default class Platform extends EventEmitter { let {retry, handleRateLimit} = options; // Guard is for errors that come from polling - if (!e.response || retry) { - throw e; - } + if (!e.response || retry) {throw e;} const {response} = e; const {status} = response; @@ -881,9 +857,7 @@ export default class Platform extends EventEmitter { await this._discovery.refreshExternalData(); } const discoveryData = await this._discovery.externalData(); - if (!discoveryData) { - throw new Error('Discovery data is missing'); - } + if (!discoveryData) {throw new Error('Discovery data is missing');} this._server = discoveryData.coreApi.baseUri; this._rcvServer = discoveryData.rcv.baseApiUri; if (discoveryData.tag) { @@ -922,12 +896,8 @@ export default class Platform extends EventEmitter { * @returns A Promise resolving to a Response object or null. */ public async ensureLoggedIn(): Promise { - if (this._authProxy) { - return null; - } - if (await this._auth.accessTokenValid()) { - return null; - } + if (this._authProxy) {return null;} + if (await this._auth.accessTokenValid()) {return null;} await this.refresh(); return null; }