move components to separate files
This commit is contained in:
@@ -168,8 +168,13 @@ export type ProfileInfo = {
|
||||
picture?: string;
|
||||
};
|
||||
|
||||
export type ObservableSession = {
|
||||
session$(): Observable<boolean>;
|
||||
export enum SessionState {
|
||||
SignedIn = 'SignedIn',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
|
||||
export type ObservableSessionStateApi = {
|
||||
sessionState$(): Observable<SessionState>;
|
||||
};
|
||||
/**
|
||||
* 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<OAuthApi & ObservableSession>({
|
||||
export const githubAuthApiRef = createApiRef<
|
||||
OAuthApi & ObservableSessionStateApi
|
||||
>({
|
||||
id: 'core.auth.github',
|
||||
description: 'Provides authentication towards Github APIs',
|
||||
});
|
||||
|
||||
@@ -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>(SessionState.SignedOut);
|
||||
|
||||
setIsSignedId(isSignedIn: boolean) {
|
||||
if (this.signedIn !== isSignedIn) {
|
||||
this.signedIn = isSignedIn;
|
||||
this.observable.next(
|
||||
this.signedIn ? SessionState.SignedIn : SessionState.SignedOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
private readonly sessionStateTracker = new SessionStateTracker();
|
||||
|
||||
session$(): Observable<boolean | undefined> {
|
||||
return this.subject;
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionStateTracker.observable;
|
||||
}
|
||||
|
||||
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
|
||||
@@ -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<string> {
|
||||
|
||||
@@ -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<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
private readonly sessionStateTracker = new SessionStateTracker();
|
||||
|
||||
session$(): Observable<boolean | undefined> {
|
||||
return this.subject;
|
||||
sessionState$(): Observable<SessionState> {
|
||||
return this.sessionStateTracker.observable;
|
||||
}
|
||||
|
||||
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,8 +58,11 @@ const useStyles = makeStyles<Theme>(theme => {
|
||||
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
lineHeight: 'auto',
|
||||
flex: '3 1 auto',
|
||||
width: '110px',
|
||||
overflow: 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
},
|
||||
iconContainer: {
|
||||
boxSizing: 'border-box',
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ApiRef,
|
||||
OAuthApi,
|
||||
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<OAuthApi & ObservableSessionStateApi>;
|
||||
};
|
||||
|
||||
export const OAuthProviderSettings: FC<OAuthProviderSidebarProps> = ({
|
||||
title,
|
||||
icon,
|
||||
apiRef,
|
||||
}) => {
|
||||
const api = useApi(apiRef);
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSession = async () => {
|
||||
const session = await api.getAccessToken('', { optional: true });
|
||||
setSignedIn(!!session);
|
||||
};
|
||||
let subscription: Subscription;
|
||||
const observeSession = () => {
|
||||
subscription = api
|
||||
.sessionState$()
|
||||
.subscribe((sessionState: SessionState) => {
|
||||
setSignedIn(sessionState === SessionState.SignedIn);
|
||||
});
|
||||
};
|
||||
|
||||
checkSession();
|
||||
observeSession();
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<ProviderSettingsItem
|
||||
title={title}
|
||||
icon={icon}
|
||||
signedIn={signedIn}
|
||||
api={api}
|
||||
signInHandler={() => api.getAccessToken()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ApiRef,
|
||||
OpenIdConnectApi,
|
||||
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<OpenIdConnectApi & ObservableSessionStateApi>;
|
||||
};
|
||||
|
||||
export const OIDCProviderSettings: FC<OIDCProviderSidebarProps> = ({
|
||||
title,
|
||||
icon,
|
||||
apiRef,
|
||||
}) => {
|
||||
const api = useApi(apiRef);
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSession = async () => {
|
||||
const session = await api.getIdToken({ optional: true });
|
||||
setSignedIn(!!session);
|
||||
};
|
||||
|
||||
let subscription: Subscription;
|
||||
const observeSession = () => {
|
||||
subscription = api
|
||||
.sessionState$()
|
||||
.subscribe((sessionState: SessionState) => {
|
||||
setSignedIn(sessionState === SessionState.SignedIn);
|
||||
});
|
||||
};
|
||||
|
||||
checkSession();
|
||||
observeSession();
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
return (
|
||||
<ProviderSettingsItem
|
||||
title={title}
|
||||
icon={icon}
|
||||
signedIn={signedIn}
|
||||
api={api}
|
||||
signInHandler={() => api.getIdToken()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api';
|
||||
import { SidebarItem } from '../Items';
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import PowerButton from '@material-ui/icons/PowerSettingsNew';
|
||||
|
||||
export const ProviderSettingsItem: FC<{
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
signedIn: boolean;
|
||||
api: OAuthApi | OpenIdConnectApi;
|
||||
signInHandler: Function;
|
||||
}> = ({ title, icon, signedIn, api, signInHandler }) => {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={title}
|
||||
text={title}
|
||||
icon={icon ?? StarBorder}
|
||||
disableSelected
|
||||
>
|
||||
<IconButton onClick={() => (signedIn ? api.logout() : signInHandler())}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
|
||||
>
|
||||
<PowerButton color={signedIn ? 'secondary' : 'primary'} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</SidebarItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useRef, useEffect } from 'react';
|
||||
import { makeStyles, Avatar, Divider } from '@material-ui/core';
|
||||
import {
|
||||
ProfileInfo,
|
||||
useApi,
|
||||
googleAuthApiRef,
|
||||
Subscription,
|
||||
SessionState,
|
||||
} from '@backstage/core-api';
|
||||
import { SidebarItem } from '../Items';
|
||||
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
avatar: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
});
|
||||
|
||||
export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
|
||||
open,
|
||||
setOpen,
|
||||
}) => {
|
||||
const [profile, setProfile] = useState<ProfileInfo>();
|
||||
const ref = useRef<Element>(); // for scrolling down when collapse item opens
|
||||
const googleAuth = useApi(googleAuthApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
await googleAuth
|
||||
.getProfile({ optional: true })
|
||||
.then((userProfile?: ProfileInfo) => {
|
||||
setProfile(userProfile);
|
||||
});
|
||||
};
|
||||
|
||||
let subscription: Subscription;
|
||||
const observeSession = () => {
|
||||
subscription = googleAuth
|
||||
.sessionState$()
|
||||
.subscribe(async (sessionState: SessionState) => {
|
||||
if (sessionState === SessionState.SignedIn) {
|
||||
await fetchProfile();
|
||||
} else {
|
||||
setProfile(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchProfile();
|
||||
observeSession();
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [googleAuth]);
|
||||
|
||||
// Handle main auth info that is shown on the collapsible SidebarItem
|
||||
let avatar;
|
||||
let displayName = 'Guest';
|
||||
if (profile) {
|
||||
const email = profile.email;
|
||||
const name = profile.name;
|
||||
const imageUrl = profile.picture;
|
||||
const emailTrimmed = email.split('@')[0];
|
||||
const displayEmail =
|
||||
emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1);
|
||||
displayName = name ?? displayEmail;
|
||||
avatar = imageUrl
|
||||
? () => (
|
||||
<Avatar alt={displayName} src={imageUrl} className={classes.avatar} />
|
||||
)
|
||||
: () => <Avatar alt={displayName} className={classes.avatar} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider innerRef={ref} />
|
||||
<SidebarItem
|
||||
text={displayName}
|
||||
onClick={handleClick}
|
||||
icon={avatar || AccountCircleIcon}
|
||||
disableSelected
|
||||
>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</SidebarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ProviderSettingsItem } from './ProviderSettingsItem';
|
||||
export { OAuthProviderSettings } from './OAuthProviderSettings';
|
||||
export { OIDCProviderSettings } from './OIDCProviderSettings';
|
||||
export { UserProfile } from './UserProfile';
|
||||
@@ -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<OAuthApi & ObservableSession>;
|
||||
};
|
||||
|
||||
type OIDCProviderSidebarProps = {
|
||||
title: string;
|
||||
icon: any;
|
||||
apiRef: ApiRef<OpenIdConnectApi & ObservableSession>;
|
||||
};
|
||||
|
||||
const OAuthProviderSidebarComponent: FC<OAuthProviderSidebarProps> = ({
|
||||
title,
|
||||
icon,
|
||||
apiRef,
|
||||
}) => {
|
||||
const api = useApi(apiRef);
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSession = async () => {
|
||||
const session = await api.getAccessToken('', { optional: true });
|
||||
setSignedIn(!!session);
|
||||
};
|
||||
|
||||
const observeSession = () => {
|
||||
api.session$().subscribe((signedInState: boolean) => {
|
||||
if (signedIn !== signedInState) {
|
||||
setSignedIn(signedInState);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
checkSession();
|
||||
observeSession();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ProviderSidebarItemComponent
|
||||
title={title}
|
||||
icon={icon}
|
||||
signedIn={signedIn}
|
||||
api={api}
|
||||
signInHandler={api.getAccessToken}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OIDCProviderSidebarComponent: FC<OIDCProviderSidebarProps> = ({
|
||||
title,
|
||||
icon,
|
||||
apiRef,
|
||||
}) => {
|
||||
const api = useApi(apiRef);
|
||||
const [signedIn, setSignedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkSession = async () => {
|
||||
const session = await api.getIdToken({ optional: true });
|
||||
setSignedIn(!!session);
|
||||
};
|
||||
|
||||
const observeSession = () => {
|
||||
api.session$().subscribe((signedInState: boolean) => {
|
||||
if (signedIn !== signedInState) {
|
||||
setSignedIn(signedInState);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
checkSession();
|
||||
observeSession();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ProviderSidebarItemComponent
|
||||
title={title}
|
||||
icon={icon}
|
||||
signedIn={signedIn}
|
||||
api={api}
|
||||
signInHandler={api.getIdToken}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ProviderSidebarItemComponent: FC<{
|
||||
title: string;
|
||||
icon: any;
|
||||
signedIn: boolean;
|
||||
api: OAuthApi | OpenIdConnectApi;
|
||||
signInHandler: Function;
|
||||
}> = ({ title, icon, signedIn, api, signInHandler }) => {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={title}
|
||||
text={title}
|
||||
icon={icon ?? StarBorder}
|
||||
disableSelected
|
||||
>
|
||||
<IconButton onClick={() => (signedIn ? api.logout() : signInHandler())}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
|
||||
>
|
||||
<PowerButton color={signedIn ? 'secondary' : 'primary'} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</SidebarItem>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
avatar: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
});
|
||||
|
||||
export const SidebarUserProfile: FC<{ setOpen: Function }> = ({ setOpen }) => {
|
||||
const [profile, setProfile] = useState<ProfileInfo>();
|
||||
const ref = useRef<Element>(); // for scrolling down when collapse item opens
|
||||
const googleAuth = useApi(googleAuthApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
await googleAuth
|
||||
.getProfile({ optional: true })
|
||||
.then((userProfile?: ProfileInfo) => {
|
||||
setProfile(userProfile);
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
? () => (
|
||||
<Avatar alt={displayName} src={imageUrl} className={classes.avatar} />
|
||||
)
|
||||
: () => <Avatar alt={displayName} className={classes.avatar} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider innerRef={ref} />
|
||||
<SidebarItem
|
||||
text={displayName}
|
||||
onClick={handleClick}
|
||||
icon={avatar || AccountCircleIcon}
|
||||
disableSelected
|
||||
>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</SidebarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
OAuthProviderSettings,
|
||||
OIDCProviderSettings,
|
||||
UserProfile as SidebarUserProfile,
|
||||
} from './Settings';
|
||||
|
||||
export function SidebarUserSettings() {
|
||||
const { isOpen: sidebarOpen } = useContext(SidebarContext);
|
||||
@@ -237,14 +36,14 @@ export function SidebarUserSettings() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarUserProfile setOpen={setOpen} />
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<OIDCProviderSidebarComponent
|
||||
<SidebarUserProfile open={open} setOpen={setOpen} />
|
||||
<Collapse in={open} timeout="auto">
|
||||
<OIDCProviderSettings
|
||||
title="Google"
|
||||
apiRef={googleAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OAuthProviderSidebarComponent
|
||||
<OAuthProviderSettings
|
||||
title="Github"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
|
||||
@@ -34,3 +34,4 @@ export {
|
||||
export type { SidebarContextType } from './config';
|
||||
export { SidebarThemeToggle } from './SidebarThemeToggle';
|
||||
export { SidebarUserSettings } from './UserSettings';
|
||||
export * from './Settings';
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user