@drax/identity-vue 3.21.0 → 3.23.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/package.json +4 -4
- package/src/components/IdentityChangeOwnPassword/IdentityChangeOwnPassword.vue +3 -6
- package/src/components/IdentityRecoveryPasswordComplete/IdentityRecoveryPasswordComplete.vue +4 -9
- package/src/components/PasswordPolicy/PasswordPolicyInput.vue +118 -0
- package/src/components/PasswordPolicy/PasswordPolicySummary.vue +244 -0
- package/src/cruds/user-crud/UserForm.vue +17 -17
- package/src/cruds/user-crud/UserPasswordDialog.vue +4 -1
- package/src/cruds/user-crud/UserPasswordForm.vue +14 -9
- package/src/pages/PasswordPolicyPage.vue +6 -164
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.23.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"@drax/crud-front": "^3.21.0",
|
|
30
30
|
"@drax/crud-share": "^3.21.0",
|
|
31
31
|
"@drax/crud-vue": "^3.21.0",
|
|
32
|
-
"@drax/identity-front": "^3.
|
|
33
|
-
"@drax/identity-share": "^3.
|
|
32
|
+
"@drax/identity-front": "^3.23.0",
|
|
33
|
+
"@drax/identity-share": "^3.23.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"pinia": "^3.0.4",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"vue-tsc": "^3.2.4",
|
|
56
56
|
"vuetify": "^3.11.8"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "5768e7721a2eaa06cf4bcf97811897613a8c9470"
|
|
59
59
|
}
|
|
@@ -5,6 +5,7 @@ import {ClientError} from "@drax/common-front";
|
|
|
5
5
|
import type {IClientInputError} from "@drax/common-front";
|
|
6
6
|
import {useI18nValidation} from "@drax/common-vue";
|
|
7
7
|
import {useI18n} from "vue-i18n";
|
|
8
|
+
import PasswordPolicyInput from "../PasswordPolicy/PasswordPolicyInput.vue";
|
|
8
9
|
|
|
9
10
|
const {t,te} = useI18n()
|
|
10
11
|
const {$ta} = useI18nValidation()
|
|
@@ -86,18 +87,14 @@ async function submitChangePassword() {
|
|
|
86
87
|
:error-messages="$ta(inputErrors?.currentPassword)"
|
|
87
88
|
></v-text-field>
|
|
88
89
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.newPassword') }}</div>
|
|
89
|
-
<
|
|
90
|
+
<PasswordPolicyInput
|
|
90
91
|
variant="outlined"
|
|
91
92
|
id="new-password-input"
|
|
92
93
|
v-model="newPassword"
|
|
93
|
-
:type="newPasswordVisibility ? 'text': 'password'"
|
|
94
94
|
required
|
|
95
|
-
prepend-inner-icon="mdi-lock-outline"
|
|
96
|
-
:append-inner-icon="newPasswordVisibility ? 'mdi-eye-off': 'mdi-eye'"
|
|
97
|
-
@click:append-inner="newPasswordVisibility = !newPasswordVisibility"
|
|
98
95
|
autocomplete="new-password"
|
|
99
96
|
:error-messages="$ta(inputErrors?.newPassword)"
|
|
100
|
-
|
|
97
|
+
/>
|
|
101
98
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.confirmPassword') }}</div>
|
|
102
99
|
<v-text-field
|
|
103
100
|
variant="outlined"
|
package/src/components/IdentityRecoveryPasswordComplete/IdentityRecoveryPasswordComplete.vue
CHANGED
|
@@ -6,6 +6,7 @@ import type {IClientInputError} from "@drax/common-front";
|
|
|
6
6
|
import {useI18nValidation} from "@drax/common-vue";
|
|
7
7
|
import {useI18n} from "vue-i18n";
|
|
8
8
|
import {useRoute, useRouter} from "vue-router"
|
|
9
|
+
import PasswordPolicyInput from "../PasswordPolicy/PasswordPolicyInput.vue";
|
|
9
10
|
|
|
10
11
|
const {t,te} = useI18n()
|
|
11
12
|
const {$ta} = useI18nValidation()
|
|
@@ -22,9 +23,7 @@ const inputErrors = ref<IClientInputError|undefined>({currentPassword: [], newPa
|
|
|
22
23
|
const errorMsg = ref('')
|
|
23
24
|
const loading = ref(false)
|
|
24
25
|
const success = ref(false)
|
|
25
|
-
|
|
26
|
-
let newPasswordVisibility = ref(false)
|
|
27
|
-
|
|
26
|
+
const newPasswordVisibility = ref(false)
|
|
28
27
|
|
|
29
28
|
const isFormValid = computed(() =>
|
|
30
29
|
recoveryCode.value.trim() !== '' && newPassword.value.trim() !== ''
|
|
@@ -107,18 +106,14 @@ async function submitResetPassword() {
|
|
|
107
106
|
|
|
108
107
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.newPassword') }}</div>
|
|
109
108
|
<!-- NEW PASSWORD-->
|
|
110
|
-
<
|
|
109
|
+
<PasswordPolicyInput
|
|
111
110
|
variant="outlined"
|
|
112
111
|
id="new-password-input"
|
|
113
112
|
v-model="newPassword"
|
|
114
|
-
:type="newPasswordVisibility ? 'text': 'password'"
|
|
115
113
|
required
|
|
116
|
-
prepend-inner-icon="mdi-lock-outline"
|
|
117
|
-
:append-inner-icon="newPasswordVisibility ? 'mdi-eye-off': 'mdi-eye'"
|
|
118
|
-
@click:append-inner="newPasswordVisibility = !newPasswordVisibility"
|
|
119
114
|
autocomplete="new-password"
|
|
120
115
|
:error-messages="$ta(inputErrors?.newPassword)"
|
|
121
|
-
|
|
116
|
+
/>
|
|
122
117
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.confirmPassword') }}</div>
|
|
123
118
|
<!-- CONFIRM PASSWORD-->
|
|
124
119
|
<v-text-field
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, ref, useAttrs} from "vue";
|
|
3
|
+
import {useI18n} from "vue-i18n";
|
|
4
|
+
import {useDisplay} from "vuetify";
|
|
5
|
+
import PasswordPolicySummary from "./PasswordPolicySummary.vue";
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
inheritAttrs: false
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const modelValue = defineModel<string>({
|
|
12
|
+
default: ''
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const attrs = useAttrs()
|
|
16
|
+
const {t} = useI18n()
|
|
17
|
+
const {xs} = useDisplay()
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
id?: string
|
|
21
|
+
label?: string
|
|
22
|
+
variant?: 'outlined' | 'plain' | 'filled' | 'solo' | 'solo-filled' | 'solo-inverted' | 'underlined'
|
|
23
|
+
rules?: Array<(value: string) => boolean | string>
|
|
24
|
+
errorMessages?: string | string[]
|
|
25
|
+
readonly?: boolean
|
|
26
|
+
required?: boolean
|
|
27
|
+
autocomplete?: string
|
|
28
|
+
prependInnerIcon?: string
|
|
29
|
+
showPolicyLink?: boolean
|
|
30
|
+
}>(), {
|
|
31
|
+
id: undefined,
|
|
32
|
+
label: undefined,
|
|
33
|
+
variant: 'outlined',
|
|
34
|
+
rules: () => [],
|
|
35
|
+
errorMessages: () => [],
|
|
36
|
+
readonly: false,
|
|
37
|
+
required: false,
|
|
38
|
+
autocomplete: 'new-password',
|
|
39
|
+
prependInnerIcon: 'mdi-lock-outline',
|
|
40
|
+
showPolicyLink: true
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const isVisible = ref(false)
|
|
44
|
+
const inputType = computed(() => isVisible.value ? 'text' : 'password')
|
|
45
|
+
const visibilityIcon = computed(() => isVisible.value ? 'mdi-eye-off' : 'mdi-eye')
|
|
46
|
+
const policyLinkLabel = computed(() => t(xs.value ? 'user.passwordPolicy.linkShort' : 'user.passwordPolicy.link'))
|
|
47
|
+
|
|
48
|
+
function toggleVisibility() {
|
|
49
|
+
isVisible.value = !isVisible.value
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<v-text-field
|
|
55
|
+
v-bind="attrs"
|
|
56
|
+
:id="id"
|
|
57
|
+
v-model="modelValue"
|
|
58
|
+
:label="label"
|
|
59
|
+
:type="inputType"
|
|
60
|
+
:variant="variant"
|
|
61
|
+
:rules="rules"
|
|
62
|
+
:required="required"
|
|
63
|
+
:prepend-inner-icon="prependInnerIcon"
|
|
64
|
+
:autocomplete="autocomplete"
|
|
65
|
+
:error-messages="errorMessages"
|
|
66
|
+
:readonly="readonly"
|
|
67
|
+
>
|
|
68
|
+
<template #append-inner>
|
|
69
|
+
<div
|
|
70
|
+
v-if="showPolicyLink"
|
|
71
|
+
class="password-policy-link-wrapper mb-2"
|
|
72
|
+
>
|
|
73
|
+
<v-menu
|
|
74
|
+
open-on-hover
|
|
75
|
+
location="top end"
|
|
76
|
+
:close-delay="120"
|
|
77
|
+
:open-delay="120"
|
|
78
|
+
>
|
|
79
|
+
<template #activator="{ props: menuProps }">
|
|
80
|
+
<v-btn
|
|
81
|
+
v-bind="menuProps"
|
|
82
|
+
tabindex="-1"
|
|
83
|
+
variant="text"
|
|
84
|
+
color="grey"
|
|
85
|
+
size="small"
|
|
86
|
+
class="mt-2"
|
|
87
|
+
>
|
|
88
|
+
{{ policyLinkLabel }}
|
|
89
|
+
</v-btn>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<v-card class="password-policy-menu" rounded="lg">
|
|
93
|
+
<v-card-text class="pa-3">
|
|
94
|
+
<PasswordPolicySummary variant="compact" />
|
|
95
|
+
</v-card-text>
|
|
96
|
+
</v-card>
|
|
97
|
+
</v-menu>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<v-icon
|
|
101
|
+
:icon="visibilityIcon"
|
|
102
|
+
@click="toggleVisibility"
|
|
103
|
+
/>
|
|
104
|
+
</template>
|
|
105
|
+
</v-text-field>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<style scoped>
|
|
109
|
+
.password-policy-link-wrapper {
|
|
110
|
+
display: flex;
|
|
111
|
+
justify-content: flex-start;
|
|
112
|
+
margin-bottom: 4px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.password-policy-menu {
|
|
116
|
+
max-width: 400px;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, onMounted, ref} from "vue";
|
|
3
|
+
import {useI18n} from "vue-i18n";
|
|
4
|
+
import {useAuth} from "../../composables/useAuth";
|
|
5
|
+
import type {IPasswordPolicy} from "@drax/identity-front";
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
variant?: 'compact' | 'full'
|
|
9
|
+
}>(), {
|
|
10
|
+
variant: 'full'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const {t} = useI18n()
|
|
14
|
+
const {passwordPolicy} = useAuth()
|
|
15
|
+
|
|
16
|
+
const loading = ref(true)
|
|
17
|
+
const errorMsg = ref('')
|
|
18
|
+
const policy = ref<IPasswordPolicy | null>(null)
|
|
19
|
+
|
|
20
|
+
const compact = computed(() => props.variant === 'compact')
|
|
21
|
+
|
|
22
|
+
const rules = computed(() => {
|
|
23
|
+
if (!policy.value) {
|
|
24
|
+
return []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
label: t('user.passwordPolicy.minLength', {count: policy.value.minLength}),
|
|
30
|
+
enabled: true,
|
|
31
|
+
icon: 'mdi-format-letter-case'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: t('user.passwordPolicy.maxLength', {count: policy.value.maxLength}),
|
|
35
|
+
enabled: true,
|
|
36
|
+
icon: 'mdi-ruler'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: t('user.passwordPolicy.requireUppercase'),
|
|
40
|
+
enabled: policy.value.requireUppercase,
|
|
41
|
+
icon: 'mdi-format-letter-case-upper'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: t('user.passwordPolicy.requireLowercase'),
|
|
45
|
+
enabled: policy.value.requireLowercase,
|
|
46
|
+
icon: 'mdi-format-letter-case-lower'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: t('user.passwordPolicy.requireNumber'),
|
|
50
|
+
enabled: policy.value.requireNumber,
|
|
51
|
+
icon: 'mdi-numeric'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: t('user.passwordPolicy.requireSpecialChar'),
|
|
55
|
+
description: policy.value.allowedSpecialChars
|
|
56
|
+
? t('user.passwordPolicy.specialCharsAllowed', {chars: policy.value.allowedSpecialChars})
|
|
57
|
+
: undefined,
|
|
58
|
+
enabled: policy.value.requireSpecialChar,
|
|
59
|
+
icon: 'mdi-asterisk'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: t('user.passwordPolicy.disallowSpaces'),
|
|
63
|
+
enabled: policy.value.disallowSpaces,
|
|
64
|
+
icon: 'mdi-keyboard-space'
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const metadata = computed(() => {
|
|
70
|
+
if (!policy.value) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const items = [
|
|
75
|
+
{
|
|
76
|
+
label: t('user.passwordPolicy.reuseLabel'),
|
|
77
|
+
value: policy.value.preventReuse > 0
|
|
78
|
+
? t('user.passwordPolicy.reuseValue', {count: policy.value.preventReuse})
|
|
79
|
+
: t('user.passwordPolicy.reuseNone')
|
|
80
|
+
},
|
|
81
|
+
// {
|
|
82
|
+
// label: t('user.passwordPolicy.expirationLabel'),
|
|
83
|
+
// value: policy.value.expirationDays
|
|
84
|
+
// ? t('user.passwordPolicy.expirationValue', {count: policy.value.expirationDays})
|
|
85
|
+
// : t('user.passwordPolicy.expirationNone')
|
|
86
|
+
// }
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
if (policy.value.requireSpecialChar && policy.value.allowedSpecialChars) {
|
|
90
|
+
items.push({
|
|
91
|
+
label: t('user.passwordPolicy.specialCharsLabel'),
|
|
92
|
+
value: policy.value.allowedSpecialChars
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return items
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
async function loadPolicy() {
|
|
100
|
+
try {
|
|
101
|
+
loading.value = true
|
|
102
|
+
errorMsg.value = ''
|
|
103
|
+
policy.value = await passwordPolicy()
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const err = error as Error
|
|
106
|
+
errorMsg.value = err.message || t('user.passwordPolicy.loadError')
|
|
107
|
+
} finally {
|
|
108
|
+
loading.value = false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(loadPolicy)
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<div>
|
|
117
|
+
<div
|
|
118
|
+
v-if="loading"
|
|
119
|
+
:class="compact ? 'text-caption text-medium-emphasis' : 'text-medium-emphasis'"
|
|
120
|
+
>
|
|
121
|
+
{{ t('user.passwordPolicy.loading') }}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<v-alert
|
|
125
|
+
v-else-if="errorMsg"
|
|
126
|
+
type="error"
|
|
127
|
+
variant="tonal"
|
|
128
|
+
:density="compact ? 'compact' : 'default'"
|
|
129
|
+
>
|
|
130
|
+
{{ errorMsg }}
|
|
131
|
+
</v-alert>
|
|
132
|
+
|
|
133
|
+
<template v-else-if="policy">
|
|
134
|
+
<template v-if="compact">
|
|
135
|
+
<div class="text-caption text-medium-emphasis mb-2">
|
|
136
|
+
{{ t('user.passwordPolicy.description') }}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<v-list class="py-0" density="compact" lines="two">
|
|
140
|
+
<v-list-item
|
|
141
|
+
v-for="rule in rules"
|
|
142
|
+
:key="rule.label"
|
|
143
|
+
class="px-0"
|
|
144
|
+
:prepend-icon="rule.icon"
|
|
145
|
+
>
|
|
146
|
+
<v-list-item-title class="text-body-2 policy-text-wrap">
|
|
147
|
+
{{ rule.label }}
|
|
148
|
+
</v-list-item-title>
|
|
149
|
+
<v-list-item-subtitle v-if="rule.enabled && rule.description" class="text-caption policy-text-wrap">
|
|
150
|
+
{{ rule.description }}
|
|
151
|
+
</v-list-item-subtitle>
|
|
152
|
+
<template #append>
|
|
153
|
+
<v-chip
|
|
154
|
+
:color="rule.enabled ? 'success' : 'default'"
|
|
155
|
+
size="x-small"
|
|
156
|
+
variant="tonal"
|
|
157
|
+
>
|
|
158
|
+
{{ rule.enabled ? t('user.passwordPolicy.required') : t('user.passwordPolicy.optional') }}
|
|
159
|
+
</v-chip>
|
|
160
|
+
</template>
|
|
161
|
+
</v-list-item>
|
|
162
|
+
</v-list>
|
|
163
|
+
|
|
164
|
+
<v-divider class="my-2" />
|
|
165
|
+
|
|
166
|
+
<div class="compact-meta">
|
|
167
|
+
<div v-for="item in metadata" :key="item.label" class="compact-meta-item">
|
|
168
|
+
<span class="text-caption text-medium-emphasis">{{ item.label }}:</span>
|
|
169
|
+
<span class="text-body-2">{{ item.value }}</span>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<template v-else>
|
|
175
|
+
<div class="text-body-1 mb-4">
|
|
176
|
+
{{ t('user.passwordPolicy.description') }}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<v-list lines="two">
|
|
180
|
+
<v-list-item
|
|
181
|
+
v-for="rule in rules"
|
|
182
|
+
:key="rule.label"
|
|
183
|
+
:prepend-icon="rule.icon"
|
|
184
|
+
>
|
|
185
|
+
<v-list-item-title>{{ rule.label }}</v-list-item-title>
|
|
186
|
+
<v-list-item-subtitle v-if="rule.enabled && rule.description">
|
|
187
|
+
{{ rule.description }}
|
|
188
|
+
</v-list-item-subtitle>
|
|
189
|
+
<template #append>
|
|
190
|
+
<v-chip
|
|
191
|
+
:color="rule.enabled ? 'success' : 'default'"
|
|
192
|
+
size="small"
|
|
193
|
+
variant="tonal"
|
|
194
|
+
>
|
|
195
|
+
{{ rule.enabled ? t('user.passwordPolicy.required') : t('user.passwordPolicy.optional') }}
|
|
196
|
+
</v-chip>
|
|
197
|
+
</template>
|
|
198
|
+
</v-list-item>
|
|
199
|
+
</v-list>
|
|
200
|
+
|
|
201
|
+
<v-divider />
|
|
202
|
+
|
|
203
|
+
<v-row class="mt-1">
|
|
204
|
+
<v-col
|
|
205
|
+
v-for="item in metadata"
|
|
206
|
+
:key="item.label"
|
|
207
|
+
cols="12"
|
|
208
|
+
sm="6"
|
|
209
|
+
>
|
|
210
|
+
<v-sheet class="info-box pa-4 fill-height" rounded>
|
|
211
|
+
<div class="text-caption text-medium-emphasis">{{ item.label }}</div>
|
|
212
|
+
<div class="text-body-1">{{ item.value }}</div>
|
|
213
|
+
</v-sheet>
|
|
214
|
+
</v-col>
|
|
215
|
+
</v-row>
|
|
216
|
+
</template>
|
|
217
|
+
</template>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
|
|
221
|
+
<style scoped>
|
|
222
|
+
.compact-meta {
|
|
223
|
+
display: grid;
|
|
224
|
+
gap: 6px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.compact-meta-item {
|
|
228
|
+
display: flex;
|
|
229
|
+
gap: 6px;
|
|
230
|
+
align-items: baseline;
|
|
231
|
+
flex-wrap: wrap;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.policy-text-wrap {
|
|
235
|
+
white-space: normal;
|
|
236
|
+
overflow: visible;
|
|
237
|
+
text-overflow: unset;
|
|
238
|
+
word-break: break-word;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.info-box {
|
|
242
|
+
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
|
243
|
+
}
|
|
244
|
+
</style>
|
|
@@ -7,10 +7,10 @@ import TenantCombobox from "../../combobox/TenantCombobox.vue";
|
|
|
7
7
|
import {useAuth} from "../../composables/useAuth";
|
|
8
8
|
import {useI18n} from "vue-i18n";
|
|
9
9
|
import {useIdentityCrudStore} from "../../stores/IdentityCrudStore";
|
|
10
|
+
import PasswordPolicyInput from "../../components/PasswordPolicy/PasswordPolicyInput.vue";
|
|
10
11
|
|
|
11
12
|
const {$ta} = useI18nValidation()
|
|
12
13
|
const {t, te} = useI18n()
|
|
13
|
-
|
|
14
14
|
const {hasPermission} = useAuth()
|
|
15
15
|
|
|
16
16
|
|
|
@@ -126,21 +126,22 @@ function confirmPasswordRule(value: string) {
|
|
|
126
126
|
</v-col>
|
|
127
127
|
|
|
128
128
|
<v-col cols="12">
|
|
129
|
-
<v-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
129
|
+
<template v-if="enablePassword">
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
<PasswordPolicyInput
|
|
133
|
+
id="password-input"
|
|
134
|
+
:label="t('user.field.password')"
|
|
135
|
+
v-model="valueModel.password"
|
|
136
|
+
:variant="variant"
|
|
137
|
+
:rules="[v => !!v || t('validation.required')]"
|
|
138
|
+
autocomplete="new-password"
|
|
139
|
+
:error-messages="$ta(store.inputErrors?.password)"
|
|
140
|
+
:readonly="readonly"
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
</template>
|
|
144
145
|
</v-col>
|
|
145
146
|
|
|
146
147
|
<v-col cols="12">
|
|
@@ -248,5 +249,4 @@ function confirmPasswordRule(value: string) {
|
|
|
248
249
|
</template>
|
|
249
250
|
|
|
250
251
|
<style scoped>
|
|
251
|
-
|
|
252
252
|
</style>
|
|
@@ -22,9 +22,11 @@ let passwordChanged = ref(false);
|
|
|
22
22
|
let inputErrors = ref<IClientInputError>()
|
|
23
23
|
let loading = ref(false);
|
|
24
24
|
let userError = ref<string>('')
|
|
25
|
+
let form = ref()
|
|
25
26
|
|
|
26
27
|
async function savePassword() {
|
|
27
|
-
|
|
28
|
+
const result = await form.value?.validate()
|
|
29
|
+
if (result?.valid && passwordForm.value.newPassword === passwordForm.value.confirmPassword) {
|
|
28
30
|
await changeUserPassword(user._id, passwordForm.value.newPassword)
|
|
29
31
|
passwordChanged.value = true
|
|
30
32
|
}
|
|
@@ -56,6 +58,7 @@ async function changeUserPassword(id: string, newPassword: string) {
|
|
|
56
58
|
<v-card-subtitle>{{ t('user.field.username') }}: {{ user.username }}</v-card-subtitle>
|
|
57
59
|
<v-card-text>
|
|
58
60
|
<user-password-form
|
|
61
|
+
ref="form"
|
|
59
62
|
v-model="passwordForm"
|
|
60
63
|
:inputErrors="inputErrors"
|
|
61
64
|
:passwordChanged="passwordChanged"
|
|
@@ -4,12 +4,14 @@ import type {IClientInputError} from "@drax/common-front";
|
|
|
4
4
|
import type {IUserPassword} from "@drax/identity-front";
|
|
5
5
|
import {useI18nValidation} from "@drax/common-vue";
|
|
6
6
|
import {useI18n} from "vue-i18n";
|
|
7
|
+
import PasswordPolicyInput from "../../components/PasswordPolicy/PasswordPolicyInput.vue";
|
|
7
8
|
|
|
8
9
|
const {t} = useI18n()
|
|
9
10
|
const {$ta} = useI18nValidation()
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const formRef = ref()
|
|
12
13
|
|
|
14
|
+
const newPasswordVisibility = ref(false)
|
|
13
15
|
|
|
14
16
|
defineProps({
|
|
15
17
|
inputErrors: {
|
|
@@ -34,10 +36,17 @@ function confirmPasswordRule(value: string) {
|
|
|
34
36
|
// Define emits
|
|
35
37
|
const emits = defineEmits(['formSubmit'])
|
|
36
38
|
|
|
39
|
+
async function validate() {
|
|
40
|
+
return await formRef.value?.validate()
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
// Function to call when form is attempted to be submitted
|
|
38
44
|
const onSubmit = () => {
|
|
39
45
|
emits('formSubmit', form); // Emitting an event with the form data
|
|
40
46
|
}
|
|
47
|
+
|
|
48
|
+
defineExpose({validate})
|
|
49
|
+
|
|
41
50
|
</script>
|
|
42
51
|
|
|
43
52
|
<template>
|
|
@@ -48,21 +57,17 @@ const onSubmit = () => {
|
|
|
48
57
|
</v-alert>
|
|
49
58
|
</v-sheet>
|
|
50
59
|
<v-sheet v-else>
|
|
51
|
-
<form ref="
|
|
60
|
+
<v-form ref="formRef" @submit.prevent="onSubmit">
|
|
52
61
|
|
|
53
62
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.newPassword') }}</div>
|
|
54
|
-
<
|
|
63
|
+
<PasswordPolicyInput
|
|
55
64
|
variant="outlined"
|
|
56
65
|
id="new-password-input"
|
|
57
66
|
v-model="form.newPassword"
|
|
58
|
-
:type="newPasswordVisibility ? 'text': 'password'"
|
|
59
67
|
required
|
|
60
|
-
prepend-inner-icon="mdi-lock-outline"
|
|
61
|
-
:append-inner-icon="newPasswordVisibility ? 'mdi-eye-off': 'mdi-eye'"
|
|
62
|
-
@click:append-inner="newPasswordVisibility = !newPasswordVisibility"
|
|
63
68
|
autocomplete="new-password"
|
|
64
69
|
:error-messages="$ta(inputErrors.newPassword)"
|
|
65
|
-
|
|
70
|
+
/>
|
|
66
71
|
|
|
67
72
|
<div class="text-subtitle-1 text-medium-emphasis">{{ t('user.field.confirmPassword') }}</div>
|
|
68
73
|
|
|
@@ -79,7 +84,7 @@ const onSubmit = () => {
|
|
|
79
84
|
:error-messages="$ta(inputErrors.confirmPassword)"
|
|
80
85
|
:rules="[confirmPasswordRule]"
|
|
81
86
|
></v-text-field>
|
|
82
|
-
</form>
|
|
87
|
+
</v-form>
|
|
83
88
|
</v-sheet>
|
|
84
89
|
|
|
85
90
|
</template>
|
|
@@ -1,105 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import type {IPasswordPolicy} from "@drax/identity-front";
|
|
2
|
+
import {useI18n} from "vue-i18n";
|
|
3
|
+
import PasswordPolicySummary from "../components/PasswordPolicy/PasswordPolicySummary.vue";
|
|
5
4
|
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
const loading = ref(true)
|
|
9
|
-
const errorMsg = ref('')
|
|
10
|
-
const policy = ref<IPasswordPolicy | null>(null)
|
|
11
|
-
|
|
12
|
-
const rules = computed(() => {
|
|
13
|
-
if (!policy.value) {
|
|
14
|
-
return []
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
label: `Longitud mínima de ${policy.value.minLength} caracteres`,
|
|
20
|
-
enabled: true,
|
|
21
|
-
icon: 'mdi-format-letter-case'
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
label: `Longitud máxima de ${policy.value.maxLength} caracteres`,
|
|
25
|
-
enabled: true,
|
|
26
|
-
icon: 'mdi-ruler'
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
label: 'Debe incluir al menos una mayúscula',
|
|
30
|
-
enabled: policy.value.requireUppercase,
|
|
31
|
-
icon: 'mdi-format-letter-case-upper'
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
label: 'Debe incluir al menos una minúscula',
|
|
35
|
-
enabled: policy.value.requireLowercase,
|
|
36
|
-
icon: 'mdi-format-letter-case-lower'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: 'Debe incluir al menos un número',
|
|
40
|
-
enabled: policy.value.requireNumber,
|
|
41
|
-
icon: 'mdi-numeric'
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
label: 'Debe incluir al menos un carácter especial',
|
|
45
|
-
description: policy.value.allowedSpecialChars
|
|
46
|
-
? `Permitidos: ${policy.value.allowedSpecialChars}`
|
|
47
|
-
: undefined,
|
|
48
|
-
enabled: policy.value.requireSpecialChar,
|
|
49
|
-
icon: 'mdi-asterisk'
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: 'No se permiten espacios',
|
|
53
|
-
enabled: policy.value.disallowSpaces,
|
|
54
|
-
icon: 'mdi-keyboard-space'
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const metadata = computed(() => {
|
|
60
|
-
if (!policy.value) {
|
|
61
|
-
return []
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const items = [
|
|
65
|
-
{
|
|
66
|
-
label: 'Reutilización',
|
|
67
|
-
value: policy.value.preventReuse > 0
|
|
68
|
-
? `No se pueden repetir las últimas ${policy.value.preventReuse} contraseñas`
|
|
69
|
-
: 'Sin restricción de reutilización'
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
label: 'Expiración',
|
|
73
|
-
value: policy.value.expirationDays
|
|
74
|
-
? `La contraseña expira cada ${policy.value.expirationDays} días`
|
|
75
|
-
: 'Sin expiración configurada'
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
if (policy.value.requireSpecialChar && policy.value.allowedSpecialChars) {
|
|
80
|
-
items.push({
|
|
81
|
-
label: 'Caracteres especiales',
|
|
82
|
-
value: policy.value.allowedSpecialChars
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return items
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
async function loadPolicy() {
|
|
90
|
-
try {
|
|
91
|
-
loading.value = true
|
|
92
|
-
errorMsg.value = ''
|
|
93
|
-
policy.value = await passwordPolicy()
|
|
94
|
-
} catch (error) {
|
|
95
|
-
const err = error as Error
|
|
96
|
-
errorMsg.value = err.message || 'No se pudo obtener la política de contraseñas'
|
|
97
|
-
} finally {
|
|
98
|
-
loading.value = false
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
onMounted(loadPolicy)
|
|
5
|
+
const {t} = useI18n()
|
|
103
6
|
</script>
|
|
104
7
|
|
|
105
8
|
<template>
|
|
@@ -108,74 +11,13 @@ onMounted(loadPolicy)
|
|
|
108
11
|
<v-col cols="12" md="8" lg="6">
|
|
109
12
|
<v-card variant="elevated">
|
|
110
13
|
<v-card-title class="pa-4">
|
|
111
|
-
|
|
14
|
+
{{ t('user.passwordPolicy.title') }}
|
|
112
15
|
</v-card-title>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<div class="text-medium-emphasis">Cargando política...</div>
|
|
116
|
-
</v-card-text>
|
|
117
|
-
|
|
118
|
-
<v-card-text v-else-if="errorMsg">
|
|
119
|
-
<v-alert type="error" variant="tonal">
|
|
120
|
-
{{ errorMsg }}
|
|
121
|
-
</v-alert>
|
|
16
|
+
<v-card-text>
|
|
17
|
+
<PasswordPolicySummary variant="full" />
|
|
122
18
|
</v-card-text>
|
|
123
|
-
|
|
124
|
-
<template v-else-if="policy">
|
|
125
|
-
<v-card-text class="pb-2">
|
|
126
|
-
Requisitos que debe cumplir una contraseña válida.
|
|
127
|
-
</v-card-text>
|
|
128
|
-
|
|
129
|
-
<v-card-text class="pt-0">
|
|
130
|
-
<v-list lines="two">
|
|
131
|
-
<v-list-item
|
|
132
|
-
v-for="rule in rules"
|
|
133
|
-
:key="rule.label"
|
|
134
|
-
:prepend-icon="rule.icon"
|
|
135
|
-
>
|
|
136
|
-
<v-list-item-title>{{ rule.label }}</v-list-item-title>
|
|
137
|
-
<v-list-item-subtitle v-if="rule.enabled && rule.description">
|
|
138
|
-
{{ rule.description }}
|
|
139
|
-
</v-list-item-subtitle>
|
|
140
|
-
<template #append>
|
|
141
|
-
<v-chip
|
|
142
|
-
:color="rule.enabled ? 'success' : 'default'"
|
|
143
|
-
size="small"
|
|
144
|
-
variant="tonal"
|
|
145
|
-
>
|
|
146
|
-
{{ rule.enabled ? 'Requerido' : 'Opcional' }}
|
|
147
|
-
</v-chip>
|
|
148
|
-
</template>
|
|
149
|
-
</v-list-item>
|
|
150
|
-
</v-list>
|
|
151
|
-
</v-card-text>
|
|
152
|
-
|
|
153
|
-
<v-divider />
|
|
154
|
-
|
|
155
|
-
<v-card-text>
|
|
156
|
-
<v-row>
|
|
157
|
-
<v-col
|
|
158
|
-
v-for="item in metadata"
|
|
159
|
-
:key="item.label"
|
|
160
|
-
cols="12"
|
|
161
|
-
sm="6"
|
|
162
|
-
>
|
|
163
|
-
<v-sheet class="info-box pa-4 fill-height" rounded>
|
|
164
|
-
<div class="text-caption text-medium-emphasis">{{ item.label }}</div>
|
|
165
|
-
<div class="text-body-1">{{ item.value }}</div>
|
|
166
|
-
</v-sheet>
|
|
167
|
-
</v-col>
|
|
168
|
-
</v-row>
|
|
169
|
-
</v-card-text>
|
|
170
|
-
</template>
|
|
171
19
|
</v-card>
|
|
172
20
|
</v-col>
|
|
173
21
|
</v-row>
|
|
174
22
|
</v-container>
|
|
175
23
|
</template>
|
|
176
|
-
|
|
177
|
-
<style scoped>
|
|
178
|
-
.info-box {
|
|
179
|
-
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
|
180
|
-
}
|
|
181
|
-
</style>
|