From fa320b5e3421d7f4e564ed75d622612f9c490cc1 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 9 Jun 2020 11:44:07 +0200 Subject: [PATCH 1/6] Observe login/logout changes for providers --- .../implementations/auth/github/GithubAuth.ts | 22 ++++++++ .../implementations/auth/google/GoogleAuth.ts | 23 +++++++++ .../core/src/layout/Sidebar/UserSettings.tsx | 50 +++++++++++++------ 3 files changed, 80 insertions(+), 15 deletions(-) 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..d369e125af 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -21,6 +21,8 @@ import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { BehaviorSubject } from '../../../../lib'; +import { Observable } from '../../../../types'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -80,12 +82,31 @@ class GithubAuth implements OAuthApi { constructor(private readonly sessionManager: SessionManager) {} + private session: GithubSession | undefined; + private readonly subject = new BehaviorSubject( + undefined, + ); + + session$(): Observable { + return this.subject; + } + + getSession(): GithubSession | undefined { + return this.session; + } + + setSession(session?: GithubSession): void { + this.session = session; + this.subject.next(session); + } + async getAccessToken(scope?: string, options?: AccessTokenOptions) { const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, scopes: normalizedScopes, }); + this.setSession(session); if (session) { return session.accessToken; } @@ -94,6 +115,7 @@ class GithubAuth implements OAuthApi { async logout() { await this.sessionManager.removeSession(); + this.setSession(); } 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..d310cdff57 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -29,6 +29,8 @@ import { import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { BehaviorSubject } from '../../../../lib'; +import { Observable } from '../../../../types'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth @@ -101,6 +103,24 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { constructor(private readonly sessionManager: SessionManager) {} + private session: GoogleSession | undefined; + private readonly subject = new BehaviorSubject( + undefined, + ); + + session$(): Observable { + return this.subject; + } + + getSession(): GoogleSession | undefined { + return this.session; + } + + setSession(session?: GoogleSession): void { + this.session = session; + this.subject.next(session); + } + async getAccessToken( scope?: string | string[], options?: AccessTokenOptions, @@ -110,6 +130,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { ...options, scopes: normalizedScopes, }); + this.setSession(session); if (session) { return session.accessToken; } @@ -118,6 +139,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { async getIdToken(options: IdTokenOptions = {}) { const session = await this.sessionManager.getSession(options); + this.setSession(session); if (session) { return session.idToken; } @@ -126,6 +148,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { async logout() { await this.sessionManager.removeSession(); + this.setSession(); } async getProfile(options: ProfileInfoOptions = {}) { diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 5179356cb8..28c4f100ae 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -60,23 +60,43 @@ const useProviders = () => { }, ]); - const setIsSignedIn = async () => { - const signInChecks = await Promise.all( - providers.map(provider => - provider.identity - ? provider.api.getIdToken({ optional: true }) - : provider.api.getAccessToken('', { optional: true }), - ), - ); + useEffect(() => { + // On page load we check the status of sign-in/sign-out for all the providers + // by making a optional getIdToken or getAccessToken request. + const setIsSignedIn = async () => { + const signInChecks = await Promise.all( + providers.map(provider => { + return provider.identity + ? provider.api.getIdToken({ optional: true }) + : provider.api.getAccessToken('', { optional: true }); + }), + ); - signInChecks.map((result, i) => { - providers[i].isSignedIn = !!result; + signInChecks.map((result, i) => { + providers[i].isSignedIn = !!result; + }); + + setProviders(providers); + }; + + setIsSignedIn(); + + // Any sign-in/sign-out activity on any provider is observed here + providers.map((provider, index) => { + provider.api.session$().subscribe((session: any) => { + if (session) { + const currentProvider = providers[index]; + currentProvider.isSignedIn = true; + setProviders([ + ...providers.slice(0, index), + currentProvider, + ...providers.slice(index + 1, providers.length), + ]); + } + }); }); - - setProviders(providers); - }; - - setIsSignedIn(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return providers; }; From 8ea1c74153c85e5b7045ecdd173ddd72ac9116d6 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 9 Jun 2020 12:01:29 +0200 Subject: [PATCH 2/6] Refactor auth provider classes to extend ObservableSession class --- .../auth/ObservableSession.tsx | 36 +++++++++++++++++++ .../implementations/auth/github/GithubAuth.ts | 25 +++---------- .../implementations/auth/google/GoogleAuth.ts | 26 +++----------- 3 files changed, 45 insertions(+), 42 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/ObservableSession.tsx diff --git a/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx b/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx new file mode 100644 index 0000000000..b080f64f08 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx @@ -0,0 +1,36 @@ +/* + * 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 '../../../lib'; +import { Observable } from '../../../types'; + +export class ObservableSession { + private session: T | undefined; + private readonly subject = new BehaviorSubject(undefined); + + session$(): Observable { + return this.subject; + } + + getSession(): T | undefined { + return this.session; + } + + setSession(session?: T): void { + this.session = session; + this.subject.next(session); + } +} 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 d369e125af..bc93af8fc7 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -21,8 +21,7 @@ import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { BehaviorSubject } from '../../../../lib'; -import { Observable } from '../../../../types'; +import { ObservableSession } from '../ObservableSession'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -48,7 +47,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi { +class GithubAuth extends ObservableSession implements OAuthApi { static create({ apiOrigin, basePath, @@ -80,24 +79,8 @@ class GithubAuth implements OAuthApi { return new GithubAuth(sessionManager); } - constructor(private readonly sessionManager: SessionManager) {} - - private session: GithubSession | undefined; - private readonly subject = new BehaviorSubject( - undefined, - ); - - session$(): Observable { - return this.subject; - } - - getSession(): GithubSession | undefined { - return this.session; - } - - setSession(session?: GithubSession): void { - this.session = session; - this.subject.next(session); + constructor(private readonly sessionManager: SessionManager) { + super(); } async getAccessToken(scope?: string, options?: AccessTokenOptions) { 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 d310cdff57..8de65ce01b 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -29,8 +29,7 @@ import { import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { BehaviorSubject } from '../../../../lib'; -import { Observable } from '../../../../types'; +import { ObservableSession } from '../ObservableSession'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth @@ -59,7 +58,8 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; -class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { +class GoogleAuth extends ObservableSession + implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { static create({ apiOrigin, basePath, @@ -101,24 +101,8 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { return new GoogleAuth(sessionManager); } - constructor(private readonly sessionManager: SessionManager) {} - - private session: GoogleSession | undefined; - private readonly subject = new BehaviorSubject( - undefined, - ); - - session$(): Observable { - return this.subject; - } - - getSession(): GoogleSession | undefined { - return this.session; - } - - setSession(session?: GoogleSession): void { - this.session = session; - this.subject.next(session); + constructor(private readonly sessionManager: SessionManager) { + super(); } async getAccessToken( From 76b8e1310d7171d16463087cf215a2a9e01087b5 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 9 Jun 2020 14:38:51 +0200 Subject: [PATCH 3/6] review fixes --- .../auth/ObservableSession.tsx | 36 ------- .../implementations/auth/github/GithubAuth.ts | 19 ++-- .../implementations/auth/google/GoogleAuth.ts | 22 +++-- .../RefreshingAuthSessionManager.ts | 2 +- .../StaticAuthSessionManager.ts | 2 +- .../src/layout/Sidebar/Sidebar.stories.tsx | 2 + .../core/src/layout/Sidebar/UserSettings.tsx | 97 +++++++++---------- 7 files changed, 79 insertions(+), 101 deletions(-) delete mode 100644 packages/core-api/src/apis/implementations/auth/ObservableSession.tsx diff --git a/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx b/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx deleted file mode 100644 index b080f64f08..0000000000 --- a/packages/core-api/src/apis/implementations/auth/ObservableSession.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 '../../../lib'; -import { Observable } from '../../../types'; - -export class ObservableSession { - private session: T | undefined; - private readonly subject = new BehaviorSubject(undefined); - - session$(): Observable { - return this.subject; - } - - getSession(): T | undefined { - return this.session; - } - - setSession(session?: T): void { - this.session = session; - this.subject.next(session); - } -} 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 bc93af8fc7..e9e40c2eab 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -21,7 +21,8 @@ import { OAuthApi, AccessTokenOptions } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { ObservableSession } from '../ObservableSession'; +import { BehaviorSubject } from '../../../../lib'; +import { Observable } from '../../../../types'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -47,7 +48,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth extends ObservableSession implements OAuthApi { +class GithubAuth implements OAuthApi { static create({ apiOrigin, basePath, @@ -79,17 +80,23 @@ class GithubAuth extends ObservableSession implements OAuthApi { return new GithubAuth(sessionManager); } - constructor(private readonly sessionManager: SessionManager) { - super(); + private readonly subject = new BehaviorSubject( + undefined, + ); + + session$(): Observable { + return this.subject; } + constructor(private readonly sessionManager: SessionManager) {} + async getAccessToken(scope?: string, options?: AccessTokenOptions) { const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, scopes: normalizedScopes, }); - this.setSession(session); + this.subject.next(!!session); if (session) { return session.accessToken; } @@ -98,7 +105,7 @@ class GithubAuth extends ObservableSession implements OAuthApi { async logout() { await this.sessionManager.removeSession(); - this.setSession(); + this.subject.next(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 8de65ce01b..0675c60e82 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -29,7 +29,8 @@ import { import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { ObservableSession } from '../ObservableSession'; +import { BehaviorSubject } from '../../../../lib'; +import { Observable } from '../../../../types'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth @@ -58,8 +59,7 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; -class GoogleAuth extends ObservableSession - implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { +class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { static create({ apiOrigin, basePath, @@ -101,10 +101,16 @@ class GoogleAuth extends ObservableSession return new GoogleAuth(sessionManager); } - constructor(private readonly sessionManager: SessionManager) { - super(); + private readonly subject = new BehaviorSubject( + undefined, + ); + + session$(): Observable { + return this.subject; } + constructor(private readonly sessionManager: SessionManager) {} + async getAccessToken( scope?: string | string[], options?: AccessTokenOptions, @@ -114,7 +120,7 @@ class GoogleAuth extends ObservableSession ...options, scopes: normalizedScopes, }); - this.setSession(session); + this.subject.next(!!session); if (session) { return session.accessToken; } @@ -123,7 +129,7 @@ class GoogleAuth extends ObservableSession async getIdToken(options: IdTokenOptions = {}) { const session = await this.sessionManager.getSession(options); - this.setSession(session); + this.subject.next(!!session); if (session) { return session.idToken; } @@ -132,7 +138,7 @@ class GoogleAuth extends ObservableSession async logout() { await this.sessionManager.removeSession(); - this.setSession(); + this.subject.next(false); } async getProfile(options: ProfileInfoOptions = {}) { 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/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/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 28c4f100ae..722d2d3f4f 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -33,6 +33,8 @@ import { import { Avatar, IconButton, makeStyles, Tooltip } from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; +type AppAuthProviders = Provider[]; + type Provider = { title: string; api: any; @@ -44,7 +46,8 @@ type Provider = { const useProviders = () => { const googleAuth = useApi(googleAuthApiRef); const githubAuth = useApi(githubAuthApiRef); - const [providers, setProviders] = useState([ + // TODO(soapraj): List all the providers supported by the app + const [providers, setProviders] = useState([ { title: 'Google', api: googleAuth, @@ -60,43 +63,40 @@ const useProviders = () => { }, ]); - useEffect(() => { - // On page load we check the status of sign-in/sign-out for all the providers - // by making a optional getIdToken or getAccessToken request. - const setIsSignedIn = async () => { - const signInChecks = await Promise.all( - providers.map(provider => { - return provider.identity - ? provider.api.getIdToken({ optional: true }) - : provider.api.getAccessToken('', { optional: true }); - }), - ); + // On page load we check the status of sign-in/sign-out for all the providers + // by making a optional getIdToken or getAccessToken request. + const setIsSignedIn = async () => { + const signInChecks = await Promise.all( + providers.map(provider => { + return provider.identity + ? provider.api.getIdToken({ optional: true }) + : provider.api.getAccessToken('', { optional: true }); + }), + ); - signInChecks.map((result, i) => { - providers[i].isSignedIn = !!result; - }); + signInChecks.map((result, i) => { + providers[i].isSignedIn = !!result; + }); - setProviders(providers); - }; + setProviders(providers); + }; - setIsSignedIn(); - - // Any sign-in/sign-out activity on any provider is observed here + // Any sign-in/sign-out activity on any provider is observed here + const observeProviderState = () => { providers.map((provider, index) => { - provider.api.session$().subscribe((session: any) => { - if (session) { - const currentProvider = providers[index]; - currentProvider.isSignedIn = true; - setProviders([ - ...providers.slice(0, index), - currentProvider, - ...providers.slice(index + 1, providers.length), - ]); - } + provider.api.session$().subscribe((signedInState: boolean) => { + const mutatedProvider = providers[index]; + mutatedProvider.isSignedIn = signedInState; + providers[index] = mutatedProvider; + setProviders(providers.slice()); }); }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }; + + useEffect(() => { + setIsSignedIn(); + observeProviderState(); + }, [setIsSignedIn, observeProviderState]); return providers; }; @@ -116,17 +116,19 @@ export function SidebarUserSettings() { 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); - }); + if (identityProvider?.isSignedIn) { + identityProvider?.api + .getProfile({ optional: true }) + .then((userProfile: ProfileInfo) => { + setProfile(userProfile); + }); + } else { + setProfile(undefined); + } }, [providers, open]); const handleClick = () => { @@ -141,30 +143,27 @@ export function SidebarUserSettings() { // Handle main auth info that is shown on the collapsible SidebarItem let avatar; - let displayName; + let displayName = 'Guest'; 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 ( <> From 4c205d039d4a2fb630515fd5adf3f42b6bb0e45b Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 9 Jun 2020 23:30:39 +0200 Subject: [PATCH 4/6] refactoring to have side item components based on provider type --- .../core-api/src/apis/definitions/auth.ts | 8 +- .../implementations/auth/github/GithubAuth.ts | 8 +- .../implementations/auth/google/GoogleAuth.ts | 4 +- .../core/src/layout/Sidebar/UserSettings.tsx | 268 +++++++++++------- 4 files changed, 173 insertions(+), 115 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 4cbc8bcfb1..9735e65977 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,9 @@ export type ProfileInfo = { picture?: string; }; +export type ObservableSession = { + session$(): Observable; +}; /** * Provides authentication towards Google APIs and identities. * @@ -176,7 +180,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 & ObservableSession >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -188,7 +192,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 e9e40c2eab..7322ddeabf 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -17,7 +17,11 @@ 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, + ObservableSession, +} from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; @@ -48,7 +52,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi { +class GithubAuth implements OAuthApi, ObservableSession { static create({ apiOrigin, basePath, 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 0675c60e82..82048ee8ae 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -25,6 +25,7 @@ import { ProfileInfoApi, ProfileInfoOptions, ProfileInfo, + ObservableSession, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -59,7 +60,8 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; -class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { +class GoogleAuth + implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, ObservableSession { static create({ apiOrigin, basePath, diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 722d2d3f4f..e63235434a 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState, useContext, useEffect, useRef } from 'react'; +import React, { useState, useContext, useEffect, useRef, FC } from 'react'; import Collapse from '@material-ui/core/Collapse'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; @@ -29,76 +29,131 @@ import { googleAuthApiRef, githubAuthApiRef, ProfileInfo, + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + ApiRef, + ObservableSession, } from '@backstage/core-api'; import { Avatar, IconButton, makeStyles, Tooltip } from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; -type AppAuthProviders = Provider[]; - type Provider = { title: string; - api: any; - identity?: boolean; - isSignedIn: boolean; icon: any; }; -const useProviders = () => { - const googleAuth = useApi(googleAuthApiRef); - const githubAuth = useApi(githubAuthApiRef); - // TODO(soapraj): List all the providers supported by the app - const [providers, setProviders] = useState([ - { - title: 'Google', - api: googleAuth, - identity: true, - isSignedIn: false, - icon: Star, - }, - { - title: 'Github', - api: githubAuth, - isSignedIn: false, - icon: StarBorder, - }, - ]); +type OAuthProviderSidebarProps = { + title: string; + icon: any; + apiRef: ApiRef; +}; - // On page load we check the status of sign-in/sign-out for all the providers - // by making a optional getIdToken or getAccessToken request. - const setIsSignedIn = async () => { - const signInChecks = await Promise.all( - providers.map(provider => { - return provider.identity - ? provider.api.getIdToken({ optional: true }) - : provider.api.getAccessToken('', { optional: true }); - }), - ); +type OIDCProviderSidebarProps = { + title: string; + icon: any; + apiRef: ApiRef; +}; - signInChecks.map((result, i) => { - providers[i].isSignedIn = !!result; - }); - - setProviders(providers); - }; - - // Any sign-in/sign-out activity on any provider is observed here - const observeProviderState = () => { - providers.map((provider, index) => { - provider.api.session$().subscribe((signedInState: boolean) => { - const mutatedProvider = providers[index]; - mutatedProvider.isSignedIn = signedInState; - providers[index] = mutatedProvider; - setProviders(providers.slice()); - }); - }); - }; +const OAuthProviderSidebarComponent: FC = ({ + title, + icon, + apiRef, +}) => { + const api = useApi(apiRef); + const [signedIn, setSignedIn] = useState(false); useEffect(() => { - setIsSignedIn(); - observeProviderState(); - }, [setIsSignedIn, observeProviderState]); + const checkSession = async () => { + const session = await api.getAccessToken('', { optional: true }); + setSignedIn(!!session); + }; - return providers; + const observeSession = () => { + api.session$().subscribe((signedInState: boolean) => { + if (signedIn !== signedInState) { + setSignedIn(signedInState); + } + }); + }; + + checkSession(); + observeSession(); + }, []); + + return ( + + ); +}; + +const OIDCProviderSidebarComponent: 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); + }; + + const observeSession = () => { + api.session$().subscribe((signedInState: boolean) => { + if (signedIn !== signedInState) { + setSignedIn(signedInState); + } + }); + }; + + checkSession(); + observeSession(); + }, []); + + return ( + + ); +}; + +const ProviderSidebarItemComponent: FC<{ + title: string; + icon: any; + signedIn: boolean; + api: OAuthApi | OpenIdConnectApi; + signInHandler: Function; +}> = ({ title, icon, signedIn, api, signInHandler }) => { + return ( + + (signedIn ? api.logout() : signInHandler())}> + + + + + + ); }; const useStyles = makeStyles({ @@ -108,38 +163,35 @@ const useStyles = makeStyles({ }, }); -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(); +export const SidebarUserProfile: FC<{ setOpen: Function }> = ({ setOpen }) => { const [profile, setProfile] = useState(); + const ref = useRef(); // for scrolling down when collapse item opens + const googleAuth = useApi(googleAuthApiRef); const classes = useStyles(); - useEffect(() => { - const identityProvider = providers.find( - (provider: Provider) => provider.identity, - ); - if (identityProvider?.isSignedIn) { - identityProvider?.api - .getProfile({ optional: true }) - .then((userProfile: ProfileInfo) => { - setProfile(userProfile); - }); - } else { - setProfile(undefined); - } - }, [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]); + const fetchProfile = async () => { + await googleAuth + .getProfile({ optional: true }) + .then((userProfile?: ProfileInfo) => { + setProfile(userProfile); + }); + }; + + const observeSession = () => { + googleAuth.session$().subscribe(() => { + fetchProfile(); + }); + }; + + fetchProfile(); + observeSession(); + }, [open]); // Handle main auth info that is shown on the collapsible SidebarItem let avatar; @@ -170,37 +222,33 @@ export function SidebarUserSettings() { > {open ? : } + + ); +}; + +export function SidebarUserSettings() { + const { isOpen: sidebarOpen } = useContext(SidebarContext); + const [open, setOpen] = React.useState(false); + + // Close the provider list when sidebar collapse + useEffect(() => { + if (!sidebarOpen && open) setOpen(false); + }, [open, sidebarOpen]); + + return ( + <> + - {providers.map((provider: Provider) => ( - - - provider.isSignedIn - ? provider.api.logout() - : provider.api.getAccessToken() - } - > - - - - - - ))} + + ); From 7accc4c0c00a9c8b83edc3fc9facada055f56deb Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 10 Jun 2020 10:28:27 +0200 Subject: [PATCH 5/6] move components to separate files --- .../core-api/src/apis/definitions/auth.ts | 15 +- .../auth/SessionStateTracker.ts | 32 +++ .../implementations/auth/github/GithubAuth.ts | 19 +- .../implementations/auth/google/GoogleAuth.ts | 25 +- .../RefreshingAuthSessionManager.test.ts | 11 +- .../StaticAuthSessionManager.test.ts | 7 +- packages/core/src/layout/Sidebar/Items.tsx | 5 +- .../Settings/OAuthProviderSettings.tsx | 73 ++++++ .../Sidebar/Settings/OIDCProviderSettings.tsx | 74 ++++++ .../Sidebar/Settings/ProviderSettingsItem.tsx | 49 ++++ .../layout/Sidebar/Settings/UserProfile.tsx | 112 +++++++++ .../core/src/layout/Sidebar/Settings/index.ts | 20 ++ .../core/src/layout/Sidebar/UserSettings.tsx | 221 +----------------- packages/core/src/layout/Sidebar/index.ts | 1 + packages/storybook/.storybook/apis.js | 11 + 15 files changed, 422 insertions(+), 253 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/SessionStateTracker.ts create mode 100644 packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/UserProfile.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/index.ts diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 9735e65977..ee51e50a1a 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -168,8 +168,13 @@ export type ProfileInfo = { picture?: string; }; -export type ObservableSession = { - session$(): Observable; +export enum SessionState { + SignedIn = 'SignedIn', + SignedOut = 'SignedOut', +} + +export type ObservableSessionStateApi = { + sessionState$(): Observable; }; /** * Provides authentication towards Google APIs and identities. @@ -180,7 +185,7 @@ export type ObservableSession = { * email and expiration information. Do not rely on any other fields, as they might not be present. */ export const googleAuthApiRef = createApiRef< - OAuthApi & OpenIdConnectApi & ProfileInfoApi & ObservableSession + OAuthApi & OpenIdConnectApi & ProfileInfoApi & ObservableSessionStateApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -192,7 +197,9 @@ 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< + OAuthApi & ObservableSessionStateApi +>({ id: 'core.auth.github', description: 'Provides authentication towards Github APIs', }); diff --git a/packages/core-api/src/apis/implementations/auth/SessionStateTracker.ts b/packages/core-api/src/apis/implementations/auth/SessionStateTracker.ts new file mode 100644 index 0000000000..794953b07f --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/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 '../../../lib'; +import { SessionState } from '../..'; + +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/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 7322ddeabf..23a642196b 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -20,13 +20,14 @@ import { GithubSession } from './types'; import { OAuthApi, AccessTokenOptions, - ObservableSession, + ObservableSessionStateApi, + SessionState, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { BehaviorSubject } from '../../../../lib'; import { Observable } from '../../../../types'; +import { SessionStateTracker } from '../SessionStateTracker'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -52,7 +53,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi, ObservableSession { +class GithubAuth implements OAuthApi, ObservableSessionStateApi { static create({ apiOrigin, basePath, @@ -84,12 +85,10 @@ class GithubAuth implements OAuthApi, ObservableSession { return new GithubAuth(sessionManager); } - private readonly subject = new BehaviorSubject( - undefined, - ); + private readonly sessionStateTracker = new SessionStateTracker(); - session$(): Observable { - return this.subject; + sessionState$(): Observable { + return this.sessionStateTracker.observable; } constructor(private readonly sessionManager: SessionManager) {} @@ -100,7 +99,7 @@ class GithubAuth implements OAuthApi, ObservableSession { ...options, scopes: normalizedScopes, }); - this.subject.next(!!session); + this.sessionStateTracker.setIsSignedId(!!session); if (session) { return session.accessToken; } @@ -109,7 +108,7 @@ class GithubAuth implements OAuthApi, ObservableSession { async logout() { await this.sessionManager.removeSession(); - this.subject.next(false); + 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 82048ee8ae..e4d2d7c81d 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -25,13 +25,14 @@ import { ProfileInfoApi, ProfileInfoOptions, ProfileInfo, - ObservableSession, + ObservableSessionStateApi, + SessionState, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; -import { BehaviorSubject } from '../../../../lib'; import { Observable } from '../../../../types'; +import { SessionStateTracker } from '../SessionStateTracker'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth @@ -61,7 +62,11 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; class GoogleAuth - implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, ObservableSession { + implements + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + ObservableSessionStateApi { static create({ apiOrigin, basePath, @@ -103,12 +108,10 @@ class GoogleAuth return new GoogleAuth(sessionManager); } - private readonly subject = new BehaviorSubject( - undefined, - ); + private readonly sessionStateTracker = new SessionStateTracker(); - session$(): Observable { - return this.subject; + sessionState$(): Observable { + return this.sessionStateTracker.observable; } constructor(private readonly sessionManager: SessionManager) {} @@ -122,7 +125,7 @@ class GoogleAuth ...options, scopes: normalizedScopes, }); - this.subject.next(!!session); + this.sessionStateTracker.setIsSignedId(!!session); if (session) { return session.accessToken; } @@ -131,7 +134,7 @@ class GoogleAuth async getIdToken(options: IdTokenOptions = {}) { const session = await this.sessionManager.getSession(options); - this.subject.next(!!session); + this.sessionStateTracker.setIsSignedId(!!session); if (session) { return session.idToken; } @@ -140,7 +143,7 @@ class GoogleAuth async logout() { await this.sessionManager.removeSession(); - this.subject.next(false); + this.sessionStateTracker.setIsSignedId(false); } async getProfile(options: ProfileInfoOptions = {}) { 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/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/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..bbc1ca8e18 --- /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, + ObservableSessionStateApi, + 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..ea1f63b76c --- /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, + ObservableSessionStateApi, + 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/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index e63235434a..8ca2580df9 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -14,217 +14,16 @@ * limitations under the License. */ -import React, { useState, useContext, useEffect, useRef, FC } 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, - OAuthApi, - OpenIdConnectApi, - ProfileInfoApi, - ApiRef, - ObservableSession, -} from '@backstage/core-api'; -import { Avatar, IconButton, makeStyles, Tooltip } from '@material-ui/core'; -import PowerButton from '@material-ui/icons/PowerSettingsNew'; - -type Provider = { - title: string; - icon: any; -}; - -type OAuthProviderSidebarProps = { - title: string; - icon: any; - apiRef: ApiRef; -}; - -type OIDCProviderSidebarProps = { - title: string; - icon: any; - apiRef: ApiRef; -}; - -const OAuthProviderSidebarComponent: 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); - }; - - const observeSession = () => { - api.session$().subscribe((signedInState: boolean) => { - if (signedIn !== signedInState) { - setSignedIn(signedInState); - } - }); - }; - - checkSession(); - observeSession(); - }, []); - - return ( - - ); -}; - -const OIDCProviderSidebarComponent: 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); - }; - - const observeSession = () => { - api.session$().subscribe((signedInState: boolean) => { - if (signedIn !== signedInState) { - setSignedIn(signedInState); - } - }); - }; - - checkSession(); - observeSession(); - }, []); - - return ( - - ); -}; - -const ProviderSidebarItemComponent: FC<{ - title: string; - icon: any; - signedIn: boolean; - api: OAuthApi | OpenIdConnectApi; - signInHandler: Function; -}> = ({ title, icon, signedIn, api, signInHandler }) => { - return ( - - (signedIn ? api.logout() : signInHandler())}> - - - - - - ); -}; - -const useStyles = makeStyles({ - avatar: { - width: 24, - height: 24, - }, -}); - -export const SidebarUserProfile: FC<{ setOpen: Function }> = ({ 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); - }); - }; - - const observeSession = () => { - googleAuth.session$().subscribe(() => { - fetchProfile(); - }); - }; - - fetchProfile(); - observeSession(); - }, [open]); - - // 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 ? : } - - - ); -}; + OAuthProviderSettings, + OIDCProviderSettings, + UserProfile as SidebarUserProfile, +} from './Settings'; export function SidebarUserSettings() { const { isOpen: sidebarOpen } = useContext(SidebarContext); @@ -237,14 +36,14 @@ export function SidebarUserSettings() { return ( <> - - - + + - Date: Wed, 10 Jun 2020 21:22:09 +0200 Subject: [PATCH 6/6] review fixes. i thought about another force update for a moment :D --- packages/core-api/src/apis/definitions/auth.ts | 8 +++----- .../apis/implementations/auth/github/GithubAuth.ts | 6 +++--- .../apis/implementations/auth/google/GoogleAuth.ts | 11 ++++------- .../AuthSessionManager}/SessionStateTracker.ts | 4 ++-- .../layout/Sidebar/Settings/OAuthProviderSettings.tsx | 4 ++-- .../layout/Sidebar/Settings/OIDCProviderSettings.tsx | 4 ++-- 6 files changed, 16 insertions(+), 21 deletions(-) rename packages/core-api/src/{apis/implementations/auth => lib/AuthSessionManager}/SessionStateTracker.ts (91%) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ee51e50a1a..7abab45780 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -173,7 +173,7 @@ export enum SessionState { SignedOut = 'SignedOut', } -export type ObservableSessionStateApi = { +export type SessionStateApi = { sessionState$(): Observable; }; /** @@ -185,7 +185,7 @@ export type ObservableSessionStateApi = { * email and expiration information. Do not rely on any other fields, as they might not be present. */ export const googleAuthApiRef = createApiRef< - OAuthApi & OpenIdConnectApi & ProfileInfoApi & ObservableSessionStateApi + OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -197,9 +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 & ObservableSessionStateApi ->({ +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 23a642196b..ac05b718e5 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -20,14 +20,14 @@ import { GithubSession } from './types'; import { OAuthApi, AccessTokenOptions, - ObservableSessionStateApi, + 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 '../SessionStateTracker'; +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 @@ -53,7 +53,7 @@ const DEFAULT_PROVIDER = { icon: GithubIcon, }; -class GithubAuth implements OAuthApi, ObservableSessionStateApi { +class GithubAuth implements OAuthApi, SessionStateApi { static create({ apiOrigin, basePath, 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 e4d2d7c81d..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,14 +25,14 @@ import { ProfileInfoApi, ProfileInfoOptions, ProfileInfo, - ObservableSessionStateApi, + 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 '../SessionStateTracker'; +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 @@ -62,11 +62,7 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; class GoogleAuth - implements - OAuthApi, - OpenIdConnectApi, - ProfileInfoApi, - ObservableSessionStateApi { + implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi { static create({ apiOrigin, basePath, @@ -148,6 +144,7 @@ class GoogleAuth 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/apis/implementations/auth/SessionStateTracker.ts b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts similarity index 91% rename from packages/core-api/src/apis/implementations/auth/SessionStateTracker.ts rename to packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts index 794953b07f..de308acb0c 100644 --- a/packages/core-api/src/apis/implementations/auth/SessionStateTracker.ts +++ b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { BehaviorSubject } from '../../../lib'; -import { SessionState } from '../..'; +import { BehaviorSubject } from '..'; +import { SessionState } from '../../apis'; export class SessionStateTracker { private signedIn: boolean = false; diff --git a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx index bbc1ca8e18..b81ba2aaf7 100644 --- a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx @@ -17,7 +17,7 @@ import { ApiRef, OAuthApi, - ObservableSessionStateApi, + SessionStateApi, useApi, Subscription, IconComponent, @@ -29,7 +29,7 @@ import { ProviderSettingsItem } from './ProviderSettingsItem'; type OAuthProviderSidebarProps = { title: string; icon: IconComponent; - apiRef: ApiRef; + apiRef: ApiRef; }; export const OAuthProviderSettings: FC = ({ diff --git a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx index ea1f63b76c..139d79213f 100644 --- a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx @@ -17,7 +17,7 @@ import { ApiRef, OpenIdConnectApi, - ObservableSessionStateApi, + SessionStateApi, useApi, Subscription, IconComponent, @@ -29,7 +29,7 @@ import { ProviderSettingsItem } from './ProviderSettingsItem'; export type OIDCProviderSidebarProps = { title: string; icon: IconComponent; - apiRef: ApiRef; + apiRef: ApiRef; }; export const OIDCProviderSettings: FC = ({