From 578760956f1efd3c46fc3402e0666df39c25ba90 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 2 Jun 2020 17:10:46 +0200 Subject: [PATCH 1/6] Auth state working --- .../core-api/src/apis/definitions/auth.ts | 28 ++- .../implementations/auth/google/GoogleAuth.ts | 15 +- .../apis/implementations/auth/google/types.ts | 3 + .../RefreshingAuthSessionManager.ts | 4 + .../src/layout/Sidebar/LoggedUserBadge.tsx | 230 +++++++++++++++++- .../core/src/layout/Sidebar/UserBadge.tsx | 31 ++- plugins/auth-backend/package.json | 4 +- .../src/providers/OAuthProvider.ts | 2 + .../src/providers/google/provider.ts | 23 +- plugins/auth-backend/src/providers/types.ts | 11 +- yarn.lock | 10 + 11 files changed, 334 insertions(+), 27 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index c066c83b73..4cbc8bcfb1 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -143,6 +143,30 @@ export type OpenIdConnectApi = { logout(): Promise; }; +export type ProfileInfoOptions = { + /** + * If this is set to true, the user will not be prompted to log in, + * and an empty profile will be returned if there is no existing session. + * + * This can be used to perform a check whether the user is logged in, or if you don't + * want to force a user to be logged in, but provide functionality if they already are. + * + * @default false + */ + optional?: boolean; +}; + +export type ProfileInfoApi = { + getProfile(options?: ProfileInfoOptions): Promise; +}; + +export type ProfileInfo = { + provider: string; + email: string; + name?: string; + picture?: string; +}; + /** * Provides authentication towards Google APIs and identities. * @@ -151,7 +175,9 @@ export type OpenIdConnectApi = { * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, * email and expiration information. Do not rely on any other fields, as they might not be present. */ -export const googleAuthApiRef = createApiRef({ +export const googleAuthApiRef = createApiRef< + OAuthApi & OpenIdConnectApi & ProfileInfoApi +>({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', }); 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 582afbcfea..f40e164c82 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -22,6 +22,9 @@ import { OpenIdConnectApi, IdTokenOptions, AccessTokenOptions, + ProfileInfoApi, + ProfileInfoOptions, + ProfileInfo, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -39,6 +42,7 @@ type CreateOptions = { }; export type GoogleAuthResponse = { + profile: any; accessToken: string; idToken: string; scope: string; @@ -53,7 +57,7 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; -class GoogleAuth implements OAuthApi, OpenIdConnectApi { +class GoogleAuth implements OAuthApi, OpenIdConnectApi, ProfileInfoApi { static create({ apiOrigin, basePath, @@ -69,6 +73,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { oauthRequestApi: oauthRequestApi, sessionTransform(res: GoogleAuthResponse): GoogleSession { return { + profile: res.profile, idToken: res.idToken, accessToken: res.accessToken, scopes: GoogleAuth.normalizeScopes(res.scope), @@ -123,6 +128,14 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { await this.sessionManager.removeSession(); } + async getProfile(options: ProfileInfoOptions = {}) { + const session = await this.sessionManager.getSession(options); + if (!session) { + return undefined; + } + return session.profile; + } + static normalizeScopes(scopes?: string | string[]): Set { if (!scopes) { return new Set(); diff --git a/packages/core-api/src/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts index 96c69c5d4f..ea251c7006 100644 --- a/packages/core-api/src/apis/implementations/auth/google/types.ts +++ b/packages/core-api/src/apis/implementations/auth/google/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import { ProfileInfo } from '../../../definitions'; + export type GoogleSession = { + profile: ProfileInfo; idToken: string; accessToken: string; scopes: Set; diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index 64df3deca4..3df21af3f3 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -117,6 +117,10 @@ export class RefreshingAuthSessionManager implements SessionManager { window.location.reload(); // TODO(Rugvip): make this work without reload? } + async getCurrentSession() { + return this.currentSession; + } + private async collapsedSessionRefresh(): Promise { if (this.refreshPromise) { return this.refreshPromise; diff --git a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx index f9bd4ed2ab..9f1fe1d3f7 100644 --- a/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx +++ b/packages/core/src/layout/Sidebar/LoggedUserBadge.tsx @@ -14,48 +14,254 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { sidebarConfig } from './config'; -import { Avatar, Typography } from '@material-ui/core'; +import { + Avatar, + ListItem, + ListItemAvatar, + ListItemText, + Popover, + List, + ListItemIcon, + ListItemSecondaryAction, + IconButton, + Tooltip, +} from '@material-ui/core'; +import { blueGrey } from '@material-ui/core/colors'; +import { useSetState } from 'react-use'; +import { Skeleton } from '@material-ui/lab'; +import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core'; +import LogoutIcon from '@material-ui/icons/PowerSettingsNew'; +import ControlPointIcon from '@material-ui/icons/ControlPoint'; +import AccountCircleIcon from '@material-ui/icons/AccountCircle'; -const useStyles = makeStyles(() => { +const useStyles = makeStyles(theme => { const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig; return { root: { width: drawerWidthOpen, display: 'flex', alignItems: 'center', - color: '#b5b5b5', paddingLeft: 18, paddingTop: 14, paddingBottom: 14, + color: '#b5b5b5', }, avatar: { width: userBadgeDiameter, height: userBadgeDiameter, marginRight: 8, }, + purple: { + color: theme.palette.getContrastText(blueGrey[500]), + backgroundColor: blueGrey[500], + }, }; }); type Props = { - imageUrl: string; - name: string; - hideName?: boolean; + email: string; + imageUrl?: string; + name?: string; + collapsedMode?: boolean; }; export const LoggedUserBadge: FC = ({ imageUrl, name, - hideName = false, + email, + collapsedMode = false, }) => { + const [state, setState] = useSetState({ + open: false, + anchorEl: null, + }); + const googleAuth = useApi(googleAuthApiRef); + const googleLogin = useGoogleLoginState(state.open); + + const handleOpen = (event: { + preventDefault: () => void; + currentTarget: any; + }) => { + // This prevents ghost click. + event.preventDefault(); + setState({ + open: true, + anchorEl: event.currentTarget, + }); + }; + + const handleClose = () => { + setState({ + open: false, + }); + }; + + const handleGoogleSignIn = () => { + googleAuth.getIdToken(); + handleClose(); + }; + + const handleGoogleSignOut = () => { + googleAuth.logout(); + }; + const classes = useStyles(); + const avatarFallback = email.charAt(0).toUpperCase() + email.slice(1); + const emailTrimmed = email.split('@')[0]; + const displayEmail = + emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); + const displayName = name ?? displayEmail; return ( -
- - {!hideName && {name}} -
+ <> + + + {imageUrl ? ( + + ) : ( + + {avatarFallback[0]} + + )} + + {!collapsedMode && } + + + + + + + ); }; + +const SessionListItem: FC<{ + loading: boolean; + title: string; + icon: any; + user: any; + onSignIn: Function; + onSignOut: Function; +}> = ({ loading, title, icon, user, onSignIn, onSignOut, ...props }) => { + if (loading) { + return ( + + + + + } + secondary={} + /> + + + + + + + ); + } + + if (!user) { + return ( + + {icon} + + + + onSignIn()}> + + + + + + ); + } + + const { id, avatarUrl, avatarAlt } = user; + + return ( + + + + {avatarAlt && avatarAlt[0].toUpperCase()} + + + + + + onSignOut()}> + + + + + + ); +}; + +const useGoogleLoginState = (open: boolean) => { + const googleAuth = useApi(googleAuthApiRef); + const [loading, setLoading] = useState(true); + const [profile, setProfile] = useState(); + + useEffect(() => { + if (!open) { + return; + } + + let didCancel = false; + + googleAuth.getProfile().then(profile => { + if (didCancel) { + return; + } + + setProfile(profile); + setLoading(false); + }); + + return () => { + didCancel = true; + }; + }, [open]); + + if (loading) { + return { loading: true }; + } + return { loading: false, isLoggedIn: !!profile, profile }; +}; diff --git a/packages/core/src/layout/Sidebar/UserBadge.tsx b/packages/core/src/layout/Sidebar/UserBadge.tsx index b030ed2996..0a1424ee6e 100644 --- a/packages/core/src/layout/Sidebar/UserBadge.tsx +++ b/packages/core/src/layout/Sidebar/UserBadge.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useContext } from 'react'; +import React, { FC, useContext, useEffect, useState } from 'react'; import { makeStyles } from '@material-ui/core'; import People from '@material-ui/icons/People'; import { SidebarContext } from './config'; @@ -23,6 +23,7 @@ import { LoggedUserBadge } from './LoggedUserBadge'; import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; +import { useApi, googleAuthApiRef, ProfileInfo } from '@backstage/core'; const ARROW_BUTTON_SIZE = 20; const useStyles = makeStyles(theme => { @@ -58,18 +59,30 @@ export const SidebarUserBadge: FC<{}> = () => { SidebarPinStateContext, ); const classes = useStyles({ isPinned }); + const googleAuth = useApi(googleAuthApiRef); + const [profile, setProfile] = useState(); + + useEffect(() => { + //TODO(soapraj): How to observe if the user is logged in + //TODO(soapraj): Enumerate all the providers supported by the app and let user log in from here + googleAuth.getProfile({ optional: true }).then(googleProfile => { + setProfile(googleProfile); + }); + }, [googleAuth]); - const isUserLoggedIn = false; return (
- {isUserLoggedIn ? ( - + {profile ? ( + <> + + ) : ( - + )} {isOpen && (