@drax/identity-vue 3.21.0 → 3.22.1

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.21.0",
6
+ "version": "3.22.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -29,7 +29,7 @@
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.21.0",
32
+ "@drax/identity-front": "^3.22.1",
33
33
  "@drax/identity-share": "^3.21.0"
34
34
  },
35
35
  "peerDependencies": {
@@ -55,5 +55,5 @@
55
55
  "vue-tsc": "^3.2.4",
56
56
  "vuetify": "^3.11.8"
57
57
  },
58
- "gitHead": "9ceebbba20e7abf7337387e2a81d1475b9ba1bca"
58
+ "gitHead": "9a2d949464ddf1a3b2da49be53d73f7d5b8955b4"
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
- <v-text-field
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
- ></v-text-field>
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"
@@ -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
- <v-text-field
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
- ></v-text-field>
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-text-field
130
- v-if="enablePassword"
131
- id="password-input"
132
- :label="t('user.field.password')"
133
- v-model="valueModel.password"
134
- :type="passwordVisibility ? 'text': 'password'"
135
- :variant="variant"
136
- :rules="[v => !!v || t('validation.required')]"
137
- prepend-inner-icon="mdi-lock-outline"
138
- :append-inner-icon="passwordVisibility ? 'mdi-eye-off': 'mdi-eye'"
139
- @click:append-inner="passwordVisibility = !passwordVisibility"
140
- autocomplete="new-password"
141
- :error-messages="$ta(store.inputErrors?.password)"
142
- :readonly="readonly"
143
- ></v-text-field>
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
- if (passwordForm.value.newPassword === passwordForm.value.confirmPassword) {
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
- let newPasswordVisibility = ref(false)
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="changeUserPassword" @submit.prevent="onSubmit">
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
- <v-text-field
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
- ></v-text-field>
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 {computed, onMounted, ref} from "vue";
3
- import {useAuth} from "../composables/useAuth";
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 {passwordPolicy} = useAuth()
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
- Política de contraseñas
14
+ {{ t('user.passwordPolicy.title') }}
112
15
  </v-card-title>
113
-
114
- <v-card-text v-if="loading">
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>