Merge pull request #2262 from umbraco/v15/feature/member-client-credentials

Feature: Member client credentials
This commit is contained in:
Niels Lyngsø
2024-09-10 21:08:53 +02:00
committed by GitHub
16 changed files with 158 additions and 87 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "14.3.0",
"version": "15.0.0",
"type": "module",
"exports": {
".": null,

View File

@@ -357,6 +357,7 @@ export default {
member: {
createNewMember: 'Opret et nyt medlem',
allMembers: 'Alle medlemmer',
kind: 'Slags',
memberGroupNoProperties: 'Medlemgrupper har ingen yderligere egenskaber til redigering.',
'2fa': 'Totrinsbekræftelse',
duplicateMemberLogin: 'A member with this login already exists',
@@ -364,6 +365,8 @@ export default {
memberHasPassword: 'The member already has a password set',
memberLockoutNotEnabled: 'Lockout is not enabled for this member',
memberNotInGroup: "The member is not in group '%0%'",
memberKindDefault: 'Bruger',
memberKindApi: 'API Bruger',
},
contentType: {
copyFailed: 'Kopiering af indholdstypen fejlede',

View File

@@ -364,12 +364,15 @@ export default {
createNewMember: 'Create a new member',
allMembers: 'All Members',
duplicateMemberLogin: 'A member with this login already exists',
kind: 'Kind',
memberGroupNoProperties: 'Member groups have no additional properties for editing.',
memberHasGroup: "The member is already in group '%0%'",
memberHasPassword: 'The member already has a password set',
memberLockoutNotEnabled: 'Lockout is not enabled for this member',
memberNotInGroup: "The member is not in group '%0%'",
'2fa': 'Two-Factor Authentication',
memberKindDefault: 'Member',
memberKindApi: 'API Member',
},
contentType: {
copyFailed: 'Failed to copy content type',

View File

@@ -50,6 +50,7 @@ export class UmbMemberCollectionServerDataSource implements UmbCollectionDataSou
email: item.email,
variants: item.variants as UmbVariantModel[],
unique: item.id,
kind: item.kind,
lastLoginDate: item.lastLoginDate || null,
lastLockoutDate: item.lastLockoutDate || null,
lastPasswordChangeDate: item.lastPasswordChangeDate || null,

View File

@@ -1,4 +1,4 @@
import type { UmbMemberEntityType } from '../entity.js';
import type { UmbMemberDetailModel } from '../types.js';
export interface UmbMemberCollectionFilterModel {
skip?: number;
@@ -7,8 +7,5 @@ export interface UmbMemberCollectionFilterModel {
filter?: string;
}
export interface UmbMemberCollectionModel {
unique: string;
entityType: UmbMemberEntityType;
variants: Array<any>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UmbMemberCollectionModel extends UmbMemberDetailModel {}

View File

@@ -1,10 +1,12 @@
import type { UmbMemberCollectionModel } from '../../types.js';
import { UMB_MEMBER_COLLECTION_CONTEXT } from '../../member-collection.context-token.js';
import type { UmbMemberCollectionContext } from '../../member-collection.context.js';
import { UmbMemberKind } from '../../../utils/index.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbMemberTypeItemRepository } from '@umbraco-cms/backoffice/member-type';
@customElement('umb-member-table-collection-view')
export class UmbMemberTableCollectionViewElement extends UmbLitElement {
@@ -19,12 +21,29 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement {
name: this.localize.term('general_name'),
alias: 'memberName',
},
{
name: this.localize.term('general_username'),
alias: 'memberUsername',
},
{
name: this.localize.term('general_email'),
alias: 'memberEmail',
},
{
name: this.localize.term('content_membertype'),
alias: 'memberType',
},
{
name: this.localize.term('member_kind'),
alias: 'memberKind',
},
];
@state()
private _tableItems: Array<UmbTableItem> = [];
#collectionContext?: UmbMemberCollectionContext;
#memberTypeItemRepository = new UmbMemberTypeItemRepository(this);
constructor() {
super();
@@ -40,19 +59,44 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement {
this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver');
}
#createTableItems(members: Array<UmbMemberCollectionModel>) {
async #createTableItems(members: Array<UmbMemberCollectionModel>) {
const memberTypeUniques = members.map((member) => member.memberType.unique);
const { data: memberTypes } = await this.#memberTypeItemRepository.requestItems(memberTypeUniques);
this._tableItems = members.map((member) => {
// TODO: get correct variant name
const name = member.variants[0].name;
const kind =
member.kind === UmbMemberKind.API
? this.localize.term('member_memberKindApi')
: this.localize.term('member_memberKindDefault');
const memberType = memberTypes?.find((type) => type.unique === member.memberType.unique);
return {
id: member.unique,
icon: 'icon-user',
icon: memberType?.icon,
data: [
{
columnAlias: 'memberName',
value: html`<a href=${'section/member-management/workspace/member/edit/' + member.unique}>${name}</a>`,
},
{
columnAlias: 'memberUsername',
value: member.username,
},
{
columnAlias: 'memberEmail',
value: member.email,
},
{
columnAlias: 'memberType',
value: memberType?.name,
},
{
columnAlias: 'memberKind',
value: kind,
},
],
};
});

View File

@@ -6,6 +6,7 @@ import type { CreateMemberRequestModel, UpdateMemberRequestModel } from '@umbrac
import { MemberService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { UmbMemberKind } from '../../utils/index.js';
/**
* A data source for the Member that fetches data from the server
@@ -42,6 +43,7 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource<UmbMemberD
isApproved: false,
isLockedOut: false,
isTwoFactorEnabled: false,
kind: UmbMemberKind.DEFAULT,
failedPasswordAttempts: 0,
lastLoginDate: null,
lastLockoutDate: null,
@@ -90,6 +92,7 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource<UmbMemberD
isApproved: data.isApproved,
isLockedOut: data.isLockedOut,
isTwoFactorEnabled: data.isTwoFactorEnabled,
kind: data.kind,
failedPasswordAttempts: data.failedPasswordAttempts,
lastLoginDate: data.lastLoginDate || null,
lastLockoutDate: data.lastLockoutDate || null,

View File

@@ -35,6 +35,7 @@ const mapper = (item: MemberItemResponseModel): UmbMemberItemModel => {
entityType: UMB_MEMBER_ENTITY_TYPE,
unique: item.id,
name: item.variants[0].name || '',
kind: item.kind,
memberType: {
unique: item.memberType.id,
icon: item.memberType.icon,

View File

@@ -1,4 +1,5 @@
import type { UmbMemberEntityType } from '../../entity.js';
import type { UmbMemberKindType } from '../../utils/index.js';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export interface UmbMemberItemModel {
@@ -11,6 +12,7 @@ export interface UmbMemberItemModel {
collection: UmbReferenceByUnique | null;
};
variants: Array<UmbMemberVariantItemModel>;
kind: UmbMemberKindType;
}
export interface UmbMemberVariantItemModel {

View File

@@ -43,6 +43,7 @@ export class UmbMemberSearchServerDataSource implements UmbSearchDataSource<UmbM
entityType: UMB_MEMBER_ENTITY_TYPE,
unique: item.id,
name: item.variants[0].name || '',
kind: item.kind,
memberType: {
unique: item.memberType.id,
icon: item.memberType.icon,

View File

@@ -1,4 +1,5 @@
import type { UmbMemberEntityType } from './entity.js';
import type { UmbMemberKindType } from './utils/index.js';
import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant';
export interface UmbMemberDetailModel {
@@ -9,6 +10,7 @@ export interface UmbMemberDetailModel {
isApproved: boolean;
isLockedOut: boolean;
isTwoFactorEnabled: boolean;
kind: UmbMemberKindType;
lastLockoutDate: string | null;
lastLoginDate: string | null;
lastPasswordChangeDate: string | null;

View File

@@ -0,0 +1,6 @@
export type UmbMemberKindType = 'Default' | 'Api';
export const UmbMemberKind = Object.freeze({
DEFAULT: 'Default',
API: 'Api',
});

View File

@@ -53,6 +53,7 @@ export class UmbMemberWorkspaceContext
readonly createDate = this.#currentData.asObservablePart((data) => data?.variants[0].createDate);
readonly updateDate = this.#currentData.asObservablePart((data) => data?.variants[0].updateDate);
readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.memberType.unique);
readonly kind = this.#currentData.asObservablePart((data) => data?.kind);
readonly structure = new UmbContentTypeStructureManager(this, new UmbMemberTypeDetailRepository(this));
readonly varies = this.structure.ownerContentTypePart((x) =>

View File

@@ -1,5 +1,6 @@
// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js';
import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context-token.js';
import { UmbMemberKind, type UmbMemberKindType } from '../../../utils/index.js';
import { TimeFormatOptions } from './utils.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
@@ -13,8 +14,10 @@ import { UmbMemberTypeItemRepository } from '@umbraco-cms/backoffice/member-type
export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement {
@state()
private _memberTypeUnique = '';
@state()
private _memberTypeName = '';
@state()
private _memberTypeIcon = '';
@@ -30,6 +33,9 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple
@state()
private _unique = '';
@state()
private _memberKind?: UmbMemberKindType;
#workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE;
#memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this);
@@ -51,6 +57,7 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple
this.observe(this.#workspaceContext.createDate, (date) => (this._createDate = this.#setDateFormat(date)));
this.observe(this.#workspaceContext.updateDate, (date) => (this._updateDate = this.#setDateFormat(date)));
this.observe(this.#workspaceContext.unique, (unique) => (this._unique = unique || ''));
this.observe(this.#workspaceContext.kind, (kind) => (this._memberKind = kind));
const memberType = (await this.#memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0];
if (!memberType) return;
@@ -70,41 +77,45 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple
#renderGeneralSection() {
return html`
<div class="general-item">
<strong><umb-localize key="content_createDate">Created</umb-localize></strong>
<span> ${this._createDate} </span>
</div>
<div class="general-item">
<strong><umb-localize key="content_updateDate">Last edited</umb-localize></strong>
<span> ${this._updateDate} </span>
</div>
<div class="general-item">
<strong><umb-localize key="content_membertype">Member Type</umb-localize></strong>
<uui-ref-node
standalone
.name=${this._memberTypeName}
.href=${this._editMemberTypePath + 'edit/' + this._memberTypeUnique}>
<umb-icon slot="icon" .name=${this._memberTypeIcon}></umb-icon>
</uui-ref-node>
</div>
<div class="general-item">
<strong><umb-localize key="template_id">Id</umb-localize></strong>
<span>${this._unique}</span>
</div>
<umb-stack look="compact">
<div>
<h4><umb-localize key="content_createDate">Created</umb-localize></h4>
<span> ${this._createDate} </span>
</div>
<div>
<h4><umb-localize key="content_updateDate">Last edited</umb-localize></h4>
<span> ${this._updateDate} </span>
</div>
<div>
<h4><umb-localize key="content_membertype">Member Type</umb-localize></h4>
<uui-ref-node
standalone
.name=${this._memberTypeName}
.href=${this._editMemberTypePath + 'edit/' + this._memberTypeUnique}>
<umb-icon slot="icon" .name=${this._memberTypeIcon}></umb-icon>
</uui-ref-node>
</div>
<div>
<h4><umb-localize key="member_kind"></umb-localize></h4>
<span
>${this._memberKind === UmbMemberKind.API
? this.localize.term('member_memberKindApi')
: this.localize.term('member_memberKindDefault')}</span
>
</div>
<div>
<h4><umb-localize key="template_id">Id</umb-localize></h4>
<span>${this._unique}</span>
</div>
</umb-stack>
`;
}
static override styles = [
UmbTextStyles,
css`
.general-item {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-1);
}
.general-item:not(:last-child) {
margin-bottom: var(--uui-size-space-6);
h4 {
margin: 0;
}
`,
];

View File

@@ -208,34 +208,36 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
return html`
<div id="right-column">
<uui-box>
<div class="general-item">
<strong><umb-localize key="user_failedPasswordAttempts">Failed login attempts</umb-localize></strong>
<span>${this._workspaceContext.failedPasswordAttempts}</span>
</div>
<div class="general-item">
<strong><umb-localize key="user_lastLockoutDate">Last lockout date</umb-localize></strong>
<span>
${this._workspaceContext.lastLockOutDate
? this.localize.date(this._workspaceContext.lastLockOutDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
<div class="general-item">
<strong><umb-localize key="user_lastLogin">Last login</umb-localize></strong>
<span>
${this._workspaceContext.lastLoginDate
? this.localize.date(this._workspaceContext.lastLoginDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
<div class="general-item">
<strong><umb-localize key="user_passwordChangedGeneric">Password changed</umb-localize></strong>
<span>
${this._workspaceContext.lastPasswordChangeDate
? this.localize.date(this._workspaceContext.lastPasswordChangeDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
<umb-stack look="compact">
<div>
<h4><umb-localize key="user_failedPasswordAttempts">Failed login attempts</umb-localize></h4>
<span>${this._workspaceContext.failedPasswordAttempts}</span>
</div>
<div>
<h4><umb-localize key="user_lastLockoutDate">Last lockout date</umb-localize></h4>
<span>
${this._workspaceContext.lastLockOutDate
? this.localize.date(this._workspaceContext.lastLockOutDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
<div>
<h4><umb-localize key="user_lastLogin">Last login</umb-localize></h4>
<span>
${this._workspaceContext.lastLoginDate
? this.localize.date(this._workspaceContext.lastLoginDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
<div>
<h4><umb-localize key="user_passwordChangedGeneric">Password changed</umb-localize></h4>
<span>
${this._workspaceContext.lastPasswordChangeDate
? this.localize.date(this._workspaceContext.lastPasswordChangeDate, TimeFormatOptions)
: this.localize.term('general_never')}
</span>
</div>
</umb-stack>
</uui-box>
<uui-box>
@@ -291,14 +293,8 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
color: var(--uui-color-danger);
}
.general-item {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-1);
}
.general-item:not(:last-child) {
margin-bottom: var(--uui-size-space-6);
h4 {
margin: 0;
}
`,
];

View File

@@ -97,18 +97,20 @@ export class UmbUserWorkspaceInfoElement extends UmbLitElement {
#renderInfoList() {
return html`
${repeat(
this._userInfo,
(item) => item.labelKey,
(item) => this.#renderInfoItem(item.labelKey, item.value),
)}
<umb-stack look="compact">
${repeat(
this._userInfo,
(item) => item.labelKey,
(item) => this.#renderInfoItem(item.labelKey, item.value),
)}
</umb-stack>
`;
}
#renderInfoItem(labelKey: string, value?: string | number) {
return html`
<div class="user-info-item">
<b><umb-localize key=${labelKey}></umb-localize></b>
<div>
<h4><umb-localize key=${labelKey}></umb-localize></h4>
<span>${value}</span>
</div>
`;
@@ -125,16 +127,14 @@ export class UmbUserWorkspaceInfoElement extends UmbLitElement {
width: fit-content;
}
h4 {
margin: 0;
}
#state {
border-bottom: 1px solid var(--uui-color-divider);
padding-bottom: var(--uui-size-space-4);
}
.user-info-item {
display: flex;
flex-direction: column;
margin-bottom: var(--uui-size-space-3);
}
`,
];
}