Merge pull request #2262 from umbraco/v15/feature/member-client-credentials
Feature: Member client credentials
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "14.3.0",
|
||||
"version": "15.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type UmbMemberKindType = 'Default' | 'Api';
|
||||
|
||||
export const UmbMemberKind = Object.freeze({
|
||||
DEFAULT: 'Default',
|
||||
API: 'Api',
|
||||
});
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user