Skip to content

Commit

Permalink
fix: separate api auth to plugin folder
Browse files Browse the repository at this point in the history
Signed-off-by: Nastya <[email protected]>
  • Loading branch information
anrusina committed May 24, 2022
1 parent 30d4715 commit b1e7351
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 89 deletions.
4 changes: 2 additions & 2 deletions packages/plugins/flyte-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flyteconsole/flyte-api",
"version": "0.0.1-rc.1",
"version": "0.0.1",
"description": "FlyteConsole plugin to allow access FlyteAPI",
"main": "./dist/index.js",
"module": "./lib/esm/index.js",
Expand All @@ -16,7 +16,7 @@
"build": "yarn build:esm && yarn build:cjs",
"build:esm": "tsc --module esnext --outDir lib/esm --project ./tsconfig.build.json",
"build:cjs": "tsc --project ./tsconfig.build.json",
"publish": "yarn clean && yarn build:esm && npm publish",
"push:update": "yarn clean && yarn build:esm && yarn publish",
"test": "NODE_ENV=test jest"
},
"dependencies": {
Expand Down
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;
}

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;
}
74 changes: 74 additions & 0 deletions packages/plugins/flyte-api/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import axios, {
AxiosRequestConfig,
AxiosRequestTransformer,
AxiosResponseTransformer,
} from 'axios';
import * as snakecaseKeys from 'snakecase-keys';
import * as camelcaseKeys from 'camelcase-keys';
import { AdminEndpoint, adminApiPrefix, RawEndpoint } from './constants';
import { isObject } from './nodeChecks';
import { transformRequestError } from './errors';

/** Ensures that a string is slash-prefixed */
function ensureSlashPrefixed(path: string) {
return path.startsWith('/') ? path : `/${path}`;
}

/** Creates a URL to the same host with a given path */
function createLocalURL(path: string) {
return `${window.location.origin}${ensureSlashPrefixed(path)}`;
}

/** Updates Enpoint url depending on admin domain */
export function getEndpointUrl(endpoint: RawEndpoint | string, adminUrl?: string) {
if (adminUrl) {
return `${adminUrl}${endpoint}`;
}

return createLocalURL(endpoint);
}

/** Adds admin api prefix to domain Url */
export function getAdminApiUrl(endpoint: AdminEndpoint | string, adminUrl?: string) {
const finalUrl = `${adminApiPrefix}${ensureSlashPrefixed(endpoint)}`;

if (adminUrl) {
return `${adminUrl}${finalUrl}`;
}

return createLocalURL(finalUrl);
}

/** Config object that can be used for requests that are not sent to
* the Admin entity API (`/api/v1/...`), such as the `/me` endpoint. This config
* ensures that requests/responses are correctly converted and that cookies are
* included.
*/
export const defaultAxiosConfig: AxiosRequestConfig = {
transformRequest: [
(data: any) => (isObject(data) ? snakecaseKeys(data) : data),
...(axios.defaults.transformRequest as AxiosRequestTransformer[]),
],
transformResponse: [
...(axios.defaults.transformResponse as AxiosResponseTransformer[] as any),
camelcaseKeys,
],
withCredentials: true,
};

/**
* @deprecated Please use `axios-hooks` instead, it will allow you to get full call status.
* example usage https://www.npmjs.com/package/axios-hooks:
* const [{ data: profile, loading, errot }] = useAxios({url: path, method: 'GET', ...defaultAxiosConfig});
*/
export const getAxiosApiCall = async <T>(path: string): Promise<T | null> => {
try {
const { data } = await axios.get<T>(path, defaultAxiosConfig);
return data;
} catch (e) {
const { message } = transformRequestError(e, path);
// eslint-disable-next-line no-console
console.error(`Failed to fetch data: ${message}`);
return null;
}
};
4 changes: 4 additions & 0 deletions packages/plugins/flyte-api/src/utils/nodeChecks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// recommended util.d.ts implementation
export const isObject = (value: unknown): boolean => {
return value !== null && typeof value === 'object';
};

0 comments on commit b1e7351

Please sign in to comment.