Merge pull request #779 from umbraco/feature/current-user

Feature/current-user
This commit is contained in:
Jacob Overgaard
2023-06-13 12:49:03 +02:00
committed by GitHub
18 changed files with 174 additions and 137 deletions

View File

@@ -1,5 +1,5 @@
import type { UmbAppErrorElement } from './app-error.element.js';
import { UMB_AUTH, UmbAuthFlow } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH, UmbAuthFlow, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { UMB_APP, UmbAppContext } from '@umbraco-cms/backoffice/context';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
@@ -81,7 +81,9 @@ export class UmbAppElement extends UmbLitElement {
this.#authFlow = new UmbAuthFlow(this.serverUrl, redirectUrl);
this.provideContext(UMB_AUTH, this.#authFlow);
const authContext = new UmbAuthContext(this, this.#authFlow);
this.provideContext(UMB_AUTH, authContext);
this.provideContext(UMB_APP, new UmbAppContext({ backofficePath: this.backofficePath, serverUrl: this.serverUrl }));
@@ -99,6 +101,8 @@ export class UmbAppElement extends UmbLitElement {
OpenAPI.WITH_CREDENTIALS = true;
}
authContext.isLoggedIn.next(true);
// Initialise the router
this.#redirect();
} catch (error) {

View File

@@ -1,3 +1,4 @@
import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UmbData } from './data.js';
import { PagedUserResponseModel, UserResponseModel, UserStateModel } from '@umbraco-cms/backoffice/backend-api';
@@ -18,6 +19,25 @@ class UmbUsersData extends UmbData<UserResponseModel> {
return this.data.find((user) => user.id === id);
}
getCurrentUser(): UmbLoggedInUser {
const firstUser = this.data[0];
return {
$type: 'CurrentUserResponseModel',
id: firstUser.id,
name: firstUser.name,
email: firstUser.email,
userName: firstUser.email,
avatarUrls: [],
hasAccessToAllLanguages: true,
languageIsoCode: firstUser.languageIsoCode,
languages: [],
contentStartNodeIds: firstUser.contentStartNodeIds,
mediaStartNodeIds: firstUser.mediaStartNodeIds,
permissions: [],
};
}
save(id: string, saveItem: UserResponseModel) {
const foundIndex = this.data.findIndex((item) => item.id === id);
if (foundIndex !== -1) {
@@ -96,9 +116,9 @@ export const data: Array<UserResponseModel & { type: string }> = [
$type: 'UserResponseModel',
contentStartNodeIds: [],
mediaStartNodeIds: [],
name: 'Erny Baptista',
email: 'ebaptista1@csmonitor.com',
languageIsoCode: 'Kannada',
name: 'Umbraco User',
email: 'noreply@umbraco.com',
languageIsoCode: 'en-US',
state: UserStateModel.ACTIVE,
lastLoginDate: '9/10/2022',
lastLockoutDate: '11/23/2021',

View File

@@ -3,18 +3,31 @@ const { rest } = window.MockServiceWorker;
import { umbUsersData } from '../data/users.data.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
let isAuthenticated = true;
const slug = '/user';
export const handlers = [
rest.get(umbracoPath(`${slug}`), (req, res, ctx) => {
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
//TODO: Implementer filter
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
}),
rest.get(umbracoPath(`${slug}/filter`), (req, res, ctx) => {
//TODO: Implementer filter
rest.get(umbracoPath(`${slug}/current`), (_req, res, ctx) => {
const loggedInUser = umbUsersData.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
rest.get(umbracoPath(`${slug}/sections`), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings', 'My.Section.Custom'],
})
);
}),
rest.get(umbracoPath(`${slug}`), (req, res, ctx) => {
const response = umbUsersData.getAll();
return res(ctx.status(200), ctx.json(response));
@@ -40,51 +53,4 @@ export const handlers = [
return res(ctx.status(200), ctx.json(saved));
}),
rest.post(umbracoPath('/user/login'), (_req, res, ctx) => {
// Persist user's authentication in the session
isAuthenticated = true;
return res(
// Respond with a 200 status code
ctx.status(201)
);
}),
rest.post(umbracoPath('/user/logout'), (_req, res, ctx) => {
// Persist user's authentication in the session
isAuthenticated = false;
return res(
// Respond with a 200 status code
ctx.status(201)
);
}),
rest.get(umbracoPath('/user'), (_req, res, ctx) => {
// Check if the user is authenticated in this session
if (!isAuthenticated) {
// If not authenticated, respond with a 403 error
return res(
ctx.status(403),
ctx.json({
errorMessage: 'Not authorized',
})
);
}
// If authenticated, return a mocked user details
return res(
ctx.status(200),
ctx.json({
username: 'admin',
role: 'administrator',
})
);
}),
rest.get(umbracoPath('/user/sections'), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
sections: ['Umb.Section.Content', 'Umb.Section.Media', 'Umb.Section.Settings', 'My.Section.Custom'],
})
);
}),
];

View File

@@ -1,12 +1,40 @@
import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UMB_AUTH } from '@umbraco-cms/backoffice/auth';
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 LitElement {
export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
#auth?: typeof UMB_AUTH.TYPE;
@state()
private name = '';
constructor() {
super();
this.consumeContext(UMB_AUTH, (instance) => {
this.#auth = instance;
this.#observeCurrentUser();
});
}
#observeCurrentUser(): void {
if (!this.#auth) return;
this.observe(this.#auth.currentUser, (user) => {
this.name = user?.name ?? '';
});
}
render() {
return html`
<uui-box>
<h1>Welcome</h1>
<p>You can find details about the POC in the readme.md file.</p>
<h1>Welcome, ${this.name}</h1>
<p>This is a preview version of Umbraco, where you can have a first-hand look at the new Backoffice.</p>
<p>There is currently very limited functionality.</p>
<p>
Please refer to the
<a target="_blank" href="http://docs.umbraco.com/umbraco-backoffice/">documentation</a> to learn more about
what is possible.
</p>
</uui-box>
`;
}

View File

@@ -1,5 +1,3 @@
import type { UmbLoggedInUser } from './types.js';
import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './current-user.store.js';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { css, CSSResultGroup, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import {
@@ -8,13 +6,14 @@ import {
UMB_CURRENT_USER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
@customElement('umb-current-user-header-app')
export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
@state()
private _currentUser?: UmbLoggedInUser;
private _currentUserStore?: UmbCurrentUserStore;
private _auth?: typeof UMB_AUTH.TYPE;
private _modalContext?: UmbModalManagerContext;
constructor() {
@@ -24,16 +23,16 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
this._modalContext = instance;
});
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, (instance) => {
this._currentUserStore = instance;
this.consumeContext(UMB_AUTH, (instance) => {
this._auth = instance;
this._observeCurrentUser();
});
}
private async _observeCurrentUser() {
if (!this._currentUserStore) return;
if (!this._auth) return;
this.observe(this._currentUserStore.currentUser, (currentUser) => {
this.observe(this._auth.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}

View File

@@ -1,10 +0,0 @@
import type { UmbLoggedInUser } from './types.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
export const UMB_CURRENT_USER_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbCurrentUserStore>('UmbCurrentUserStore');
export class UmbCurrentUserStore {
#currentUser = new UmbObjectState<UmbLoggedInUser | undefined>(undefined);
public readonly currentUser = this.#currentUser.asObservable();
}

View File

@@ -1,4 +1,2 @@
export * from './types.js';
// TODO:Do not export store, but instead export future repository
export * from './current-user.store.js';
export * from './current-user-history.store.js';

View File

@@ -1,6 +1,4 @@
import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../current-user.store.js';
import type { UmbLoggedInUser } from '../../types.js';
import { UMB_AUTH } from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
import { UMB_APP } from '@umbraco-cms/backoffice/context';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { css, CSSResultGroup, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
@@ -15,8 +13,6 @@ export class UmbCurrentUserModalElement extends UmbLitElement {
@state()
private _currentUser?: UmbLoggedInUser;
private _currentUserStore?: UmbCurrentUserStore;
#auth?: typeof UMB_AUTH.TYPE;
#appContext?: typeof UMB_APP.TYPE;
@@ -24,8 +20,8 @@ export class UmbCurrentUserModalElement extends UmbLitElement {
constructor() {
super();
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, (instance) => {
this._currentUserStore = instance;
this.consumeContext(UMB_AUTH, (instance) => {
this.#auth = instance;
this._observeCurrentUser();
});
@@ -41,9 +37,9 @@ export class UmbCurrentUserModalElement extends UmbLitElement {
}
private async _observeCurrentUser() {
if (!this._currentUserStore) return;
if (!this.#auth) return;
this.observe(this._currentUserStore.currentUser, (currentUser) => {
this.observe(this.#auth.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}

View File

@@ -1,22 +0,0 @@
import type { UmbEntityBase } from '@umbraco-cms/backoffice/models';
export interface UserEntity extends UmbEntityBase {
type: 'user';
}
export type UserStatus = 'enabled' | 'inactive' | 'invited' | 'disabled';
export interface UmbLoggedInUser extends UserEntity {
email: string;
status: UserStatus;
language: string;
lastLoginDate?: string;
lastLockoutDate?: string;
lastPasswordChangeDate?: string;
updateDate: string;
createDate: string;
failedLoginAttempts: number;
userGroups: Array<string>;
contentStartNodes: Array<string>;
mediaStartNodes: Array<string>;
}

View File

@@ -1,5 +1,3 @@
import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../current-user.store.js';
import type { UmbLoggedInUser } from '../types.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -8,6 +6,7 @@ import {
UMB_CHANGE_PASSWORD_MODAL,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
} from '@umbraco-cms/backoffice/modal';
import { UMB_AUTH, type UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
@customElement('umb-user-profile-app-profile')
export class UmbUserProfileAppProfileElement extends UmbLitElement {
@@ -15,7 +14,7 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
private _currentUser?: UmbLoggedInUser;
private _modalContext?: UmbModalManagerContext;
private _currentUserStore?: UmbCurrentUserStore;
private _auth?: typeof UMB_AUTH.TYPE;
constructor() {
super();
@@ -24,8 +23,8 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
this._modalContext = instance;
});
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, (instance) => {
this._currentUserStore = instance;
this.consumeContext(UMB_AUTH, (instance) => {
this._auth = instance;
this._observeCurrentUser();
});
@@ -33,9 +32,9 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
}
private async _observeCurrentUser() {
if (!this._currentUserStore) return;
if (!this._auth) return;
this.observe(this._currentUserStore.currentUser, (currentUser) => {
this.observe(this._auth.currentUser, (currentUser) => {
this._currentUser = currentUser;
});
}

View File

@@ -3,7 +3,6 @@ import { manifests as userManifests } from './users/manifests.js';
import { manifests as userSectionManifests } from './user-section/manifests.js';
import { manifests as currentUserManifests } from './current-user/manifests.js';
import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './current-user/current-user.store.js';
import {
UmbCurrentUserHistoryStore,
UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN,
@@ -21,7 +20,6 @@ export const manifests = [...userGroupManifests, ...userManifests, ...userSectio
export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
extensionRegistry.registerMany(manifests);
new UmbContextProviderController(host, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, new UmbCurrentUserStore());
new UmbUserItemStore(host);
new UmbUserGroupItemStore(host);
new UmbContextProviderController(

View File

@@ -1,4 +1,3 @@
import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from '../../current-user/current-user.store.js';
import { getLookAndColorFromUserStatus } from '../../utils.js';
import { UmbUserRepository } from '../repository/user.repository.js';
import { UmbUserGroupInputElement } from '../../user-groups/components/input-user-group/user-group-input.element.js';
@@ -24,16 +23,17 @@ import { UserResponseModel, UserStateModel } from '@umbraco-cms/backoffice/backe
import { createExtensionClass } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
@customElement('umb-user-workspace-editor')
export class UmbUserWorkspaceEditorElement extends UmbLitElement {
@state()
private _currentUser?: any;
private _currentUser?: UmbLoggedInUser;
@state()
private _user?: UserResponseModel;
#currentUserStore?: UmbCurrentUserStore;
#auth?: typeof UMB_AUTH.TYPE;
#modalContext?: UmbModalManagerContext;
#languages = []; //TODO Add languages
#workspaceContext?: UmbUserWorkspaceContext;
@@ -43,8 +43,8 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
constructor() {
super();
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, (store) => {
this.#currentUserStore = store;
this.consumeContext(UMB_AUTH, (instance) => {
this.#auth = instance;
this.#observeCurrentUser();
});
@@ -76,8 +76,8 @@ export class UmbUserWorkspaceEditorElement extends UmbLitElement {
}
#observeCurrentUser() {
if (!this.#currentUserStore) return;
this.observe(this.#currentUserStore.currentUser, (currentUser) => (this._currentUser = currentUser));
if (!this.#auth) return;
this.observe(this.#auth.currentUser, (currentUser) => (this._currentUser = currentUser));
}
#onUserStatusChange() {

View File

@@ -13,7 +13,6 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
import type { IUmbAuth } from './auth.interface.js';
import {
BaseTokenRequestHandler,
BasicQueryStringUtils,
@@ -81,7 +80,7 @@ class UmbNoHashQueryStringUtils extends BasicQueryStringUtils {
* a. This will redirect the user to the authorization endpoint of the server
* 4. After login, get the latest token before each request to the server by calling the `performWithFreshTokens` method
*/
export class UmbAuthFlow implements IUmbAuth {
export class UmbAuthFlow {
// handlers
readonly #notifier: AuthorizationNotifier;
readonly #authorizationHandler: RedirectRequestHandler;
@@ -164,7 +163,6 @@ export class UmbAuthFlow implements IUmbAuth {
if (response.isValid()) {
this.#accessTokenResponse = response;
this.#refreshToken = this.#accessTokenResponse.refreshToken;
return;
}
}
@@ -225,7 +223,7 @@ export class UmbAuthFlow implements IUmbAuth {
}
/**
* This method will check if the user is logged in by validting the timestamp of the stored token.
* This method will check if the user is logged in by validating the timestamp of the stored token.
* If no token is stored, it will return false.
*
* @returns true if the user is logged in, false otherwise.

View File

@@ -0,0 +1,46 @@
import { IUmbAuth } from './auth.interface.js';
import { UmbAuthFlow } from './auth-flow.js';
import { UmbLoggedInUser } from './types.js';
import { UserResource } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { ReplaySubject } from '@umbraco-cms/backoffice/external/rxjs';
export class UmbAuthContext implements IUmbAuth {
#currentUser = new UmbObjectState<UmbLoggedInUser | undefined>(undefined);
readonly currentUser = this.#currentUser.asObservable();
readonly isLoggedIn = new ReplaySubject<boolean>(1);
#host;
#authFlow;
constructor(host: UmbControllerHostElement, authFlow: UmbAuthFlow) {
this.#host = host;
this.#authFlow = authFlow;
this.isLoggedIn.subscribe((isLoggedIn) => {
if (isLoggedIn) {
this.fetchCurrentUser();
}
});
}
async fetchCurrentUser(): Promise<UmbLoggedInUser | undefined> {
const { data } = await tryExecuteAndNotify(this.#host, UserResource.getUserCurrent());
if (!data) return;
this.#currentUser.next(data);
return data;
}
performWithFreshTokens(): Promise<string> {
return this.#authFlow.performWithFreshTokens();
}
signOut(): Promise<void> {
return this.#authFlow.signOut();
}
}

View File

@@ -1,3 +1,6 @@
import type { UmbLoggedInUser } from './types.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface IUmbAuth {
/**
* Get the current user's access token.
@@ -10,6 +13,16 @@ export interface IUmbAuth {
*/
performWithFreshTokens(): Promise<string>;
/**
* Get the current user model of the current user.
*/
get currentUser(): Observable<UmbLoggedInUser | undefined>;
/**
* Make a server request for the current user and save the state
*/
fetchCurrentUser(): Promise<UmbLoggedInUser | undefined>;
/**
* Sign out the current user.
*/

View File

@@ -0,0 +1,7 @@
import { IUmbAuth } from './auth.interface.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_AUTH = new UmbContextToken<IUmbAuth>(
'UmbAuth',
'An instance of UmbAuthFlow that should be shared across the app.'
);

View File

@@ -1,10 +1,6 @@
import { IUmbAuth } from './auth.interface.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export type { IUmbAuth } from './auth.interface.js';
export { UmbAuthFlow } from './auth-flow.js';
export { UmbAuthContext } from './auth.context.js';
export const UMB_AUTH = new UmbContextToken<IUmbAuth>(
'UmbAuth',
'An instance of UmbAuthFlow that should be shared across the app.'
);
export * from './types.js';
export * from './auth.token.js';

View File

@@ -0,0 +1 @@
export type { CurrentUserResponseModel as UmbLoggedInUser } from '@umbraco-cms/backoffice/backend-api';