diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts index f4ccd4b79f..912029a1e3 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts @@ -1,13 +1,16 @@ -import { UmbAppContextConfig } from '../../apps/app/app-context-config.interface.js'; +import { UmbAppContextConfig } from './app-context-config.interface.js'; +import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -export class UmbAppContext { +export class UmbAppContext extends UmbBaseController { #serverUrl: string; #backofficePath: string; - constructor(config: UmbAppContextConfig) { + constructor(host: UmbControllerHost, config: UmbAppContextConfig) { + super(host); this.#serverUrl = config.serverUrl; this.#backofficePath = config.backofficePath; + this.provideContext(UMB_APP_CONTEXT, this); } getBackofficePath() { @@ -19,4 +22,4 @@ export class UmbAppContext { } } -export const UMB_APP = new UmbContextToken('UMB_APP'); +export const UMB_APP_CONTEXT = new UmbContextToken('UMB_APP'); 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 eea994ba17..745e057c76 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 type { UmbAppErrorElement } from './app-error.element.js'; -import { UMB_APP, UmbAppContext } from './app.context.js'; -import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization'; +import { UmbAppContext } from './app.context.js'; +import { UmbServerConnection } from './server-connection.js'; import { UMB_AUTH_CONTEXT, UmbAuthContext } from '@umbraco-cms/backoffice/auth'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui'; @@ -8,9 +8,8 @@ import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { Guard, UmbRoute } from '@umbraco-cms/backoffice/router'; import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router'; -import { tryExecute } from '@umbraco-cms/backoffice/resources'; -import { OpenAPI, RuntimeLevelModel, ServerResource } from '@umbraco-cms/backoffice/backend-api'; -import { contextData, umbDebugContextEventType } from '@umbraco-cms/backoffice/context-api'; +import { OpenAPI, RuntimeLevelModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbContextDebugController } from '@umbraco-cms/backoffice/debug'; @customElement('umb-app') export class UmbAppElement extends UmbLitElement { @@ -56,73 +55,44 @@ export class UmbAppElement extends UmbLitElement { }, ]; - #authContext?: UmbAuthContext; + #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; #umbIconRegistry = new UmbIconRegistry(); #uuiIconRegistry = new UUIIconRegistryEssential(); - #runtimeLevel = RuntimeLevelModel.UNKNOWN; + #serverConnection?: UmbServerConnection; constructor() { super(); + new UmbContextDebugController(this); + this.#umbIconRegistry.attach(this); this.#uuiIconRegistry.attach(this); } connectedCallback(): void { super.connectedCallback(); - - this.#setLanguage(); this.#setup(); } - #setLanguage() { - if (this.lang) { - umbLocalizationRegistry.loadLanguage(this.lang); - } - } - - #listenForLanguageChange() { - // This will wait for the default language to be loaded before attempting to load the current user language - // just in case the user language is not the default language. - // We **need** to do this because the default language (typically en-us) holds all the fallback keys for all the other languages. - // This way we can ensure that the document language is always loaded first and subsequently registered as the fallback language. - this.observe(umbLocalizationRegistry.isDefaultLoaded, (isDefaultLoaded) => { - if (!this.#authContext) { - throw new Error('[Fatal] AuthContext requested before it was initialized'); - } - - if (!isDefaultLoaded) return; - - this.observe( - this.#authContext.languageIsoCode, - (currentLanguageIsoCode) => { - umbLocalizationRegistry.loadLanguage(currentLanguageIsoCode); - }, - 'languageIsoCode', - ); - }); - } - async #setup() { if (this.serverUrl === undefined) throw new Error('No serverUrl provided'); + /* All requests to the server requires the base URL to be set. + We make sure it happens before we get the server status. + TODO: find the right place to set this + */ OpenAPI.BASE = this.serverUrl; - const redirectUrl = `${window.location.origin}${this.backofficePath}`; - this.#authContext = new UmbAuthContext(this, this.serverUrl, redirectUrl); + this.#serverConnection = await new UmbServerConnection(this.serverUrl).connect(); - this.provideContext(UMB_AUTH_CONTEXT, this.#authContext); - - this.provideContext(UMB_APP, new UmbAppContext({ backofficePath: this.backofficePath, serverUrl: this.serverUrl })); + this.#authContext = new UmbAuthContext(this, this.serverUrl, this.backofficePath, this.bypassAuth); + new UmbAppContext(this, { backofficePath: this.backofficePath, serverUrl: this.serverUrl }); // Try to initialise the auth flow and get the runtime status try { - // Get the current runtime level - await this.#setInitStatus(); - // If the runtime level is "install" we should clear any cached tokens // else we should try and set the auth status - if (this.#runtimeLevel === RuntimeLevelModel.INSTALL) { + if (this.#serverConnection.getStatus() === RuntimeLevelModel.INSTALL) { await this.#authContext.signOut(); } else { await this.#setAuthStatus(); @@ -150,63 +120,26 @@ export class UmbAppElement extends UmbLitElement { // Redirect to the error page this.#errorPage(errorMsg, error); } - - // TODO: wrap all debugging logic in a separate class. Maybe this could be part of the context-api? When we create a new root, we could attach the debugger to it? - // Listen for the debug event from the component - this.addEventListener(umbDebugContextEventType, (event: any) => { - // Once we got to the outter most component - // we can send the event containing all the contexts - // we have collected whilst coming up through the DOM - // and pass it back down to the callback in - // the component that originally fired the event - if (event.callback) { - event.callback(event.instances); - } - - // Massage the data into a simplier format - // Why? Can't send contexts data directly - browser seems to not serialize it and says its null - // But a simple object works fine for browser extension to consume - const data = { - contexts: contextData(event.instances), - }; - - // Emit this new event for the browser extension to listen for - this.dispatchEvent(new CustomEvent('umb:debug-contexts:data', { detail: data, bubbles: true })); - }); - } - - async #setInitStatus() { - const { data, error } = await tryExecute(ServerResource.getServerStatus()); - if (error) { - throw error; - } - this.#runtimeLevel = data?.serverStatus ?? RuntimeLevelModel.UNKNOWN; } + // TODO: move set initial auth state into auth context async #setAuthStatus() { - if (this.bypassAuth === false) { - if (!this.#authContext) { - throw new Error('[Fatal] AuthContext requested before it was initialized'); - } + if (this.bypassAuth) return; - // Get service configuration from authentication server - await this.#authContext.setInitialState(); - - // Instruct all requests to use the auth flow to get and use the access_token for all subsequent requests - OpenAPI.TOKEN = () => this.#authContext!.getLatestToken(); + if (!this.#authContext) { + throw new Error('[Fatal] AuthContext requested before it was initialized'); } - this.#listenForLanguageChange(); + // Get service configuration from authentication server + await this.#authContext?.setInitialState(); - if (this.#authContext?.isAuthorized()) { - this.#authContext?.setLoggedIn(true); - } else { - this.#authContext?.setLoggedIn(false); - } + // Instruct all requests to use the auth flow to get and use the access_token for all subsequent requests + OpenAPI.TOKEN = () => this.#authContext!.getLatestToken(); + OpenAPI.WITH_CREDENTIALS = true; } #redirect() { - switch (this.#runtimeLevel) { + switch (this.#serverConnection?.getStatus()) { case RuntimeLevelModel.INSTALL: history.replaceState(null, '', 'install'); break; @@ -238,18 +171,17 @@ export class UmbAppElement extends UmbLitElement { default: // Redirect to the error page - this.#errorPage(`Unsupported runtime level: ${this.#runtimeLevel}`); + this.#errorPage(`Unsupported runtime level: ${this.#serverConnection?.getStatus()}`); } } - #isAuthorized(): boolean { - if (!this.#authContext) return false; - return this.bypassAuth ? true : this.#authContext.isAuthorized(); - } - #isAuthorizedGuard(): Guard { return () => { - if (this.#isAuthorized()) { + if (!this.#authContext) { + throw new Error('[Fatal] AuthContext requested before it was initialized'); + } + + if (this.#authContext.getIsAuthorized()) { return true; } @@ -257,7 +189,8 @@ export class UmbAppElement extends UmbLitElement { window.sessionStorage.setItem('umb:auth:redirect', location.href); // Make a request to the auth server to start the auth flow - this.#authContext!.login(); + // TODO: find better name for this method + this.#authContext.login(); // Return false to prevent the route from being rendered return false; diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts b/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts new file mode 100644 index 0000000000..5655be20f2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts @@ -0,0 +1,62 @@ +import { RuntimeLevelModel, ServerResource } from '@umbraco-cms/backoffice/backend-api'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { tryExecute } from '@umbraco-cms/backoffice/resources'; + +export class UmbServerConnection { + #url: string; + #status: RuntimeLevelModel = RuntimeLevelModel.UNKNOWN; + + #isConnected = new UmbBooleanState(false); + isConnected = this.#isConnected.asObservable(); + + constructor(serverUrl: string) { + this.#url = serverUrl; + } + + /** + * Connects to the server. + * @memberof UmbServerConnection + */ + async connect() { + await this.#setStatus(); + return this; + } + + /** + * Gets the URL of the server. + * @return {*} + * @memberof UmbServerConnection + */ + getUrl() { + return this.#url; + } + + /** + * Gets the status of the server. + * @return {string} + * @memberof UmbServerConnection + */ + getStatus() { + if (!this.getIsConnected()) throw new Error('Server is not connected. Remember to await connect()'); + return this.#status; + } + + /** + * Checks if the server is connected. + * @return {boolean} + * @memberof UmbServerConnection + */ + getIsConnected() { + return this.#isConnected.getValue(); + } + + async #setStatus() { + const { data, error } = await tryExecute(ServerResource.getServerStatus()); + if (error) { + throw error; + } + + this.#isConnected.next(true); + this.#status = data?.serverStatus ?? RuntimeLevelModel.UNKNOWN; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user.data.ts index 7f662c0eb5..e19b07a4b9 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user.data.ts @@ -2,7 +2,7 @@ import { UmbEntityData } from './entity.data.js'; import { umbUserGroupData } from './user-group.data.js'; import { arrayFilter, stringFilter, queryFilter } from './utils.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; import { CreateUserRequestModel, CreateUserResponseModel, @@ -21,8 +21,10 @@ const createUserItem = (item: UserResponseModel): UserItemResponseModel => { }; }; -const userGroupFilter = (filterOptions: any, item: UserResponseModel) => arrayFilter(filterOptions.userGroupIds, item.userGroupIds); -const userStateFilter = (filterOptions: any, item: UserResponseModel) => stringFilter(filterOptions.userStates, item.state); +const userGroupFilter = (filterOptions: any, item: UserResponseModel) => + arrayFilter(filterOptions.userGroupIds, item.userGroupIds); +const userStateFilter = (filterOptions: any, item: UserResponseModel) => + stringFilter(filterOptions.userStates, item.state); const userQueryFilter = (filterOptions: any, item: UserResponseModel) => queryFilter(filterOptions.filter, item.name); // Temp mocked database @@ -89,7 +91,7 @@ class UmbUserData extends UmbEntityData { * @return {*} {UmbLoggedInUser} * @memberof UmbUserData */ - getCurrentUser(): UmbLoggedInUser { + getCurrentUser(): UmbCurrentUser { const firstUser = this.data[0]; const permissions = firstUser.userGroupIds?.length ? umbUserGroupData.getPermissions(firstUser.userGroupIds) : []; @@ -159,26 +161,31 @@ class UmbUserData extends UmbEntityData { this.createUser(invitedUser); } - filter (options: any): PagedUserResponseModel { + filter(options: any): PagedUserResponseModel { const { items: allItems } = this.getAll(); - const filterOptions = { - skip: options.skip || 0, - take: options.take || 25, - orderBy: options.orderBy || 'name', - orderDirection: options.orderDirection || 'asc', - userGroupIds: options.userGroupIds, - userStates: options.userStates, - filter: options.filter, - }; + const filterOptions = { + skip: options.skip || 0, + take: options.take || 25, + orderBy: options.orderBy || 'name', + orderDirection: options.orderDirection || 'asc', + userGroupIds: options.userGroupIds, + userStates: options.userStates, + filter: options.filter, + }; - const filteredItems = allItems.filter((item) => userGroupFilter(filterOptions, item) && userStateFilter(filterOptions, item) && userQueryFilter(filterOptions, item)); + const filteredItems = allItems.filter( + (item) => + userGroupFilter(filterOptions, item) && + userStateFilter(filterOptions, item) && + userQueryFilter(filterOptions, item), + ); const totalItems = filteredItems.length; const paginatedItems = filteredItems.slice(filterOptions.skip, filterOptions.skip + filterOptions.take); return { total: totalItems, items: paginatedItems }; - }; + } } export const data: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts index cebf5169f1..33cfa23e5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -12,7 +12,7 @@ import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext, } from '@umbraco-cms/backoffice/modal'; -import { UMB_APP } from '@umbraco-cms/backoffice/app'; +import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; /** * @element umb-input-markdown @@ -49,7 +49,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { this._modalContext = instance; }); - this.consumeContext(UMB_APP, (instance) => { + this.consumeContext(UMB_APP_CONTEXT, (instance) => { this.serverUrl = instance.getServerUrl(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts index efd7fd62c3..d167d400c5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts @@ -9,7 +9,7 @@ import { type RawEditorOptions, renderEditor, } from '@umbraco-cms/backoffice/external/tinymce'; -import { UMB_AUTH_CONTEXT, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; import { ClassConstructor, hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api'; import { ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -26,7 +26,7 @@ import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbMediaHelper } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import { UMB_APP } from '@umbraco-cms/backoffice/app'; +import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import { UmbStylesheetRepository } from '@umbraco-cms/backoffice/stylesheet'; // TODO => integrate macro picker, update stylesheet fetch when backend CLI exists (ref tinymce.service.js in existing backoffice) @@ -39,8 +39,8 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { private _tinyConfig: RawEditorOptions = {}; #mediaHelper = new UmbMediaHelper(); - #currentUser?: UmbLoggedInUser; - #auth?: typeof UMB_AUTH_CONTEXT.TYPE; + #currentUser?: UmbCurrentUser; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; #plugins: Array UmbTinyMcePluginBase> = []; #editorRef?: Editor | null = null; #stylesheetRepository?: UmbStylesheetRepository; @@ -56,7 +56,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { constructor() { super(); - this.consumeContext(UMB_APP, (instance) => { + this.consumeContext(UMB_APP_CONTEXT, (instance) => { this.#serverUrl = instance.getServerUrl(); }); @@ -71,9 +71,9 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { } async #observeCurrentUser() { - if (!this.#auth) return; + if (!this.#currentUserContext) return; - this.observe(this.#auth.currentUser, (currentUser) => (this.#currentUser = currentUser)); + this.observe(this.#currentUserContext.currentUser, (currentUser) => (this.#currentUser = currentUser)); } protected async firstUpdated(_changedProperties: PropertyValueMap | Map): Promise { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/context-debug.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/context-debug.controller.ts new file mode 100644 index 0000000000..7c68b47b21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/context-debug.controller.ts @@ -0,0 +1,45 @@ +import { contextData, umbDebugContextEventType } from '@umbraco-cms/backoffice/context-api'; +import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +// Temp controller to get the code away from the app.element.ts +export class UmbContextDebugController extends UmbBaseController { + constructor(host: UmbControllerHost) { + super(host); + } + + hostConnected(): void { + super.hostConnected(); + // Maybe this could be part of the context-api? When we create a new root, we could attach the debugger to it? + // Listen for the debug event from the component + this.getHostElement().addEventListener(umbDebugContextEventType, this.#onContextDebug as unknown as EventListener); + } + + #onContextDebug = (event: any) => { + // Once we got to the outter most component + // we can send the event containing all the contexts + // we have collected whilst coming up through the DOM + // and pass it back down to the callback in + // the component that originally fired the event + if (event.callback) { + event.callback(event.instances); + } + + // Massage the data into a simplier format + // Why? Can't send contexts data directly - browser seems to not serialize it and says its null + // But a simple object works fine for browser extension to consume + const data = { + contexts: contextData(event.instances), + }; + + // Emit this new event for the browser extension to listen for + this.getHostElement().dispatchEvent(new CustomEvent('umb:debug-contexts:data', { detail: data, bubbles: true })); + }; + + hostDisconnected(): void { + super.hostDisconnected(); + this.getHostElement().removeEventListener( + umbDebugContextEventType, + this.#onContextDebug as unknown as EventListener, + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/index.ts index 018153cb82..3aae472235 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/index.ts @@ -1,2 +1,3 @@ export * from './debug.element.js'; +export * from './context-debug.controller.js'; export * from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 49c4f80ac8..66825ac3e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -5,7 +5,7 @@ import { UmbModalManagerContext, UMB_MODAL_MANAGER_CONTEXT_TOKEN, } from '@umbraco-cms/backoffice/modal'; -import { UMB_AUTH_CONTEXT, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; interface MediaPickerTargetData { altText?: string; @@ -26,9 +26,9 @@ interface MediaPickerResultData { export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { #mediaHelper: UmbMediaHelper; - #currentUser?: UmbLoggedInUser; + #currentUser?: UmbCurrentUser; #modalContext?: UmbModalManagerContext; - #auth?: typeof UMB_AUTH_CONTEXT.TYPE; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; constructor(args: TinyMcePluginArguments) { super(args); @@ -41,8 +41,8 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { // TODO => this breaks tests. disabling for now // will ignore user media start nodes - // this.host.consumeContext(UMB_AUTH, (instance) => { - // this.#auth = instance; + // this.host.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + // this.#currentUserContext = instance; // this.#observeCurrentUser(); // }); @@ -55,9 +55,9 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { } async #observeCurrentUser() { - if (!this.#auth) return; + if (!this.#currentUserContext) return; - this.observe(this.#auth.currentUser, (currentUser) => (this.#currentUser = currentUser)); + this.observe(this.#currentUserContext.currentUser, (currentUser) => (this.#currentUser = currentUser)); } async #onAction() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts index d8aacf5d52..1e810cdacf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts @@ -1,26 +1,26 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; @customElement('umb-umbraco-news-dashboard') export class UmbUmbracoNewsDashboardElement extends UmbLitElement { - #auth?: typeof UMB_AUTH_CONTEXT.TYPE; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; @state() private name = ''; constructor() { super(); - this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { - this.#auth = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this.#observeCurrentUser(); }); } #observeCurrentUser(): void { - if (!this.#auth) return; - this.observe(this.#auth.currentUser, (user) => { + if (!this.#currentUserContext) return; + this.observe(this.#currentUserContext.currentUser, (user) => { this.name = user?.name ?? ''; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts index 527e33d922..2a9cf401ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts @@ -6,39 +6,43 @@ import { UMB_CURRENT_USER_MODAL, } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; @customElement('umb-current-user-header-app') export class UmbCurrentUserHeaderAppElement extends UmbLitElement { @state() - private _currentUser?: UmbLoggedInUser; + private _currentUser?: UmbCurrentUser; - private _auth?: typeof UMB_AUTH_CONTEXT.TYPE; - private _modalContext?: UmbModalManagerContext; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; + #modalManagerContext?: UmbModalManagerContext; constructor() { super(); this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; + this.#modalManagerContext = instance; }); - this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { - this._auth = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this._observeCurrentUser(); }); } private async _observeCurrentUser() { - if (!this._auth) return; + if (!this.#currentUserContext) return; - this.observe(this._auth.currentUser, (currentUser) => { - this._currentUser = currentUser; - }, 'umbCurrentUserObserver'); + this.observe( + this.#currentUserContext.currentUser, + (currentUser) => { + this._currentUser = currentUser; + }, + 'umbCurrentUserObserver', + ); } private _handleUserClick() { - this._modalContext?.open(UMB_CURRENT_USER_MODAL); + this.#modalManagerContext?.open(UMB_CURRENT_USER_MODAL); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts new file mode 100644 index 0000000000..d6bc1232be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user.context.ts @@ -0,0 +1,72 @@ +import { UmbCurrentUser } from './types.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { UserResource } from '@umbraco-cms/backoffice/backend-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization'; + +export class UmbCurrentUserContext extends UmbBaseController { + #currentUser = new UmbObjectState(undefined); + readonly currentUser = this.#currentUser.asObservable(); + + readonly languageIsoCode = this.#currentUser.asObservablePart((user) => user?.languageIsoCode ?? 'en-us'); + + #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { + this.#authContext = instance; + this.#observeIsAuthorized(); + }); + + // TODO: revisit this. It can probably be simplified + this.observe(umbLocalizationRegistry.isDefaultLoaded, (isDefaultLoaded) => { + if (!isDefaultLoaded) return; + + this.observe( + this.languageIsoCode, + (currentLanguageIsoCode) => { + umbLocalizationRegistry.loadLanguage(currentLanguageIsoCode); + }, + 'umbCurrentUserLanguageIsoCode', + ); + }); + + this.provideContext(UMB_CURRENT_USER_CONTEXT, this); + } + + async requestCurrentUser() { + // TODO: use repository + const { data, error } = await tryExecuteAndNotify(this._host, UserResource.getUserCurrent()); + // TODO: add current user store + this.#currentUser.next(data); + return { data, error }; + } + + /** + * Checks if a user is the current user. + * + * @param userId The user id to check + * @returns True if the user is the current user, otherwise false + */ + async isUserCurrentUser(userId: string): Promise { + const currentUser = await firstValueFrom(this.currentUser); + return currentUser?.id === userId; + } + + #observeIsAuthorized() { + if (!this.#authContext) return; + this.observe(this.#authContext.isAuthorized, (isAuthorized) => { + if (isAuthorized) { + this.requestCurrentUser(); + } + }); + } +} + +export const UMB_CURRENT_USER_CONTEXT = new UmbContextToken('UmbCurrentUserContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/index.ts index 95606adf10..8d63217a6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/index.ts @@ -1,3 +1,5 @@ // TODO:Do not export store, but instead export future repository export * from './current-user-history.store.js'; export * from './utils/index.js'; +export * from './current-user.context.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts index f6fe418913..aae734cfa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts @@ -1,3 +1,4 @@ +import { UmbCurrentUserContext } from './current-user.context.js'; import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as userProfileAppsManifests } from './user-profile-apps/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; @@ -9,6 +10,12 @@ export const headerApps: Array = [ name: 'Current User Store', loader: () => import('./current-user-history.store.js'), }, + { + type: 'globalContext', + alias: 'Umb.GlobalContext.CurrentUser', + name: 'Current User', + api: UmbCurrentUserContext, + }, { type: 'headerApp', alias: 'Umb.HeaderApp.CurrentUser', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.element.ts index 53318ed666..540b51923b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user/current-user-modal.element.ts @@ -1,9 +1,11 @@ -import { UMB_APP } from '@umbraco-cms/backoffice/app'; -import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '../../current-user.context.js'; +import { UmbCurrentUser } from '../../types.js'; +import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, CSSResultGroup, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; @customElement('umb-current-user-modal') export class UmbCurrentUserModalElement extends UmbLitElement { @@ -11,31 +13,39 @@ export class UmbCurrentUserModalElement extends UmbLitElement { modalContext?: UmbModalContext; @state() - private _currentUser?: UmbLoggedInUser; + private _currentUser?: UmbCurrentUser; #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; - - #appContext?: typeof UMB_APP.TYPE; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; + #appContext?: typeof UMB_APP_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { - this.#authContext = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this._observeCurrentUser(); }); - this.consumeContext(UMB_APP, (instance) => { + this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { + this.#authContext = instance; + }); + + this.consumeContext(UMB_APP_CONTEXT, (instance) => { this.#appContext = instance; }); } private async _observeCurrentUser() { - if (!this.#authContext) return; + if (!this.#currentUserContext) return; - this.observe(this.#authContext.currentUser, (currentUser) => { - this._currentUser = currentUser; - }, 'umbCurrentUserObserver'); + this.observe( + this.#currentUserContext.currentUser, + (currentUser) => { + this._currentUser = currentUser; + }, + 'umbCurrentUserObserver', + ); } private _close() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts new file mode 100644 index 0000000000..1eff3f298e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts @@ -0,0 +1 @@ +export type { CurrentUserResponseModel as UmbCurrentUser } from '@umbraco-cms/backoffice/backend-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/user-profile-apps/user-profile-app-profile.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/user-profile-apps/user-profile-app-profile.element.ts index 510f3900fe..9166851006 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/user-profile-apps/user-profile-app-profile.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/user-profile-apps/user-profile-app-profile.element.ts @@ -6,35 +6,39 @@ import { UMB_CHANGE_PASSWORD_MODAL, UMB_MODAL_MANAGER_CONTEXT_TOKEN, } from '@umbraco-cms/backoffice/modal'; -import { UMB_AUTH_CONTEXT, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; @customElement('umb-user-profile-app-profile') export class UmbUserProfileAppProfileElement extends UmbLitElement { @state() - private _currentUser?: UmbLoggedInUser; + private _currentUser?: UmbCurrentUser; - private _modalContext?: UmbModalManagerContext; - private _auth?: typeof UMB_AUTH_CONTEXT.TYPE; + #modalManagerContext?: UmbModalManagerContext; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; constructor() { super(); this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { - this._modalContext = instance; + this.#modalManagerContext = instance; }); - this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { - this._auth = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this._observeCurrentUser(); }); } private async _observeCurrentUser() { - if (!this._auth) return; + if (!this.#currentUserContext) return; - this.observe(this._auth.currentUser, (currentUser) => { - this._currentUser = currentUser; - }, 'umbCurrentUserObserver'); + this.observe( + this.#currentUserContext.currentUser, + (currentUser) => { + this._currentUser = currentUser; + }, + 'umbCurrentUserObserver', + ); } private _edit() { @@ -44,9 +48,9 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement { //TODO Implement modal routing for the current-user-modal, so that the modal closes when navigating to the edit profile page } private _changePassword() { - if (!this._modalContext) return; - - this._modalContext.open(UMB_CHANGE_PASSWORD_MODAL, { + if (!this.#modalManagerContext) return; + + this.#modalManagerContext.open(UMB_CHANGE_PASSWORD_MODAL, { userId: this._currentUser?.id ?? '', }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts index 8b79ad283f..bfa34694e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/utils/is-current-user.function.ts @@ -1,13 +1,13 @@ -import { IUmbAuth, UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '../current-user.context.js'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export const isCurrentUser = async (host: UmbControllerHost, userId: string) => { - let authContext: IUmbAuth | undefined = undefined; + let currentUserContext: typeof UMB_CURRENT_USER_CONTEXT.TYPE | undefined; - await new UmbContextConsumerController(host, UMB_AUTH_CONTEXT, (context) => { - authContext = context; + await new UmbContextConsumerController(host, UMB_CURRENT_USER_CONTEXT, (context) => { + currentUserContext = context; }).asPromise(); - return await authContext!.isUserCurrentUser(userId); + return await currentUserContext!.isUserCurrentUser(userId); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/modals/change-password/change-password-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/modals/change-password/change-password-modal.element.ts index dfeed427aa..4c81a48e2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/modals/change-password/change-password-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/modals/change-password/change-password-modal.element.ts @@ -1,11 +1,18 @@ import { UmbUserItemRepository } from '../../user/repository/item/user-item.repository.js'; +import { UMB_CURRENT_USER_CONTEXT } from '../../current-user/current-user.context.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, CSSResultGroup, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbChangePasswordModalData, UmbChangePasswordModalValue, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { + UmbChangePasswordModalData, + UmbChangePasswordModalValue, + UmbModalBaseElement, +} from '@umbraco-cms/backoffice/modal'; @customElement('umb-change-password-modal') -export class UmbChangePasswordModalElement extends UmbModalBaseElement { +export class UmbChangePasswordModalElement extends UmbModalBaseElement< + UmbChangePasswordModalData, + UmbChangePasswordModalValue +> { @state() private _headline: string = 'Change password'; @@ -13,7 +20,7 @@ export class UmbChangePasswordModalElement extends UmbModalBaseElement { - this.#authContext = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this.#setIsCurrentUser(); }); } @@ -53,12 +60,12 @@ export class UmbChangePasswordModalElement extends UmbModalBaseElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts index 5fdb9a17d1..18ad039224 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts @@ -1,4 +1,4 @@ -import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '../../current-user/current-user.context.js'; import { UmbBaseController } from '@umbraco-cms/backoffice/controller-api'; import { ManifestCondition, @@ -17,11 +17,15 @@ export class UmbUserPermissionCondition extends UmbBaseController implements Umb this.config = args.config; this.#onChange = args.onChange; - this.consumeContext(UMB_AUTH_CONTEXT, (context) => { - this.observe(context.currentUser, (currentUser) => { - this.permitted = currentUser?.permissions?.includes(this.config.match) || false; - this.#onChange(); - }, 'umbUserPermissionConditionObserver'); + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { + this.observe( + context.currentUser, + (currentUser) => { + this.permitted = currentUser?.permissions?.includes(this.config.match) || false; + this.#onChange(); + }, + 'umbUserPermissionConditionObserver', + ); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts index eef6d27d86..75ec003003 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts @@ -4,7 +4,7 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UUISelectElement } from '@umbraco-cms/backoffice/external/uui'; import { UserResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UMB_AUTH_CONTEXT, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -14,19 +14,19 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement { private _user?: UserResponseModel; @state() - private _currentUser?: UmbLoggedInUser; + private _currentUser?: UmbCurrentUser; @state() private languages: Array<{ name: string; value: string; selected: boolean }> = []; - #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; #userWorkspaceContext?: typeof UMB_USER_WORKSPACE_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_AUTH_CONTEXT, (instance) => { - this.#authContext = instance; + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; this.#observeCurrentUser(); }); @@ -45,42 +45,46 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement { } #observeCurrentUser() { - if (!this.#authContext) return; - this.observe(this.#authContext.currentUser, async (currentUser) => { - this._currentUser = currentUser; + if (!this.#currentUserContext) return; + this.observe( + this.#currentUserContext.currentUser, + async (currentUser) => { + this._currentUser = currentUser; - if (!currentUser) { - return; - } + if (!currentUser) { + return; + } - // Find all translations and make a unique list of iso codes - const translations = await firstValueFrom(umbExtensionsRegistry.extensionsOfType('localization')); + // Find all translations and make a unique list of iso codes + const translations = await firstValueFrom(umbExtensionsRegistry.extensionsOfType('localization')); - this.languages = translations - .filter((isoCode) => isoCode !== undefined) - .map((translation) => ({ - value: translation.meta.culture.toLowerCase(), - name: translation.name, - selected: false, - })); + this.languages = translations + .filter((isoCode) => isoCode !== undefined) + .map((translation) => ({ + value: translation.meta.culture.toLowerCase(), + name: translation.name, + selected: false, + })); - const currentUserLanguageCode = currentUser.languageIsoCode?.toLowerCase(); + const currentUserLanguageCode = currentUser.languageIsoCode?.toLowerCase(); - // Set the current user's language as selected - const currentUserLanguage = this.languages.find((language) => language.value === currentUserLanguageCode); + // Set the current user's language as selected + const currentUserLanguage = this.languages.find((language) => language.value === currentUserLanguageCode); - if (currentUserLanguage) { - currentUserLanguage.selected = true; - } else { - // If users language code did not fit any of the options. We will create an option that fits, named unknown. - // In this way the user can keep their choice though a given language was not present at this time. - this.languages.push({ - value: currentUserLanguageCode ?? 'en-us', - name: currentUserLanguageCode ? `${currentUserLanguageCode} (unknown)` : 'Unknown', - selected: true, - }); - } - }, 'umbUserObserver'); + if (currentUserLanguage) { + currentUserLanguage.selected = true; + } else { + // If users language code did not fit any of the options. We will create an option that fits, named unknown. + // In this way the user can keep their choice though a given language was not present at this time. + this.languages.push({ + value: currentUserLanguageCode ?? 'en-us', + name: currentUserLanguageCode ? `${currentUserLanguageCode} (unknown)` : 'Unknown', + selected: true, + }); + } + }, + 'umbUserObserver', + ); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts index 055a83d351..914aa6a617 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts @@ -5,20 +5,20 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle import type { UpdateUserRequestModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextConsumerController, UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; export class UmbUserWorkspaceContext extends UmbWorkspaceContext implements UmbSaveableWorkspaceContextInterface { - #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; + #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; constructor(host: UmbControllerHostElement) { super(host, 'Umb.Workspace.User', new UmbUserRepository(host)); - new UmbContextConsumerController(host, UMB_AUTH_CONTEXT, (auth) => { - this.#authContext = auth; + new UmbContextConsumerController(host, UMB_CURRENT_USER_CONTEXT, (instance) => { + this.#currentUserContext = instance; }); } @@ -82,10 +82,10 @@ export class UmbUserWorkspaceContext } async #reloadCurrentUser(savedUserId: string): Promise { - if (!this.#authContext) return; - const currentUser = await firstValueFrom(this.#authContext.currentUser); + if (!this.#currentUserContext) return; + const currentUser = await firstValueFrom(this.#currentUserContext.currentUser); if (currentUser?.id === savedUserId) { - await this.#authContext.fetchCurrentUser(); + await this.#currentUserContext.requestCurrentUser(); } } diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.interface.ts similarity index 55% rename from src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts rename to src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.interface.ts index cbd3cc2372..33b70eceac 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.interface.ts @@ -1,7 +1,8 @@ -import type { UmbLoggedInUser } from './types.js'; -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export interface IUmbAuthContext { + isAuthorized: Observable; -export interface IUmbAuth { /** * Initiates the login flow. */ @@ -15,7 +16,7 @@ export interface IUmbAuth { /** * Checks if there is a token and it is still valid. */ - isAuthorized(): boolean; + getIsAuthorized(): boolean; /** * Gets the latest token from the Management API. @@ -33,28 +34,8 @@ export interface IUmbAuth { */ getLatestToken(): Promise; - /** - * Get the current user model of the current user. - */ - get currentUser(): Observable; - - /** - * Get the current user's language ISO code. - */ - languageIsoCode: Observable; - - /** - * Make a server request for the current user and save the state - */ - fetchCurrentUser(): Promise; - /** * Signs the user out by removing any tokens from the browser. */ signOut(): Promise; - - /** - * Check if the given user is the current user. - */ - isUserCurrentUser(userId: string): Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.token.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.token.ts new file mode 100644 index 0000000000..f1ce0984da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.token.ts @@ -0,0 +1,4 @@ +import { IUmbAuthContext } from './auth.context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_AUTH_CONTEXT = new UmbContextToken('UmbAuthContext'); diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts index 7c45ea7793..7b6b143058 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts @@ -1,32 +1,25 @@ -import { IUmbAuth } from './auth.interface.js'; +import { IUmbAuthContext } from './auth.context.interface.js'; import { UmbAuthFlow } from './auth-flow.js'; -import { UmbLoggedInUser } from './types.js'; -import { UserResource } from '@umbraco-cms/backoffice/backend-api'; +import { UMB_AUTH_CONTEXT } from './auth.context.token.js'; import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; -export class UmbAuthContext extends UmbBaseController implements IUmbAuth { +export class UmbAuthContext extends UmbBaseController implements IUmbAuthContext { + #isAuthorized = new UmbBooleanState(false); + readonly isAuthorized = this.#isAuthorized.asObservable(); - #currentUser = new UmbObjectState(undefined); - readonly currentUser = this.#currentUser.asObservable(); - - #isLoggedIn = new UmbBooleanState(false); - readonly isLoggedIn = this.#isLoggedIn.asObservable(); - readonly languageIsoCode = this.#currentUser.asObservablePart((user) => user?.languageIsoCode ?? 'en-us'); + #isBypassed = false; + #backofficePath: string; #authFlow; - constructor(host: UmbControllerHostElement, serverUrl: string, redirectUrl: string) { - super(host) - this.#authFlow = new UmbAuthFlow(serverUrl, redirectUrl); + constructor(host: UmbControllerHostElement, serverUrl: string, backofficePath: string, isBypassed: boolean) { + super(host); + this.#isBypassed = isBypassed; + this.#backofficePath = backofficePath; - this.observe(this.isLoggedIn, (isLoggedIn) => { - if (isLoggedIn) { - this.fetchCurrentUser(); - } - }); + this.#authFlow = new UmbAuthFlow(serverUrl, this.#getRedirectUrl()); + this.provideContext(UMB_AUTH_CONTEXT, this); } /** @@ -36,27 +29,29 @@ export class UmbAuthContext extends UmbBaseController implements IUmbAuth { return this.#authFlow.makeAuthorizationRequest(); } - /* TEMPORARY METHOD UNTIL RESPONSIBILITY IS MOVED TO CONTEXT */ - setLoggedIn(newValue: boolean): void { - return this.#isLoggedIn.next(newValue); - } - - isAuthorized() { - return this.#authFlow.isAuthorized(); + /** + * Checks if the user is authorized. If Authorization is bypassed, the user is always authorized. + * @returns {boolean} True if the user is authorized, otherwise false. + */ + getIsAuthorized() { + if (this.#isBypassed) { + this.#isAuthorized.next(true); + return true; + } else { + const isAuthorized = this.#authFlow.isAuthorized(); + this.#isAuthorized.next(isAuthorized); + return isAuthorized; + } } + /** + * Sets the initial state of the auth flow. + * @returns {Promise} + */ setInitialState(): Promise { return this.#authFlow.setInitialState(); } - async fetchCurrentUser(): Promise { - const { data } = await tryExecuteAndNotify(this._host, UserResource.getUserCurrent()); - - this.#currentUser.next(data); - - return data; - } - /** * Gets the latest token from the Management API. * If the token is expired, it will be refreshed. @@ -71,19 +66,14 @@ export class UmbAuthContext extends UmbBaseController implements IUmbAuth { /** * Signs the user out by removing any tokens from the browser. + * @return {*} {Promise} + * @memberof UmbAuthContext */ signOut(): Promise { return this.#authFlow.signOut(); } - /** - * Checks if a user is the current user. - * - * @param userId The user id to check - * @returns True if the user is the current user, otherwise false - */ - async isUserCurrentUser(userId: string): Promise { - const currentUser = await firstValueFrom(this.currentUser); - return currentUser?.id === userId; + #getRedirectUrl() { + return `${window.location.origin}${this.#backofficePath}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.token.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.token.ts deleted file mode 100644 index 5c47874e77..0000000000 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.token.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IUmbAuth } from './auth.interface.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; - -export const UMB_AUTH_CONTEXT = new UmbContextToken( - 'UmbAuthContext' -); diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts index b4989d9932..f2e7f83bb1 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts @@ -1,5 +1,3 @@ -export * from './auth.interface.js'; +export * from './auth.context.interface.js'; export * from './auth.context.js'; - -export * from './types.js'; -export * from './auth.token.js'; +export * from './auth.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/types.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/types.ts deleted file mode 100644 index 3b481a3c47..0000000000 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { CurrentUserResponseModel as UmbLoggedInUser } from '@umbraco-cms/backoffice/backend-api';