Merge pull request #1210 from spotify/mob/observe-auth

auth: Observe changes in session for providers.
This commit is contained in:
Raghunandan Balachandran
2020-06-11 09:51:10 +02:00
committed by GitHub
18 changed files with 444 additions and 176 deletions
+11 -2
View File
@@ -15,6 +15,7 @@
*/
import { createApiRef } from '../ApiRef';
import { Observable } from '../..';
/**
* This file contains declarations for common interfaces of auth-related APIs.
@@ -167,6 +168,14 @@ export type ProfileInfo = {
picture?: string;
};
export enum SessionState {
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
}
export type SessionStateApi = {
sessionState$(): Observable<SessionState>;
};
/**
* Provides authentication towards Google APIs and identities.
*
@@ -176,7 +185,7 @@ export type ProfileInfo = {
* email and expiration information. Do not rely on any other fields, as they might not be present.
*/
export const googleAuthApiRef = createApiRef<
OAuthApi & OpenIdConnectApi & ProfileInfoApi
OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi
>({
id: 'core.auth.google',
description: 'Provides authentication towards Google APIs and identities',
@@ -188,7 +197,7 @@ export const googleAuthApiRef = createApiRef<
* See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
* for a full list of supported scopes.
*/
export const githubAuthApiRef = createApiRef<OAuthApi>({
export const githubAuthApiRef = createApiRef<OAuthApi & SessionStateApi>({
id: 'core.auth.github',
description: 'Provides authentication towards Github APIs',
});
@@ -17,10 +17,17 @@
import GithubIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { GithubSession } from './types';
import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth';
import {
OAuthApi,
AccessTokenOptions,
SessionStateApi,
SessionState,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth
@@ -46,7 +53,7 @@ const DEFAULT_PROVIDER = {
icon: GithubIcon,
};
class GithubAuth implements OAuthApi {
class GithubAuth implements OAuthApi, SessionStateApi {
static create({
apiOrigin,
basePath,
@@ -78,6 +85,12 @@ class GithubAuth implements OAuthApi {
return new GithubAuth(sessionManager);
}
private readonly sessionStateTracker = new SessionStateTracker();
sessionState$(): Observable<SessionState> {
return this.sessionStateTracker.observable;
}
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
async getAccessToken(scope?: string, options?: AccessTokenOptions) {
@@ -86,6 +99,7 @@ class GithubAuth implements OAuthApi {
...options,
scopes: normalizedScopes,
});
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.accessToken;
}
@@ -94,6 +108,7 @@ class GithubAuth implements OAuthApi {
async logout() {
await this.sessionManager.removeSession();
this.sessionStateTracker.setIsSignedId(false);
}
static normalizeScope(scope?: string): Set<string> {
@@ -25,10 +25,14 @@ import {
ProfileInfoApi,
ProfileInfoOptions,
ProfileInfo,
SessionStateApi,
SessionState,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { Observable } from '../../../../types';
import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
@@ -57,7 +61,8 @@ const DEFAULT_PROVIDER = {
const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
class GoogleAuth
implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi {
static create({
apiOrigin,
basePath,
@@ -99,6 +104,12 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
return new GoogleAuth(sessionManager);
}
private readonly sessionStateTracker = new SessionStateTracker();
sessionState$(): Observable<SessionState> {
return this.sessionStateTracker.observable;
}
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
async getAccessToken(
@@ -110,6 +121,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
...options,
scopes: normalizedScopes,
});
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.accessToken;
}
@@ -118,6 +130,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
async getIdToken(options: IdTokenOptions = {}) {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
if (session) {
return session.idToken;
}
@@ -126,10 +139,12 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi {
async logout() {
await this.sessionManager.removeSession();
this.sessionStateTracker.setIsSignedId(false);
}
async getProfile(options: ProfileInfoOptions = {}) {
const session = await this.sessionManager.getSession(options);
this.sessionStateTracker.setIsSignedId(!!session);
if (!session) {
return undefined;
}
@@ -131,15 +131,6 @@ describe('RefreshingAuthSessionManager', () => {
});
it('should remove session and reload', async () => {
// This is a workaround that is used by Facebook and the Jest core team
// It is a limitation with the newest versions of JSDOM, and newer browser standards
// where window.location and all of its properties are read-only. So we re-construct it!
// See https://github.com/facebook/jest/issues/890#issuecomment-209698782
const location = { ...window.location };
delete window.location;
window.location = location;
jest.spyOn(window.location, 'reload').mockImplementation();
const removeSession = jest.fn();
const manager = new RefreshingAuthSessionManager({
connector: { removeSession },
@@ -147,7 +138,7 @@ describe('RefreshingAuthSessionManager', () => {
} as any);
await manager.removeSession();
expect(window.location.reload).toHaveBeenCalled();
expect(removeSession).toHaveBeenCalled();
expect(await manager.getSession({ optional: true })).toBe(undefined);
});
});
@@ -113,8 +113,8 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
}
async removeSession() {
this.currentSession = undefined;
await this.connector.removeSession();
window.location.reload(); // TODO(Rugvip): make this work without reload?
}
async getCurrentSession() {
@@ -0,0 +1,32 @@
/*
* 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 { BehaviorSubject } from '..';
import { SessionState } from '../../apis';
export class SessionStateTracker {
private signedIn: boolean = false;
observable = new BehaviorSubject<SessionState>(SessionState.SignedOut);
setIsSignedId(isSignedIn: boolean) {
if (this.signedIn !== isSignedIn) {
this.signedIn = isSignedIn;
this.observable.next(
this.signedIn ? SessionState.SignedIn : SessionState.SignedOut,
);
}
}
}
@@ -84,11 +84,6 @@ describe('StaticAuthSessionManager', () => {
});
it('should remove session and reload', async () => {
const location = { ...window.location };
delete window.location;
window.location = location;
jest.spyOn(window.location, 'reload').mockImplementation();
const removeSession = jest.fn();
const manager = new StaticAuthSessionManager({
connector: { removeSession },
@@ -96,7 +91,7 @@ describe('StaticAuthSessionManager', () => {
} as any);
await manager.removeSession();
expect(window.location.reload).toHaveBeenCalled();
expect(removeSession).toHaveBeenCalled();
expect(await manager.getSession({ optional: true })).toBe(undefined);
});
});
@@ -64,7 +64,7 @@ export class StaticAuthSessionManager<T> implements SessionManager<T> {
}
async removeSession() {
this.currentSession = undefined;
await this.connector.removeSession();
window.location.reload(); // TODO(Rugvip): make this work without reload?
}
}
+4 -1
View File
@@ -58,8 +58,11 @@ const useStyles = makeStyles<Theme>(theme => {
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
fontWeight: 'bold',
whiteSpace: 'nowrap',
lineHeight: 1.0,
lineHeight: 'auto',
flex: '3 1 auto',
width: '110px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
},
iconContainer: {
boxSizing: 'border-box',
@@ -0,0 +1,73 @@
/*
* 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 {
ApiRef,
OAuthApi,
SessionStateApi,
useApi,
Subscription,
IconComponent,
SessionState,
} from '@backstage/core-api';
import React, { FC, useState, useEffect } from 'react';
import { ProviderSettingsItem } from './ProviderSettingsItem';
type OAuthProviderSidebarProps = {
title: string;
icon: IconComponent;
apiRef: ApiRef<OAuthApi & SessionStateApi>;
};
export const OAuthProviderSettings: FC<OAuthProviderSidebarProps> = ({
title,
icon,
apiRef,
}) => {
const api = useApi(apiRef);
const [signedIn, setSignedIn] = useState(false);
useEffect(() => {
const checkSession = async () => {
const session = await api.getAccessToken('', { optional: true });
setSignedIn(!!session);
};
let subscription: Subscription;
const observeSession = () => {
subscription = api
.sessionState$()
.subscribe((sessionState: SessionState) => {
setSignedIn(sessionState === SessionState.SignedIn);
});
};
checkSession();
observeSession();
return () => {
subscription.unsubscribe();
};
}, [api]);
return (
<ProviderSettingsItem
title={title}
icon={icon}
signedIn={signedIn}
api={api}
signInHandler={() => api.getAccessToken()}
/>
);
};
@@ -0,0 +1,74 @@
/*
* 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 {
ApiRef,
OpenIdConnectApi,
SessionStateApi,
useApi,
Subscription,
IconComponent,
SessionState,
} from '@backstage/core-api';
import React, { FC, useState, useEffect } from 'react';
import { ProviderSettingsItem } from './ProviderSettingsItem';
export type OIDCProviderSidebarProps = {
title: string;
icon: IconComponent;
apiRef: ApiRef<OpenIdConnectApi & SessionStateApi>;
};
export const OIDCProviderSettings: FC<OIDCProviderSidebarProps> = ({
title,
icon,
apiRef,
}) => {
const api = useApi(apiRef);
const [signedIn, setSignedIn] = useState(false);
useEffect(() => {
const checkSession = async () => {
const session = await api.getIdToken({ optional: true });
setSignedIn(!!session);
};
let subscription: Subscription;
const observeSession = () => {
subscription = api
.sessionState$()
.subscribe((sessionState: SessionState) => {
setSignedIn(sessionState === SessionState.SignedIn);
});
};
checkSession();
observeSession();
return () => {
subscription.unsubscribe();
};
}, [api]);
return (
<ProviderSettingsItem
title={title}
icon={icon}
signedIn={signedIn}
api={api}
signInHandler={() => api.getIdToken()}
/>
);
};
@@ -0,0 +1,49 @@
/*
* 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 React, { FC } from 'react';
import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api';
import { SidebarItem } from '../Items';
import { IconButton, Tooltip } from '@material-ui/core';
import StarBorder from '@material-ui/icons/StarBorder';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
export const ProviderSettingsItem: FC<{
title: string;
icon: IconComponent;
signedIn: boolean;
api: OAuthApi | OpenIdConnectApi;
signInHandler: Function;
}> = ({ title, icon, signedIn, api, signInHandler }) => {
return (
<SidebarItem
key={title}
text={title}
icon={icon ?? StarBorder}
disableSelected
>
<IconButton onClick={() => (signedIn ? api.logout() : signInHandler())}>
<Tooltip
placement="top"
arrow
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
>
<PowerButton color={signedIn ? 'secondary' : 'primary'} />
</Tooltip>
</IconButton>
</SidebarItem>
);
};
@@ -0,0 +1,112 @@
/*
* 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 React, { FC, useState, useRef, useEffect } from 'react';
import { makeStyles, Avatar, Divider } from '@material-ui/core';
import {
ProfileInfo,
useApi,
googleAuthApiRef,
Subscription,
SessionState,
} from '@backstage/core-api';
import { SidebarItem } from '../Items';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
const useStyles = makeStyles({
avatar: {
width: 24,
height: 24,
},
});
export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
open,
setOpen,
}) => {
const [profile, setProfile] = useState<ProfileInfo>();
const ref = useRef<Element>(); // for scrolling down when collapse item opens
const googleAuth = useApi(googleAuthApiRef);
const classes = useStyles();
const handleClick = () => {
setOpen(!open);
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
};
useEffect(() => {
const fetchProfile = async () => {
await googleAuth
.getProfile({ optional: true })
.then((userProfile?: ProfileInfo) => {
setProfile(userProfile);
});
};
let subscription: Subscription;
const observeSession = () => {
subscription = googleAuth
.sessionState$()
.subscribe(async (sessionState: SessionState) => {
if (sessionState === SessionState.SignedIn) {
await fetchProfile();
} else {
setProfile(undefined);
}
});
};
fetchProfile();
observeSession();
return () => {
subscription.unsubscribe();
};
}, [googleAuth]);
// Handle main auth info that is shown on the collapsible SidebarItem
let avatar;
let displayName = 'Guest';
if (profile) {
const email = profile.email;
const name = profile.name;
const imageUrl = profile.picture;
const emailTrimmed = email.split('@')[0];
const displayEmail =
emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1);
displayName = name ?? displayEmail;
avatar = imageUrl
? () => (
<Avatar alt={displayName} src={imageUrl} className={classes.avatar} />
)
: () => <Avatar alt={displayName} className={classes.avatar} />;
}
return (
<>
<Divider innerRef={ref} />
<SidebarItem
text={displayName}
onClick={handleClick}
icon={avatar || AccountCircleIcon}
disableSelected
>
{open ? <ExpandLess /> : <ExpandMore />}
</SidebarItem>
</>
);
};
@@ -0,0 +1,20 @@
/*
* 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.
*/
export { ProviderSettingsItem } from './ProviderSettingsItem';
export { OAuthProviderSettings } from './OAuthProviderSettings';
export { OIDCProviderSettings } from './OIDCProviderSettings';
export { UserProfile } from './UserProfile';
@@ -22,6 +22,7 @@ import {
SidebarDivider,
SidebarSearchField,
SidebarSpace,
SidebarUserSettings,
} from '.';
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
@@ -54,5 +55,6 @@ export const SampleSidebar = () => (
<SidebarIntro />
<SidebarSpace />
<SidebarDivider />
<SidebarUserSettings />
</Sidebar>
);
+18 -152
View File
@@ -14,174 +14,40 @@
* limitations under the License.
*/
import React, { useState, useContext, useEffect, useRef } from 'react';
import React, { useContext, useEffect } from 'react';
import Collapse from '@material-ui/core/Collapse';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import StarBorder from '@material-ui/icons/StarBorder';
import Star from '@material-ui/icons/Star';
import { SidebarContext } from './config';
import { SidebarItem } from './Items';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import Divider from '@material-ui/core/Divider';
import { googleAuthApiRef, githubAuthApiRef } from '@backstage/core-api';
import {
useApi,
googleAuthApiRef,
githubAuthApiRef,
ProfileInfo,
} from '@backstage/core-api';
import { Avatar, IconButton, makeStyles, Tooltip } from '@material-ui/core';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
type Provider = {
title: string;
api: any;
identity?: boolean;
isSignedIn: boolean;
icon: any;
};
const useProviders = () => {
const googleAuth = useApi(googleAuthApiRef);
const githubAuth = useApi(githubAuthApiRef);
const [providers, setProviders] = useState<Provider[]>([
{
title: 'Google',
api: googleAuth,
identity: true,
isSignedIn: false,
icon: Star,
},
{
title: 'Github',
api: githubAuth,
isSignedIn: false,
icon: StarBorder,
},
]);
const setIsSignedIn = async () => {
const signInChecks = await Promise.all(
providers.map(provider =>
provider.identity
? provider.api.getIdToken({ optional: true })
: provider.api.getAccessToken('', { optional: true }),
),
);
signInChecks.map((result, i) => {
providers[i].isSignedIn = !!result;
});
setProviders(providers);
};
setIsSignedIn();
return providers;
};
const useStyles = makeStyles({
avatar: {
width: 24,
height: 24,
},
});
OAuthProviderSettings,
OIDCProviderSettings,
UserProfile as SidebarUserProfile,
} from './Settings';
export function SidebarUserSettings() {
const { isOpen: sidebarOpen } = useContext(SidebarContext);
const [open, setOpen] = React.useState(false);
const ref = useRef<Element>(); // for scrolling down when collapse item opens
const providers = useProviders();
const [profile, setProfile] = useState<ProfileInfo>();
const classes = useStyles();
// TODO(soapraj): List all the providers supported by the app and let user log in from here
// TODO(soapraj): How to observe if the user is logged in
useEffect(() => {
const identityProvider = providers.find(
(provider: Provider) => provider.identity,
);
identityProvider?.api
.getProfile({ optional: true })
.then((userProfile: ProfileInfo) => {
setProfile(userProfile);
});
}, [providers, open]);
const handleClick = () => {
setOpen(!open);
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
};
// Close the provider list when sidebar collapse
useEffect(() => {
if (!sidebarOpen && open) setOpen(false);
}, [open, sidebarOpen]);
// Handle main auth info that is shown on the collapsible SidebarItem
let avatar;
let displayName;
if (profile) {
const email = profile.email;
const name = profile.name;
const imageUrl = profile.picture;
const avatarFallback = email.charAt(0).toUpperCase() + email.slice(1);
const emailTrimmed = email.split('@')[0];
const displayEmail =
emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1);
displayName = name ?? displayEmail;
avatar = imageUrl
? () => <Avatar alt={name} src={imageUrl} className={classes.avatar} />
: () => (
<Avatar alt={name} className={classes.avatar}>
{avatarFallback[0]}
</Avatar>
);
}
return (
<>
<Divider innerRef={ref} />
<SidebarItem
text={displayName || 'Guest'}
onClick={handleClick}
icon={avatar || AccountCircleIcon}
disableSelected
>
{open ? <ExpandLess /> : <ExpandMore />}
</SidebarItem>
<Collapse in={open} timeout="auto" unmountOnExit>
{providers.map((provider: Provider) => (
<SidebarItem
key={provider.title}
text={provider.title}
icon={provider.icon ?? StarBorder}
disableSelected
>
<IconButton
onClick={() =>
provider.isSignedIn
? provider.api.logout()
: provider.api.getAccessToken()
}
>
<Tooltip
placement="top"
arrow
title={
provider.isSignedIn
? `Logout from ${provider.title}`
: `Sign in to ${provider.title}`
}
>
<PowerButton
color={provider.isSignedIn ? 'secondary' : 'primary'}
/>
</Tooltip>
</IconButton>
</SidebarItem>
))}
<SidebarUserProfile open={open} setOpen={setOpen} />
<Collapse in={open} timeout="auto">
<OIDCProviderSettings
title="Google"
apiRef={googleAuthApiRef}
icon={Star}
/>
<OAuthProviderSettings
title="Github"
apiRef={githubAuthApiRef}
icon={Star}
/>
</Collapse>
</>
);
@@ -34,3 +34,4 @@ export {
export type { SidebarContextType } from './config';
export { SidebarThemeToggle } from './SidebarThemeToggle';
export { SidebarUserSettings } from './UserSettings';
export * from './Settings';
+11
View File
@@ -5,10 +5,12 @@ import {
oauthRequestApiRef,
OAuthRequestManager,
googleAuthApiRef,
githubAuthApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
GoogleAuth,
GithubAuth,
} from '@backstage/core';
const builder = ApiRegistry.builder();
@@ -31,4 +33,13 @@ builder.add(
}),
);
builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7000',
basePath: '/auth/',
oauthRequestApi,
}),
);
export const apis = builder.build();