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

fix: separate api auth to plugin folder #495

Merged
merged 8 commits into from
May 25, 2022
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
6 changes: 3 additions & 3 deletions packages/composites/ui-atoms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.1-rc.2",
"description": "Flyteconsole UI atoms, which didn't plan to be published and would be consumed as is internally",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm",
"build:esm": "tsc --module esnext --outDir lib",
"build:cjs": "tsc",
"test": "NODE_ENV=test jest"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/plugins/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.1-rc.2",
"description": "Flyteconsole Components module, which is published as npm package and can be consumed by 3rd parties",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
Expand All @@ -13,7 +13,7 @@
},
"scripts": {
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm --project ./tsconfig.build.json",
"build:esm": "tsc --module esnext --outDir lib --project ./tsconfig.build.json",
"build:cjs": "tsc --project ./tsconfig.build.json",
"test": "NODE_ENV=test jest"
},
Expand Down
40 changes: 39 additions & 1 deletion packages/plugins/flyte-api/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
This is a flyte-API package for flyteconsole plugin system
## @flyteconsole/flyte-api

This package provides ability to do FlyteAdmin API calls from JS/TS code.

At this point it allows to get though authentication steps, request user profile and FlyteAdmin version.
In future releases we will add ability to do all types of FlyteAdmin API calls.

### Installation

To install the package please run:
```bash
yarn add @flyteconsole/flyte-api
```

### Usage

To use in your application

- Wrap parent component with <FlyteApiProvider flyteApiDomain={ADMIN_API_URL ?? ''}>

`ADMIN_API_URL` is a flyte admin domain URL to which `/api/v1/_endpoint` part would be added, to perform REST API call.
`
Then from any child component

```js
import useAxios from 'axios-hooks';
import { useFlyteApi, defaultAxiosConfig } from '@flyteconsole/flyte-api';

...
/** Get profile information */
const apiContext = useFlyteApi();

const profilePath = apiContext.getProfileUrl();
const [{ data: profile, loading }] = useAxios({
url: profilePath,
method: 'GET',
...defaultAxiosConfig,
});
```
16 changes: 9 additions & 7 deletions packages/plugins/flyte-api/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
{
"name": "@flyteconsole/flyte-api",
"version": "0.0.1-rc.1",
"version": "0.0.2",
"description": "FlyteConsole plugin to allow access FlyteAPI",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
"types": "./lib/esm/index.d.ts",
"module": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"clean": "rm -rf dist && rm -rf lib",
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm --project ./tsconfig.build.json",
"build:esm": "tsc --module esnext --outDir lib --project ./tsconfig.build.json",
"build:cjs": "tsc --project ./tsconfig.build.json",
"push:update": "yarn clean && yarn build && yarn publish",
"test": "NODE_ENV=test jest"
},
"dependencies": {
"@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0",
"classnames": "^2.3.1"
"axios": "^0.27.2",
"camelcase-keys": "^7.0.2",
"snakecase-keys": "^5.4.2"
},
"peerDependencies": {
"react": "^16.13.1",
Expand Down
47 changes: 47 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/apiProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { FlyteApiProvider, useFlyteApi } from '.';
import { AdminEndpoint } from '../utils/constants';
import { getLoginUrl } from './login';

const MockCoponent = () => {
const context = useFlyteApi();

return (
<>
<div>{context.getProfileUrl()}</div>
<div>{context.getAdminApiUrl('/magic')}</div>
<div>{context.getLoginUrl()}</div>
</>
);
};

describe('fltyte-api/ApiProvider', () => {
it('getLoginUrl properly adds redirect url', () => {
const result = getLoginUrl(AdminEndpoint.Version, `http://some.nonsense`);
expect(result).toEqual('/version/login?redirect_url=http://some.nonsense');
});

it('If FlyteApiContext is not defined, returned URL uses default value', () => {
const { getAllByText } = render(<MockCoponent />);
expect(getAllByText('#').length).toBe(3);
});

it('If FlyteApiContext is defined, but flyteApiDomain is not point to localhost', () => {
const { getByText } = render(
<FlyteApiProvider>
<MockCoponent />
</FlyteApiProvider>,
);
expect(getByText('http://localhost/me')).toBeInTheDocument();
});

it('If FlyteApiContext provides flyteApiDomain value', () => {
const { getByText } = render(
<FlyteApiProvider flyteApiDomain="https://some.domain.here">
<MockCoponent />
</FlyteApiProvider>,
);
expect(getByText('https://some.domain.here/me')).toBeInTheDocument();
});
});
56 changes: 56 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import { createContext, useContext } from 'react';
import { getAdminApiUrl, getEndpointUrl } from '../utils';
import { AdminEndpoint, RawEndpoint } from '../utils/constants';
import { defaultLoginStatus, getLoginUrl, LoginStatus } from './login';

export interface FlyteApiContextState {
loginStatus: LoginStatus;
getLoginUrl: (redirect?: string) => string;
getProfileUrl: () => string;
getAdminApiUrl: (endpoint: AdminEndpoint | string) => string;
}

const FlyteApiContext = createContext<FlyteApiContextState>({
// default values - used when Provider wrapper is not found
loginStatus: defaultLoginStatus,
getLoginUrl: () => '#',
getProfileUrl: () => '#',
getAdminApiUrl: () => '#',
});

interface FlyteApiProviderProps {
flyteApiDomain?: string;
children?: React.ReactNode;
}

export const useFlyteApi = () => useContext(FlyteApiContext);

export const FlyteApiProvider = (props: FlyteApiProviderProps) => {
const { flyteApiDomain } = props;

const [loginExpired, setLoginExpired] = React.useState(false);

// Whenever we detect expired credentials, trigger a login redirect automatically
React.useEffect(() => {
if (loginExpired) {
window.location.href = getLoginUrl(flyteApiDomain);
}
}, [loginExpired]);

return (
<FlyteApiContext.Provider
value={{
loginStatus: {
expired: loginExpired,
setExpired: setLoginExpired,
},
getLoginUrl: (redirect) => getLoginUrl(flyteApiDomain, redirect),
getProfileUrl: () => getEndpointUrl(RawEndpoint.Profile, flyteApiDomain),
getAdminApiUrl: (endpoint) => getAdminApiUrl(endpoint, flyteApiDomain),
}}
>
{props.children}
</FlyteApiContext.Provider>
);
};
22 changes: 22 additions & 0 deletions packages/plugins/flyte-api/src/ApiProvider/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getEndpointUrl } from '../utils';
import { RawEndpoint } from '../utils/constants';

export interface LoginStatus {
expired: boolean;
setExpired(expired: boolean): void;
}

export const defaultLoginStatus: LoginStatus = {
expired: true,
setExpired: () => {
/** Do nothing */
},
};

/** Constructs a url for redirecting to the Admin login endpoint and returning
* to the current location after completing the flow.
*/
export function getLoginUrl(adminUrl?: string, redirectUrl: string = window.location.href) {
const baseUrl = getEndpointUrl(RawEndpoint.Login, adminUrl);
return `${baseUrl}?redirect_url=${redirectUrl}`;
}
41 changes: 0 additions & 41 deletions packages/plugins/flyte-api/src/Sample/index.tsx

This file was deleted.

34 changes: 0 additions & 34 deletions packages/plugins/flyte-api/src/Sample/sample.stories.tsx

This file was deleted.

11 changes: 0 additions & 11 deletions packages/plugins/flyte-api/src/Sample/sample.test.tsx

This file was deleted.

5 changes: 4 additions & 1 deletion packages/plugins/flyte-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { SampleComponent } from './Sample';
export { FlyteApiProvider, useFlyteApi, type FlyteApiContextState } from './ApiProvider';

export { AdminEndpoint, RawEndpoint } from './utils/constants';
export { getAxiosApiCall, defaultAxiosConfig } from './utils';
10 changes: 10 additions & 0 deletions packages/plugins/flyte-api/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum RawEndpoint {
Login = '/login',
Profile = '/me',
}

export const adminApiPrefix = '/api/v1';

export enum AdminEndpoint {
Version = '/version',
}
36 changes: 36 additions & 0 deletions packages/plugins/flyte-api/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable max-classes-per-file */
import { AxiosError } from 'axios';

export class NotFoundError extends Error {
constructor(public override name: string, msg = 'The requested item could not be found') {
super(msg);
}
}

/** Indicates failure to fetch a resource because the user is not authorized (401) */
export class NotAuthorizedError extends Error {
constructor(msg = 'User is not authorized to view this resource') {
super(msg);
}
}

/** Detects special cases for errors returned from Axios and lets others pass through. */
export function transformRequestError(err: unknown, path: string) {
const error = err as AxiosError;

if (!error.response) {
return error;
}

// For some status codes, we'll throw a special error to allow
// client code and components to handle separately
if (error.response.status === 404) {
return new NotFoundError(path);
}
if (error.response.status === 401) {
return new NotAuthorizedError();
}

// this error is not decoded.
return error;
}
Loading