@backstage/plugin-app 0.1.9-next.1 → 0.1.9-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.esm.js +2 -2
- package/dist/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.esm.js.map +1 -1
- package/dist/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.esm.js +72 -8
- package/dist/packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.esm.js.map +1 -1
- package/dist/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.esm.js +22 -11
- package/dist/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.esm.js.map +1 -1
- package/dist/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.esm.js +4 -5
- package/dist/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.esm.js.map +1 -1
- package/dist/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.esm.js +3 -3
- package/dist/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.esm.js.map +1 -1
- package/dist/packages/core-app-api/src/lib/loginPopup.esm.js +4 -3
- package/dist/packages/core-app-api/src/lib/loginPopup.esm.js.map +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @backstage/plugin-app
|
|
2
2
|
|
|
3
|
+
## 0.1.9-next.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @backstage/core-components@0.17.2-next.1
|
|
9
|
+
- @backstage/core-plugin-api@1.10.7-next.0
|
|
10
|
+
- @backstage/frontend-plugin-api@0.10.2-next.1
|
|
11
|
+
- @backstage/integration-react@1.2.7-next.3
|
|
12
|
+
- @backstage/theme@0.6.6-next.0
|
|
13
|
+
- @backstage/types@1.2.1
|
|
14
|
+
- @backstage/plugin-permission-react@0.4.34-next.1
|
|
15
|
+
|
|
16
|
+
## 0.1.9-next.2
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @backstage/core-plugin-api@1.10.7-next.0
|
|
22
|
+
- @backstage/core-components@0.17.2-next.1
|
|
23
|
+
- @backstage/frontend-plugin-api@0.10.2-next.1
|
|
24
|
+
- @backstage/integration-react@1.2.7-next.2
|
|
25
|
+
- @backstage/plugin-permission-react@0.4.34-next.1
|
|
26
|
+
- @backstage/theme@0.6.6-next.0
|
|
27
|
+
- @backstage/types@1.2.1
|
|
28
|
+
|
|
3
29
|
## 0.1.9-next.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { OAuthPendingRequests } from './OAuthPendingRequests.esm.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PublishSubject } from '../../../lib/subjects.esm.js';
|
|
3
3
|
|
|
4
4
|
class OAuthRequestManager {
|
|
5
|
-
subject = new
|
|
5
|
+
subject = new PublishSubject();
|
|
6
6
|
currentRequests = [];
|
|
7
7
|
handlerCount = 0;
|
|
8
8
|
createAuthRequester(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuthRequestManager.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n OAuthRequestApi,\n PendingOAuthRequest,\n OAuthRequester,\n OAuthRequesterOptions,\n} from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';\nimport {
|
|
1
|
+
{"version":3,"file":"OAuthRequestManager.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n OAuthRequestApi,\n PendingOAuthRequest,\n OAuthRequester,\n OAuthRequesterOptions,\n} from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';\nimport { PublishSubject } from '../../../lib/subjects';\n\n/**\n * The OAuthRequestManager is an implementation of the OAuthRequestApi.\n *\n * The purpose of this class and the API is to read a stream of incoming requests\n * of OAuth access tokens from different providers with varying scope, and funnel\n * them all together into a single request for each OAuth provider.\n *\n * @public\n */\nexport class OAuthRequestManager implements OAuthRequestApi {\n private readonly subject = new PublishSubject<PendingOAuthRequest[]>();\n private currentRequests: PendingOAuthRequest[] = [];\n private handlerCount = 0;\n\n createAuthRequester<T>(options: OAuthRequesterOptions<T>): OAuthRequester<T> {\n const handler = new OAuthPendingRequests<T>();\n\n const index = this.handlerCount;\n this.handlerCount++;\n\n handler.pending().subscribe({\n next: scopeRequest => {\n const newRequests = this.currentRequests.slice();\n const request = this.makeAuthRequest(scopeRequest, options);\n if (!request) {\n delete newRequests[index];\n } else {\n newRequests[index] = request;\n }\n this.currentRequests = newRequests;\n // Convert from sparse array to array of present items only\n this.subject.next(newRequests.filter(Boolean));\n },\n });\n\n return scopes => {\n return handler.request(scopes);\n };\n }\n\n // Converts the pending request and popup options into a popup request that we can forward to subscribers.\n private makeAuthRequest(\n request: PendingRequest<any>,\n options: OAuthRequesterOptions<any>,\n ): PendingOAuthRequest | undefined {\n const { scopes } = request;\n if (!scopes) {\n return undefined;\n }\n\n return {\n provider: options.provider,\n trigger: async () => {\n const result = await options.onAuthRequest(scopes);\n request.resolve(result);\n },\n reject: () => {\n const error = new Error('Login failed, rejected by user');\n error.name = 'RejectedError';\n request.reject(error);\n },\n };\n }\n\n authRequest$(): Observable<PendingOAuthRequest[]> {\n return this.subject;\n }\n}\n"],"names":[],"mappings":";;;AAmCO,MAAM,mBAA+C,CAAA;AAAA,EACzC,OAAA,GAAU,IAAI,cAAsC,EAAA;AAAA,EAC7D,kBAAyC,EAAC;AAAA,EAC1C,YAAe,GAAA,CAAA;AAAA,EAEvB,oBAAuB,OAAsD,EAAA;AAC3E,IAAM,MAAA,OAAA,GAAU,IAAI,oBAAwB,EAAA;AAE5C,IAAA,MAAM,QAAQ,IAAK,CAAA,YAAA;AACnB,IAAK,IAAA,CAAA,YAAA,EAAA;AAEL,IAAQ,OAAA,CAAA,OAAA,GAAU,SAAU,CAAA;AAAA,MAC1B,MAAM,CAAgB,YAAA,KAAA;AACpB,QAAM,MAAA,WAAA,GAAc,IAAK,CAAA,eAAA,CAAgB,KAAM,EAAA;AAC/C,QAAA,MAAM,OAAU,GAAA,IAAA,CAAK,eAAgB,CAAA,YAAA,EAAc,OAAO,CAAA;AAC1D,QAAA,IAAI,CAAC,OAAS,EAAA;AACZ,UAAA,OAAO,YAAY,KAAK,CAAA;AAAA,SACnB,MAAA;AACL,UAAA,WAAA,CAAY,KAAK,CAAI,GAAA,OAAA;AAAA;AAEvB,QAAA,IAAA,CAAK,eAAkB,GAAA,WAAA;AAEvB,QAAA,IAAA,CAAK,OAAQ,CAAA,IAAA,CAAK,WAAY,CAAA,MAAA,CAAO,OAAO,CAAC,CAAA;AAAA;AAC/C,KACD,CAAA;AAED,IAAA,OAAO,CAAU,MAAA,KAAA;AACf,MAAO,OAAA,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,KAC/B;AAAA;AACF;AAAA,EAGQ,eAAA,CACN,SACA,OACiC,EAAA;AACjC,IAAM,MAAA,EAAE,QAAW,GAAA,OAAA;AACnB,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAO,OAAA,KAAA,CAAA;AAAA;AAGT,IAAO,OAAA;AAAA,MACL,UAAU,OAAQ,CAAA,QAAA;AAAA,MAClB,SAAS,YAAY;AACnB,QAAA,MAAM,MAAS,GAAA,MAAM,OAAQ,CAAA,aAAA,CAAc,MAAM,CAAA;AACjD,QAAA,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,OACxB;AAAA,MACA,QAAQ,MAAM;AACZ,QAAM,MAAA,KAAA,GAAQ,IAAI,KAAA,CAAM,gCAAgC,CAAA;AACxD,QAAA,KAAA,CAAM,IAAO,GAAA,eAAA;AACb,QAAA,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA;AACtB,KACF;AAAA;AACF,EAEA,YAAkD,GAAA;AAChD,IAAA,OAAO,IAAK,CAAA,OAAA;AAAA;AAEhB;;;;"}
|
|
@@ -3,6 +3,7 @@ import ObservableImpl from 'zen-observable';
|
|
|
3
3
|
import { toInternalTranslationResource } from '../../../../../core-plugin-api/src/translation/TranslationResource.esm.js';
|
|
4
4
|
import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/translation/TranslationRef.esm.js';
|
|
5
5
|
import { DEFAULT_LANGUAGE } from '../AppLanguageApi/AppLanguageSelector.esm.js';
|
|
6
|
+
import { isValidElement, createElement, Fragment } from 'react';
|
|
6
7
|
|
|
7
8
|
function removeNulls(messages) {
|
|
8
9
|
return Object.fromEntries(
|
|
@@ -69,6 +70,65 @@ class ResourceLoader {
|
|
|
69
70
|
await load;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
class JsxInterpolator {
|
|
74
|
+
#setFormatHook;
|
|
75
|
+
#marker;
|
|
76
|
+
#pattern;
|
|
77
|
+
static fromI18n(i18n) {
|
|
78
|
+
const interpolator = i18n.services.interpolator;
|
|
79
|
+
const originalFormat = interpolator.format;
|
|
80
|
+
let formatHook;
|
|
81
|
+
interpolator.format = (value, format, lng, formatOpts) => {
|
|
82
|
+
if (format) {
|
|
83
|
+
return originalFormat(value, format, lng, formatOpts);
|
|
84
|
+
}
|
|
85
|
+
return formatHook?.(value, format, lng, formatOpts) ?? value;
|
|
86
|
+
};
|
|
87
|
+
return new JsxInterpolator(
|
|
88
|
+
// Using a random marker to ensure it can't be misused
|
|
89
|
+
Math.random().toString(36).substring(2, 8),
|
|
90
|
+
(hook) => {
|
|
91
|
+
formatHook = hook;
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
constructor(marker, setFormatHook) {
|
|
96
|
+
this.#setFormatHook = setFormatHook;
|
|
97
|
+
this.#marker = marker;
|
|
98
|
+
this.#pattern = new RegExp(`\\$${marker}\\(([^)]+)\\)`);
|
|
99
|
+
}
|
|
100
|
+
wrapT(originalT) {
|
|
101
|
+
return (key, options) => {
|
|
102
|
+
let elementsMap = void 0;
|
|
103
|
+
this.#setFormatHook((value) => {
|
|
104
|
+
if (isValidElement(value)) {
|
|
105
|
+
if (!elementsMap) {
|
|
106
|
+
elementsMap = /* @__PURE__ */ new Map();
|
|
107
|
+
}
|
|
108
|
+
const elementKey = elementsMap.size.toString();
|
|
109
|
+
elementsMap.set(elementKey, value);
|
|
110
|
+
return `$${this.#marker}(${elementKey})`;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
});
|
|
114
|
+
const result = originalT(key, options);
|
|
115
|
+
if (!elementsMap) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
const split = result.split(this.#pattern);
|
|
119
|
+
return createElement(
|
|
120
|
+
Fragment,
|
|
121
|
+
null,
|
|
122
|
+
...split.map((part, index) => {
|
|
123
|
+
if (index % 2 === 0) {
|
|
124
|
+
return part;
|
|
125
|
+
}
|
|
126
|
+
return elementsMap?.get(part);
|
|
127
|
+
}).filter(Boolean)
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
72
132
|
class I18nextTranslationApi {
|
|
73
133
|
static create(options) {
|
|
74
134
|
const { languages } = options.languageApi.getAvailableLanguages();
|
|
@@ -76,7 +136,9 @@ class I18nextTranslationApi {
|
|
|
76
136
|
fallbackLng: DEFAULT_LANGUAGE,
|
|
77
137
|
supportedLngs: languages,
|
|
78
138
|
interpolation: {
|
|
79
|
-
escapeValue: false
|
|
139
|
+
escapeValue: false,
|
|
140
|
+
// Used for the JsxInterpolator format hook
|
|
141
|
+
alwaysFormat: true
|
|
80
142
|
},
|
|
81
143
|
ns: [],
|
|
82
144
|
defaultNS: false,
|
|
@@ -88,6 +150,7 @@ class I18nextTranslationApi {
|
|
|
88
150
|
if (!i18n.isInitialized) {
|
|
89
151
|
throw new Error("i18next was unexpectedly not initialized");
|
|
90
152
|
}
|
|
153
|
+
const interpolator = JsxInterpolator.fromI18n(i18n);
|
|
91
154
|
const { language: initialLanguage } = options.languageApi.getLanguage();
|
|
92
155
|
if (initialLanguage !== DEFAULT_LANGUAGE) {
|
|
93
156
|
i18n.changeLanguage(initialLanguage);
|
|
@@ -123,7 +186,8 @@ class I18nextTranslationApi {
|
|
|
123
186
|
const instance = new I18nextTranslationApi(
|
|
124
187
|
i18n,
|
|
125
188
|
loader,
|
|
126
|
-
options.languageApi.getLanguage().language
|
|
189
|
+
options.languageApi.getLanguage().language,
|
|
190
|
+
interpolator
|
|
127
191
|
);
|
|
128
192
|
options.languageApi.language$().subscribe(({ language }) => {
|
|
129
193
|
instance.#changeLanguage(language);
|
|
@@ -133,14 +197,16 @@ class I18nextTranslationApi {
|
|
|
133
197
|
#i18n;
|
|
134
198
|
#loader;
|
|
135
199
|
#language;
|
|
200
|
+
#jsxInterpolator;
|
|
136
201
|
/** Keep track of which refs we have registered default resources for */
|
|
137
202
|
#registeredRefs = /* @__PURE__ */ new Set();
|
|
138
203
|
/** Notify observers when language changes */
|
|
139
204
|
#languageChangeListeners = /* @__PURE__ */ new Set();
|
|
140
|
-
constructor(i18n, loader, language) {
|
|
205
|
+
constructor(i18n, loader, language, jsxInterpolator) {
|
|
141
206
|
this.#i18n = i18n;
|
|
142
207
|
this.#loader = loader;
|
|
143
208
|
this.#language = language;
|
|
209
|
+
this.#jsxInterpolator = jsxInterpolator;
|
|
144
210
|
}
|
|
145
211
|
getTranslation(translationRef) {
|
|
146
212
|
const internalRef = toInternalTranslationRef(translationRef);
|
|
@@ -199,10 +265,8 @@ class I18nextTranslationApi {
|
|
|
199
265
|
if (this.#loader.needsLoading(this.#language, internalRef.id)) {
|
|
200
266
|
return { ready: false };
|
|
201
267
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
internalRef.id
|
|
205
|
-
);
|
|
268
|
+
const unwrappedT = this.#i18n.getFixedT(null, internalRef.id);
|
|
269
|
+
const t = this.#jsxInterpolator.wrapT(unwrappedT);
|
|
206
270
|
return {
|
|
207
271
|
ready: true,
|
|
208
272
|
t
|
|
@@ -230,5 +294,5 @@ class I18nextTranslationApi {
|
|
|
230
294
|
}
|
|
231
295
|
}
|
|
232
296
|
|
|
233
|
-
export { I18nextTranslationApi };
|
|
297
|
+
export { I18nextTranslationApi, JsxInterpolator };
|
|
234
298
|
//# sourceMappingURL=I18nextTranslationApi.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"I18nextTranslationApi.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AppLanguageApi,\n TranslationApi,\n TranslationFunction,\n TranslationMessages,\n TranslationRef,\n TranslationResource,\n TranslationSnapshot,\n} from '@backstage/core-plugin-api/alpha';\nimport { createInstance as createI18n, type i18n as I18n } from 'i18next';\nimport ObservableImpl from 'zen-observable';\n\n// Internal import to avoid code duplication, this will lead to duplication in build output\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport {\n toInternalTranslationResource,\n InternalTranslationResourceLoader,\n} from '../../../../../core-plugin-api/src/translation/TranslationResource';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport {\n toInternalTranslationRef,\n InternalTranslationRef,\n} from '../../../../../core-plugin-api/src/translation/TranslationRef';\nimport { Observable } from '@backstage/types';\nimport { DEFAULT_LANGUAGE } from '../AppLanguageApi/AppLanguageSelector';\n\n/** @alpha */\nexport interface I18nextTranslationApiOptions {\n languageApi: AppLanguageApi;\n resources?: Array<TranslationMessages | TranslationResource>;\n}\n\nfunction removeNulls(\n messages: Record<string, string | null>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(messages).filter(\n (e): e is [string, string] => e[1] !== null,\n ),\n );\n}\n\n/**\n * The built-in i18next backend loading logic doesn't handle on the fly switches\n * of language very well. It gets a bit confused about whether resources are actually\n * loaded or not, so instead we implement our own resource loader.\n */\nclass ResourceLoader {\n /** Loaded resources by loader key */\n #loaded = new Set<string>();\n /** Resource loading promises by loader key */\n #loading = new Map<string, Promise<void>>();\n /** Loaders for each resource language */\n #loaders = new Map<string, InternalTranslationResourceLoader>();\n\n constructor(\n private readonly onLoad: (loaded: {\n language: string;\n namespace: string;\n messages: Record<string, string | null>;\n }) => void,\n ) {}\n\n addTranslationResource(resource: TranslationResource) {\n const internalResource = toInternalTranslationResource(resource);\n for (const entry of internalResource.resources) {\n const key = this.#getLoaderKey(entry.language, internalResource.id);\n\n // First loader to register wins, this means that resources registered in the app\n // have priority over default resource from translation refs\n if (!this.#loaders.has(key)) {\n this.#loaders.set(key, entry.loader);\n }\n }\n }\n\n #getLoaderKey(language: string, namespace: string) {\n return `${language}/${namespace}`;\n }\n\n needsLoading(language: string, namespace: string) {\n const key = this.#getLoaderKey(language, namespace);\n const loader = this.#loaders.get(key);\n if (!loader) {\n return false;\n }\n\n return !this.#loaded.has(key);\n }\n\n async load(language: string, namespace: string): Promise<void> {\n const key = this.#getLoaderKey(language, namespace);\n\n const loader = this.#loaders.get(key);\n if (!loader) {\n return;\n }\n\n if (this.#loaded.has(key)) {\n return;\n }\n\n const loading = this.#loading.get(key);\n if (loading) {\n await loading;\n return;\n }\n\n const load = loader().then(\n result => {\n this.onLoad({ language, namespace, messages: result.messages });\n this.#loaded.add(key);\n },\n error => {\n this.#loaded.add(key); // Do not try to load failed resources again\n throw error;\n },\n );\n this.#loading.set(key, load);\n await load;\n }\n}\n\n/** @alpha */\nexport class I18nextTranslationApi implements TranslationApi {\n static create(options: I18nextTranslationApiOptions) {\n const { languages } = options.languageApi.getAvailableLanguages();\n\n const i18n = createI18n({\n fallbackLng: DEFAULT_LANGUAGE,\n supportedLngs: languages,\n interpolation: {\n escapeValue: false,\n },\n ns: [],\n defaultNS: false,\n fallbackNS: false,\n\n // Disable resource loading on init, meaning i18n will be ready to use immediately\n initImmediate: false,\n });\n\n i18n.init();\n if (!i18n.isInitialized) {\n throw new Error('i18next was unexpectedly not initialized');\n }\n\n const { language: initialLanguage } = options.languageApi.getLanguage();\n if (initialLanguage !== DEFAULT_LANGUAGE) {\n i18n.changeLanguage(initialLanguage);\n }\n\n const loader = new ResourceLoader(loaded => {\n i18n.addResourceBundle(\n loaded.language,\n loaded.namespace,\n removeNulls(loaded.messages),\n false, // do not merge with existing translations\n true, // overwrite translations\n );\n });\n\n const resources = options?.resources || [];\n // Iterate in reverse, giving higher priority to resources registered later\n for (let i = resources.length - 1; i >= 0; i--) {\n const resource = resources[i];\n if (resource.$$type === '@backstage/TranslationResource') {\n loader.addTranslationResource(resource);\n } else if (resource.$$type === '@backstage/TranslationMessages') {\n // Overrides for default messages, created with createTranslationMessages and installed via app\n i18n.addResourceBundle(\n DEFAULT_LANGUAGE,\n resource.id,\n removeNulls(resource.messages),\n true, // merge with existing translations\n false, // do not overwrite translations\n );\n }\n }\n\n const instance = new I18nextTranslationApi(\n i18n,\n loader,\n options.languageApi.getLanguage().language,\n );\n\n options.languageApi.language$().subscribe(({ language }) => {\n instance.#changeLanguage(language);\n });\n\n return instance;\n }\n\n #i18n: I18n;\n #loader: ResourceLoader;\n #language: string;\n\n /** Keep track of which refs we have registered default resources for */\n #registeredRefs = new Set<string>();\n /** Notify observers when language changes */\n #languageChangeListeners = new Set<() => void>();\n\n private constructor(i18n: I18n, loader: ResourceLoader, language: string) {\n this.#i18n = i18n;\n this.#loader = loader;\n this.#language = language;\n }\n\n getTranslation<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): TranslationSnapshot<TMessages> {\n const internalRef = toInternalTranslationRef(translationRef);\n\n this.#registerDefaults(internalRef);\n\n return this.#createSnapshot(internalRef);\n }\n\n translation$<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): Observable<TranslationSnapshot<TMessages>> {\n const internalRef = toInternalTranslationRef(translationRef);\n\n this.#registerDefaults(internalRef);\n\n return new ObservableImpl<TranslationSnapshot<TMessages>>(subscriber => {\n let loadTicket = {}; // To check for stale loads\n\n const loadResource = () => {\n loadTicket = {};\n const ticket = loadTicket;\n this.#loader.load(this.#language, internalRef.id).then(\n () => {\n if (ticket === loadTicket) {\n const snapshot = this.#createSnapshot(internalRef);\n if (snapshot.ready) {\n subscriber.next(snapshot);\n }\n }\n },\n error => {\n if (ticket === loadTicket) {\n subscriber.error(Array.isArray(error) ? error[0] : error);\n }\n },\n );\n };\n\n const onChange = () => {\n const snapshot = this.#createSnapshot(internalRef);\n if (snapshot.ready) {\n subscriber.next(snapshot);\n } else {\n loadResource();\n }\n };\n\n if (this.#loader.needsLoading(this.#language, internalRef.id)) {\n loadResource();\n }\n\n this.#languageChangeListeners.add(onChange);\n return () => {\n this.#languageChangeListeners.delete(onChange);\n };\n });\n }\n\n #changeLanguage(language: string): void {\n if (this.#language !== language) {\n this.#language = language;\n this.#i18n.changeLanguage(language);\n this.#languageChangeListeners.forEach(listener => listener());\n }\n }\n\n #createSnapshot<TMessages extends { [key in string]: string }>(\n internalRef: InternalTranslationRef<string, TMessages>,\n ): TranslationSnapshot<TMessages> {\n if (this.#loader.needsLoading(this.#language, internalRef.id)) {\n return { ready: false };\n }\n\n const t = this.#i18n.getFixedT(\n null,\n internalRef.id,\n ) as TranslationFunction<TMessages>;\n\n return {\n ready: true,\n t,\n };\n }\n\n #registerDefaults(internalRef: InternalTranslationRef): void {\n if (this.#registeredRefs.has(internalRef.id)) {\n return;\n }\n this.#registeredRefs.add(internalRef.id);\n\n const defaultMessages = internalRef.getDefaultMessages();\n this.#i18n.addResourceBundle(\n DEFAULT_LANGUAGE,\n internalRef.id,\n defaultMessages,\n true, // merge with existing translations\n false, // do not overwrite translations\n );\n\n const defaultResource = internalRef.getDefaultResource();\n if (defaultResource) {\n this.#loader.addTranslationResource(defaultResource);\n }\n }\n}\n"],"names":["createI18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"I18nextTranslationApi.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AppLanguageApi,\n TranslationApi,\n TranslationFunction,\n TranslationMessages,\n TranslationRef,\n TranslationResource,\n TranslationSnapshot,\n} from '@backstage/core-plugin-api/alpha';\nimport {\n createInstance as createI18n,\n FormatFunction,\n Interpolator,\n TFunction,\n type i18n as I18n,\n} from 'i18next';\nimport ObservableImpl from 'zen-observable';\n\n// Internal import to avoid code duplication, this will lead to duplication in build output\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport {\n toInternalTranslationResource,\n InternalTranslationResourceLoader,\n} from '../../../../../core-plugin-api/src/translation/TranslationResource';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport {\n toInternalTranslationRef,\n InternalTranslationRef,\n} from '../../../../../core-plugin-api/src/translation/TranslationRef';\nimport { Observable } from '@backstage/types';\nimport { DEFAULT_LANGUAGE } from '../AppLanguageApi/AppLanguageSelector';\nimport { createElement, Fragment, ReactNode, isValidElement } from 'react';\n\n/** @alpha */\nexport interface I18nextTranslationApiOptions {\n languageApi: AppLanguageApi;\n resources?: Array<TranslationMessages | TranslationResource>;\n}\n\nfunction removeNulls(\n messages: Record<string, string | null>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(messages).filter(\n (e): e is [string, string] => e[1] !== null,\n ),\n );\n}\n\n/**\n * The built-in i18next backend loading logic doesn't handle on the fly switches\n * of language very well. It gets a bit confused about whether resources are actually\n * loaded or not, so instead we implement our own resource loader.\n */\nclass ResourceLoader {\n /** Loaded resources by loader key */\n #loaded = new Set<string>();\n /** Resource loading promises by loader key */\n #loading = new Map<string, Promise<void>>();\n /** Loaders for each resource language */\n #loaders = new Map<string, InternalTranslationResourceLoader>();\n\n constructor(\n private readonly onLoad: (loaded: {\n language: string;\n namespace: string;\n messages: Record<string, string | null>;\n }) => void,\n ) {}\n\n addTranslationResource(resource: TranslationResource) {\n const internalResource = toInternalTranslationResource(resource);\n for (const entry of internalResource.resources) {\n const key = this.#getLoaderKey(entry.language, internalResource.id);\n\n // First loader to register wins, this means that resources registered in the app\n // have priority over default resource from translation refs\n if (!this.#loaders.has(key)) {\n this.#loaders.set(key, entry.loader);\n }\n }\n }\n\n #getLoaderKey(language: string, namespace: string) {\n return `${language}/${namespace}`;\n }\n\n needsLoading(language: string, namespace: string) {\n const key = this.#getLoaderKey(language, namespace);\n const loader = this.#loaders.get(key);\n if (!loader) {\n return false;\n }\n\n return !this.#loaded.has(key);\n }\n\n async load(language: string, namespace: string): Promise<void> {\n const key = this.#getLoaderKey(language, namespace);\n\n const loader = this.#loaders.get(key);\n if (!loader) {\n return;\n }\n\n if (this.#loaded.has(key)) {\n return;\n }\n\n const loading = this.#loading.get(key);\n if (loading) {\n await loading;\n return;\n }\n\n const load = loader().then(\n result => {\n this.onLoad({ language, namespace, messages: result.messages });\n this.#loaded.add(key);\n },\n error => {\n this.#loaded.add(key); // Do not try to load failed resources again\n throw error;\n },\n );\n this.#loading.set(key, load);\n await load;\n }\n}\n\n/**\n * A helper for implementing JSX interpolation\n */\nexport class JsxInterpolator {\n readonly #setFormatHook: (hook: FormatFunction) => void;\n readonly #marker: string;\n readonly #pattern: RegExp;\n\n static fromI18n(i18n: I18n) {\n const interpolator = i18n.services.interpolator as Interpolator & {\n format: FormatFunction;\n };\n const originalFormat = interpolator.format;\n\n let formatHook: FormatFunction | undefined;\n\n // This is the only way to override the format function of the interpolator\n // without overriding the default formatters. See the behavior here:\n // https://github.com/i18next/i18next/blob/c633121e57e2b6024080142d78027842bf2a6e5e/src/i18next.js#L120-L125\n interpolator.format = (value, format, lng, formatOpts) => {\n if (format) {\n return originalFormat(value, format, lng, formatOpts);\n }\n return formatHook?.(value, format, lng, formatOpts) ?? value;\n };\n\n return new JsxInterpolator(\n // Using a random marker to ensure it can't be misused\n Math.random().toString(36).substring(2, 8),\n hook => {\n formatHook = hook;\n },\n );\n }\n\n private constructor(\n marker: string,\n setFormatHook: (hook: FormatFunction) => void,\n ) {\n this.#setFormatHook = setFormatHook;\n this.#marker = marker;\n this.#pattern = new RegExp(`\\\\$${marker}\\\\(([^)]+)\\\\)`);\n }\n\n wrapT<TMessages extends { [key in string]: string }>(\n originalT: TFunction,\n ): TranslationFunction<TMessages> {\n return ((key, options) => {\n let elementsMap: Map<string, ReactNode> | undefined = undefined;\n\n // There's no way to override the format hook via the translation function\n // options, event though types indicate that it might be possible.\n // Instead, override the format function hook before every invocation and\n // rely on synchronous execution.\n this.#setFormatHook(value => {\n if (isValidElement(value)) {\n if (!elementsMap) {\n elementsMap = new Map();\n }\n const elementKey = elementsMap.size.toString();\n elementsMap.set(elementKey, value);\n\n return `$${this.#marker}(${elementKey})`;\n }\n return value;\n });\n\n // Overriding the return options is not allowed via TranslationFunction,\n // so this will always be a string\n const result = originalT(key, options as any) as unknown as string;\n if (!elementsMap) {\n return result;\n }\n\n const split = result.split(this.#pattern);\n\n return createElement(\n Fragment,\n null,\n ...split\n .map((part, index) => {\n if (index % 2 === 0) {\n return part;\n }\n return elementsMap?.get(part);\n })\n .filter(Boolean),\n );\n }) as TranslationFunction<TMessages>;\n }\n}\n\n/** @alpha */\nexport class I18nextTranslationApi implements TranslationApi {\n static create(options: I18nextTranslationApiOptions) {\n const { languages } = options.languageApi.getAvailableLanguages();\n\n const i18n = createI18n({\n fallbackLng: DEFAULT_LANGUAGE,\n supportedLngs: languages,\n interpolation: {\n escapeValue: false,\n // Used for the JsxInterpolator format hook\n alwaysFormat: true,\n },\n ns: [],\n defaultNS: false,\n fallbackNS: false,\n\n // Disable resource loading on init, meaning i18n will be ready to use immediately\n initImmediate: false,\n });\n\n i18n.init();\n if (!i18n.isInitialized) {\n throw new Error('i18next was unexpectedly not initialized');\n }\n\n const interpolator = JsxInterpolator.fromI18n(i18n);\n\n const { language: initialLanguage } = options.languageApi.getLanguage();\n if (initialLanguage !== DEFAULT_LANGUAGE) {\n i18n.changeLanguage(initialLanguage);\n }\n\n const loader = new ResourceLoader(loaded => {\n i18n.addResourceBundle(\n loaded.language,\n loaded.namespace,\n removeNulls(loaded.messages),\n false, // do not merge with existing translations\n true, // overwrite translations\n );\n });\n\n const resources = options?.resources || [];\n // Iterate in reverse, giving higher priority to resources registered later\n for (let i = resources.length - 1; i >= 0; i--) {\n const resource = resources[i];\n if (resource.$$type === '@backstage/TranslationResource') {\n loader.addTranslationResource(resource);\n } else if (resource.$$type === '@backstage/TranslationMessages') {\n // Overrides for default messages, created with createTranslationMessages and installed via app\n i18n.addResourceBundle(\n DEFAULT_LANGUAGE,\n resource.id,\n removeNulls(resource.messages),\n true, // merge with existing translations\n false, // do not overwrite translations\n );\n }\n }\n\n const instance = new I18nextTranslationApi(\n i18n,\n loader,\n options.languageApi.getLanguage().language,\n interpolator,\n );\n\n options.languageApi.language$().subscribe(({ language }) => {\n instance.#changeLanguage(language);\n });\n\n return instance;\n }\n\n #i18n: I18n;\n #loader: ResourceLoader;\n #language: string;\n #jsxInterpolator: JsxInterpolator;\n\n /** Keep track of which refs we have registered default resources for */\n #registeredRefs = new Set<string>();\n /** Notify observers when language changes */\n #languageChangeListeners = new Set<() => void>();\n\n private constructor(\n i18n: I18n,\n loader: ResourceLoader,\n language: string,\n jsxInterpolator: JsxInterpolator,\n ) {\n this.#i18n = i18n;\n this.#loader = loader;\n this.#language = language;\n this.#jsxInterpolator = jsxInterpolator;\n }\n\n getTranslation<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): TranslationSnapshot<TMessages> {\n const internalRef = toInternalTranslationRef(translationRef);\n\n this.#registerDefaults(internalRef);\n\n return this.#createSnapshot(internalRef);\n }\n\n translation$<TMessages extends { [key in string]: string }>(\n translationRef: TranslationRef<string, TMessages>,\n ): Observable<TranslationSnapshot<TMessages>> {\n const internalRef = toInternalTranslationRef(translationRef);\n\n this.#registerDefaults(internalRef);\n\n return new ObservableImpl<TranslationSnapshot<TMessages>>(subscriber => {\n let loadTicket = {}; // To check for stale loads\n\n const loadResource = () => {\n loadTicket = {};\n const ticket = loadTicket;\n this.#loader.load(this.#language, internalRef.id).then(\n () => {\n if (ticket === loadTicket) {\n const snapshot = this.#createSnapshot(internalRef);\n if (snapshot.ready) {\n subscriber.next(snapshot);\n }\n }\n },\n error => {\n if (ticket === loadTicket) {\n subscriber.error(Array.isArray(error) ? error[0] : error);\n }\n },\n );\n };\n\n const onChange = () => {\n const snapshot = this.#createSnapshot(internalRef);\n if (snapshot.ready) {\n subscriber.next(snapshot);\n } else {\n loadResource();\n }\n };\n\n if (this.#loader.needsLoading(this.#language, internalRef.id)) {\n loadResource();\n }\n\n this.#languageChangeListeners.add(onChange);\n return () => {\n this.#languageChangeListeners.delete(onChange);\n };\n });\n }\n\n #changeLanguage(language: string): void {\n if (this.#language !== language) {\n this.#language = language;\n this.#i18n.changeLanguage(language);\n this.#languageChangeListeners.forEach(listener => listener());\n }\n }\n\n #createSnapshot<TMessages extends { [key in string]: string }>(\n internalRef: InternalTranslationRef<string, TMessages>,\n ): TranslationSnapshot<TMessages> {\n if (this.#loader.needsLoading(this.#language, internalRef.id)) {\n return { ready: false };\n }\n\n const unwrappedT = this.#i18n.getFixedT(null, internalRef.id);\n const t = this.#jsxInterpolator.wrapT<TMessages>(unwrappedT);\n\n return {\n ready: true,\n t,\n };\n }\n\n #registerDefaults(internalRef: InternalTranslationRef): void {\n if (this.#registeredRefs.has(internalRef.id)) {\n return;\n }\n this.#registeredRefs.add(internalRef.id);\n\n const defaultMessages = internalRef.getDefaultMessages();\n this.#i18n.addResourceBundle(\n DEFAULT_LANGUAGE,\n internalRef.id,\n defaultMessages,\n true, // merge with existing translations\n false, // do not overwrite translations\n );\n\n const defaultResource = internalRef.getDefaultResource();\n if (defaultResource) {\n this.#loader.addTranslationResource(defaultResource);\n }\n }\n}\n"],"names":["createI18n"],"mappings":"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"}
|
|
@@ -9,18 +9,20 @@ const DEFAULT_PROVIDER = {
|
|
|
9
9
|
icon: () => null
|
|
10
10
|
};
|
|
11
11
|
class OAuth2 {
|
|
12
|
-
static
|
|
12
|
+
static createAuthConnector(options) {
|
|
13
|
+
if ("authConnector" in options) {
|
|
14
|
+
return options.authConnector;
|
|
15
|
+
}
|
|
13
16
|
const {
|
|
17
|
+
scopeTransform = (x) => x,
|
|
14
18
|
configApi,
|
|
15
19
|
discoveryApi,
|
|
16
20
|
environment = "development",
|
|
17
21
|
provider = DEFAULT_PROVIDER,
|
|
18
22
|
oauthRequestApi,
|
|
19
|
-
defaultScopes = [],
|
|
20
|
-
scopeTransform = (x) => x,
|
|
21
23
|
popupOptions
|
|
22
24
|
} = options;
|
|
23
|
-
|
|
25
|
+
return new DefaultAuthConnector({
|
|
24
26
|
configApi,
|
|
25
27
|
discoveryApi,
|
|
26
28
|
environment,
|
|
@@ -35,10 +37,9 @@ class OAuth2 {
|
|
|
35
37
|
providerInfo: {
|
|
36
38
|
idToken: res.providerInfo.idToken,
|
|
37
39
|
accessToken: res.providerInfo.accessToken,
|
|
38
|
-
scopes: OAuth2.normalizeScopes(
|
|
39
|
-
scopeTransform
|
|
40
|
-
|
|
41
|
-
),
|
|
40
|
+
scopes: OAuth2.normalizeScopes(res.providerInfo.scope, {
|
|
41
|
+
scopeTransform
|
|
42
|
+
}),
|
|
42
43
|
expiresAt: res.providerInfo.expiresInSeconds ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1e3) : void 0
|
|
43
44
|
}
|
|
44
45
|
};
|
|
@@ -53,6 +54,10 @@ class OAuth2 {
|
|
|
53
54
|
},
|
|
54
55
|
popupOptions
|
|
55
56
|
});
|
|
57
|
+
}
|
|
58
|
+
static create(options) {
|
|
59
|
+
const { defaultScopes = [], scopeTransform = (x) => x } = options;
|
|
60
|
+
const connector = OAuth2.createAuthConnector(options);
|
|
56
61
|
const sessionManager = new RefreshingAuthSessionManager({
|
|
57
62
|
connector,
|
|
58
63
|
defaultScopes: new Set(defaultScopes),
|
|
@@ -92,7 +97,9 @@ class OAuth2 {
|
|
|
92
97
|
return this.sessionManager.sessionState$();
|
|
93
98
|
}
|
|
94
99
|
async getAccessToken(scope, options) {
|
|
95
|
-
const normalizedScopes = OAuth2.normalizeScopes(
|
|
100
|
+
const normalizedScopes = OAuth2.normalizeScopes(scope, {
|
|
101
|
+
scopeTransform: this.scopeTransform
|
|
102
|
+
});
|
|
96
103
|
const session = await this.sessionManager.getSession({
|
|
97
104
|
...options,
|
|
98
105
|
scopes: normalizedScopes
|
|
@@ -114,12 +121,16 @@ class OAuth2 {
|
|
|
114
121
|
const session = await this.sessionManager.getSession(options);
|
|
115
122
|
return session?.profile;
|
|
116
123
|
}
|
|
117
|
-
|
|
124
|
+
/**
|
|
125
|
+
* @public
|
|
126
|
+
*/
|
|
127
|
+
static normalizeScopes(scopes, options) {
|
|
118
128
|
if (!scopes) {
|
|
119
129
|
return /* @__PURE__ */ new Set();
|
|
120
130
|
}
|
|
121
131
|
const scopeList = Array.isArray(scopes) ? scopes : scopes.split(/[\s|,]/).filter(Boolean);
|
|
122
|
-
|
|
132
|
+
const transformedScopes = options ? options.scopeTransform(scopeList) : scopeList;
|
|
133
|
+
return new Set(transformedScopes);
|
|
123
134
|
}
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OAuth2.esm.js","sources":["../../../../../../../../../../packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DefaultAuthConnector,\n PopupOptions,\n} from '../../../../lib/AuthConnector';\nimport { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';\nimport { SessionManager } from '../../../../lib/AuthSessionManager/types';\nimport {\n AuthRequestOptions,\n BackstageIdentityResponse,\n OAuthApi,\n OpenIdConnectApi,\n ProfileInfo,\n ProfileInfoApi,\n SessionState,\n SessionApi,\n BackstageIdentityApi,\n BackstageUserIdentity,\n} from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { OAuth2Session } from './types';\nimport { OAuthApiCreateOptions } from '../types';\n\n/**\n * OAuth2 create options.\n * @public\n */\nexport type OAuth2CreateOptions = OAuthApiCreateOptions & {\n scopeTransform?: (scopes: string[]) => string[];\n popupOptions?: PopupOptions;\n};\n\nexport type OAuth2Response = {\n providerInfo: {\n accessToken: string;\n idToken: string;\n scope: string;\n expiresInSeconds?: number;\n };\n profile: ProfileInfo;\n backstageIdentity: {\n token: string;\n expiresInSeconds?: number;\n identity: BackstageUserIdentity;\n };\n};\n\nconst DEFAULT_PROVIDER = {\n id: 'oauth2',\n title: 'Your Identity Provider',\n icon: () => null,\n};\n\n/**\n * Implements a generic OAuth2 flow for auth.\n *\n * @public\n */\nexport default class OAuth2\n implements\n OAuthApi,\n OpenIdConnectApi,\n ProfileInfoApi,\n BackstageIdentityApi,\n SessionApi\n{\n static create(options: OAuth2CreateOptions) {\n const {\n configApi,\n discoveryApi,\n environment = 'development',\n provider = DEFAULT_PROVIDER,\n oauthRequestApi,\n defaultScopes = [],\n scopeTransform = x => x,\n popupOptions,\n } = options;\n\n const connector = new DefaultAuthConnector({\n configApi,\n discoveryApi,\n environment,\n provider,\n oauthRequestApi: oauthRequestApi,\n sessionTransform({\n backstageIdentity,\n ...res\n }: OAuth2Response): OAuth2Session {\n const session: OAuth2Session = {\n ...res,\n providerInfo: {\n idToken: res.providerInfo.idToken,\n accessToken: res.providerInfo.accessToken,\n scopes: OAuth2.normalizeScopes(\n scopeTransform,\n res.providerInfo.scope,\n ),\n expiresAt: res.providerInfo.expiresInSeconds\n ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)\n : undefined,\n },\n };\n if (backstageIdentity) {\n session.backstageIdentity = {\n token: backstageIdentity.token,\n identity: backstageIdentity.identity,\n expiresAt: backstageIdentity.expiresInSeconds\n ? new Date(Date.now() + backstageIdentity.expiresInSeconds * 1000)\n : undefined,\n };\n }\n return session;\n },\n popupOptions,\n });\n\n const sessionManager = new RefreshingAuthSessionManager({\n connector,\n defaultScopes: new Set(defaultScopes),\n sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,\n sessionShouldRefresh: (session: OAuth2Session) => {\n // TODO(Rugvip): Optimize to use separate checks for provider vs backstage session expiration\n let min = Infinity;\n if (session.providerInfo?.expiresAt) {\n min = Math.min(\n min,\n (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000,\n );\n }\n if (session.backstageIdentity?.expiresAt) {\n min = Math.min(\n min,\n (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000,\n );\n }\n return min < 60 * 3;\n },\n });\n\n return new OAuth2({ sessionManager, scopeTransform });\n }\n\n private readonly sessionManager: SessionManager<OAuth2Session>;\n private readonly scopeTransform: (scopes: string[]) => string[];\n\n private constructor(options: {\n sessionManager: SessionManager<OAuth2Session>;\n scopeTransform: (scopes: string[]) => string[];\n }) {\n this.sessionManager = options.sessionManager;\n this.scopeTransform = options.scopeTransform;\n }\n\n async signIn() {\n await this.getAccessToken();\n }\n\n async signOut() {\n await this.sessionManager.removeSession();\n }\n\n sessionState$(): Observable<SessionState> {\n return this.sessionManager.sessionState$();\n }\n\n async getAccessToken(\n scope?: string | string[],\n options?: AuthRequestOptions,\n ) {\n const normalizedScopes = OAuth2.normalizeScopes(this.scopeTransform, scope);\n const session = await this.sessionManager.getSession({\n ...options,\n scopes: normalizedScopes,\n });\n return session?.providerInfo.accessToken ?? '';\n }\n\n async getIdToken(options: AuthRequestOptions = {}) {\n const session = await this.sessionManager.getSession({\n ...options,\n scopes: new Set(['openid']),\n });\n return session?.providerInfo.idToken ?? '';\n }\n\n async getBackstageIdentity(\n options: AuthRequestOptions = {},\n ): Promise<BackstageIdentityResponse | undefined> {\n const session = await this.sessionManager.getSession(options);\n return session?.backstageIdentity;\n }\n\n async getProfile(options: AuthRequestOptions = {}) {\n const session = await this.sessionManager.getSession(options);\n return session?.profile;\n }\n\n private static normalizeScopes(\n scopeTransform: (scopes: string[]) => string[],\n scopes?: string | string[],\n ): Set<string> {\n if (!scopes) {\n return new Set();\n }\n\n const scopeList = Array.isArray(scopes)\n ? scopes\n : scopes.split(/[\\s|,]/).filter(Boolean);\n\n return new Set(scopeTransform(scopeList));\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OAuth2.esm.js","sources":["../../../../../../../../../../packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DefaultAuthConnector } from '../../../../lib/AuthConnector';\nimport { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';\nimport { SessionManager } from '../../../../lib/AuthSessionManager/types';\nimport {\n AuthRequestOptions,\n BackstageIdentityApi,\n BackstageIdentityResponse,\n BackstageUserIdentity,\n OAuthApi,\n OpenIdConnectApi,\n ProfileInfo,\n ProfileInfoApi,\n SessionApi,\n SessionState,\n} from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport {\n OAuth2CreateOptions,\n OAuth2CreateOptionsWithAuthConnector,\n OAuth2Session,\n} from './types';\n\nconst DEFAULT_PROVIDER = {\n id: 'oauth2',\n title: 'Your Identity Provider',\n icon: () => null,\n};\n\nexport type OAuth2Response = {\n providerInfo: {\n accessToken: string;\n idToken: string;\n scope: string;\n expiresInSeconds?: number;\n };\n profile: ProfileInfo;\n backstageIdentity: {\n token: string;\n expiresInSeconds?: number;\n identity: BackstageUserIdentity;\n };\n};\n\n/**\n * Implements a generic OAuth2 flow for auth.\n *\n * @public\n */\nexport default class OAuth2\n implements\n OAuthApi,\n OpenIdConnectApi,\n ProfileInfoApi,\n BackstageIdentityApi,\n SessionApi\n{\n private static createAuthConnector(\n options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,\n ) {\n if ('authConnector' in options) {\n return options.authConnector;\n }\n const {\n scopeTransform = x => x,\n configApi,\n discoveryApi,\n environment = 'development',\n provider = DEFAULT_PROVIDER,\n oauthRequestApi,\n popupOptions,\n } = options;\n\n return new DefaultAuthConnector({\n configApi,\n discoveryApi,\n environment,\n provider,\n oauthRequestApi: oauthRequestApi,\n sessionTransform({\n backstageIdentity,\n ...res\n }: OAuth2Response): OAuth2Session {\n const session: OAuth2Session = {\n ...res,\n providerInfo: {\n idToken: res.providerInfo.idToken,\n accessToken: res.providerInfo.accessToken,\n scopes: OAuth2.normalizeScopes(res.providerInfo.scope, {\n scopeTransform,\n }),\n expiresAt: res.providerInfo.expiresInSeconds\n ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)\n : undefined,\n },\n };\n if (backstageIdentity) {\n session.backstageIdentity = {\n token: backstageIdentity.token,\n identity: backstageIdentity.identity,\n expiresAt: backstageIdentity.expiresInSeconds\n ? new Date(Date.now() + backstageIdentity.expiresInSeconds * 1000)\n : undefined,\n };\n }\n return session;\n },\n popupOptions,\n });\n }\n\n static create(\n options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,\n ) {\n const { defaultScopes = [], scopeTransform = x => x } = options;\n\n const connector = OAuth2.createAuthConnector(options);\n\n const sessionManager = new RefreshingAuthSessionManager({\n connector,\n defaultScopes: new Set(defaultScopes),\n sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes,\n sessionShouldRefresh: (session: OAuth2Session) => {\n // TODO(Rugvip): Optimize to use separate checks for provider vs backstage session expiration\n let min = Infinity;\n if (session.providerInfo?.expiresAt) {\n min = Math.min(\n min,\n (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000,\n );\n }\n if (session.backstageIdentity?.expiresAt) {\n min = Math.min(\n min,\n (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000,\n );\n }\n return min < 60 * 3;\n },\n });\n\n return new OAuth2({ sessionManager, scopeTransform });\n }\n\n private readonly sessionManager: SessionManager<OAuth2Session>;\n private readonly scopeTransform: (scopes: string[]) => string[];\n\n private constructor(options: {\n sessionManager: SessionManager<OAuth2Session>;\n scopeTransform: (scopes: string[]) => string[];\n }) {\n this.sessionManager = options.sessionManager;\n this.scopeTransform = options.scopeTransform;\n }\n\n async signIn() {\n await this.getAccessToken();\n }\n\n async signOut() {\n await this.sessionManager.removeSession();\n }\n\n sessionState$(): Observable<SessionState> {\n return this.sessionManager.sessionState$();\n }\n\n async getAccessToken(\n scope?: string | string[],\n options?: AuthRequestOptions,\n ) {\n const normalizedScopes = OAuth2.normalizeScopes(scope, {\n scopeTransform: this.scopeTransform,\n });\n const session = await this.sessionManager.getSession({\n ...options,\n scopes: normalizedScopes,\n });\n return session?.providerInfo.accessToken ?? '';\n }\n\n async getIdToken(options: AuthRequestOptions = {}) {\n const session = await this.sessionManager.getSession({\n ...options,\n scopes: new Set(['openid']),\n });\n return session?.providerInfo.idToken ?? '';\n }\n\n async getBackstageIdentity(\n options: AuthRequestOptions = {},\n ): Promise<BackstageIdentityResponse | undefined> {\n const session = await this.sessionManager.getSession(options);\n return session?.backstageIdentity;\n }\n\n async getProfile(options: AuthRequestOptions = {}) {\n const session = await this.sessionManager.getSession(options);\n return session?.profile;\n }\n\n /**\n * @public\n */\n public static normalizeScopes(\n scopes?: string | string[],\n options?: { scopeTransform: (scopes: string[]) => string[] },\n ): Set<string> {\n if (!scopes) {\n return new Set();\n }\n\n const scopeList = Array.isArray(scopes)\n ? scopes\n : scopes.split(/[\\s|,]/).filter(Boolean);\n\n const transformedScopes = options\n ? options.scopeTransform(scopeList)\n : scopeList;\n\n return new Set(transformedScopes);\n }\n}\n"],"names":[],"mappings":";;;;;AAsCA,MAAM,gBAAmB,GAAA;AAAA,EACvB,EAAI,EAAA,QAAA;AAAA,EACJ,KAAO,EAAA,wBAAA;AAAA,EACP,MAAM,MAAM;AACd,CAAA;AAsBA,MAAqB,MAOrB,CAAA;AAAA,EACE,OAAe,oBACb,OACA,EAAA;AACA,IAAA,IAAI,mBAAmB,OAAS,EAAA;AAC9B,MAAA,OAAO,OAAQ,CAAA,aAAA;AAAA;AAEjB,IAAM,MAAA;AAAA,MACJ,iBAAiB,CAAK,CAAA,KAAA,CAAA;AAAA,MACtB,SAAA;AAAA,MACA,YAAA;AAAA,MACA,WAAc,GAAA,aAAA;AAAA,MACd,QAAW,GAAA,gBAAA;AAAA,MACX,eAAA;AAAA,MACA;AAAA,KACE,GAAA,OAAA;AAEJ,IAAA,OAAO,IAAI,oBAAqB,CAAA;AAAA,MAC9B,SAAA;AAAA,MACA,YAAA;AAAA,MACA,WAAA;AAAA,MACA,QAAA;AAAA,MACA,eAAA;AAAA,MACA,gBAAiB,CAAA;AAAA,QACf,iBAAA;AAAA,QACA,GAAG;AAAA,OAC6B,EAAA;AAChC,QAAA,MAAM,OAAyB,GAAA;AAAA,UAC7B,GAAG,GAAA;AAAA,UACH,YAAc,EAAA;AAAA,YACZ,OAAA,EAAS,IAAI,YAAa,CAAA,OAAA;AAAA,YAC1B,WAAA,EAAa,IAAI,YAAa,CAAA,WAAA;AAAA,YAC9B,MAAQ,EAAA,MAAA,CAAO,eAAgB,CAAA,GAAA,CAAI,aAAa,KAAO,EAAA;AAAA,cACrD;AAAA,aACD,CAAA;AAAA,YACD,SAAW,EAAA,GAAA,CAAI,YAAa,CAAA,gBAAA,GACxB,IAAI,IAAA,CAAK,IAAK,CAAA,GAAA,EAAQ,GAAA,GAAA,CAAI,YAAa,CAAA,gBAAA,GAAmB,GAAI,CAC9D,GAAA,KAAA;AAAA;AACN,SACF;AACA,QAAA,IAAI,iBAAmB,EAAA;AACrB,UAAA,OAAA,CAAQ,iBAAoB,GAAA;AAAA,YAC1B,OAAO,iBAAkB,CAAA,KAAA;AAAA,YACzB,UAAU,iBAAkB,CAAA,QAAA;AAAA,YAC5B,SAAA,EAAW,iBAAkB,CAAA,gBAAA,GACzB,IAAI,IAAA,CAAK,IAAK,CAAA,GAAA,EAAQ,GAAA,iBAAA,CAAkB,gBAAmB,GAAA,GAAI,CAC/D,GAAA,KAAA;AAAA,WACN;AAAA;AAEF,QAAO,OAAA,OAAA;AAAA,OACT;AAAA,MACA;AAAA,KACD,CAAA;AAAA;AACH,EAEA,OAAO,OACL,OACA,EAAA;AACA,IAAA,MAAM,EAAE,aAAgB,GAAA,IAAI,cAAiB,GAAA,CAAA,CAAA,KAAK,GAAM,GAAA,OAAA;AAExD,IAAM,MAAA,SAAA,GAAY,MAAO,CAAA,mBAAA,CAAoB,OAAO,CAAA;AAEpD,IAAM,MAAA,cAAA,GAAiB,IAAI,4BAA6B,CAAA;AAAA,MACtD,SAAA;AAAA,MACA,aAAA,EAAe,IAAI,GAAA,CAAI,aAAa,CAAA;AAAA,MACpC,aAAe,EAAA,CAAC,OAA2B,KAAA,OAAA,CAAQ,YAAa,CAAA,MAAA;AAAA,MAChE,oBAAA,EAAsB,CAAC,OAA2B,KAAA;AAEhD,QAAA,IAAI,GAAM,GAAA,QAAA;AACV,QAAI,IAAA,OAAA,CAAQ,cAAc,SAAW,EAAA;AACnC,UAAA,GAAA,GAAM,IAAK,CAAA,GAAA;AAAA,YACT,GAAA;AAAA,YAAA,CACC,QAAQ,YAAa,CAAA,SAAA,CAAU,SAAY,GAAA,IAAA,CAAK,KAAS,IAAA;AAAA,WAC5D;AAAA;AAEF,QAAI,IAAA,OAAA,CAAQ,mBAAmB,SAAW,EAAA;AACxC,UAAA,GAAA,GAAM,IAAK,CAAA,GAAA;AAAA,YACT,GAAA;AAAA,YAAA,CACC,QAAQ,iBAAkB,CAAA,SAAA,CAAU,SAAY,GAAA,IAAA,CAAK,KAAS,IAAA;AAAA,WACjE;AAAA;AAEF,QAAA,OAAO,MAAM,EAAK,GAAA,CAAA;AAAA;AACpB,KACD,CAAA;AAED,IAAA,OAAO,IAAI,MAAA,CAAO,EAAE,cAAA,EAAgB,gBAAgB,CAAA;AAAA;AACtD,EAEiB,cAAA;AAAA,EACA,cAAA;AAAA,EAET,YAAY,OAGjB,EAAA;AACD,IAAA,IAAA,CAAK,iBAAiB,OAAQ,CAAA,cAAA;AAC9B,IAAA,IAAA,CAAK,iBAAiB,OAAQ,CAAA,cAAA;AAAA;AAChC,EAEA,MAAM,MAAS,GAAA;AACb,IAAA,MAAM,KAAK,cAAe,EAAA;AAAA;AAC5B,EAEA,MAAM,OAAU,GAAA;AACd,IAAM,MAAA,IAAA,CAAK,eAAe,aAAc,EAAA;AAAA;AAC1C,EAEA,aAA0C,GAAA;AACxC,IAAO,OAAA,IAAA,CAAK,eAAe,aAAc,EAAA;AAAA;AAC3C,EAEA,MAAM,cACJ,CAAA,KAAA,EACA,OACA,EAAA;AACA,IAAM,MAAA,gBAAA,GAAmB,MAAO,CAAA,eAAA,CAAgB,KAAO,EAAA;AAAA,MACrD,gBAAgB,IAAK,CAAA;AAAA,KACtB,CAAA;AACD,IAAA,MAAM,OAAU,GAAA,MAAM,IAAK,CAAA,cAAA,CAAe,UAAW,CAAA;AAAA,MACnD,GAAG,OAAA;AAAA,MACH,MAAQ,EAAA;AAAA,KACT,CAAA;AACD,IAAO,OAAA,OAAA,EAAS,aAAa,WAAe,IAAA,EAAA;AAAA;AAC9C,EAEA,MAAM,UAAA,CAAW,OAA8B,GAAA,EAAI,EAAA;AACjD,IAAA,MAAM,OAAU,GAAA,MAAM,IAAK,CAAA,cAAA,CAAe,UAAW,CAAA;AAAA,MACnD,GAAG,OAAA;AAAA,MACH,MAAQ,kBAAA,IAAI,GAAI,CAAA,CAAC,QAAQ,CAAC;AAAA,KAC3B,CAAA;AACD,IAAO,OAAA,OAAA,EAAS,aAAa,OAAW,IAAA,EAAA;AAAA;AAC1C,EAEA,MAAM,oBAAA,CACJ,OAA8B,GAAA,EACkB,EAAA;AAChD,IAAA,MAAM,OAAU,GAAA,MAAM,IAAK,CAAA,cAAA,CAAe,WAAW,OAAO,CAAA;AAC5D,IAAA,OAAO,OAAS,EAAA,iBAAA;AAAA;AAClB,EAEA,MAAM,UAAA,CAAW,OAA8B,GAAA,EAAI,EAAA;AACjD,IAAA,MAAM,OAAU,GAAA,MAAM,IAAK,CAAA,cAAA,CAAe,WAAW,OAAO,CAAA;AAC5D,IAAA,OAAO,OAAS,EAAA,OAAA;AAAA;AAClB;AAAA;AAAA;AAAA,EAKA,OAAc,eACZ,CAAA,MAAA,EACA,OACa,EAAA;AACb,IAAA,IAAI,CAAC,MAAQ,EAAA;AACX,MAAA,2BAAW,GAAI,EAAA;AAAA;AAGjB,IAAM,MAAA,SAAA,GAAY,KAAM,CAAA,OAAA,CAAQ,MAAM,CAAA,GAClC,MACA,GAAA,MAAA,CAAO,KAAM,CAAA,QAAQ,CAAE,CAAA,MAAA,CAAO,OAAO,CAAA;AAEzC,IAAA,MAAM,iBAAoB,GAAA,OAAA,GACtB,OAAQ,CAAA,cAAA,CAAe,SAAS,CAChC,GAAA,SAAA;AAEJ,IAAO,OAAA,IAAI,IAAI,iBAAiB,CAAA;AAAA;AAEpC;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openLoginPopup } from '../loginPopup.esm.js';
|
|
2
2
|
|
|
3
3
|
let warned = false;
|
|
4
4
|
function defaultJoinScopes(scopes) {
|
|
@@ -56,11 +56,11 @@ class DefaultAuthConnector {
|
|
|
56
56
|
}
|
|
57
57
|
return this.authRequester(options.scopes);
|
|
58
58
|
}
|
|
59
|
-
async refreshSession(
|
|
59
|
+
async refreshSession(options) {
|
|
60
60
|
const res = await fetch(
|
|
61
61
|
await this.buildUrl("/refresh", {
|
|
62
62
|
optional: true,
|
|
63
|
-
...
|
|
63
|
+
...options && { scope: this.joinScopesFunc(options.scopes) }
|
|
64
64
|
}),
|
|
65
65
|
{
|
|
66
66
|
headers: {
|
|
@@ -113,10 +113,9 @@ class DefaultAuthConnector {
|
|
|
113
113
|
});
|
|
114
114
|
const width = this.popupOptions?.size?.fullscreen ? window.screen.width : this.popupOptions?.size?.width || 450;
|
|
115
115
|
const height = this.popupOptions?.size?.fullscreen ? window.screen.height : this.popupOptions?.size?.height || 730;
|
|
116
|
-
const payload = await
|
|
116
|
+
const payload = await openLoginPopup({
|
|
117
117
|
url: popupUrl,
|
|
118
118
|
name: `${this.provider.title} Login`,
|
|
119
|
-
origin: new URL(popupUrl).origin,
|
|
120
119
|
width,
|
|
121
120
|
height
|
|
122
121
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultAuthConnector.esm.js","sources":["../../../../../../../../packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n AuthProviderInfo,\n ConfigApi,\n DiscoveryApi,\n OAuthRequestApi,\n OAuthRequester,\n} from '@backstage/core-plugin-api';\nimport { showLoginPopup } from '../loginPopup';\nimport { AuthConnector, CreateSessionOptions, PopupOptions } from './types';\n\nlet warned = false;\n\ntype Options<AuthSession> = {\n /**\n * DiscoveryApi instance used to locate the auth backend endpoint.\n */\n discoveryApi: DiscoveryApi;\n /**\n * Environment hint passed on to auth backend, for example 'production' or 'development'\n */\n environment: string;\n /**\n * Information about the auth provider to be shown to the user.\n * The ID Must match the backend auth plugin configuration, for example 'google'.\n */\n provider: AuthProviderInfo;\n /**\n * API used to instantiate an auth requester.\n */\n oauthRequestApi: OAuthRequestApi;\n /**\n * Function used to join together a set of scopes, defaults to joining with a space character.\n */\n joinScopes?: (scopes: Set<string>) => string;\n /**\n * Function used to transform an auth response into the session type.\n */\n sessionTransform?(response: any): AuthSession | Promise<AuthSession>;\n /**\n * ConfigApi instance used to configure authentication flow of pop-up or redirect.\n */\n configApi?: ConfigApi;\n /**\n * Options used to configure auth popup\n */\n popupOptions?: PopupOptions;\n};\n\nfunction defaultJoinScopes(scopes: Set<string>) {\n return [...scopes].join(' ');\n}\n\n/**\n * DefaultAuthConnector is the default auth connector in Backstage. It talks to the\n * backend auth plugin through the standardized API, and requests user permission\n * via the OAuthRequestApi.\n */\nexport class DefaultAuthConnector<AuthSession>\n implements AuthConnector<AuthSession>\n{\n private readonly discoveryApi: DiscoveryApi;\n private readonly environment: string;\n private readonly provider: AuthProviderInfo;\n private readonly joinScopesFunc: (scopes: Set<string>) => string;\n private readonly authRequester: OAuthRequester<AuthSession>;\n private readonly sessionTransform: (response: any) => Promise<AuthSession>;\n private readonly enableExperimentalRedirectFlow: boolean;\n private readonly popupOptions: PopupOptions | undefined;\n constructor(options: Options<AuthSession>) {\n const {\n configApi,\n discoveryApi,\n environment,\n provider,\n joinScopes = defaultJoinScopes,\n oauthRequestApi,\n sessionTransform = id => id,\n popupOptions,\n } = options;\n\n if (!warned && !configApi) {\n // eslint-disable-next-line no-console\n console.warn(\n 'DEPRECATION WARNING: Authentication providers require a configApi instance to configure the authentication flow. Please provide one to the authentication provider constructor.',\n );\n warned = true;\n }\n\n this.enableExperimentalRedirectFlow = configApi\n ? configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false\n : false;\n\n this.authRequester = oauthRequestApi.createAuthRequester({\n provider,\n onAuthRequest: async scopes => {\n if (!this.enableExperimentalRedirectFlow) {\n return this.showPopup(scopes);\n }\n return this.executeRedirect(scopes);\n },\n });\n\n this.discoveryApi = discoveryApi;\n this.environment = environment;\n this.provider = provider;\n this.joinScopesFunc = joinScopes;\n this.sessionTransform = sessionTransform;\n this.popupOptions = popupOptions;\n }\n\n async createSession(options: CreateSessionOptions): Promise<AuthSession> {\n if (options.instantPopup) {\n if (this.enableExperimentalRedirectFlow) {\n return this.executeRedirect(options.scopes);\n }\n return this.showPopup(options.scopes);\n }\n return this.authRequester(options.scopes);\n }\n\n async refreshSession(scopes?: Set<string>): Promise<any> {\n const res = await fetch(\n await this.buildUrl('/refresh', {\n optional: true,\n ...(scopes && { scope: this.joinScopesFunc(scopes) }),\n }),\n {\n headers: {\n 'x-requested-with': 'XMLHttpRequest',\n },\n credentials: 'include',\n },\n ).catch(error => {\n throw new Error(`Auth refresh request failed, ${error}`);\n });\n\n if (!res.ok) {\n const error: any = new Error(\n `Auth refresh request failed, ${res.statusText}`,\n );\n error.status = res.status;\n throw error;\n }\n\n const authInfo = await res.json();\n\n if (authInfo.error) {\n const error = new Error(authInfo.error.message);\n if (authInfo.error.name) {\n error.name = authInfo.error.name;\n }\n throw error;\n }\n return await this.sessionTransform(authInfo);\n }\n\n async removeSession(): Promise<void> {\n const res = await fetch(await this.buildUrl('/logout'), {\n method: 'POST',\n headers: {\n 'x-requested-with': 'XMLHttpRequest',\n },\n credentials: 'include',\n }).catch(error => {\n throw new Error(`Logout request failed, ${error}`);\n });\n\n if (!res.ok) {\n const error: any = new Error(`Logout request failed, ${res.statusText}`);\n error.status = res.status;\n throw error;\n }\n }\n\n private async showPopup(scopes: Set<string>): Promise<AuthSession> {\n const scope = this.joinScopesFunc(scopes);\n const popupUrl = await this.buildUrl('/start', {\n scope,\n origin: window.location.origin,\n flow: 'popup',\n });\n\n const width = this.popupOptions?.size?.fullscreen\n ? window.screen.width\n : this.popupOptions?.size?.width || 450;\n\n const height = this.popupOptions?.size?.fullscreen\n ? window.screen.height\n : this.popupOptions?.size?.height || 730;\n\n const payload = await showLoginPopup({\n url: popupUrl,\n name: `${this.provider.title} Login`,\n origin: new URL(popupUrl).origin,\n width,\n height,\n });\n\n return await this.sessionTransform(payload);\n }\n\n private async executeRedirect(scopes: Set<string>): Promise<AuthSession> {\n const scope = this.joinScopesFunc(scopes);\n // redirect to auth api\n window.location.href = await this.buildUrl('/start', {\n scope,\n origin: window.location.origin,\n redirectUrl: window.location.href,\n flow: 'redirect',\n });\n // return a promise that never resolves\n return new Promise(() => {});\n }\n\n private async buildUrl(\n path: string,\n query?: { [key: string]: string | boolean | undefined },\n ): Promise<string> {\n const baseUrl = await this.discoveryApi.getBaseUrl('auth');\n const queryString = this.buildQueryString({\n ...query,\n env: this.environment,\n });\n\n return `${baseUrl}/${this.provider.id}${path}${queryString}`;\n }\n\n private buildQueryString(query?: {\n [key: string]: string | boolean | undefined;\n }): string {\n if (!query) {\n return '';\n }\n\n const queryString = Object.entries<string | boolean | undefined>(query)\n .map(([key, value]) => {\n if (typeof value === 'string') {\n return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;\n } else if (value) {\n return encodeURIComponent(key);\n }\n return undefined;\n })\n .filter(Boolean)\n .join('&');\n\n if (!queryString) {\n return '';\n }\n return `?${queryString}`;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DefaultAuthConnector.esm.js","sources":["../../../../../../../../packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n AuthProviderInfo,\n ConfigApi,\n DiscoveryApi,\n OAuthRequestApi,\n OAuthRequester,\n} from '@backstage/core-plugin-api';\nimport { openLoginPopup } from '../loginPopup';\nimport {\n AuthConnector,\n AuthConnectorCreateSessionOptions,\n PopupOptions,\n AuthConnectorRefreshSessionOptions,\n} from './types';\n\nlet warned = false;\n\ntype Options<AuthSession> = {\n /**\n * DiscoveryApi instance used to locate the auth backend endpoint.\n */\n discoveryApi: DiscoveryApi;\n /**\n * Environment hint passed on to auth backend, for example 'production' or 'development'\n */\n environment: string;\n /**\n * Information about the auth provider to be shown to the user.\n * The ID Must match the backend auth plugin configuration, for example 'google'.\n */\n provider: AuthProviderInfo;\n /**\n * API used to instantiate an auth requester.\n */\n oauthRequestApi: OAuthRequestApi;\n /**\n * Function used to join together a set of scopes, defaults to joining with a space character.\n */\n joinScopes?: (scopes: Set<string>) => string;\n /**\n * Function used to transform an auth response into the session type.\n */\n sessionTransform?(response: any): AuthSession | Promise<AuthSession>;\n /**\n * ConfigApi instance used to configure authentication flow of pop-up or redirect.\n */\n configApi?: ConfigApi;\n /**\n * Options used to configure auth popup\n */\n popupOptions?: PopupOptions;\n};\n\nfunction defaultJoinScopes(scopes: Set<string>) {\n return [...scopes].join(' ');\n}\n\n/**\n * DefaultAuthConnector is the default auth connector in Backstage. It talks to the\n * backend auth plugin through the standardized API, and requests user permission\n * via the OAuthRequestApi.\n */\nexport class DefaultAuthConnector<AuthSession>\n implements AuthConnector<AuthSession>\n{\n private readonly discoveryApi: DiscoveryApi;\n private readonly environment: string;\n private readonly provider: AuthProviderInfo;\n private readonly joinScopesFunc: (scopes: Set<string>) => string;\n private readonly authRequester: OAuthRequester<AuthSession>;\n private readonly sessionTransform: (response: any) => Promise<AuthSession>;\n private readonly enableExperimentalRedirectFlow: boolean;\n private readonly popupOptions: PopupOptions | undefined;\n constructor(options: Options<AuthSession>) {\n const {\n configApi,\n discoveryApi,\n environment,\n provider,\n joinScopes = defaultJoinScopes,\n oauthRequestApi,\n sessionTransform = id => id,\n popupOptions,\n } = options;\n\n if (!warned && !configApi) {\n // eslint-disable-next-line no-console\n console.warn(\n 'DEPRECATION WARNING: Authentication providers require a configApi instance to configure the authentication flow. Please provide one to the authentication provider constructor.',\n );\n warned = true;\n }\n\n this.enableExperimentalRedirectFlow = configApi\n ? configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false\n : false;\n\n this.authRequester = oauthRequestApi.createAuthRequester({\n provider,\n onAuthRequest: async scopes => {\n if (!this.enableExperimentalRedirectFlow) {\n return this.showPopup(scopes);\n }\n return this.executeRedirect(scopes);\n },\n });\n\n this.discoveryApi = discoveryApi;\n this.environment = environment;\n this.provider = provider;\n this.joinScopesFunc = joinScopes;\n this.sessionTransform = sessionTransform;\n this.popupOptions = popupOptions;\n }\n\n async createSession(\n options: AuthConnectorCreateSessionOptions,\n ): Promise<AuthSession> {\n if (options.instantPopup) {\n if (this.enableExperimentalRedirectFlow) {\n return this.executeRedirect(options.scopes);\n }\n return this.showPopup(options.scopes);\n }\n return this.authRequester(options.scopes);\n }\n\n async refreshSession(\n options?: AuthConnectorRefreshSessionOptions,\n ): Promise<any> {\n const res = await fetch(\n await this.buildUrl('/refresh', {\n optional: true,\n ...(options && { scope: this.joinScopesFunc(options.scopes) }),\n }),\n {\n headers: {\n 'x-requested-with': 'XMLHttpRequest',\n },\n credentials: 'include',\n },\n ).catch(error => {\n throw new Error(`Auth refresh request failed, ${error}`);\n });\n\n if (!res.ok) {\n const error: any = new Error(\n `Auth refresh request failed, ${res.statusText}`,\n );\n error.status = res.status;\n throw error;\n }\n\n const authInfo = await res.json();\n\n if (authInfo.error) {\n const error = new Error(authInfo.error.message);\n if (authInfo.error.name) {\n error.name = authInfo.error.name;\n }\n throw error;\n }\n return await this.sessionTransform(authInfo);\n }\n\n async removeSession(): Promise<void> {\n const res = await fetch(await this.buildUrl('/logout'), {\n method: 'POST',\n headers: {\n 'x-requested-with': 'XMLHttpRequest',\n },\n credentials: 'include',\n }).catch(error => {\n throw new Error(`Logout request failed, ${error}`);\n });\n\n if (!res.ok) {\n const error: any = new Error(`Logout request failed, ${res.statusText}`);\n error.status = res.status;\n throw error;\n }\n }\n\n private async showPopup(scopes: Set<string>): Promise<AuthSession> {\n const scope = this.joinScopesFunc(scopes);\n const popupUrl = await this.buildUrl('/start', {\n scope,\n origin: window.location.origin,\n flow: 'popup',\n });\n\n const width = this.popupOptions?.size?.fullscreen\n ? window.screen.width\n : this.popupOptions?.size?.width || 450;\n\n const height = this.popupOptions?.size?.fullscreen\n ? window.screen.height\n : this.popupOptions?.size?.height || 730;\n\n const payload = await openLoginPopup({\n url: popupUrl,\n name: `${this.provider.title} Login`,\n width,\n height,\n });\n\n return await this.sessionTransform(payload);\n }\n\n private async executeRedirect(scopes: Set<string>): Promise<AuthSession> {\n const scope = this.joinScopesFunc(scopes);\n // redirect to auth api\n window.location.href = await this.buildUrl('/start', {\n scope,\n origin: window.location.origin,\n redirectUrl: window.location.href,\n flow: 'redirect',\n });\n // return a promise that never resolves\n return new Promise(() => {});\n }\n\n private async buildUrl(\n path: string,\n query?: { [key: string]: string | boolean | undefined },\n ): Promise<string> {\n const baseUrl = await this.discoveryApi.getBaseUrl('auth');\n const queryString = this.buildQueryString({\n ...query,\n env: this.environment,\n });\n\n return `${baseUrl}/${this.provider.id}${path}${queryString}`;\n }\n\n private buildQueryString(query?: {\n [key: string]: string | boolean | undefined;\n }): string {\n if (!query) {\n return '';\n }\n\n const queryString = Object.entries<string | boolean | undefined>(query)\n .map(([key, value]) => {\n if (typeof value === 'string') {\n return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;\n } else if (value) {\n return encodeURIComponent(key);\n }\n return undefined;\n })\n .filter(Boolean)\n .join('&');\n\n if (!queryString) {\n return '';\n }\n return `?${queryString}`;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.esm.js
CHANGED
|
@@ -74,9 +74,9 @@ class RefreshingAuthSessionManager {
|
|
|
74
74
|
if (this.refreshPromise) {
|
|
75
75
|
return this.refreshPromise;
|
|
76
76
|
}
|
|
77
|
-
this.refreshPromise = this.connector.refreshSession(
|
|
78
|
-
this.helper.getExtendedScope(this.currentSession, scopes)
|
|
79
|
-
);
|
|
77
|
+
this.refreshPromise = this.connector.refreshSession({
|
|
78
|
+
scopes: this.helper.getExtendedScope(this.currentSession, scopes)
|
|
79
|
+
});
|
|
80
80
|
try {
|
|
81
81
|
const session = await this.refreshPromise;
|
|
82
82
|
if (!this.helper.sessionExistsAndHasScope(session, scopes)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RefreshingAuthSessionManager.esm.js","sources":["../../../../../../../../packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n SessionManager,\n SessionScopesFunc,\n SessionShouldRefreshFunc,\n GetSessionOptions,\n} from './types';\nimport { AuthConnector } from '../AuthConnector';\nimport { SessionScopeHelper, hasScopes } from './common';\nimport { SessionStateTracker } from './SessionStateTracker';\n\ntype Options<T> = {\n /** The connector used for acting on the auth session */\n connector: AuthConnector<T>;\n /** Used to get the scope of the session */\n sessionScopes: SessionScopesFunc<T>;\n /** Used to check if the session needs to be refreshed */\n sessionShouldRefresh: SessionShouldRefreshFunc<T>;\n /** The default scopes that should always be present in a session, defaults to none. */\n defaultScopes?: Set<string>;\n};\n\n/**\n * RefreshingAuthSessionManager manages an underlying session that has\n * and expiration time and needs to be refreshed periodically.\n */\nexport class RefreshingAuthSessionManager<T> implements SessionManager<T> {\n private readonly connector: AuthConnector<T>;\n private readonly helper: SessionScopeHelper<T>;\n private readonly sessionScopesFunc: SessionScopesFunc<T>;\n private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;\n private readonly stateTracker = new SessionStateTracker();\n\n private refreshPromise?: Promise<T>;\n private currentSession: T | undefined;\n\n constructor(options: Options<T>) {\n const {\n connector,\n defaultScopes = new Set(),\n sessionScopes,\n sessionShouldRefresh,\n } = options;\n\n this.connector = connector;\n this.sessionScopesFunc = sessionScopes;\n this.sessionShouldRefreshFunc = sessionShouldRefresh;\n this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });\n }\n\n async getSession(options: GetSessionOptions): Promise<T | undefined> {\n if (\n this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)\n ) {\n const shouldRefresh = this.sessionShouldRefreshFunc(this.currentSession!);\n if (!shouldRefresh) {\n return this.currentSession!;\n }\n\n try {\n const refreshedSession = await this.collapsedSessionRefresh(\n options.scopes,\n );\n const currentScopes = this.sessionScopesFunc(this.currentSession!);\n const refreshedScopes = this.sessionScopesFunc(refreshedSession);\n if (hasScopes(refreshedScopes, currentScopes)) {\n this.currentSession = refreshedSession;\n }\n return refreshedSession;\n } catch (error) {\n if (options.optional) {\n return undefined;\n }\n throw error;\n }\n }\n\n // The user may still have a valid refresh token in their cookies. Attempt to\n // initiate a fresh session through the backend using that refresh token.\n //\n // We skip this check if an instant login popup is requested, as we need to\n // stay in a synchronous call stack from the user interaction. The downside\n // is that the user will sometimes be requested to log in even if they\n // already had an existing session.\n if (!options.instantPopup) {\n try {\n const newSession = await this.collapsedSessionRefresh(options.scopes);\n this.currentSession = newSession;\n // The session might not have the scopes requested so go back and check again\n return this.getSession(options);\n } catch {\n // If the refresh attempt fails we assume we don't have a session, so continue to create one.\n }\n }\n\n // If we continue here we will show a popup, so exit if this is an optional session request.\n if (options.optional) {\n return undefined;\n }\n\n // We can call authRequester multiple times, the returned session will contain all requested scopes.\n this.currentSession = await this.connector.createSession({\n ...options,\n scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),\n });\n this.stateTracker.setIsSignedIn(true);\n return this.currentSession;\n }\n\n async removeSession() {\n this.currentSession = undefined;\n await this.connector.removeSession();\n this.stateTracker.setIsSignedIn(false);\n }\n\n sessionState$() {\n return this.stateTracker.sessionState$();\n }\n\n private async collapsedSessionRefresh(scopes?: Set<string>): Promise<T> {\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.connector.refreshSession(\n this.helper.getExtendedScope(this.currentSession, scopes),\n );\n\n try {\n const session = await this.refreshPromise;\n if (!this.helper.sessionExistsAndHasScope(session, scopes)) {\n throw new Error(\n 'Refreshed session did not receive the required scopes',\n );\n }\n this.stateTracker.setIsSignedIn(true);\n return session;\n } finally {\n delete this.refreshPromise;\n }\n }\n}\n"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"RefreshingAuthSessionManager.esm.js","sources":["../../../../../../../../packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n SessionManager,\n SessionScopesFunc,\n SessionShouldRefreshFunc,\n GetSessionOptions,\n} from './types';\nimport { AuthConnector } from '../AuthConnector';\nimport { SessionScopeHelper, hasScopes } from './common';\nimport { SessionStateTracker } from './SessionStateTracker';\n\ntype Options<T> = {\n /** The connector used for acting on the auth session */\n connector: AuthConnector<T>;\n /** Used to get the scope of the session */\n sessionScopes: SessionScopesFunc<T>;\n /** Used to check if the session needs to be refreshed */\n sessionShouldRefresh: SessionShouldRefreshFunc<T>;\n /** The default scopes that should always be present in a session, defaults to none. */\n defaultScopes?: Set<string>;\n};\n\n/**\n * RefreshingAuthSessionManager manages an underlying session that has\n * and expiration time and needs to be refreshed periodically.\n */\nexport class RefreshingAuthSessionManager<T> implements SessionManager<T> {\n private readonly connector: AuthConnector<T>;\n private readonly helper: SessionScopeHelper<T>;\n private readonly sessionScopesFunc: SessionScopesFunc<T>;\n private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;\n private readonly stateTracker = new SessionStateTracker();\n\n private refreshPromise?: Promise<T>;\n private currentSession: T | undefined;\n\n constructor(options: Options<T>) {\n const {\n connector,\n defaultScopes = new Set(),\n sessionScopes,\n sessionShouldRefresh,\n } = options;\n\n this.connector = connector;\n this.sessionScopesFunc = sessionScopes;\n this.sessionShouldRefreshFunc = sessionShouldRefresh;\n this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });\n }\n\n async getSession(options: GetSessionOptions): Promise<T | undefined> {\n if (\n this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)\n ) {\n const shouldRefresh = this.sessionShouldRefreshFunc(this.currentSession!);\n if (!shouldRefresh) {\n return this.currentSession!;\n }\n\n try {\n const refreshedSession = await this.collapsedSessionRefresh(\n options.scopes,\n );\n const currentScopes = this.sessionScopesFunc(this.currentSession!);\n const refreshedScopes = this.sessionScopesFunc(refreshedSession);\n if (hasScopes(refreshedScopes, currentScopes)) {\n this.currentSession = refreshedSession;\n }\n return refreshedSession;\n } catch (error) {\n if (options.optional) {\n return undefined;\n }\n throw error;\n }\n }\n\n // The user may still have a valid refresh token in their cookies. Attempt to\n // initiate a fresh session through the backend using that refresh token.\n //\n // We skip this check if an instant login popup is requested, as we need to\n // stay in a synchronous call stack from the user interaction. The downside\n // is that the user will sometimes be requested to log in even if they\n // already had an existing session.\n if (!options.instantPopup) {\n try {\n const newSession = await this.collapsedSessionRefresh(options.scopes);\n this.currentSession = newSession;\n // The session might not have the scopes requested so go back and check again\n return this.getSession(options);\n } catch {\n // If the refresh attempt fails we assume we don't have a session, so continue to create one.\n }\n }\n\n // If we continue here we will show a popup, so exit if this is an optional session request.\n if (options.optional) {\n return undefined;\n }\n\n // We can call authRequester multiple times, the returned session will contain all requested scopes.\n this.currentSession = await this.connector.createSession({\n ...options,\n scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),\n });\n this.stateTracker.setIsSignedIn(true);\n return this.currentSession;\n }\n\n async removeSession() {\n this.currentSession = undefined;\n await this.connector.removeSession();\n this.stateTracker.setIsSignedIn(false);\n }\n\n sessionState$() {\n return this.stateTracker.sessionState$();\n }\n\n private async collapsedSessionRefresh(scopes?: Set<string>): Promise<T> {\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.connector.refreshSession({\n scopes: this.helper.getExtendedScope(this.currentSession, scopes),\n });\n\n try {\n const session = await this.refreshPromise;\n if (!this.helper.sessionExistsAndHasScope(session, scopes)) {\n throw new Error(\n 'Refreshed session did not receive the required scopes',\n );\n }\n this.stateTracker.setIsSignedIn(true);\n return session;\n } finally {\n delete this.refreshPromise;\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
function
|
|
1
|
+
function openLoginPopup(options) {
|
|
2
2
|
return new Promise((resolve, reject) => {
|
|
3
3
|
const width = options.width || 500;
|
|
4
4
|
const height = options.height || 700;
|
|
5
5
|
const left = window.screen.width / 2 - width / 2;
|
|
6
6
|
const top = window.screen.height / 2 - height / 2;
|
|
7
|
+
const origin = new URL(options.url).origin;
|
|
7
8
|
const popup = window.open(
|
|
8
9
|
options.url,
|
|
9
10
|
options.name,
|
|
@@ -20,7 +21,7 @@ function showLoginPopup(options) {
|
|
|
20
21
|
if (event.source !== popup) {
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
|
-
if (event.origin !==
|
|
24
|
+
if (event.origin !== origin) {
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
const { data } = event;
|
|
@@ -58,5 +59,5 @@ function showLoginPopup(options) {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export {
|
|
62
|
+
export { openLoginPopup };
|
|
62
63
|
//# sourceMappingURL=loginPopup.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loginPopup.esm.js","sources":["../../../../../../../packages/core-app-api/src/lib/loginPopup.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Options used to open a login popup.\n */\nexport type
|
|
1
|
+
{"version":3,"file":"loginPopup.esm.js","sources":["../../../../../../../packages/core-app-api/src/lib/loginPopup.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Options used to open a login popup.\n *\n * @public\n */\nexport type OpenLoginPopupOptions = {\n /**\n * The URL that the auth popup should point to\n */\n url: string;\n\n /**\n * The name of the popup, as in second argument to window.open\n */\n name: string;\n\n /**\n * The width of the popup in pixels, defaults to 500\n */\n width?: number;\n\n /**\n * The height of the popup in pixels, defaults to 700\n */\n height?: number;\n};\n\ntype AuthResult =\n | {\n type: 'authorization_response';\n response: unknown;\n }\n | {\n type: 'authorization_response';\n error: {\n name: string;\n message: string;\n };\n };\n\n/**\n * Show a popup pointing to a URL that starts an auth flow. Implementing the receiving\n * end of the postMessage mechanism outlined in https://tools.ietf.org/html/draft-sakimura-oauth-wmrm-00\n *\n * The redirect handler of the flow should use postMessage to communicate back\n * to the app window. The message posted to the app must match the AuthResult type.\n *\n * The returned promise resolves to the response of the message that was posted from the auth popup.\n *\n * @public\n */\nexport function openLoginPopup(\n options: OpenLoginPopupOptions,\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const width = options.width || 500;\n const height = options.height || 700;\n const left = window.screen.width / 2 - width / 2;\n const top = window.screen.height / 2 - height / 2;\n\n const origin = new URL(options.url).origin;\n\n const popup = window.open(\n options.url,\n options.name,\n `menubar=no,location=no,resizable=no,scrollbars=no,status=no,width=${width},height=${height},top=${top},left=${left}`,\n );\n\n let targetOrigin = '';\n\n if (!popup || typeof popup.closed === 'undefined' || popup.closed) {\n const error = new Error('Failed to open auth popup.');\n error.name = 'PopupRejectedError';\n reject(error);\n return;\n }\n\n const messageListener = (event: MessageEvent) => {\n if (event.source !== popup) {\n return;\n }\n if (event.origin !== origin) {\n return;\n }\n const { data } = event;\n\n if (data.type === 'config_info') {\n targetOrigin = data.targetOrigin;\n return;\n }\n\n if (data.type !== 'authorization_response') {\n return;\n }\n const authResult = data as AuthResult;\n\n if ('error' in authResult) {\n const error = new Error(authResult.error.message);\n error.name = authResult.error.name;\n // TODO: proper error type\n // error.extra = authResult.error.extra;\n reject(error);\n } else {\n resolve(authResult.response);\n }\n done();\n };\n\n const intervalId = setInterval(() => {\n if (popup.closed) {\n const errMessage = `Login failed, ${\n targetOrigin && targetOrigin !== window.location.origin\n ? `Incorrect app origin, expected ${targetOrigin}`\n : 'popup was closed'\n }`;\n const error = new Error(errMessage);\n error.name = 'PopupClosedError';\n reject(error);\n done();\n }\n }, 100);\n\n function done() {\n window.removeEventListener('message', messageListener);\n clearInterval(intervalId);\n }\n\n window.addEventListener('message', messageListener);\n });\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-app",
|
|
3
|
-
"version": "0.1.9-next.
|
|
3
|
+
"version": "0.1.9-next.3",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "frontend-plugin",
|
|
6
6
|
"pluginId": "app",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"test": "backstage-cli package test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@backstage/core-components": "0.17.2-next.
|
|
44
|
-
"@backstage/core-plugin-api": "1.10.
|
|
45
|
-
"@backstage/frontend-plugin-api": "0.10.2-next.
|
|
46
|
-
"@backstage/integration-react": "1.2.7-next.
|
|
47
|
-
"@backstage/plugin-permission-react": "0.4.34-next.
|
|
43
|
+
"@backstage/core-components": "0.17.2-next.1",
|
|
44
|
+
"@backstage/core-plugin-api": "1.10.7-next.0",
|
|
45
|
+
"@backstage/frontend-plugin-api": "0.10.2-next.1",
|
|
46
|
+
"@backstage/integration-react": "1.2.7-next.3",
|
|
47
|
+
"@backstage/plugin-permission-react": "0.4.34-next.1",
|
|
48
48
|
"@backstage/theme": "0.6.6-next.0",
|
|
49
49
|
"@backstage/types": "1.2.1",
|
|
50
50
|
"@material-ui/core": "^4.9.13",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"react-use": "^17.2.4"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@backstage/cli": "0.32.1-next.
|
|
57
|
-
"@backstage/dev-utils": "1.1.10-next.
|
|
58
|
-
"@backstage/frontend-test-utils": "0.3.2-next.
|
|
56
|
+
"@backstage/cli": "0.32.1-next.3",
|
|
57
|
+
"@backstage/dev-utils": "1.1.10-next.3",
|
|
58
|
+
"@backstage/frontend-test-utils": "0.3.2-next.3",
|
|
59
59
|
"@testing-library/jest-dom": "^6.0.0",
|
|
60
60
|
"@testing-library/react": "^16.0.0",
|
|
61
61
|
"@testing-library/user-event": "^14.0.0",
|