Merge pull request #979 from umbraco/feature-split-auth-and-user
Improvement: Split Current User from auth
This commit is contained in:
@@ -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<UmbAppContext>('UMB_APP');
|
||||
export const UMB_APP_CONTEXT = new UmbContextToken<UmbAppContext>('UMB_APP');
|
||||
|
||||
@@ -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 <umb-debug> component
|
||||
this.addEventListener(umbDebugContextEventType, (event: any) => {
|
||||
// Once we got to the outter most component <umb-app>
|
||||
// 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 <umb-debug> 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;
|
||||
|
||||
62
src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts
Normal file
62
src/Umbraco.Web.UI.Client/src/apps/app/server-connection.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<UserResponseModel> {
|
||||
* @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<UserResponseModel> {
|
||||
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<UserResponseModel & { type: string }> = [
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<new (args: TinyMcePluginArguments) => 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<any> | Map<PropertyKey, unknown>): Promise<void> {
|
||||
|
||||
@@ -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 <umb-debug> component
|
||||
this.getHostElement().addEventListener(umbDebugContextEventType, this.#onContextDebug as unknown as EventListener);
|
||||
}
|
||||
|
||||
#onContextDebug = (event: any) => {
|
||||
// Once we got to the outter most component <umb-app>
|
||||
// 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 <umb-debug> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './debug.element.js';
|
||||
export * from './context-debug.controller.js';
|
||||
export * from './manifests.js';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<UmbCurrentUser | undefined>(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<boolean> {
|
||||
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>('UmbCurrentUserContext');
|
||||
@@ -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';
|
||||
|
||||
@@ -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<ManifestTypes> = [
|
||||
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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type { CurrentUserResponseModel as UmbCurrentUser } from '@umbraco-cms/backoffice/backend-api';
|
||||
@@ -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 ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<UmbChangePasswordModalData, UmbChangePasswordModalValue> {
|
||||
export class UmbChangePasswordModalElement extends UmbModalBaseElement<
|
||||
UmbChangePasswordModalData,
|
||||
UmbChangePasswordModalValue
|
||||
> {
|
||||
@state()
|
||||
private _headline: string = 'Change password';
|
||||
|
||||
@@ -13,7 +20,7 @@ export class UmbChangePasswordModalElement extends UmbModalBaseElement<UmbChange
|
||||
private _isCurrentUser: boolean = false;
|
||||
|
||||
#userItemRepository = new UmbUserItemRepository(this);
|
||||
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
|
||||
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
|
||||
|
||||
#onClose() {
|
||||
this.modalContext?.reject();
|
||||
@@ -41,8 +48,8 @@ export class UmbChangePasswordModalElement extends UmbModalBaseElement<UmbChange
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
|
||||
this.#authContext = instance;
|
||||
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => {
|
||||
this.#currentUserContext = instance;
|
||||
this.#setIsCurrentUser();
|
||||
});
|
||||
}
|
||||
@@ -53,12 +60,12 @@ export class UmbChangePasswordModalElement extends UmbModalBaseElement<UmbChange
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#authContext) {
|
||||
if (!this.#currentUserContext) {
|
||||
this._isCurrentUser = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._isCurrentUser = await this.#authContext.isUserCurrentUser(this.data.userId);
|
||||
this._isCurrentUser = await this.#currentUserContext.isUserCurrentUser(this.data.userId);
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<UmbUserRepository, UmbUserDetail>
|
||||
implements UmbSaveableWorkspaceContextInterface<UmbUserDetail | undefined>
|
||||
{
|
||||
#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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
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<string>;
|
||||
|
||||
/**
|
||||
* Get the current user model of the current user.
|
||||
*/
|
||||
get currentUser(): Observable<UmbLoggedInUser | undefined>;
|
||||
|
||||
/**
|
||||
* Get the current user's language ISO code.
|
||||
*/
|
||||
languageIsoCode: Observable<string>;
|
||||
|
||||
/**
|
||||
* Make a server request for the current user and save the state
|
||||
*/
|
||||
fetchCurrentUser(): Promise<UmbLoggedInUser | undefined>;
|
||||
|
||||
/**
|
||||
* Signs the user out by removing any tokens from the browser.
|
||||
*/
|
||||
signOut(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Check if the given user is the current user.
|
||||
*/
|
||||
isUserCurrentUser(userId: string): Promise<boolean>;
|
||||
}
|
||||
@@ -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<IUmbAuthContext>('UmbAuthContext');
|
||||
@@ -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<boolean>(false);
|
||||
readonly isAuthorized = this.#isAuthorized.asObservable();
|
||||
|
||||
#currentUser = new UmbObjectState<UmbLoggedInUser | undefined>(undefined);
|
||||
readonly currentUser = this.#currentUser.asObservable();
|
||||
|
||||
#isLoggedIn = new UmbBooleanState<boolean>(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<void>}
|
||||
*/
|
||||
setInitialState(): Promise<void> {
|
||||
return this.#authFlow.setInitialState();
|
||||
}
|
||||
|
||||
async fetchCurrentUser(): Promise<UmbLoggedInUser | undefined> {
|
||||
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<void>}
|
||||
* @memberof UmbAuthContext
|
||||
*/
|
||||
signOut(): Promise<void> {
|
||||
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<boolean> {
|
||||
const currentUser = await firstValueFrom(this.currentUser);
|
||||
return currentUser?.id === userId;
|
||||
#getRedirectUrl() {
|
||||
return `${window.location.origin}${this.#backofficePath}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IUmbAuth>(
|
||||
'UmbAuthContext'
|
||||
);
|
||||
@@ -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';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type { CurrentUserResponseModel as UmbLoggedInUser } from '@umbraco-cms/backoffice/backend-api';
|
||||
Reference in New Issue
Block a user