Hang identity api methods until implementation is set.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-01-14 21:58:51 +01:00
parent 0127060226
commit 599f2929f2
3 changed files with 63 additions and 26 deletions
@@ -46,11 +46,20 @@ type CompatibilityIdentityApi = IdentityApi & {
* and sign-in page.
*/
export class AppIdentityProxy implements IdentityApi {
private target?: CompatibilityIdentityApi;
private target?: CompatibilityIdentityApi;
private waitForTarget: Promise<IdentityApi>;
private resolveTarget: (api: IdentityApi) => void = () => {};
constructor() {
this.waitForTarget = new Promise<IdentityApi>(resolve => {
this.resolveTarget = resolve;
});
}
// This is called by the app manager once the sign-in page provides us with an implementation
setTarget(identityApi: CompatibilityIdentityApi) {
this.target = identityApi;
this.resolveTarget(identityApi);
}
getUserId(): string {
@@ -76,17 +85,11 @@ export class AppIdentityProxy implements IdentityApi {
}
async getProfileInfo(): Promise<ProfileInfo> {
if (!this.target) {
throw mkError('getProfileInfo');
}
return this.target.getProfileInfo();
return this.waitForTarget.then(target => target.getProfileInfo());
}
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
if (!this.target) {
throw mkError('getBackstageIdentity');
}
const identity = await this.target.getBackstageIdentity();
const identity = await this.waitForTarget.then(target => target.getBackstageIdentity());
if (!identity.userEntityRef.match(/^.*:.*\/.*$/)) {
// eslint-disable-next-line no-console
console.warn(
@@ -99,28 +102,21 @@ export class AppIdentityProxy implements IdentityApi {
}
async getCredentials(): Promise<{ token?: string | undefined }> {
if (!this.target) {
throw mkError('getCredentials');
}
return this.target.getCredentials();
return this.waitForTarget.then(target => target.getCredentials());
}
async getIdToken(): Promise<string | undefined> {
if (!this.target) {
throw mkError('getIdToken');
}
if (!this.target.getIdToken) {
throw new Error('IdentityApi does not implement getIdToken');
}
logDeprecation('getIdToken');
return this.target.getIdToken();
return this.waitForTarget.then((target: CompatibilityIdentityApi) => {
if (!target.getIdToken) {
throw new Error('IdentityApi does not implement getIdToken');
}
logDeprecation('getIdToken');
return target.getIdToken()
});
}
async signOut(): Promise<void> {
if (!this.target) {
throw mkError('signOut');
}
await this.target.signOut();
await this.waitForTarget.then(target => target.signOut());
location.reload();
}
}
@@ -53,6 +53,24 @@ export const ApiProvider = (props: PropsWithChildren<ApiProviderProps>) => {
);
};
/**
* Same as above, but does not inherit APIs from API providers further up in
* the react tree.
*
* Private to (and only used in) this package.
*/
export const IsolatedApiProvider = (
props: PropsWithChildren<ApiProviderProps>,
) => {
const { apis, children } = props;
return (
<ApiContext.Provider
value={createVersionedValueMap({ 1: apis })}
children={children}
/>
);
};
ApiProvider.propTypes = {
apis: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired,
children: PropTypes.node,
+24 -1
View File
@@ -79,6 +79,7 @@ import { AppThemeProvider } from './AppThemeProvider';
import { defaultConfigLoader } from './defaultConfigLoader';
import { ApiRegistry } from '../apis/system/ApiRegistry';
import { resolveRouteBindings } from './resolveRouteBindings';
import { IsolatedApiProvider } from '../apis/system/ApiProvider';
type CompatiblePlugin =
| BackstagePlugin<any, any>
@@ -335,7 +336,14 @@ export class AppManager implements BackstageApp {
const [identityApi, setIdentityApi] = useState<IdentityApi>();
if (!identityApi) {
return <Component onSignInSuccess={setIdentityApi} />;
// Encapsulate the sign in page component in an API provider which
// contains all APIs except the identity API. Provides fast feedback in
// case the identity API is accidentally required on the sign-in page.
return (
<IsolatedApiProvider apis={this.getIdentitylessApiHolder()}>
<Component onSignInSuccess={setIdentityApi} />
</IsolatedApiProvider>
);
}
this.appIdentityProxy.setTarget(identityApi);
@@ -473,6 +481,21 @@ export class AppManager implements BackstageApp {
return this.apiHolder;
}
private getIdentitylessApiHolder() {
const registrySansIdentity = new ApiFactoryRegistry();
// Assume all APIs in the current registry are valid.
this.apiFactoryRegistry.getAllApis().forEach(apiRef => {
const factory = this.apiFactoryRegistry.get(apiRef);
// Add all API factories except identity!
if (factory && apiRef !== identityApiRef) {
registrySansIdentity.register('default', factory);
}
});
return new ApiResolver(registrySansIdentity);
}
private verifyPlugins(plugins: Iterable<CompatiblePlugin>) {
const pluginIds = new Set<string>();