diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 1aea6b2222..96f6d3bcfd 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -11,6 +11,8 @@ "./element-api": "./dist-cms/libs/element-api/index.js", "./extension-api": "./dist-cms/libs/extension-api/index.js", "./observable-api": "./dist-cms/libs/observable-api/index.js", + "./auth": "./dist-cms/shared/auth/index.js", + "./context": "./dist-cms/shared/context/index.js", "./events": "./dist-cms/shared/umb-events/index.js", "./models": "./dist-cms/shared/models/index.js", "./repository": "./dist-cms/shared/repository/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index ed09065f09..db309ee6dc 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -1,6 +1,6 @@ +import { UMB_AUTH, UmbAuthFlow } from '@umbraco-cms/backoffice/auth'; +import { UMB_APP, UmbAppContext } from '@umbraco-cms/backoffice/context'; import type { UmbAppErrorElement } from './app-error.element.js'; -import { UmbAuthFlow } from './auth/index.js'; -import { UMB_APP, UmbAppContext } from './app.context.js'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui'; import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon'; @@ -81,6 +81,8 @@ export class UmbAppElement extends UmbLitElement { this.#authFlow = new UmbAuthFlow(this.serverUrl, redirectUrl); + this.provideContext(UMB_AUTH, this.#authFlow); + this.provideContext(UMB_APP, new UmbAppContext({ backofficePath: this.backofficePath, serverUrl: this.serverUrl })); // Try to initialise the auth flow and get the runtime status diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/auth/index.ts b/src/Umbraco.Web.UI.Client/src/apps/app/auth/index.ts deleted file mode 100644 index 16f8c38e67..0000000000 --- a/src/Umbraco.Web.UI.Client/src/apps/app/auth/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth-flow.js'; diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/index.ts b/src/Umbraco.Web.UI.Client/src/apps/app/index.ts index 8eb4f65238..0834e3b2c8 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/index.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/index.ts @@ -1,4 +1,3 @@ export * from './app-context-config.interface.js'; export * from './app-error.element.js'; -export * from './app.context.js'; export * from './app.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts index 4f759ef460..b0cbbf80ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/users/current-user/modals/current-user/current-user-modal.element.ts @@ -1,3 +1,5 @@ +import { UMB_AUTH } from '@umbraco-cms/backoffice/auth'; +import { UMB_APP } from '@umbraco-cms/backoffice/context'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../current-user.store.js'; import type { UmbLoggedInUser } from '../../types.js'; import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; @@ -15,6 +17,10 @@ export class UmbCurrentUserModalElement extends UmbLitElement { private _currentUserStore?: UmbCurrentUserStore; + #auth?: typeof UMB_AUTH.TYPE; + + #appContext?: typeof UMB_APP.TYPE; + constructor() { super(); @@ -23,6 +29,14 @@ export class UmbCurrentUserModalElement extends UmbLitElement { this._observeCurrentUser(); }); + this.consumeContext(UMB_AUTH, (instance) => { + this.#auth = instance; + }); + + this.consumeContext(UMB_APP, (instance) => { + this.#appContext = instance; + }); + this._observeCurrentUser(); } @@ -38,8 +52,13 @@ export class UmbCurrentUserModalElement extends UmbLitElement { this.modalContext?.submit(); } - private _logout() { - alert('implement log out'); + private async _logout() { + if (!this.#auth) return; + this.#auth.performWithFreshTokens; + await this.#auth.signOut(); + let newUrl = this.#appContext ? `${this.#appContext.getBackofficePath()}/login` : '/'; + newUrl = newUrl.replace(/\/\//g, '/'); + location.href = newUrl; } render() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth-flow.ts similarity index 96% rename from src/Umbraco.Web.UI.Client/src/apps/app/auth/auth-flow.ts rename to src/Umbraco.Web.UI.Client/src/shared/auth/auth-flow.ts index 191a61aafa..1820234ff6 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth-flow.ts @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ +import type { IUmbAuth } from './auth.interface.js'; import { BaseTokenRequestHandler, BasicQueryStringUtils, @@ -32,6 +33,8 @@ import { const requestor = new FetchRequestor(); +const TOKEN_RESPONSE_NAME = 'umb:userAuthTokenResponse'; + /** * This class is needed to prevent the hash from being parsed as part of the query string. */ @@ -78,7 +81,7 @@ class UmbNoHashQueryStringUtils extends BasicQueryStringUtils { * a. This will redirect the user to the authorization endpoint of the server * 4. After login, get the latest token before each request to the server by calling the `performWithFreshTokens` method */ -export class UmbAuthFlow { +export class UmbAuthFlow implements IUmbAuth { // handlers readonly #notifier: AuthorizationNotifier; readonly #authorizationHandler: RedirectRequestHandler; @@ -155,7 +158,7 @@ export class UmbAuthFlow { async setInitialState() { // Ensure there is a connection to the server await this.fetchServiceConfiguration(); - const tokenResponseJson = await this.#storageBackend.getItem('tokenResponse'); + const tokenResponseJson = await this.#storageBackend.getItem(TOKEN_RESPONSE_NAME); if (tokenResponseJson) { const response = new TokenResponse(JSON.parse(tokenResponseJson)); if (response.isValid()) { @@ -238,7 +241,7 @@ export class UmbAuthFlow { // forget all cached token state this.#accessTokenResponse = undefined; this.#refreshToken = undefined; - await this.#storageBackend.removeItem('tokenResponse'); + await this.#storageBackend.removeItem(TOKEN_RESPONSE_NAME); } /** @@ -282,7 +285,7 @@ export class UmbAuthFlow { */ async #saveTokenState() { if (this.#accessTokenResponse) { - await this.#storageBackend.setItem('tokenResponse', JSON.stringify(this.#accessTokenResponse.toJson())); + await this.#storageBackend.setItem(TOKEN_RESPONSE_NAME, JSON.stringify(this.#accessTokenResponse.toJson())); } } diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts new file mode 100644 index 0000000000..a71be30e2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts @@ -0,0 +1,17 @@ +export interface IUmbAuth { + /** + * Get the current user's access token. + * + * @example + * ```js + * const token = await auth.getAccessToken(); + * const result = await fetch('https://my-api.com', { headers: { Authorization: `Bearer ${token}` } }); + * ``` + */ + performWithFreshTokens(): Promise; + + /** + * Sign out the current user. + */ + signOut(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts new file mode 100644 index 0000000000..391382b79c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts @@ -0,0 +1,10 @@ +import { IUmbAuth } from './auth.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export type { IUmbAuth } from './auth.interface.js'; +export { UmbAuthFlow } from './auth-flow.js'; + +export const UMB_AUTH = new UmbContextToken( + 'UmbAuth', + 'An instance of UmbAuthFlow that should be shared across the app.' +); diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts b/src/Umbraco.Web.UI.Client/src/shared/context/app.context.ts similarity index 84% rename from src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts rename to src/Umbraco.Web.UI.Client/src/shared/context/app.context.ts index 42e314745d..f4ccd4b79f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/context/app.context.ts @@ -1,4 +1,4 @@ -import { UmbAppContextConfig } from './app-context-config.interface.js'; +import { UmbAppContextConfig } from '../../apps/app/app-context-config.interface.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export class UmbAppContext { diff --git a/src/Umbraco.Web.UI.Client/src/shared/context/index.ts b/src/Umbraco.Web.UI.Client/src/shared/context/index.ts new file mode 100644 index 0000000000..cbe999e3a3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/context/index.ts @@ -0,0 +1 @@ +export * from './app.context.js'; diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 65b090a30a..a3baeab158 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -35,6 +35,8 @@ "@umbraco-cms/backoffice/observable-api": ["src/libs/observable-api"], // SHARED + "@umbraco-cms/backoffice/auth": ["src/shared/auth"], + "@umbraco-cms/backoffice/context": ["src/shared/context"], "@umbraco-cms/backoffice/events": ["src/shared/umb-events"], "@umbraco-cms/backoffice/models": ["src/shared/models"], "@umbraco-cms/backoffice/repository": ["src/shared/repository"], diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index d5627f2078..fe00353f9c 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -48,6 +48,8 @@ export default { '@umbraco-cms/backoffice/extension-api': './src/libs/extension-api/index.ts', '@umbraco-cms/backoffice/observable-api': './src/libs/observable-api/index.ts', + '@umbraco-cms/backoffice/auth': './src/shared/auth/index.ts', + '@umbraco-cms/backoffice/context': './src/shared/context/index.ts', '@umbraco-cms/backoffice/events': './src/shared/umb-events/index.ts', '@umbraco-cms/backoffice/models': './src/shared/models/index.ts', '@umbraco-cms/backoffice/repository': './src/shared/repository/index.ts',