diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 4cbc8bcfb1..7abab45780 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -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; +}; /** * 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({ +export const githubAuthApiRef = createApiRef({ id: 'core.auth.github', description: 'Provides authentication towards Github APIs', }); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index d75bceef97..ac05b718e5 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -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 { + return this.sessionStateTracker.observable; + } + constructor(private readonly sessionManager: SessionManager) {} 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 { diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index d65a2c71ee..1fc6f4f6b8 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -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 { + return this.sessionStateTracker.observable; + } + constructor(private readonly sessionManager: SessionManager) {} 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; } diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index ee7cbab2ae..793c6f1708 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -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); }); }); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index 3df21af3f3..f7d5bcf7ca 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -113,8 +113,8 @@ export class RefreshingAuthSessionManager implements SessionManager { } async removeSession() { + this.currentSession = undefined; await this.connector.removeSession(); - window.location.reload(); // TODO(Rugvip): make this work without reload? } async getCurrentSession() { diff --git a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts new file mode 100644 index 0000000000..de308acb0c --- /dev/null +++ b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -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.SignedOut); + + setIsSignedId(isSignedIn: boolean) { + if (this.signedIn !== isSignedIn) { + this.signedIn = isSignedIn; + this.observable.next( + this.signedIn ? SessionState.SignedIn : SessionState.SignedOut, + ); + } + } +} diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts index 7d47fa91df..6280750875 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts @@ -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); }); }); diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index 6e6db47a99..5ecbbc0c4e 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -64,7 +64,7 @@ export class StaticAuthSessionManager implements SessionManager { } async removeSession() { + this.currentSession = undefined; await this.connector.removeSession(); - window.location.reload(); // TODO(Rugvip): make this work without reload? } } diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 0a875c9035..4c7d4487da 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -58,8 +58,11 @@ const useStyles = makeStyles(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', diff --git a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx new file mode 100644 index 0000000000..b81ba2aaf7 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx @@ -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; +}; + +export const OAuthProviderSettings: FC = ({ + 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 ( + api.getAccessToken()} + /> + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx new file mode 100644 index 0000000000..139d79213f --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx @@ -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; +}; + +export const OIDCProviderSettings: FC = ({ + 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 ( + api.getIdToken()} + /> + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx new file mode 100644 index 0000000000..8fbc945cc2 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx @@ -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 ( + + (signedIn ? api.logout() : signInHandler())}> + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx new file mode 100644 index 0000000000..ebf1d0877d --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -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(); + const ref = useRef(); // 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 + ? () => ( + + ) + : () => ; + } + + return ( + <> + + + {open ? : } + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts new file mode 100644 index 0000000000..6557ace53a --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/index.ts @@ -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'; diff --git a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx index a450a09d88..906317fe9c 100644 --- a/packages/core/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core/src/layout/Sidebar/Sidebar.stories.tsx @@ -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 = () => ( + ); diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 5179356cb8..8ca2580df9 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -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([ - { - 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(); // for scrolling down when collapse item opens - const providers = useProviders(); - const [profile, setProfile] = useState(); - 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 - ? () => - : () => ( - - {avatarFallback[0]} - - ); - } - return ( <> - - - {open ? : } - - - {providers.map((provider: Provider) => ( - - - provider.isSignedIn - ? provider.api.logout() - : provider.api.getAccessToken() - } - > - - - - - - ))} + + + + ); diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index ec7e532333..355ead14e0 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -34,3 +34,4 @@ export { export type { SidebarContextType } from './config'; export { SidebarThemeToggle } from './SidebarThemeToggle'; export { SidebarUserSettings } from './UserSettings'; +export * from './Settings'; diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index cacc5af354..d3150400cf 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -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();