Merge pull request #1470 from spotify/fix-gitops-plugin

[Plugin] GitOps: Use new GitHub Auth
This commit is contained in:
Fredrik Adelöw
2020-06-30 10:20:32 +02:00
committed by GitHub
14 changed files with 248 additions and 158 deletions
@@ -27,7 +27,10 @@ import {
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import {
AuthSessionStore,
StaticAuthSessionManager,
} from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
type CreateOptions = {
@@ -91,7 +94,13 @@ class GithubAuth implements OAuthApi, SessionStateApi {
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});
return new GithubAuth(sessionManager);
const authSessionStore = new AuthSessionStore<GithubSession>({
manager: sessionManager,
storageKey: 'githubSession',
sessionScopes: (session: GithubSession) => session.providerInfo.scopes,
});
return new GithubAuth(authSessionStore);
}
sessionState$(): Observable<SessionState> {
@@ -36,6 +36,8 @@ type Options<T> = {
/**
* AuthSessionStore decorates another SessionManager with a functionality
* to store the session in local storage.
*
* Session is serialized to JSON with special support for following types: Set.
*/
export class AuthSessionStore<T> implements SessionManager<T> {
private readonly manager: SessionManager<T>;
@@ -90,7 +92,12 @@ export class AuthSessionStore<T> implements SessionManager<T> {
try {
const sessionJson = localStorage.getItem(this.storageKey);
if (sessionJson) {
const session = JSON.parse(sessionJson);
const session = JSON.parse(sessionJson, (_key, value) => {
if (value?.__type === 'Set') {
return new Set(value.__value);
}
return value;
});
return session;
}
@@ -105,7 +112,18 @@ export class AuthSessionStore<T> implements SessionManager<T> {
if (session === undefined) {
localStorage.removeItem(this.storageKey);
} else {
localStorage.setItem(this.storageKey, JSON.stringify(session));
localStorage.setItem(
this.storageKey,
JSON.stringify(session, (_key, value) => {
if (value instanceof Set) {
return {
__type: 'Set',
__value: Array.from(value),
};
}
return value;
}),
);
}
}
}
@@ -16,4 +16,5 @@
export { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager';
export { StaticAuthSessionManager } from './StaticAuthSessionManager';
export { AuthSessionStore } from './AuthSessionStore';
export * from './types';
@@ -33,6 +33,7 @@ export type Options = {
providerId: string;
secure: boolean;
disableRefresh?: boolean;
persistScopes?: boolean;
baseUrl: string;
appOrigin: string;
tokenIssuer: TokenIssuer;
@@ -105,6 +106,10 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
throw new InputError('missing scope parameter');
}
if (this.options.persistScopes) {
this.setScopesCookie(res, scope);
}
const nonce = crypto.randomBytes(16).toString('base64');
// set a nonce cookie before redirecting to oauth provider
this.setNonceCookie(res, nonce);
@@ -137,6 +142,14 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
req,
);
if (this.options.persistScopes) {
const grantedScopes = this.getScopesFromCookie(
req,
this.options.providerId,
);
response.providerInfo.scope = grantedScopes;
}
if (!this.options.disableRefresh) {
// throw error if missing refresh token
if (!refreshToken) {
@@ -241,6 +254,21 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
});
};
private setScopesCookie = (res: express.Response, scope: string) => {
res.cookie(`${this.options.providerId}-scope`, scope, {
maxAge: TEN_MINUTES_MS,
secure: this.options.secure,
sameSite: 'none',
domain: this.domain,
path: `${this.basePath}/${this.options.providerId}/handler`,
httpOnly: true,
});
};
private getScopesFromCookie = (req: express.Request, providerId: string) => {
return req.cookies[`${providerId}-scope`];
};
private setRefreshTokenCookie = (
res: express.Response,
refreshToken: string,
@@ -115,6 +115,7 @@ export function createGithubProvider(
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
disableRefresh: true,
persistScopes: true,
providerId: 'github',
secure,
baseUrl,
+1 -1
View File
@@ -28,7 +28,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0"
},
"devDependencies": {
+22
View File
@@ -79,6 +79,14 @@ export interface ListClusterRequest {
gitHubToken: string;
}
export interface GithubUserInfoRequest {
accessToken: string;
}
export interface GithubUserInfoResponse {
login: string;
}
export class FetchError extends Error {
get name(): string {
return this.constructor.name;
@@ -100,6 +108,7 @@ export type GitOpsApi = {
cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise<any>;
applyProfiles(req: ApplyProfileRequest): Promise<any>;
listClusters(req: ListClusterRequest): Promise<ListClusterStatusesResponse>;
fetchUserInfo(req: GithubUserInfoRequest): Promise<GithubUserInfoResponse>;
};
export const gitOpsApiRef = createApiRef<GitOpsApi>({
@@ -116,6 +125,19 @@ export class GitOpsRestApi implements GitOpsApi {
return await resp.json();
}
async fetchUserInfo(
req: GithubUserInfoRequest,
): Promise<GithubUserInfoResponse> {
const resp = await fetch(`https://api.github.com/user`, {
method: 'get',
headers: new Headers({
Authorization: `token ${req.accessToken}`,
}),
});
if (!resp.ok) throw await FetchError.forResponse(resp);
return await resp.json();
}
async fetchLog(req: PollLogRequest): Promise<StatusResponse> {
return await this.fetch<StatusResponse>(`/api/cluster/run-status`, {
method: 'post',
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import React, { FC, useState } from 'react';
import {
Content,
ContentHeader,
@@ -25,32 +25,30 @@ import {
Progress,
HeaderLabel,
useApi,
githubAuthApiRef,
} from '@backstage/core';
import ClusterTable from '../ClusterTable/ClusterTable';
import { Button } from '@material-ui/core';
import { useAsync, useLocalStorage } from 'react-use';
import { useAsync } from 'react-use';
import { gitOpsApiRef, ListClusterStatusesResponse } from '../../api';
import { Alert } from '@material-ui/lab';
const ClusterList: FC<{}> = () => {
const [loginInfo] = useLocalStorage<{
token: string;
username: string;
name: string;
}>('githubLoginDetails', {
token: '',
username: '',
name: 'Guest',
});
const api = useApi(gitOpsApiRef);
const githubAuth = useApi(githubAuthApiRef);
const [githubUsername, setGithubUsername] = useState(String);
const { loading, error, value } = useAsync<ListClusterStatusesResponse>(
() => {
async () => {
const accessToken = await githubAuth.getAccessToken(['repo', 'user']);
if (!githubUsername) {
const userInfo = await api.fetchUserInfo({ accessToken });
setGithubUsername(userInfo.login);
}
return api.listClusters({
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
gitHubToken: accessToken,
gitHubUser: githubUsername,
});
},
);
@@ -73,9 +71,6 @@ const ClusterList: FC<{}> = () => {
Please make sure that you start GitOps-API backend on localhost port
3008 before using this plugin.
</Alert>
<Alert severity="info">
If you're Guest, please login via GitHub first.
</Alert>
</div>
</Content>
);
@@ -100,7 +95,7 @@ const ClusterList: FC<{}> = () => {
return (
<Page theme={pageTheme.home}>
<Header title="GitOps-managed Clusters">
<HeaderLabel label="Welcome" value={loginInfo.name} />
<HeaderLabel label="Welcome" value={githubUsername} />
</Header>
{content}
</Page>
@@ -24,21 +24,16 @@ import {
Progress,
HeaderLabel,
useApi,
githubAuthApiRef,
} from '@backstage/core';
import { Link } from '@material-ui/core';
import { useParams } from 'react-router-dom';
import { useLocalStorage } from 'react-use';
import { gitOpsApiRef, Status } from '../../api';
import { transformRunStatus } from '../ProfileCatalog';
const ClusterPage: FC<{}> = () => {
const params = useParams() as { owner: string; repo: string };
const [loginInfo] = useLocalStorage<{
token: string;
username: string;
name: string;
}>('githubLoginDetails');
const [pollingLog, setPollingLog] = useState(true);
const [runStatus, setRunStatus] = useState<Status[]>([]);
@@ -46,6 +41,9 @@ const ClusterPage: FC<{}> = () => {
const [showProgress, setShowProgress] = useState(true);
const api = useApi(gitOpsApiRef);
const githubAuth = useApi(githubAuthApiRef);
const [githubAccessToken, setGithubAccessToken] = useState(String);
const [githubUsername, setGithubUsername] = useState(String);
const columns = [
{ field: 'status', title: 'Status' },
@@ -53,31 +51,43 @@ const ClusterPage: FC<{}> = () => {
];
useEffect(() => {
if (pollingLog) {
const interval = setInterval(async () => {
const resp = await api.fetchLog({
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
targetOrg: params.owner,
targetRepo: params.repo,
});
const fetchGithubUserInfo = async () => {
const accessToken = await githubAuth.getAccessToken(['repo', 'user']);
const userInfo = await api.fetchUserInfo({ accessToken });
setGithubAccessToken(accessToken);
setGithubUsername(userInfo.login);
};
setRunStatus(resp.result);
setRunLink(resp.link);
if (resp.status === 'completed') {
setPollingLog(false);
setShowProgress(false);
}
}, 10000);
return () => clearInterval(interval);
if (!githubAccessToken || !githubUsername) {
fetchGithubUserInfo();
} else {
if (pollingLog) {
const interval = setInterval(async () => {
const resp = await api.fetchLog({
gitHubToken: githubAccessToken,
gitHubUser: githubUsername,
targetOrg: params.owner,
targetRepo: params.repo,
});
setRunStatus(resp.result);
setRunLink(resp.link);
if (resp.status === 'completed') {
setPollingLog(false);
setShowProgress(false);
}
}, 10000);
return () => clearInterval(interval);
}
}
return () => {};
}, [pollingLog, api, loginInfo, params]);
}, [pollingLog, api, params, githubAuth, githubAccessToken, githubUsername]);
return (
<Page theme={pageTheme.home}>
<Header title={`Cluster ${params.owner}/${params.repo}`}>
<HeaderLabel label="Welcome" value={loginInfo.name} />
<HeaderLabel label="Welcome" value={githubUsername} />
</Header>
<Content>
<Progress hidden={!showProgress} />
@@ -20,13 +20,28 @@ import mockFetch from 'jest-fetch-mock';
import ProfileCatalog from './ProfileCatalog';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
githubAuthApiRef,
GithubAuth,
OAuthRequestManager,
} from '@backstage/core';
import { gitOpsApiRef, GitOpsRestApi } from '../../api';
describe('ProfileCatalog', () => {
it('should render', () => {
const oauthRequestApi = new OAuthRequestManager();
const apis = ApiRegistry.from([
[gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')],
[
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
}),
],
]);
mockFetch.mockResponse(() => new Promise(() => {}));
const rendered = render(
@@ -35,6 +35,7 @@ import {
StatusPending,
StatusAborted,
useApi,
githubAuthApiRef,
} from '@backstage/core';
import { TextField, List, ListItem, Link } from '@material-ui/core';
@@ -111,17 +112,12 @@ const ProfileCatalog: FC<{}> = () => {
},
]);
const [loginInfo] = useLocalStorage('githubLoginDetails', {
name: 'Guest',
username: '',
token: '',
});
const [templateRepo] = useLocalStorage<string>('gitops-template-repo');
const [gitopsProfiles] = useLocalStorage<string[]>('gitops-profiles');
const [showProgress, setShowProgress] = useState(false);
const [pollingLog, setPollingLog] = useState(false);
const [gitHubOrg, setGitHubOrg] = useState(loginInfo.username);
const [gitHubOrg, setGitHubOrg] = useState(String);
const [gitHubRepo, setGitHubRepo] = useState('new-cluster');
const [awsAccessKeyId, setAwsAccessKeyId] = useState(String);
const [awsSecretAccessKey, setAwsSecretAccessKey] = useState(String);
@@ -129,28 +125,52 @@ const ProfileCatalog: FC<{}> = () => {
const [runLink, setRunLink] = useState<string>('');
const api = useApi(gitOpsApiRef);
const githubAuth = useApi(githubAuthApiRef);
const [githubAccessToken, setGithubAccessToken] = useState(String);
const [githubUsername, setGithubUsername] = useState(String);
useEffect(() => {
if (pollingLog) {
const interval = setInterval(async () => {
const resp = await api.fetchLog({
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
targetOrg: gitHubOrg,
targetRepo: gitHubRepo,
});
const fetchGithubUserInfo = async () => {
const accessToken = await githubAuth.getAccessToken(['repo', 'user']);
const userInfo = await api.fetchUserInfo({ accessToken });
setGithubAccessToken(accessToken);
setGithubUsername(userInfo.login);
setGitHubOrg(userInfo.login);
};
setRunStatus(resp.result);
setRunLink(resp.link);
if (resp.status === 'completed') {
setPollingLog(false);
setShowProgress(false);
}
}, 10000);
return () => clearInterval(interval);
if (!githubAccessToken || !githubUsername) {
fetchGithubUserInfo();
} else {
if (pollingLog) {
const interval = setInterval(async () => {
const resp = await api.fetchLog({
gitHubToken: githubAccessToken,
gitHubUser: githubUsername,
targetOrg: gitHubOrg,
targetRepo: gitHubRepo,
});
setRunStatus(resp.result);
setRunLink(resp.link);
if (resp.status === 'completed') {
setPollingLog(false);
setShowProgress(false);
}
}, 10000);
return () => clearInterval(interval);
}
}
return () => {};
}, [pollingLog, api, gitHubOrg, gitHubRepo, loginInfo]);
}, [
pollingLog,
api,
gitHubOrg,
gitHubRepo,
githubAuth,
githubAccessToken,
githubUsername,
]);
const showFailureMessage = (msg: string) => {
setRunStatus(
@@ -182,8 +202,8 @@ const ProfileCatalog: FC<{}> = () => {
const cloneResponse = await api.cloneClusterFromTemplate({
templateRepository: templateRepo,
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
gitHubToken: githubAccessToken,
gitHubUser: githubUsername,
targetOrg: gitHubOrg,
targetRepo: gitHubRepo,
secrets: {
@@ -200,8 +220,8 @@ const ProfileCatalog: FC<{}> = () => {
}
const applyProfileResp = await api.applyProfiles({
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
gitHubToken: githubAccessToken,
gitHubUser: githubUsername,
targetOrg: gitHubOrg,
targetRepo: gitHubRepo,
profiles: gitopsProfiles,
@@ -215,8 +235,8 @@ const ProfileCatalog: FC<{}> = () => {
}
const clusterStateResp = await api.changeClusterState({
gitHubToken: loginInfo.token,
gitHubUser: loginInfo.username,
gitHubToken: githubAccessToken,
gitHubUser: githubUsername,
targetOrg: gitHubOrg,
targetRepo: gitHubRepo,
clusterState: 'present',
@@ -244,7 +264,7 @@ const ProfileCatalog: FC<{}> = () => {
title="Create GitOps-managed Cluster"
subtitle="Kubernetes cluster with ready-to-use profiles"
>
<HeaderLabel label="Welcome" value={loginInfo.name} />
<HeaderLabel label="Welcome" value={githubUsername} />
</Header>
<Content>
<ContentHeader title="Create Cluster">
+8 -3
View File
@@ -18,12 +18,17 @@ import { createPlugin } from '@backstage/core';
import ProfileCatalog from './components/ProfileCatalog';
import ClusterPage from './components/ClusterPage';
import ClusterList from './components/ClusterList';
import {
gitOpsClusterListRoute,
gitOpsClusterDetailsRoute,
gitOpsClusterCreateRoute,
} from './routes';
export const plugin = createPlugin({
id: 'gitops-profiles',
register({ router }) {
router.registerRoute('/gitops-clusters', ClusterList);
router.registerRoute('/gitops-cluster/:owner/:repo', ClusterPage);
router.registerRoute('/gitops-cluster-create', ProfileCatalog);
router.addRoute(gitOpsClusterListRoute, ClusterList);
router.addRoute(gitOpsClusterDetailsRoute, ClusterPage);
router.addRoute(gitOpsClusterCreateRoute, ProfileCatalog);
},
});
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core';
const NoIcon = () => null;
export const gitOpsClusterListRoute = createRouteRef({
icon: NoIcon,
path: '/gitops-clusters',
title: 'GitOps Clusters',
});
export const gitOpsClusterDetailsRoute = createRouteRef({
icon: NoIcon,
path: '/gitops-cluster/:owner/:repo',
title: 'GitOps Cluster details',
});
export const gitOpsClusterCreateRoute = createRouteRef({
icon: NoIcon,
path: '/gitops-cluster-create',
title: 'GitOps Cluster create',
});
+5 -76
View File
@@ -9768,18 +9768,6 @@ history@5.0.0-beta.9:
dependencies:
"@babel/runtime" "^7.7.6"
history@^4.9.0:
version "4.10.1"
resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -9789,7 +9777,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -10912,11 +10900,6 @@ is-yarn-global@^0.3.0:
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -12238,7 +12221,7 @@ loglevel@^1.6.8:
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171"
integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -12680,14 +12663,6 @@ min-indent@^1.0.0:
resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
mini-create-react-context@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==
dependencies:
"@babel/runtime" "^7.5.5"
tiny-warning "^1.0.3"
mini-css-extract-plugin@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz#5ba8290fbb4179a43dd27cca444ba150bee743a0"
@@ -14151,13 +14126,6 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -15404,7 +15372,7 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -15483,35 +15451,6 @@ react-router-dom@6.0.0-alpha.5, react-router-dom@^6.0.0-alpha.5:
history "5.0.0-beta.9"
prop-types "^15.7.2"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@6.0.0-alpha.5, react-router@^6.0.0-alpha.5:
version "6.0.0-alpha.5"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-alpha.5.tgz#c98805e50dc0e64787aa8aa4fa6753b435f2496b"
@@ -16130,11 +16069,6 @@ resolve-from@^5.0.0:
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -17939,12 +17873,12 @@ tiny-emitter@^2.0.0:
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -18706,11 +18640,6 @@ validate-npm-package-name@^3.0.0:
dependencies:
builtins "^1.0.3"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"