Skip to content

Commit

Permalink
Merge pull request #979 from umbraco/feature-split-auth-and-user
Browse files Browse the repository at this point in the history
Improvement: Split Current User from auth
  • Loading branch information
madsrasmussen committed Nov 13, 2023
2 parents 4af43be + cf63a15 commit 34509c5
Show file tree
Hide file tree
Showing 28 changed files with 459 additions and 327 deletions.
11 changes: 7 additions & 4 deletions src/apps/app/app.context.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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');
135 changes: 34 additions & 101 deletions src/apps/app/app.element.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
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';
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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -238,26 +171,26 @@ 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;
}

// Save location.href so we can redirect to it after login
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;
Expand Down
62 changes: 62 additions & 0 deletions src/apps/app/server-connection.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
39 changes: 23 additions & 16 deletions src/mocks/data/user.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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) : [];

Expand Down Expand Up @@ -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 }> = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
});
}
Expand Down
Loading

0 comments on commit 34509c5

Please sign in to comment.