@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,615 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
import { ApiError } from '../api/http'
|
|
5
|
+
import NgbDrawer from '../components/NgbDrawer.vue'
|
|
6
|
+
import NgbValidationSummary from '../components/forms/NgbValidationSummary.vue'
|
|
7
|
+
import NgbBadge from '../primitives/NgbBadge.vue'
|
|
8
|
+
import NgbButton from '../primitives/NgbButton.vue'
|
|
9
|
+
import NgbConfirmDialog from '../components/NgbConfirmDialog.vue'
|
|
10
|
+
import NgbIcon from '../primitives/NgbIcon.vue'
|
|
11
|
+
import NgbInput from '../primitives/NgbInput.vue'
|
|
12
|
+
import NgbStatusIcon from '../primitives/NgbStatusIcon.vue'
|
|
13
|
+
import NgbSwitch from '../primitives/NgbSwitch.vue'
|
|
14
|
+
import { useOptionalToasts } from '../primitives/toast'
|
|
15
|
+
import NgbEntityAuditSidebar from '../editor/NgbEntityAuditSidebar.vue'
|
|
16
|
+
import NgbPageHeader from '../site/NgbPageHeader.vue'
|
|
17
|
+
import { toErrorMessage } from '../utils/errorMessage'
|
|
18
|
+
import NgbAccessDeniedState from './NgbAccessDeniedState.vue'
|
|
19
|
+
import NgbEffectiveAccessPanel from './NgbEffectiveAccessPanel.vue'
|
|
20
|
+
import { USER_AUDIT_BEHAVIOR } from './audit'
|
|
21
|
+
import {
|
|
22
|
+
createUser,
|
|
23
|
+
deactivateUser,
|
|
24
|
+
getRoles,
|
|
25
|
+
getUser,
|
|
26
|
+
getUserEffectiveAccess,
|
|
27
|
+
reactivateUser,
|
|
28
|
+
updateUser,
|
|
29
|
+
} from './api'
|
|
30
|
+
import { useAccessStore } from './useAccessStore'
|
|
31
|
+
import type { EffectiveAccessDto, RoleListItemDto, UserDetailsDto } from './types'
|
|
32
|
+
|
|
33
|
+
type UserForm = {
|
|
34
|
+
email: string
|
|
35
|
+
displayName: string
|
|
36
|
+
password: string
|
|
37
|
+
confirmPassword: string
|
|
38
|
+
requirePasswordUpdate: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type UserFieldErrors = Partial<Record<keyof Omit<UserForm, 'requirePasswordUpdate'>, string>>
|
|
42
|
+
|
|
43
|
+
const AUDIT_ENTITY_KIND_SECURITY_USER = 8
|
|
44
|
+
|
|
45
|
+
const route = useRoute()
|
|
46
|
+
const router = useRouter()
|
|
47
|
+
const access = useAccessStore()
|
|
48
|
+
|
|
49
|
+
const loading = ref(false)
|
|
50
|
+
const saving = ref(false)
|
|
51
|
+
const activating = ref(false)
|
|
52
|
+
const effectiveLoading = ref(false)
|
|
53
|
+
const error = ref<string | null>(null)
|
|
54
|
+
const effectiveError = ref<string | null>(null)
|
|
55
|
+
const serverValidationMessages = ref<string[]>([])
|
|
56
|
+
const accessDenied = ref(false)
|
|
57
|
+
const user = ref<UserDetailsDto | null>(null)
|
|
58
|
+
const roles = ref<RoleListItemDto[]>([])
|
|
59
|
+
const selectedRoleIds = ref<string[]>([])
|
|
60
|
+
const effectiveAccess = ref<EffectiveAccessDto | null>(null)
|
|
61
|
+
const confirmMode = ref<'deactivate' | 'reactivate' | null>(null)
|
|
62
|
+
const auditOpen = ref(false)
|
|
63
|
+
const attemptedSave = ref(false)
|
|
64
|
+
const showPassword = ref(false)
|
|
65
|
+
const showConfirmPassword = ref(false)
|
|
66
|
+
const changePasswordMode = ref(false)
|
|
67
|
+
const toasts = useOptionalToasts()
|
|
68
|
+
|
|
69
|
+
const form = ref<UserForm>({
|
|
70
|
+
email: '',
|
|
71
|
+
displayName: '',
|
|
72
|
+
password: '',
|
|
73
|
+
confirmPassword: '',
|
|
74
|
+
requirePasswordUpdate: true,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const userId = computed(() => String(route.params.userId ?? 'new'))
|
|
78
|
+
const isNew = computed(() => userId.value === 'new')
|
|
79
|
+
const canEdit = computed(() => access.canManageUsers)
|
|
80
|
+
const canOpenAudit = computed(() => !isNew.value && !!user.value)
|
|
81
|
+
const shouldShowPasswordFields = computed(() => isNew.value || changePasswordMode.value)
|
|
82
|
+
const title = computed(() => {
|
|
83
|
+
if (isNew.value) return 'New user'
|
|
84
|
+
return user.value?.displayName?.trim() || user.value?.email?.trim() || 'User'
|
|
85
|
+
})
|
|
86
|
+
const activeRoles = computed(() => roles.value.filter((role) => role.isActive || selectedRoleIds.value.includes(role.roleId)))
|
|
87
|
+
const auditEntityTitle = computed(() => title.value)
|
|
88
|
+
const fieldErrors = computed<UserFieldErrors>(() => {
|
|
89
|
+
if (!attemptedSave.value) return {}
|
|
90
|
+
|
|
91
|
+
const errors: UserFieldErrors = {}
|
|
92
|
+
const values = form.value
|
|
93
|
+
const passwordRequired = shouldShowPasswordFields.value
|
|
94
|
+
|
|
95
|
+
if (!values.email.trim()) errors.email = 'Email is required.'
|
|
96
|
+
else if (!isEmail(values.email)) errors.email = 'Enter a valid email address.'
|
|
97
|
+
|
|
98
|
+
if (!values.displayName.trim()) errors.displayName = 'Display name is required.'
|
|
99
|
+
|
|
100
|
+
if (passwordRequired && values.password.length === 0) errors.password = 'Password is required.'
|
|
101
|
+
if (passwordRequired && values.confirmPassword.length === 0) errors.confirmPassword = 'Confirm password is required.'
|
|
102
|
+
|
|
103
|
+
if (passwordRequired && values.password.length > 0 && values.confirmPassword.length > 0 && values.password !== values.confirmPassword) {
|
|
104
|
+
errors.confirmPassword = 'Passwords do not match.'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return errors
|
|
108
|
+
})
|
|
109
|
+
const validationMessages = computed(() => uniqueMessages([
|
|
110
|
+
...Object.values(fieldErrors.value).filter((message): message is string => !!message),
|
|
111
|
+
...serverValidationMessages.value,
|
|
112
|
+
]))
|
|
113
|
+
|
|
114
|
+
function resetForm(): void {
|
|
115
|
+
user.value = null
|
|
116
|
+
selectedRoleIds.value = []
|
|
117
|
+
effectiveAccess.value = null
|
|
118
|
+
auditOpen.value = false
|
|
119
|
+
attemptedSave.value = false
|
|
120
|
+
serverValidationMessages.value = []
|
|
121
|
+
showPassword.value = false
|
|
122
|
+
showConfirmPassword.value = false
|
|
123
|
+
changePasswordMode.value = false
|
|
124
|
+
form.value = {
|
|
125
|
+
email: '',
|
|
126
|
+
displayName: '',
|
|
127
|
+
password: '',
|
|
128
|
+
confirmPassword: '',
|
|
129
|
+
requirePasswordUpdate: true,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyUser(next: UserDetailsDto): void {
|
|
134
|
+
user.value = next
|
|
135
|
+
selectedRoleIds.value = next.roles.map((role) => role.roleId)
|
|
136
|
+
form.value = {
|
|
137
|
+
email: next.email ?? '',
|
|
138
|
+
displayName: next.displayName ?? '',
|
|
139
|
+
password: '',
|
|
140
|
+
confirmPassword: '',
|
|
141
|
+
requirePasswordUpdate: true,
|
|
142
|
+
}
|
|
143
|
+
changePasswordMode.value = false
|
|
144
|
+
showPassword.value = false
|
|
145
|
+
showConfirmPassword.value = false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadEffectiveAccess(): Promise<void> {
|
|
149
|
+
if (isNew.value || !user.value) return
|
|
150
|
+
|
|
151
|
+
effectiveLoading.value = true
|
|
152
|
+
effectiveError.value = null
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
effectiveAccess.value = await getUserEffectiveAccess(user.value.userId)
|
|
156
|
+
} catch (cause) {
|
|
157
|
+
effectiveAccess.value = null
|
|
158
|
+
effectiveError.value = toErrorMessage(cause, 'Failed to load effective access')
|
|
159
|
+
} finally {
|
|
160
|
+
effectiveLoading.value = false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function load(): Promise<void> {
|
|
165
|
+
loading.value = true
|
|
166
|
+
error.value = null
|
|
167
|
+
accessDenied.value = false
|
|
168
|
+
resetForm()
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await access.load()
|
|
172
|
+
if (isNew.value && !access.canManageUsers) {
|
|
173
|
+
accessDenied.value = true
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
roles.value = await getRoles()
|
|
178
|
+
if (!isNew.value) {
|
|
179
|
+
const nextUser = await getUser(userId.value)
|
|
180
|
+
applyUser(nextUser)
|
|
181
|
+
await loadEffectiveAccess()
|
|
182
|
+
}
|
|
183
|
+
} catch (cause) {
|
|
184
|
+
accessDenied.value = cause instanceof ApiError && cause.status === 403
|
|
185
|
+
error.value = accessDenied.value ? null : toErrorMessage(cause, 'Failed to load user')
|
|
186
|
+
} finally {
|
|
187
|
+
loading.value = false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isRoleSelected(roleId: string): boolean {
|
|
192
|
+
return selectedRoleIds.value.includes(roleId)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function setRoleSelected(roleId: string, selected: boolean): void {
|
|
196
|
+
if (!canEdit.value) return
|
|
197
|
+
const next = new Set(selectedRoleIds.value)
|
|
198
|
+
if (selected) next.add(roleId)
|
|
199
|
+
else next.delete(roleId)
|
|
200
|
+
selectedRoleIds.value = Array.from(next)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isEmail(value: string): boolean {
|
|
204
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cleanOptional(value: string): string | null {
|
|
208
|
+
const trimmed = value.trim()
|
|
209
|
+
return trimmed.length > 0 ? trimmed : null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function uniqueMessages(messages: string[]): string[] {
|
|
213
|
+
const seen = new Set<string>()
|
|
214
|
+
const result: string[] = []
|
|
215
|
+
for (const message of messages) {
|
|
216
|
+
const text = String(message ?? '').trim()
|
|
217
|
+
if (!text || seen.has(text)) continue
|
|
218
|
+
seen.add(text)
|
|
219
|
+
result.push(text)
|
|
220
|
+
}
|
|
221
|
+
return result
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mapApiIssueMessage(path: string, message: string): string {
|
|
225
|
+
const normalizedPath = path.toLowerCase()
|
|
226
|
+
const normalizedMessage = message.trim()
|
|
227
|
+
if (normalizedPath.includes('email') && /valid email|email/i.test(normalizedMessage)) return 'Enter a valid email address.'
|
|
228
|
+
if (normalizedPath.includes('password') && /policy|invalid|weak|length/i.test(normalizedMessage)) return 'Password does not meet the password policy.'
|
|
229
|
+
return normalizedMessage
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function mapKeycloakError(cause: ApiError): string | null {
|
|
233
|
+
if (cause.errorCode !== 'ngb.keycloak.admin_request_failed') return null
|
|
234
|
+
|
|
235
|
+
const body = typeof cause.context?.keycloakErrorBody === 'string' ? cause.context.keycloakErrorBody : ''
|
|
236
|
+
const text = body.toLowerCase()
|
|
237
|
+
const statusCode = typeof cause.context?.statusCode === 'number' ? cause.context.statusCode : cause.status
|
|
238
|
+
|
|
239
|
+
if (statusCode === 409 || text.includes('already exists') || text.includes('duplicate')) {
|
|
240
|
+
return 'A user with this email already exists.'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ((text.includes('email') && text.includes('invalid')) || text.includes('invalid email')) {
|
|
244
|
+
return 'Enter a valid email address.'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (text.includes('password')) {
|
|
248
|
+
return 'Password does not meet the password policy.'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return 'The identity provider rejected the user data. Check the email and password, then try again.'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function mapUserSaveError(cause: unknown): string[] {
|
|
255
|
+
if (!(cause instanceof ApiError)) return []
|
|
256
|
+
|
|
257
|
+
const keycloak = mapKeycloakError(cause)
|
|
258
|
+
if (keycloak) return [keycloak]
|
|
259
|
+
|
|
260
|
+
const issueMessages = (cause.issues ?? [])
|
|
261
|
+
.map((issue) => mapApiIssueMessage(issue.path, issue.message))
|
|
262
|
+
.filter((message) => message.length > 0)
|
|
263
|
+
|
|
264
|
+
const errorMessages = Object.entries(cause.errors ?? {})
|
|
265
|
+
.flatMap(([path, messages]) => messages.map((message) => mapApiIssueMessage(path, message)))
|
|
266
|
+
.filter((message) => message.length > 0)
|
|
267
|
+
|
|
268
|
+
return uniqueMessages([...issueMessages, ...errorMessages])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validateBeforeSave(): boolean {
|
|
272
|
+
attemptedSave.value = true
|
|
273
|
+
serverValidationMessages.value = []
|
|
274
|
+
return Object.keys(fieldErrors.value).length === 0
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function startChangePassword(): void {
|
|
278
|
+
if (!canEdit.value) return
|
|
279
|
+
changePasswordMode.value = true
|
|
280
|
+
form.value.password = ''
|
|
281
|
+
form.value.confirmPassword = ''
|
|
282
|
+
serverValidationMessages.value = []
|
|
283
|
+
attemptedSave.value = false
|
|
284
|
+
showPassword.value = false
|
|
285
|
+
showConfirmPassword.value = false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function cancelChangePassword(): void {
|
|
289
|
+
changePasswordMode.value = false
|
|
290
|
+
form.value.password = ''
|
|
291
|
+
form.value.confirmPassword = ''
|
|
292
|
+
serverValidationMessages.value = []
|
|
293
|
+
attemptedSave.value = false
|
|
294
|
+
showPassword.value = false
|
|
295
|
+
showConfirmPassword.value = false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function save(): Promise<void> {
|
|
299
|
+
if (!canEdit.value || saving.value) return
|
|
300
|
+
|
|
301
|
+
if (!validateBeforeSave()) {
|
|
302
|
+
error.value = null
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
saving.value = true
|
|
307
|
+
error.value = null
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
if (isNew.value) {
|
|
311
|
+
const created = await createUser({
|
|
312
|
+
email: form.value.email.trim(),
|
|
313
|
+
firstName: null,
|
|
314
|
+
lastName: null,
|
|
315
|
+
displayName: cleanOptional(form.value.displayName),
|
|
316
|
+
enabled: true,
|
|
317
|
+
temporaryPassword: form.value.password,
|
|
318
|
+
requirePasswordUpdate: form.value.requirePasswordUpdate,
|
|
319
|
+
roleIds: selectedRoleIds.value,
|
|
320
|
+
})
|
|
321
|
+
applyUser(created)
|
|
322
|
+
toasts?.push({ title: 'User created', message: 'User was saved.', tone: 'success' })
|
|
323
|
+
await router.replace(`/admin/security/users/${encodeURIComponent(created.userId)}`)
|
|
324
|
+
await loadEffectiveAccess()
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const password = changePasswordMode.value ? form.value.password : null
|
|
329
|
+
const updated = await updateUser(userId.value, {
|
|
330
|
+
email: cleanOptional(form.value.email),
|
|
331
|
+
firstName: null,
|
|
332
|
+
lastName: null,
|
|
333
|
+
displayName: cleanOptional(form.value.displayName),
|
|
334
|
+
enabled: user.value?.keycloakEnabled ?? user.value?.isActive ?? true,
|
|
335
|
+
temporaryPassword: password,
|
|
336
|
+
requirePasswordUpdate: false,
|
|
337
|
+
roleIds: selectedRoleIds.value,
|
|
338
|
+
})
|
|
339
|
+
applyUser(updated)
|
|
340
|
+
toasts?.push({ title: 'User saved', message: 'Changes were saved.', tone: 'success' })
|
|
341
|
+
await loadEffectiveAccess()
|
|
342
|
+
} catch (cause) {
|
|
343
|
+
const messages = mapUserSaveError(cause)
|
|
344
|
+
if (messages.length > 0) serverValidationMessages.value = messages
|
|
345
|
+
else error.value = toErrorMessage(cause, 'Failed to save user')
|
|
346
|
+
} finally {
|
|
347
|
+
saving.value = false
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function confirmActivationChange(): Promise<void> {
|
|
352
|
+
if (!user.value || !confirmMode.value) return
|
|
353
|
+
|
|
354
|
+
activating.value = true
|
|
355
|
+
error.value = null
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
if (confirmMode.value === 'deactivate') await deactivateUser(user.value.userId)
|
|
359
|
+
else await reactivateUser(user.value.userId)
|
|
360
|
+
confirmMode.value = null
|
|
361
|
+
await load()
|
|
362
|
+
} catch (cause) {
|
|
363
|
+
error.value = toErrorMessage(cause, 'Failed to update user status')
|
|
364
|
+
} finally {
|
|
365
|
+
activating.value = false
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function goBack(): void {
|
|
370
|
+
void router.push('/admin/security/users')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function openRole(roleId: string): void {
|
|
374
|
+
void router.push(`/admin/security/roles/${encodeURIComponent(roleId)}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function openAuditLog(): void {
|
|
378
|
+
if (!canOpenAudit.value) return
|
|
379
|
+
auditOpen.value = true
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function closeAuditLog(): void {
|
|
383
|
+
auditOpen.value = false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
watch(
|
|
387
|
+
() => route.params.userId,
|
|
388
|
+
() => {
|
|
389
|
+
void load()
|
|
390
|
+
},
|
|
391
|
+
{ immediate: true },
|
|
392
|
+
)
|
|
393
|
+
</script>
|
|
394
|
+
|
|
395
|
+
<template>
|
|
396
|
+
<NgbAccessDeniedState v-if="accessDenied" />
|
|
397
|
+
|
|
398
|
+
<div v-else class="flex h-full min-h-0 flex-col">
|
|
399
|
+
<NgbPageHeader :title="title" :can-back="true" :breadcrumbs="['Users']" @back="goBack">
|
|
400
|
+
<template #secondary>
|
|
401
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
402
|
+
<NgbBadge v-if="user" :tone="user.isActive ? 'success' : 'danger'">{{ user.isActive ? 'Active' : 'Inactive' }}</NgbBadge>
|
|
403
|
+
</div>
|
|
404
|
+
</template>
|
|
405
|
+
<template #actions>
|
|
406
|
+
<button v-if="canOpenAudit" type="button" class="ngb-iconbtn" title="Audit log" :disabled="loading || saving" @click="openAuditLog">
|
|
407
|
+
<NgbIcon name="history" />
|
|
408
|
+
</button>
|
|
409
|
+
<button
|
|
410
|
+
v-if="user && canEdit && user.isActive"
|
|
411
|
+
type="button"
|
|
412
|
+
class="ngb-iconbtn"
|
|
413
|
+
title="Deactivate"
|
|
414
|
+
aria-label="Deactivate"
|
|
415
|
+
:disabled="saving || activating"
|
|
416
|
+
@click="confirmMode = 'deactivate'"
|
|
417
|
+
>
|
|
418
|
+
<NgbStatusIcon status="marked" title="Deactivate" />
|
|
419
|
+
</button>
|
|
420
|
+
<button
|
|
421
|
+
v-if="user && canEdit && !user.isActive"
|
|
422
|
+
type="button"
|
|
423
|
+
class="ngb-iconbtn"
|
|
424
|
+
title="Reactivate"
|
|
425
|
+
aria-label="Reactivate"
|
|
426
|
+
:disabled="saving || activating"
|
|
427
|
+
@click="confirmMode = 'reactivate'"
|
|
428
|
+
>
|
|
429
|
+
<NgbStatusIcon status="posted" title="Reactivate" />
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
class="ngb-iconbtn"
|
|
434
|
+
title="Save"
|
|
435
|
+
aria-label="Save"
|
|
436
|
+
:disabled="!canEdit || loading || saving"
|
|
437
|
+
@click="save"
|
|
438
|
+
>
|
|
439
|
+
<NgbIcon name="save" />
|
|
440
|
+
</button>
|
|
441
|
+
</template>
|
|
442
|
+
</NgbPageHeader>
|
|
443
|
+
|
|
444
|
+
<main class="flex-1 min-h-0 overflow-auto p-6 pb-8">
|
|
445
|
+
<div v-if="loading" class="text-sm text-ngb-muted">Loading...</div>
|
|
446
|
+
|
|
447
|
+
<div v-else class="grid min-h-[calc(100vh-270px)] items-stretch gap-6 xl:grid-cols-[minmax(0,1fr),420px]">
|
|
448
|
+
<div class="flex min-w-0 flex-col gap-6">
|
|
449
|
+
<section class="rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card p-4 shadow-card">
|
|
450
|
+
<NgbValidationSummary v-if="validationMessages.length > 0" class="mb-4" :messages="validationMessages" />
|
|
451
|
+
|
|
452
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
453
|
+
<div>
|
|
454
|
+
<NgbInput v-model="form.email" label="Email" type="email" :disabled="!canEdit" />
|
|
455
|
+
<div v-if="fieldErrors.email" class="mt-1 text-xs text-ngb-danger">{{ fieldErrors.email }}</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div>
|
|
458
|
+
<NgbInput v-model="form.displayName" label="Display name" :disabled="!canEdit" />
|
|
459
|
+
<div v-if="fieldErrors.displayName" class="mt-1 text-xs text-ngb-danger">{{ fieldErrors.displayName }}</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div v-if="shouldShowPasswordFields" class="mt-4 grid gap-4 md:grid-cols-2">
|
|
464
|
+
<div>
|
|
465
|
+
<label class="mb-1 block text-xs font-semibold text-ngb-muted">Password</label>
|
|
466
|
+
<div class="relative">
|
|
467
|
+
<input
|
|
468
|
+
:type="showPassword ? 'text' : 'password'"
|
|
469
|
+
:value="form.password"
|
|
470
|
+
:disabled="!canEdit"
|
|
471
|
+
autocomplete="new-password"
|
|
472
|
+
class="h-9 w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 pr-10 text-sm text-ngb-text placeholder:text-ngb-muted/70 ngb-focus"
|
|
473
|
+
:class="!canEdit ? 'cursor-not-allowed opacity-60' : ''"
|
|
474
|
+
@input="form.password = ($event.target as HTMLInputElement).value"
|
|
475
|
+
/>
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
class="ngb-iconbtn absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
|
|
479
|
+
:disabled="!canEdit"
|
|
480
|
+
:title="showPassword ? 'Hide password' : 'Show password'"
|
|
481
|
+
@click="showPassword = !showPassword"
|
|
482
|
+
>
|
|
483
|
+
<NgbIcon :name="showPassword ? 'eye-off' : 'eye'" :size="16" />
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
<div v-if="fieldErrors.password" class="mt-1 text-xs text-ngb-danger">{{ fieldErrors.password }}</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div>
|
|
490
|
+
<label class="mb-1 block text-xs font-semibold text-ngb-muted">Confirm password</label>
|
|
491
|
+
<div class="relative">
|
|
492
|
+
<input
|
|
493
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
494
|
+
:value="form.confirmPassword"
|
|
495
|
+
:disabled="!canEdit"
|
|
496
|
+
autocomplete="new-password"
|
|
497
|
+
class="h-9 w-full rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card px-3 pr-10 text-sm text-ngb-text placeholder:text-ngb-muted/70 ngb-focus"
|
|
498
|
+
:class="!canEdit ? 'cursor-not-allowed opacity-60' : ''"
|
|
499
|
+
@input="form.confirmPassword = ($event.target as HTMLInputElement).value"
|
|
500
|
+
/>
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
class="ngb-iconbtn absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
|
|
504
|
+
:disabled="!canEdit"
|
|
505
|
+
:title="showConfirmPassword ? 'Hide password' : 'Show password'"
|
|
506
|
+
@click="showConfirmPassword = !showConfirmPassword"
|
|
507
|
+
>
|
|
508
|
+
<NgbIcon :name="showConfirmPassword ? 'eye-off' : 'eye'" :size="16" />
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
<div v-if="fieldErrors.confirmPassword" class="mt-1 text-xs text-ngb-danger">{{ fieldErrors.confirmPassword }}</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<div v-if="isNew" class="mt-4">
|
|
516
|
+
<NgbSwitch v-model="form.requirePasswordUpdate" label="Require password update" :disabled="!canEdit" />
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<div v-else class="mt-4 flex flex-wrap items-center gap-2">
|
|
520
|
+
<NgbButton v-if="!changePasswordMode" size="sm" variant="secondary" :disabled="!canEdit" @click="startChangePassword">
|
|
521
|
+
<NgbIcon name="shield" :size="15" />
|
|
522
|
+
Change password
|
|
523
|
+
</NgbButton>
|
|
524
|
+
<NgbButton v-else size="sm" variant="ghost" :disabled="!canEdit" @click="cancelChangePassword">
|
|
525
|
+
Cancel
|
|
526
|
+
</NgbButton>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<div
|
|
530
|
+
v-if="error"
|
|
531
|
+
class="mt-4 rounded-[var(--ngb-radius)] border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-100"
|
|
532
|
+
>
|
|
533
|
+
{{ error }}
|
|
534
|
+
</div>
|
|
535
|
+
</section>
|
|
536
|
+
|
|
537
|
+
<NgbEffectiveAccessPanel
|
|
538
|
+
v-if="!isNew"
|
|
539
|
+
class="min-h-[560px] flex-1"
|
|
540
|
+
:access="effectiveAccess"
|
|
541
|
+
:loading="effectiveLoading"
|
|
542
|
+
:error="effectiveError"
|
|
543
|
+
:show-refresh="false"
|
|
544
|
+
@refresh="loadEffectiveAccess"
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<section class="flex min-h-full flex-col rounded-[var(--ngb-radius)] border border-ngb-border bg-ngb-card shadow-card">
|
|
549
|
+
<div class="border-b border-ngb-border px-4 py-3">
|
|
550
|
+
<h2 class="text-sm font-semibold text-ngb-text">Roles</h2>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="min-h-0 flex-1 overflow-auto divide-y divide-ngb-border">
|
|
553
|
+
<div
|
|
554
|
+
v-for="role in activeRoles"
|
|
555
|
+
:key="role.roleId"
|
|
556
|
+
class="grid grid-cols-[minmax(0,1fr),2rem] items-start gap-2 px-4 py-3 hover:bg-[var(--ngb-row-hover)]"
|
|
557
|
+
:class="!canEdit ? 'opacity-70' : ''"
|
|
558
|
+
>
|
|
559
|
+
<label class="grid min-w-0 cursor-pointer grid-cols-[1.25rem,minmax(0,1fr)] gap-3" :class="!canEdit ? 'cursor-not-allowed' : ''">
|
|
560
|
+
<input
|
|
561
|
+
type="checkbox"
|
|
562
|
+
class="mt-1 h-4 w-4"
|
|
563
|
+
:checked="isRoleSelected(role.roleId)"
|
|
564
|
+
:disabled="!canEdit"
|
|
565
|
+
@change="setRoleSelected(role.roleId, ($event.target as HTMLInputElement).checked)"
|
|
566
|
+
/>
|
|
567
|
+
<span class="min-w-0">
|
|
568
|
+
<span class="flex min-w-0 items-center gap-2">
|
|
569
|
+
<span class="truncate text-sm font-medium text-ngb-text">{{ role.name }}</span>
|
|
570
|
+
<NgbBadge v-if="!role.isActive" tone="danger">Inactive</NgbBadge>
|
|
571
|
+
<NgbBadge v-if="role.isSystem" tone="neutral">System</NgbBadge>
|
|
572
|
+
</span>
|
|
573
|
+
<span class="mt-0.5 block truncate font-mono text-xs text-ngb-muted">{{ role.code }}</span>
|
|
574
|
+
</span>
|
|
575
|
+
</label>
|
|
576
|
+
<button
|
|
577
|
+
type="button"
|
|
578
|
+
class="ngb-iconbtn"
|
|
579
|
+
:title="`Open ${role.name}`"
|
|
580
|
+
:aria-label="`Open role ${role.name}`"
|
|
581
|
+
@click.stop="openRole(role.roleId)"
|
|
582
|
+
>
|
|
583
|
+
<NgbIcon name="open-in-new" :size="16" />
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
<div v-if="activeRoles.length === 0" class="px-4 py-6 text-sm text-ngb-muted">No roles available.</div>
|
|
587
|
+
</div>
|
|
588
|
+
</section>
|
|
589
|
+
</div>
|
|
590
|
+
</main>
|
|
591
|
+
|
|
592
|
+
<NgbConfirmDialog
|
|
593
|
+
:open="confirmMode !== null"
|
|
594
|
+
:title="confirmMode === 'reactivate' ? 'Reactivate user?' : 'Deactivate user?'"
|
|
595
|
+
:message="confirmMode === 'reactivate' ? 'This enables the NGB application user and attempts to enable the linked Keycloak user.' : 'This disables access without deleting history, audit records, or ownership links.'"
|
|
596
|
+
:confirm-text="confirmMode === 'reactivate' ? 'Reactivate' : 'Deactivate'"
|
|
597
|
+
:danger="confirmMode === 'deactivate'"
|
|
598
|
+
:confirm-loading="activating"
|
|
599
|
+
@update:open="(value) => { if (!value) confirmMode = null }"
|
|
600
|
+
@confirm="confirmActivationChange"
|
|
601
|
+
/>
|
|
602
|
+
|
|
603
|
+
<NgbDrawer v-model:open="auditOpen" title="Audit Log" hide-header flush-body>
|
|
604
|
+
<NgbEntityAuditSidebar
|
|
605
|
+
:open="auditOpen"
|
|
606
|
+
:entity-kind="AUDIT_ENTITY_KIND_SECURITY_USER"
|
|
607
|
+
:entity-id="user?.userId ?? null"
|
|
608
|
+
:entity-title="auditEntityTitle"
|
|
609
|
+
:behavior="USER_AUDIT_BEHAVIOR"
|
|
610
|
+
@back="closeAuditLog"
|
|
611
|
+
@close="closeAuditLog"
|
|
612
|
+
/>
|
|
613
|
+
</NgbDrawer>
|
|
614
|
+
</div>
|
|
615
|
+
</template>
|