@ngbplatform/ui 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +44 -0
- package/package.json +61 -0
- package/public/favicon.svg +6 -0
- package/public/silent-check-sso.html +8 -0
- package/src/assets/ngb_logo.svg +17 -0
- package/src/assets/ngb_logo_color.svg +17 -0
- package/src/index.ts +1023 -0
- package/src/ngb/README.md +27 -0
- package/src/ngb/accounting/NgbAccountingPeriodClosingPage.vue +1157 -0
- package/src/ngb/accounting/NgbChartOfAccountEditor.vue +590 -0
- package/src/ngb/accounting/NgbChartOfAccountsPage.vue +420 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryEditPage.vue +796 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryLinesEditor.vue +454 -0
- package/src/ngb/accounting/NgbGeneralJournalEntryListPage.vue +186 -0
- package/src/ngb/accounting/api.ts +79 -0
- package/src/ngb/accounting/generalJournalEntry.ts +127 -0
- package/src/ngb/accounting/generalJournalEntryApi.ts +112 -0
- package/src/ngb/accounting/generalJournalEntryTypes.ts +155 -0
- package/src/ngb/accounting/navigation.ts +83 -0
- package/src/ngb/accounting/periodClosing.ts +75 -0
- package/src/ngb/accounting/periodClosingApi.ts +60 -0
- package/src/ngb/accounting/periodClosingTypes.ts +73 -0
- package/src/ngb/accounting/types.ts +57 -0
- package/src/ngb/api/audit.ts +23 -0
- package/src/ngb/api/catalogs.ts +50 -0
- package/src/ngb/api/contracts.ts +285 -0
- package/src/ngb/api/documents.ts +170 -0
- package/src/ngb/api/http.ts +446 -0
- package/src/ngb/api/lookups.ts +14 -0
- package/src/ngb/api/types.ts +6 -0
- package/src/ngb/auth/index.ts +13 -0
- package/src/ngb/auth/keycloak.ts +374 -0
- package/src/ngb/auth/router.ts +35 -0
- package/src/ngb/auth/types.ts +12 -0
- package/src/ngb/auth/useAuthStore.ts +209 -0
- package/src/ngb/command-palette/NgbCommandPaletteDialog.vue +342 -0
- package/src/ngb/command-palette/NgbCommandPaletteEmptyState.vue +55 -0
- package/src/ngb/command-palette/NgbCommandPaletteFooterHints.vue +29 -0
- package/src/ngb/command-palette/api.ts +10 -0
- package/src/ngb/command-palette/config.ts +16 -0
- package/src/ngb/command-palette/presentation.ts +85 -0
- package/src/ngb/command-palette/search.ts +130 -0
- package/src/ngb/command-palette/staticItems.ts +243 -0
- package/src/ngb/command-palette/storage.ts +45 -0
- package/src/ngb/command-palette/store.ts +716 -0
- package/src/ngb/command-palette/types.ts +120 -0
- package/src/ngb/command-palette/useCommandPaletteHotkeys.ts +27 -0
- package/src/ngb/command-palette/useCommandPalettePageContext.ts +18 -0
- package/src/ngb/components/NgbConfirmDialog.vue +63 -0
- package/src/ngb/components/NgbDialog.vue +44 -0
- package/src/ngb/components/NgbDrawer.vue +156 -0
- package/src/ngb/components/NgbHeaderActionCluster.vue +140 -0
- package/src/ngb/components/NgbModalShell.vue +99 -0
- package/src/ngb/components/NgbToolbar.vue +14 -0
- package/src/ngb/components/forms/NgbFormLayout.vue +13 -0
- package/src/ngb/components/forms/NgbFormRow.vue +21 -0
- package/src/ngb/components/forms/NgbFormSection.vue +16 -0
- package/src/ngb/components/forms/NgbValidationSummary.vue +18 -0
- package/src/ngb/components/navigation/NgbNavigationTree.vue +68 -0
- package/src/ngb/components/navigation/NgbTreeNode.vue +94 -0
- package/src/ngb/components/register/NgbColumnChooser.vue +101 -0
- package/src/ngb/components/register/NgbRegisterGrid.vue +473 -0
- package/src/ngb/components/register/registerTypes.ts +51 -0
- package/src/ngb/components/register/useRegisterColumnResize.ts +99 -0
- package/src/ngb/components/register/useRegisterColumnState.ts +220 -0
- package/src/ngb/components/register/useRegisterRows.ts +209 -0
- package/src/ngb/components/register/useRegisterViewport.ts +56 -0
- package/src/ngb/editor/NgbDocumentEffectsPage.vue +512 -0
- package/src/ngb/editor/NgbDocumentFlowPage.vue +409 -0
- package/src/ngb/editor/NgbDocumentPrintPage.vue +712 -0
- package/src/ngb/editor/NgbEditorDiscardDialog.vue +40 -0
- package/src/ngb/editor/NgbEntityAuditSidebar.vue +288 -0
- package/src/ngb/editor/NgbEntityEditor.vue +250 -0
- package/src/ngb/editor/NgbEntityEditorDrawerActions.vue +88 -0
- package/src/ngb/editor/NgbEntityEditorHeader.vue +114 -0
- package/src/ngb/editor/catalogNavigation.ts +13 -0
- package/src/ngb/editor/config.ts +252 -0
- package/src/ngb/editor/documentCopyDraft.ts +245 -0
- package/src/ngb/editor/documentEffects.ts +43 -0
- package/src/ngb/editor/documentEffectsDateFormatting.ts +18 -0
- package/src/ngb/editor/documentNavigation.ts +115 -0
- package/src/ngb/editor/documentStatus.ts +45 -0
- package/src/ngb/editor/entityEditorErrors.ts +162 -0
- package/src/ngb/editor/entityEditorPersistence.ts +241 -0
- package/src/ngb/editor/extensions.ts +40 -0
- package/src/ngb/editor/types.ts +295 -0
- package/src/ngb/editor/useConfiguredEntityEditorDocumentActions.ts +183 -0
- package/src/ngb/editor/useDocumentEditorDrawerState.ts +152 -0
- package/src/ngb/editor/useEditorDrawerState.ts +88 -0
- package/src/ngb/editor/useEntityEditorBusinessContext.ts +97 -0
- package/src/ngb/editor/useEntityEditorCapabilities.ts +190 -0
- package/src/ngb/editor/useEntityEditorCommandPalette.ts +154 -0
- package/src/ngb/editor/useEntityEditorCommitHandlers.ts +102 -0
- package/src/ngb/editor/useEntityEditorHeaderActions.ts +253 -0
- package/src/ngb/editor/useEntityEditorLeaveGuard.ts +66 -0
- package/src/ngb/editor/useEntityEditorNavigationActions.ts +235 -0
- package/src/ngb/editor/useEntityEditorOutputs.ts +59 -0
- package/src/ngb/editor/useEntityEditorPageActions.ts +57 -0
- package/src/ngb/editor/useRouteQueryEditorDrawer.ts +173 -0
- package/src/ngb/env/runtimeConfig.ts +25 -0
- package/src/ngb/lookup/config.ts +42 -0
- package/src/ngb/lookup/defaultConfig.ts +136 -0
- package/src/ngb/lookup/navigation.ts +76 -0
- package/src/ngb/lookup/prefetch.ts +40 -0
- package/src/ngb/lookup/store.ts +261 -0
- package/src/ngb/metadata/NgbDocumentListFiltersDrawer.vue +78 -0
- package/src/ngb/metadata/NgbDocumentPeriodFilter.vue +53 -0
- package/src/ngb/metadata/NgbEntityForm.vue +84 -0
- package/src/ngb/metadata/NgbEntityFormFieldsBlock.vue +110 -0
- package/src/ngb/metadata/NgbEntityListPageHeader.vue +80 -0
- package/src/ngb/metadata/NgbFilterFieldControl.vue +104 -0
- package/src/ngb/metadata/NgbMetadataCatalogEditPage.vue +42 -0
- package/src/ngb/metadata/NgbMetadataCatalogListPage.vue +269 -0
- package/src/ngb/metadata/NgbMetadataDocumentEditPage.vue +68 -0
- package/src/ngb/metadata/NgbMetadataDocumentListPage.vue +353 -0
- package/src/ngb/metadata/NgbMetadataFieldRenderer.vue +124 -0
- package/src/ngb/metadata/NgbMetadataLookupControl.vue +92 -0
- package/src/ngb/metadata/NgbRecycleBinFilter.vue +53 -0
- package/src/ngb/metadata/NgbRegisterPageLayout.vue +143 -0
- package/src/ngb/metadata/config.ts +28 -0
- package/src/ngb/metadata/dataTypes.ts +27 -0
- package/src/ngb/metadata/entityForm.ts +114 -0
- package/src/ngb/metadata/entityModel.ts +34 -0
- package/src/ngb/metadata/entityValueFormatting.ts +83 -0
- package/src/ngb/metadata/fieldRendererState.ts +84 -0
- package/src/ngb/metadata/filtering.ts +153 -0
- package/src/ngb/metadata/lookup.ts +25 -0
- package/src/ngb/metadata/monthPagedListQuery.ts +108 -0
- package/src/ngb/metadata/normalization.ts +100 -0
- package/src/ngb/metadata/referenceHydration.ts +99 -0
- package/src/ngb/metadata/register.ts +135 -0
- package/src/ngb/metadata/routePages.ts +86 -0
- package/src/ngb/metadata/store.ts +35 -0
- package/src/ngb/metadata/types.ts +276 -0
- package/src/ngb/metadata/useMetadataListFilters.ts +306 -0
- package/src/ngb/metadata/useMetadataRegisterPageData.ts +246 -0
- package/src/ngb/metadata/useValidationFocus.ts +49 -0
- package/src/ngb/primitives/NgbBadge.vue +31 -0
- package/src/ngb/primitives/NgbButton.vue +53 -0
- package/src/ngb/primitives/NgbCheckbox.vue +33 -0
- package/src/ngb/primitives/NgbDatePicker.vue +141 -0
- package/src/ngb/primitives/NgbIcon.vue +545 -0
- package/src/ngb/primitives/NgbInput.vue +57 -0
- package/src/ngb/primitives/NgbLookup.vue +264 -0
- package/src/ngb/primitives/NgbMonthPicker.vue +132 -0
- package/src/ngb/primitives/NgbMultiSelect.vue +179 -0
- package/src/ngb/primitives/NgbPeriodPicker.vue +125 -0
- package/src/ngb/primitives/NgbPickerNavButton.vue +31 -0
- package/src/ngb/primitives/NgbPickerPopover.vue +79 -0
- package/src/ngb/primitives/NgbRadioGroup.vue +53 -0
- package/src/ngb/primitives/NgbSelect.vue +108 -0
- package/src/ngb/primitives/NgbStatusIcon.vue +63 -0
- package/src/ngb/primitives/NgbSwitch.vue +42 -0
- package/src/ngb/primitives/NgbTabs.vue +116 -0
- package/src/ngb/primitives/NgbToastHost.vue +49 -0
- package/src/ngb/primitives/NgbTooltip.vue +27 -0
- package/src/ngb/primitives/iconNames.ts +83 -0
- package/src/ngb/primitives/toast.ts +66 -0
- package/src/ngb/primitives/useAsyncComboboxQuery.ts +85 -0
- package/src/ngb/primitives/useFloatingDropdownPosition.ts +129 -0
- package/src/ngb/reporting/NgbReportComposerCollectionSection.vue +106 -0
- package/src/ngb/reporting/NgbReportComposerPanel.vue +1187 -0
- package/src/ngb/reporting/NgbReportDateRangeFilter.vue +54 -0
- package/src/ngb/reporting/NgbReportPage.vue +1124 -0
- package/src/ngb/reporting/NgbReportSheet.vue +467 -0
- package/src/ngb/reporting/api.ts +35 -0
- package/src/ngb/reporting/composer.ts +904 -0
- package/src/ngb/reporting/config.ts +120 -0
- package/src/ngb/reporting/defaultConfig.ts +15 -0
- package/src/ngb/reporting/lookupFilters.ts +43 -0
- package/src/ngb/reporting/navigation.ts +181 -0
- package/src/ngb/reporting/pageHelpers.ts +73 -0
- package/src/ngb/reporting/pageSession.ts +92 -0
- package/src/ngb/reporting/paging.ts +66 -0
- package/src/ngb/reporting/types.ts +333 -0
- package/src/ngb/router/backNavigation.ts +122 -0
- package/src/ngb/router/queryParams.ts +129 -0
- package/src/ngb/router/queryState.ts +121 -0
- package/src/ngb/router/routeAliases.ts +66 -0
- package/src/ngb/router/routeParams.ts +15 -0
- package/src/ngb/router/shareLink.ts +39 -0
- package/src/ngb/security/NgbAccessAuditPanel.vue +32 -0
- package/src/ngb/security/NgbAccessDeniedState.vue +27 -0
- package/src/ngb/security/NgbEffectiveAccessPanel.vue +60 -0
- package/src/ngb/security/NgbPermissionMatrix.vue +160 -0
- package/src/ngb/security/NgbRoleEditorPage.vue +319 -0
- package/src/ngb/security/NgbRolesPage.vue +125 -0
- package/src/ngb/security/NgbUserEditorPage.vue +615 -0
- package/src/ngb/security/NgbUsersPage.vue +125 -0
- package/src/ngb/security/api.ts +91 -0
- package/src/ngb/security/audit.ts +21 -0
- package/src/ngb/security/permissions.ts +65 -0
- package/src/ngb/security/types.ts +157 -0
- package/src/ngb/security/useAccessStore.ts +69 -0
- package/src/ngb/site/NgbDashboardAsOfToolbar.vue +100 -0
- package/src/ngb/site/NgbDashboardStatusBanner.vue +91 -0
- package/src/ngb/site/NgbLogo.vue +44 -0
- package/src/ngb/site/NgbPageHeader.vue +49 -0
- package/src/ngb/site/NgbSiteShell.vue +286 -0
- package/src/ngb/site/NgbSiteSidebar.vue +270 -0
- package/src/ngb/site/NgbTopBar.vue +425 -0
- package/src/ngb/site/NgbTrendChart.vue +224 -0
- package/src/ngb/site/dashboardData.ts +324 -0
- package/src/ngb/site/mainMenuStore.ts +49 -0
- package/src/ngb/site/types.ts +31 -0
- package/src/ngb/site/useDashboardPageState.ts +98 -0
- package/src/ngb/site/useTheme.ts +95 -0
- package/src/ngb/utils/clone.ts +43 -0
- package/src/ngb/utils/dateValues.ts +107 -0
- package/src/ngb/utils/errorMessage.ts +14 -0
- package/src/ngb/utils/guid.ts +21 -0
- package/src/ngb/utils/stableValue.ts +21 -0
- package/src/ngb/utils/storage.ts +135 -0
- package/src/styles/tailwind.css +165 -0
- package/src/vite/publicAssets.ts +104 -0
- package/vite-public-assets.js +68 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { ApiError } from '../api/http'
|
|
5
|
+
import type { RegisterColumn, RegisterDataRow } from '../components/register/registerTypes'
|
|
6
|
+
import NgbRecycleBinFilter from '../metadata/NgbRecycleBinFilter.vue'
|
|
7
|
+
import NgbRegisterPageLayout from '../metadata/NgbRegisterPageLayout.vue'
|
|
8
|
+
import type { QueryTrashMode } from '../router/queryParams'
|
|
9
|
+
import { toErrorMessage } from '../utils/errorMessage'
|
|
10
|
+
import NgbAccessDeniedState from './NgbAccessDeniedState.vue'
|
|
11
|
+
import { getUsers } from './api'
|
|
12
|
+
import { useAccessStore } from './useAccessStore'
|
|
13
|
+
import type { UserListItemDto } from './types'
|
|
14
|
+
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const access = useAccessStore()
|
|
17
|
+
|
|
18
|
+
const loading = ref(false)
|
|
19
|
+
const error = ref<string | null>(null)
|
|
20
|
+
const accessDenied = ref(false)
|
|
21
|
+
const users = ref<UserListItemDto[]>([])
|
|
22
|
+
const status = ref<QueryTrashMode>('active')
|
|
23
|
+
|
|
24
|
+
const columns: RegisterColumn[] = [
|
|
25
|
+
{
|
|
26
|
+
key: 'user',
|
|
27
|
+
title: 'User',
|
|
28
|
+
width: 320,
|
|
29
|
+
minWidth: 240,
|
|
30
|
+
pinned: 'left',
|
|
31
|
+
format: (value) => Array.isArray(value) ? value.join('\n') : String(value ?? ''),
|
|
32
|
+
},
|
|
33
|
+
{ key: 'roles', title: 'Roles', width: 280, minWidth: 200, wrap: true },
|
|
34
|
+
{ key: 'keycloakLabel', title: 'Keycloak', width: 120, minWidth: 100, align: 'center', sortable: false },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const filteredUsers = computed(() => {
|
|
38
|
+
return users.value
|
|
39
|
+
.filter((user) => {
|
|
40
|
+
if (status.value === 'active') return user.isActive
|
|
41
|
+
if (status.value === 'deleted') return !user.isActive
|
|
42
|
+
return true
|
|
43
|
+
})
|
|
44
|
+
.sort((a, b) => (a.displayName ?? a.email ?? '').localeCompare(b.displayName ?? b.email ?? ''))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const rows = computed<RegisterDataRow[]>(() => filteredUsers.value.map((user) => ({
|
|
48
|
+
key: user.userId,
|
|
49
|
+
__status: user.isActive ? 'posted' : 'marked',
|
|
50
|
+
__statusTitle: user.isActive ? 'Active' : 'Inactive',
|
|
51
|
+
user: [userTitle(user), user.email || user.authSubject],
|
|
52
|
+
roles: roleSummary(user),
|
|
53
|
+
keycloakLabel: user.keycloakEnabled === true ? 'Yes' : 'No',
|
|
54
|
+
})))
|
|
55
|
+
|
|
56
|
+
function userTitle(user: UserListItemDto): string {
|
|
57
|
+
return user.displayName?.trim() || user.email?.trim() || user.authSubject
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function roleSummary(user: UserListItemDto): string {
|
|
61
|
+
if (!user.roles.length) return 'No roles'
|
|
62
|
+
return user.roles.map((role) => role.name).join(', ')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function load(): Promise<void> {
|
|
66
|
+
loading.value = true
|
|
67
|
+
error.value = null
|
|
68
|
+
accessDenied.value = false
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await access.load()
|
|
72
|
+
users.value = await getUsers()
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
users.value = []
|
|
75
|
+
accessDenied.value = cause instanceof ApiError && cause.status === 403
|
|
76
|
+
error.value = accessDenied.value ? null : toErrorMessage(cause, 'Failed to load users')
|
|
77
|
+
} finally {
|
|
78
|
+
loading.value = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function openUser(userId: string): void {
|
|
83
|
+
void router.push(`/admin/security/users/${encodeURIComponent(userId)}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createUser(): void {
|
|
87
|
+
void router.push('/admin/security/users/new')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function goBack(): void {
|
|
91
|
+
void router.push('/home')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
void load()
|
|
96
|
+
})
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<NgbAccessDeniedState v-if="accessDenied" />
|
|
101
|
+
|
|
102
|
+
<NgbRegisterPageLayout
|
|
103
|
+
v-else
|
|
104
|
+
title="Users"
|
|
105
|
+
:items-count="filteredUsers.length"
|
|
106
|
+
:total="users.length"
|
|
107
|
+
:loading="loading"
|
|
108
|
+
:error="error"
|
|
109
|
+
:show-filter="false"
|
|
110
|
+
:disable-create="!access.canManageUsers"
|
|
111
|
+
:disable-prev="true"
|
|
112
|
+
:disable-next="true"
|
|
113
|
+
:columns="columns"
|
|
114
|
+
:rows="rows"
|
|
115
|
+
storage-key="ngb:security:users"
|
|
116
|
+
@back="goBack"
|
|
117
|
+
@refresh="load"
|
|
118
|
+
@create="createUser"
|
|
119
|
+
@rowActivate="openUser"
|
|
120
|
+
>
|
|
121
|
+
<template #filters>
|
|
122
|
+
<NgbRecycleBinFilter v-model="status" />
|
|
123
|
+
</template>
|
|
124
|
+
</NgbRegisterPageLayout>
|
|
125
|
+
</template>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { httpGet, httpPost, httpPut } from '../api/http'
|
|
2
|
+
import type {
|
|
3
|
+
CreateRoleRequestDto,
|
|
4
|
+
CreateUserRequestDto,
|
|
5
|
+
CurrentAccessDto,
|
|
6
|
+
EffectiveAccessDto,
|
|
7
|
+
PermissionDefinitionDto,
|
|
8
|
+
ReplaceRolePermissionsRequestDto,
|
|
9
|
+
ReplaceUserRolesRequestDto,
|
|
10
|
+
RoleDetailsDto,
|
|
11
|
+
RoleListItemDto,
|
|
12
|
+
UpdateRoleRequestDto,
|
|
13
|
+
UpdateUserRequestDto,
|
|
14
|
+
UserDetailsDto,
|
|
15
|
+
UserListItemDto,
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
export async function getCurrentAccess(): Promise<CurrentAccessDto> {
|
|
19
|
+
return await httpGet<CurrentAccessDto>('/api/security/me/access')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getPermissionDefinitions(): Promise<PermissionDefinitionDto[]> {
|
|
23
|
+
return await httpGet<PermissionDefinitionDto[]>('/api/security/permissions/definitions')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getUsers(): Promise<UserListItemDto[]> {
|
|
27
|
+
return await httpGet<UserListItemDto[]>('/api/security/users')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function createUser(request: CreateUserRequestDto): Promise<UserDetailsDto> {
|
|
31
|
+
return await httpPost<UserDetailsDto, CreateUserRequestDto>('/api/security/users', request)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getUser(userId: string): Promise<UserDetailsDto> {
|
|
35
|
+
return await httpGet<UserDetailsDto>(`/api/security/users/${encodeURIComponent(userId)}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function updateUser(userId: string, request: UpdateUserRequestDto): Promise<UserDetailsDto> {
|
|
39
|
+
return await httpPut<UserDetailsDto, UpdateUserRequestDto>(`/api/security/users/${encodeURIComponent(userId)}`, request)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function deactivateUser(userId: string): Promise<void> {
|
|
43
|
+
await httpPost<void>(`/api/security/users/${encodeURIComponent(userId)}/deactivate`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function reactivateUser(userId: string): Promise<void> {
|
|
47
|
+
await httpPost<void>(`/api/security/users/${encodeURIComponent(userId)}/reactivate`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function replaceUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
|
51
|
+
await httpPut<void, ReplaceUserRolesRequestDto>(
|
|
52
|
+
`/api/security/users/${encodeURIComponent(userId)}/roles`,
|
|
53
|
+
{ roleIds },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getUserEffectiveAccess(userId: string): Promise<EffectiveAccessDto> {
|
|
58
|
+
return await httpGet<EffectiveAccessDto>(`/api/security/users/${encodeURIComponent(userId)}/effective-access`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getRoles(): Promise<RoleListItemDto[]> {
|
|
62
|
+
return await httpGet<RoleListItemDto[]>('/api/security/roles')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createRole(request: CreateRoleRequestDto): Promise<RoleDetailsDto> {
|
|
66
|
+
return await httpPost<RoleDetailsDto, CreateRoleRequestDto>('/api/security/roles', request)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getRole(roleId: string): Promise<RoleDetailsDto> {
|
|
70
|
+
return await httpGet<RoleDetailsDto>(`/api/security/roles/${encodeURIComponent(roleId)}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function updateRole(roleId: string, request: UpdateRoleRequestDto): Promise<RoleDetailsDto> {
|
|
74
|
+
return await httpPut<RoleDetailsDto, UpdateRoleRequestDto>(`/api/security/roles/${encodeURIComponent(roleId)}`, request)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function deactivateRole(roleId: string): Promise<void> {
|
|
78
|
+
await httpPost<void>(`/api/security/roles/${encodeURIComponent(roleId)}/deactivate`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function reactivateRole(roleId: string): Promise<void> {
|
|
82
|
+
await httpPost<void>(`/api/security/roles/${encodeURIComponent(roleId)}/reactivate`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function replaceRolePermissions(roleId: string, permissions: ReplaceRolePermissionsRequestDto['permissions']): Promise<void> {
|
|
86
|
+
await httpPut<void, ReplaceRolePermissionsRequestDto>(
|
|
87
|
+
`/api/security/roles/${encodeURIComponent(roleId)}/permissions`,
|
|
88
|
+
{ permissions },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditorAuditBehavior } from '../editor/types'
|
|
2
|
+
|
|
3
|
+
export const USER_AUDIT_BEHAVIOR: EditorAuditBehavior = {
|
|
4
|
+
actionTitles: {
|
|
5
|
+
'security.user.create': 'User created',
|
|
6
|
+
'security.user.update': 'User updated',
|
|
7
|
+
'security.user.deactivate': 'User deactivated',
|
|
8
|
+
'security.user.reactivate': 'User reactivated',
|
|
9
|
+
'security.user.roles.replace': 'User roles changed',
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ROLE_AUDIT_BEHAVIOR: EditorAuditBehavior = {
|
|
14
|
+
actionTitles: {
|
|
15
|
+
'security.role.create': 'Role created',
|
|
16
|
+
'security.role.update': 'Role updated',
|
|
17
|
+
'security.role.deactivate': 'Role deactivated',
|
|
18
|
+
'security.role.reactivate': 'Role reactivated',
|
|
19
|
+
'security.role.permissions.replace': 'Role permissions changed',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PermissionAssignmentDto, PermissionDefinitionDto, PermissionGroupDto } from './types'
|
|
2
|
+
|
|
3
|
+
export type PermissionKeyLike = PermissionAssignmentDto | string
|
|
4
|
+
|
|
5
|
+
function normalizeSegment(value: string | null | undefined): string {
|
|
6
|
+
return String(value ?? '').trim().toLowerCase()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildPermissionKey(permission: PermissionKeyLike): string {
|
|
10
|
+
if (typeof permission === 'string') return permission.trim().toLowerCase()
|
|
11
|
+
return [
|
|
12
|
+
normalizeSegment(permission.resourceKind),
|
|
13
|
+
normalizeSegment(permission.resourceCode),
|
|
14
|
+
normalizeSegment(permission.actionCode),
|
|
15
|
+
].join('.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildPermissionAssignment(resourceKind: string, resourceCode: string, actionCode: string): PermissionAssignmentDto {
|
|
19
|
+
return {
|
|
20
|
+
resourceKind: normalizeSegment(resourceKind),
|
|
21
|
+
resourceCode: normalizeSegment(resourceCode),
|
|
22
|
+
actionCode: normalizeSegment(actionCode),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SYSTEM_PERMISSIONS = {
|
|
27
|
+
usersView: buildPermissionAssignment('system', 'users', 'view'),
|
|
28
|
+
usersManage: buildPermissionAssignment('system', 'users', 'manage'),
|
|
29
|
+
rolesView: buildPermissionAssignment('system', 'roles', 'view'),
|
|
30
|
+
rolesManage: buildPermissionAssignment('system', 'roles', 'manage'),
|
|
31
|
+
permissionsView: buildPermissionAssignment('system', 'permissions', 'view'),
|
|
32
|
+
auditView: buildPermissionAssignment('system', 'audit', 'view'),
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export function hasPermission(permissions: readonly PermissionAssignmentDto[] | null | undefined, permission: PermissionKeyLike): boolean {
|
|
36
|
+
const key = buildPermissionKey(permission)
|
|
37
|
+
if (!key || !permissions?.length) return false
|
|
38
|
+
return permissions.some((entry) => buildPermissionKey(entry) === key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toPermissionKeySet(permissions: readonly PermissionAssignmentDto[] | null | undefined): Set<string> {
|
|
42
|
+
return new Set((permissions ?? []).map((entry) => buildPermissionKey(entry)).filter(Boolean))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function groupPermissionDefinitions(definitions: readonly PermissionDefinitionDto[]): PermissionGroupDto[] {
|
|
46
|
+
const groups = new Map<string, PermissionDefinitionDto[]>()
|
|
47
|
+
|
|
48
|
+
for (const definition of definitions) {
|
|
49
|
+
const group = definition.group?.trim() || 'Other'
|
|
50
|
+
const items = groups.get(group) ?? []
|
|
51
|
+
items.push(definition)
|
|
52
|
+
groups.set(group, items)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Array.from(groups.entries())
|
|
56
|
+
.map(([group, permissions]) => ({
|
|
57
|
+
group,
|
|
58
|
+
permissions: permissions
|
|
59
|
+
.slice()
|
|
60
|
+
.sort((a, b) => a.resourceKind.localeCompare(b.resourceKind)
|
|
61
|
+
|| a.resourceCode.localeCompare(b.resourceCode)
|
|
62
|
+
|| a.actionCode.localeCompare(b.actionCode)),
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => a.group.localeCompare(b.group))
|
|
65
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export type PermissionAssignmentDto = {
|
|
2
|
+
resourceKind: string
|
|
3
|
+
resourceCode: string
|
|
4
|
+
actionCode: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type PermissionDefinitionDto = PermissionAssignmentDto & {
|
|
8
|
+
displayName: string
|
|
9
|
+
group: string
|
|
10
|
+
description?: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PermissionGroupDto = {
|
|
14
|
+
group: string
|
|
15
|
+
permissions: PermissionDefinitionDto[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RoleBadgeDto = {
|
|
19
|
+
roleId: string
|
|
20
|
+
code: string
|
|
21
|
+
name: string
|
|
22
|
+
isSystem: boolean
|
|
23
|
+
isActive: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type UserBadgeDto = {
|
|
27
|
+
userId: string
|
|
28
|
+
email?: string | null
|
|
29
|
+
displayName?: string | null
|
|
30
|
+
isActive: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type UserListItemDto = {
|
|
34
|
+
userId: string
|
|
35
|
+
authSubject: string
|
|
36
|
+
email?: string | null
|
|
37
|
+
displayName?: string | null
|
|
38
|
+
isActive: boolean
|
|
39
|
+
keycloakEnabled?: boolean | null
|
|
40
|
+
roles: RoleBadgeDto[]
|
|
41
|
+
createdAtUtc: string
|
|
42
|
+
updatedAtUtc: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type UserDetailsDto = {
|
|
46
|
+
userId: string
|
|
47
|
+
authSubject: string
|
|
48
|
+
email?: string | null
|
|
49
|
+
firstName?: string | null
|
|
50
|
+
lastName?: string | null
|
|
51
|
+
displayName?: string | null
|
|
52
|
+
isActive: boolean
|
|
53
|
+
keycloakEnabled?: boolean | null
|
|
54
|
+
roles: RoleBadgeDto[]
|
|
55
|
+
accessVersion: number
|
|
56
|
+
createdAtUtc: string
|
|
57
|
+
updatedAtUtc: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type CreateUserRequestDto = {
|
|
61
|
+
email: string
|
|
62
|
+
firstName?: string | null
|
|
63
|
+
lastName?: string | null
|
|
64
|
+
displayName?: string | null
|
|
65
|
+
enabled: boolean
|
|
66
|
+
temporaryPassword?: string | null
|
|
67
|
+
requirePasswordUpdate: boolean
|
|
68
|
+
roleIds: string[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type UpdateUserRequestDto = {
|
|
72
|
+
email?: string | null
|
|
73
|
+
firstName?: string | null
|
|
74
|
+
lastName?: string | null
|
|
75
|
+
displayName?: string | null
|
|
76
|
+
enabled: boolean
|
|
77
|
+
temporaryPassword?: string | null
|
|
78
|
+
requirePasswordUpdate: boolean
|
|
79
|
+
roleIds: string[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ReplaceUserRolesRequestDto = {
|
|
83
|
+
roleIds: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type RoleListItemDto = {
|
|
87
|
+
roleId: string
|
|
88
|
+
code: string
|
|
89
|
+
name: string
|
|
90
|
+
description?: string | null
|
|
91
|
+
isSystem: boolean
|
|
92
|
+
isActive: boolean
|
|
93
|
+
assignedUsersCount: number
|
|
94
|
+
createdAtUtc: string
|
|
95
|
+
updatedAtUtc: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type RoleDetailsDto = {
|
|
99
|
+
roleId: string
|
|
100
|
+
code: string
|
|
101
|
+
name: string
|
|
102
|
+
description?: string | null
|
|
103
|
+
isSystem: boolean
|
|
104
|
+
isActive: boolean
|
|
105
|
+
permissions: PermissionAssignmentDto[]
|
|
106
|
+
assignedUsers: UserBadgeDto[]
|
|
107
|
+
createdAtUtc: string
|
|
108
|
+
updatedAtUtc: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type CreateRoleRequestDto = {
|
|
112
|
+
code: string
|
|
113
|
+
name: string
|
|
114
|
+
description?: string | null
|
|
115
|
+
permissions: PermissionAssignmentDto[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type UpdateRoleRequestDto = {
|
|
119
|
+
code: string
|
|
120
|
+
name: string
|
|
121
|
+
description?: string | null
|
|
122
|
+
isActive: boolean
|
|
123
|
+
permissions: PermissionAssignmentDto[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ReplaceRolePermissionsRequestDto = {
|
|
127
|
+
permissions: PermissionAssignmentDto[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type CurrentAccessDto = {
|
|
131
|
+
userId?: string | null
|
|
132
|
+
authSubject?: string | null
|
|
133
|
+
isAuthenticated: boolean
|
|
134
|
+
isActive: boolean
|
|
135
|
+
isBootstrapAdmin: boolean
|
|
136
|
+
accessVersion: number
|
|
137
|
+
roles?: RoleBadgeDto[]
|
|
138
|
+
permissions: PermissionAssignmentDto[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type EffectiveAccessResourceDto = {
|
|
142
|
+
resourceKind: string
|
|
143
|
+
resourceCode: string
|
|
144
|
+
displayName: string
|
|
145
|
+
actions: string[]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type EffectiveAccessGroupDto = {
|
|
149
|
+
group: string
|
|
150
|
+
resources: EffectiveAccessResourceDto[]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type EffectiveAccessDto = {
|
|
154
|
+
userId: string
|
|
155
|
+
accessVersion: number
|
|
156
|
+
groups: EffectiveAccessGroupDto[]
|
|
157
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { toErrorMessage } from '../utils/errorMessage'
|
|
3
|
+
import { getCurrentAccess } from './api'
|
|
4
|
+
import { buildPermissionKey, hasPermission, SYSTEM_PERMISSIONS, toPermissionKeySet, type PermissionKeyLike } from './permissions'
|
|
5
|
+
import type { CurrentAccessDto } from './types'
|
|
6
|
+
|
|
7
|
+
export const useAccessStore = defineStore('access', {
|
|
8
|
+
state: () => ({
|
|
9
|
+
current: null as CurrentAccessDto | null,
|
|
10
|
+
permissionKeys: new Set<string>(),
|
|
11
|
+
isLoading: false,
|
|
12
|
+
error: null as string | null,
|
|
13
|
+
loadedAt: 0,
|
|
14
|
+
}),
|
|
15
|
+
getters: {
|
|
16
|
+
isActive: (state) => state.current?.isActive === true,
|
|
17
|
+
canViewUsers: (state) => state.current?.isBootstrapAdmin === true || hasPermission(state.current?.permissions, SYSTEM_PERMISSIONS.usersView),
|
|
18
|
+
canManageUsers: (state) => state.current?.isBootstrapAdmin === true || hasPermission(state.current?.permissions, SYSTEM_PERMISSIONS.usersManage),
|
|
19
|
+
canViewRoles: (state) => state.current?.isBootstrapAdmin === true || hasPermission(state.current?.permissions, SYSTEM_PERMISSIONS.rolesView),
|
|
20
|
+
canManageRoles: (state) => state.current?.isBootstrapAdmin === true || hasPermission(state.current?.permissions, SYSTEM_PERMISSIONS.rolesManage),
|
|
21
|
+
canViewPermissions: (state) => state.current?.isBootstrapAdmin === true || hasPermission(state.current?.permissions, SYSTEM_PERMISSIONS.permissionsView),
|
|
22
|
+
applicationRoleNames: (state) => {
|
|
23
|
+
const seen = new Set<string>()
|
|
24
|
+
return (state.current?.roles ?? [])
|
|
25
|
+
.filter((role) => role.isActive)
|
|
26
|
+
.map((role) => (role.name || role.code).trim())
|
|
27
|
+
.filter((name) => {
|
|
28
|
+
const key = name.toLowerCase()
|
|
29
|
+
if (!name || seen.has(key)) return false
|
|
30
|
+
seen.add(key)
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
actions: {
|
|
36
|
+
async load(force = false): Promise<CurrentAccessDto | null> {
|
|
37
|
+
if (this.isLoading) return this.current
|
|
38
|
+
if (!force && this.current) return this.current
|
|
39
|
+
|
|
40
|
+
this.isLoading = true
|
|
41
|
+
this.error = null
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const access = await getCurrentAccess()
|
|
45
|
+
this.current = access
|
|
46
|
+
this.permissionKeys = toPermissionKeySet(access.permissions)
|
|
47
|
+
this.loadedAt = Date.now()
|
|
48
|
+
return access
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
this.current = null
|
|
51
|
+
this.permissionKeys = new Set()
|
|
52
|
+
this.error = toErrorMessage(cause, 'Failed to load access profile')
|
|
53
|
+
return null
|
|
54
|
+
} finally {
|
|
55
|
+
this.isLoading = false
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
hasPermission(permission: PermissionKeyLike): boolean {
|
|
59
|
+
return this.permissionKeys.has(buildPermissionKey(permission))
|
|
60
|
+
},
|
|
61
|
+
reset(): void {
|
|
62
|
+
this.current = null
|
|
63
|
+
this.permissionKeys = new Set()
|
|
64
|
+
this.error = null
|
|
65
|
+
this.loadedAt = 0
|
|
66
|
+
this.isLoading = false
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
import NgbDatePicker from '../primitives/NgbDatePicker.vue'
|
|
5
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
modelValue: string | null | undefined
|
|
9
|
+
loading?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}>(), {
|
|
12
|
+
loading: false,
|
|
13
|
+
disabled: false,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: 'refresh'): void
|
|
18
|
+
(e: 'update:modelValue', value: string | null): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const isDisabled = computed(() => props.loading || props.disabled)
|
|
22
|
+
|
|
23
|
+
function onRefresh(): void {
|
|
24
|
+
if (isDisabled.value) return
|
|
25
|
+
emit('refresh')
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div class="ngb-dashboard-toolbar-control">
|
|
31
|
+
<div class="ngb-dashboard-toolbar-picker">
|
|
32
|
+
<NgbDatePicker
|
|
33
|
+
:model-value="modelValue"
|
|
34
|
+
grouped
|
|
35
|
+
:disabled="isDisabled"
|
|
36
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="ngb-dashboard-toolbar-divider" aria-hidden="true" />
|
|
41
|
+
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="ngb-dashboard-toolbar-button ngb-focus"
|
|
45
|
+
:disabled="isDisabled"
|
|
46
|
+
title="Refresh"
|
|
47
|
+
aria-label="Refresh"
|
|
48
|
+
@click="onRefresh"
|
|
49
|
+
>
|
|
50
|
+
<NgbIcon name="refresh" :size="15" />
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.ngb-dashboard-toolbar-control {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 0.375rem;
|
|
60
|
+
min-height: 28px;
|
|
61
|
+
border: 1px solid var(--ngb-border);
|
|
62
|
+
border-radius: var(--ngb-radius);
|
|
63
|
+
background: var(--ngb-card);
|
|
64
|
+
padding: 0 0.25rem;
|
|
65
|
+
box-shadow: var(--ngb-shadow-1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.ngb-dashboard-toolbar-picker {
|
|
69
|
+
width: 10rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ngb-dashboard-toolbar-divider {
|
|
73
|
+
height: 1rem;
|
|
74
|
+
width: 1px;
|
|
75
|
+
background: var(--ngb-border);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.ngb-dashboard-toolbar-button {
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
height: 26px;
|
|
83
|
+
width: 26px;
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: var(--ngb-radius);
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: var(--ngb-muted);
|
|
88
|
+
transition: background-color 140ms ease, color 140ms ease;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ngb-dashboard-toolbar-button:hover:not(:disabled) {
|
|
92
|
+
background: var(--ngb-bg);
|
|
93
|
+
color: var(--ngb-text);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ngb-dashboard-toolbar-button:disabled {
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
opacity: 0.45;
|
|
99
|
+
}
|
|
100
|
+
</style>
|