@drax/identity-vue 3.20.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.20.0",
6
+ "version": "3.22.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -26,11 +26,11 @@
26
26
  "dependencies": {
27
27
  "@drax/common-front": "^3.19.0",
28
28
  "@drax/common-vue": "^3.19.0",
29
- "@drax/crud-front": "^3.20.0",
30
- "@drax/crud-share": "^3.20.0",
31
- "@drax/crud-vue": "^3.20.0",
32
- "@drax/identity-front": "^3.20.0",
33
- "@drax/identity-share": "^3.15.0"
29
+ "@drax/crud-front": "^3.21.0",
30
+ "@drax/crud-share": "^3.21.0",
31
+ "@drax/crud-vue": "^3.21.0",
32
+ "@drax/identity-front": "^3.22.1",
33
+ "@drax/identity-share": "^3.21.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": "6d4aea4d05133be679166e398ec6a3ae61503d9e"
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
 
@@ -25,7 +25,6 @@ const store = useCrudStore(entity.name)
25
25
  const enablePassword = store.operation === 'create'
26
26
 
27
27
 
28
-
29
28
  const valid = ref()
30
29
  const formRef = ref()
31
30
 
@@ -33,12 +32,11 @@ const formRef = ref()
33
32
  const isTenantEnabled = computed(() => import.meta.env.VITE_DRAX_TENANT === 'ENABLE')
34
33
 
35
34
 
36
-
37
35
  async function submit() {
38
36
  store.resetErrors()
39
37
 
40
- if(store.operation === 'delete') {
41
- emit('submit',valueModel.value)
38
+ if (store.operation === 'delete') {
39
+ emit('submit', valueModel.value)
42
40
  return
43
41
  }
44
42
 
@@ -55,7 +53,7 @@ function cancel() {
55
53
  }
56
54
 
57
55
  const {
58
- submitColor, readonly
56
+ submitColor, readonly
59
57
  } = useFormUtils(store.operation)
60
58
 
61
59
 
@@ -74,6 +72,11 @@ const variant = computed(() => {
74
72
 
75
73
 
76
74
  let passwordVisibility = ref(false)
75
+ const confirmPassword = ref('')
76
+
77
+ function confirmPasswordRule(value: string) {
78
+ return value === valueModel.value.password || t('validation.password.confirmed')
79
+ }
77
80
 
78
81
  </script>
79
82
 
@@ -93,96 +96,135 @@ let passwordVisibility = ref(false)
93
96
 
94
97
  <v-card-text>
95
98
 
96
- <v-text-field
97
- id="name-input"
98
- :label="t('user.field.name')"
99
- v-model="valueModel.name"
100
- prepend-inner-icon="mdi-card-account-details"
101
- :variant="variant"
102
- :rules="[v => !!v || t('validation.required')]"
103
- :error-messages="$ta(store.inputErrors?.name)"
104
- :readonly="readonly"
105
- ></v-text-field>
106
-
107
- <v-text-field
108
- id="username-input"
109
- :label="t('user.field.username')"
110
- v-model="valueModel.username"
111
- prepend-inner-icon="mdi-account-question"
112
- :variant="variant"
113
- :rules="[v => !!v || t('validation.required')]"
114
- autocomplete="new-username"
115
- :error-messages="$ta(store.inputErrors?.username)"
116
- :readonly="readonly"
117
- ></v-text-field>
118
-
119
- <v-text-field
120
- v-if="enablePassword"
121
- id="password-input"
122
- :label="t('user.field.password')"
123
- v-model="valueModel.password"
124
- :type="passwordVisibility ? 'text': 'password'"
125
- :variant="variant"
126
- :rules="[v => !!v || t('validation.required')]"
127
- prepend-inner-icon="mdi-lock-outline"
128
- :append-inner-icon="passwordVisibility ? 'mdi-eye-off': 'mdi-eye'"
129
- @click:append-inner="passwordVisibility = !passwordVisibility"
130
- autocomplete="new-password"
131
- :error-messages="$ta(store.inputErrors?.password)"
132
- :readonly="readonly"
133
- ></v-text-field>
134
-
135
- <RoleCombobox
136
- v-model="valueModel.role"
137
- :label="t('user.field.role')"
138
- :variant="variant"
139
- :rules="[(v:any) => !!v || t('validation.required')]"
140
- :error-messages="$ta(store.inputErrors?.role)"
141
- :readonly="readonly"
142
- ></RoleCombobox>
143
-
144
- <TenantCombobox
145
- v-if="isTenantEnabled && hasPermission('tenant:manage')"
146
- v-model="valueModel.tenant"
147
- :label="t('user.field.tenant')"
148
- :variant="variant"
149
- :error-messages="$ta(store.inputErrors?.tenant)"
150
- clearable
151
- :readonly="readonly"
152
- ></TenantCombobox>
153
-
154
- <v-text-field
155
- v-model="valueModel.email"
156
- :variant="variant"
157
- id="email-input"
158
- :label="t('user.field.email')"
159
- prepend-inner-icon="mdi-email"
160
- :rules="[(v:any) => !!v || t('validation.required')]"
161
- :error-messages="$ta(store.inputErrors?.email)"
162
- :readonly="readonly"
163
- ></v-text-field>
164
-
165
- <v-text-field
166
- v-model="valueModel.phone"
167
- :variant="variant"
168
- id="phone-input"
169
- :label="t('user.field.phone')"
170
- prepend-inner-icon="mdi-phone"
171
- :rules="[(v:any) => !!v || t('validation.required')]"
172
- :error-messages="$ta(store.inputErrors?.phone)"
173
- :readonly="readonly"
174
- ></v-text-field>
175
-
176
- <v-switch
177
- id="active-input"
178
- v-model="valueModel.active"
179
- color="primary"
180
- label="Active"
181
- :true-value="true"
182
- :false-value="false"
183
- :readonly="readonly"
184
- ></v-switch>
185
-
99
+ <v-row>
100
+
101
+ <v-col cols="12">
102
+ <v-text-field
103
+ id="name-input"
104
+ :label="t('user.field.name')"
105
+ v-model="valueModel.name"
106
+ prepend-inner-icon="mdi-card-account-details"
107
+ :variant="variant"
108
+ :rules="[v => !!v || t('validation.required')]"
109
+ :error-messages="$ta(store.inputErrors?.name)"
110
+ :readonly="readonly"
111
+ ></v-text-field>
112
+ </v-col>
113
+
114
+ <v-col cols="12">
115
+ <v-text-field
116
+ id="username-input"
117
+ :label="t('user.field.username')"
118
+ v-model="valueModel.username"
119
+ prepend-inner-icon="mdi-account-question"
120
+ :variant="variant"
121
+ :rules="[v => !!v || t('validation.required')]"
122
+ autocomplete="new-username"
123
+ :error-messages="$ta(store.inputErrors?.username)"
124
+ :readonly="readonly"
125
+ ></v-text-field>
126
+ </v-col>
127
+
128
+ <v-col cols="12">
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>
145
+ </v-col>
146
+
147
+ <v-col cols="12">
148
+ <v-text-field
149
+ v-if="enablePassword"
150
+ id="confirm-password-input"
151
+ :label="t('user.field.confirmPassword')"
152
+ v-model="confirmPassword"
153
+ :type="passwordVisibility ? 'text': 'password'"
154
+ :variant="variant"
155
+ :rules="[
156
+ v => !!v || t('validation.required'),
157
+ confirmPasswordRule
158
+ ]"
159
+ prepend-inner-icon="mdi-lock-outline"
160
+ :append-inner-icon="passwordVisibility ? 'mdi-eye-off': 'mdi-eye'"
161
+ @click:append-inner="passwordVisibility = !passwordVisibility"
162
+ autocomplete="new-password"
163
+ :readonly="readonly"
164
+ ></v-text-field>
165
+ </v-col>
166
+
167
+ <v-col cols="12">
168
+ <RoleCombobox
169
+ v-model="valueModel.role"
170
+ :label="t('user.field.role')"
171
+ :variant="variant"
172
+ :rules="[(v:any) => !!v || t('validation.required')]"
173
+ :error-messages="$ta(store.inputErrors?.role)"
174
+ :readonly="readonly"
175
+ ></RoleCombobox>
176
+ </v-col>
177
+
178
+ <v-col cols="12">
179
+ <TenantCombobox
180
+ v-if="isTenantEnabled && hasPermission('tenant:manage')"
181
+ v-model="valueModel.tenant"
182
+ :label="t('user.field.tenant')"
183
+ :variant="variant"
184
+ :error-messages="$ta(store.inputErrors?.tenant)"
185
+ clearable
186
+ :readonly="readonly"
187
+ ></TenantCombobox>
188
+ </v-col>
189
+
190
+ <v-col cols="12">
191
+ <v-text-field
192
+ v-model="valueModel.email"
193
+ :variant="variant"
194
+ id="email-input"
195
+ :label="t('user.field.email')"
196
+ prepend-inner-icon="mdi-email"
197
+ :rules="[(v:any) => !!v || t('validation.required')]"
198
+ :error-messages="$ta(store.inputErrors?.email)"
199
+ :readonly="readonly"
200
+ ></v-text-field>
201
+ </v-col>
202
+
203
+ <v-col cols="12">
204
+ <v-text-field
205
+ v-model="valueModel.phone"
206
+ :variant="variant"
207
+ id="phone-input"
208
+ :label="t('user.field.phone')"
209
+ prepend-inner-icon="mdi-phone"
210
+ :rules="[(v:any) => !!v || t('validation.required')]"
211
+ :error-messages="$ta(store.inputErrors?.phone)"
212
+ :readonly="readonly"
213
+ ></v-text-field>
214
+ </v-col>
215
+
216
+ <v-col cols="12">
217
+ <v-switch
218
+ id="active-input"
219
+ v-model="valueModel.active"
220
+ color="primary"
221
+ label="Active"
222
+ :true-value="true"
223
+ :false-value="false"
224
+ :readonly="readonly"
225
+ ></v-switch>
226
+ </v-col>
227
+ </v-row>
186
228
  </v-card-text>
187
229
 
188
230
  <v-card-actions>
@@ -207,5 +249,4 @@ let passwordVisibility = ref(false)
207
249
  </template>
208
250
 
209
251
  <style scoped>
210
-
211
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,93 +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
- enabled: policy.value.requireSpecialChar,
46
- icon: 'mdi-asterisk'
47
- },
48
- {
49
- label: 'No se permiten espacios',
50
- enabled: policy.value.disallowSpaces,
51
- icon: 'mdi-keyboard-space'
52
- }
53
- ]
54
- })
55
-
56
- const metadata = computed(() => {
57
- if (!policy.value) {
58
- return []
59
- }
60
-
61
- return [
62
- {
63
- label: 'Reutilización',
64
- value: policy.value.preventReuse > 0
65
- ? `No se pueden repetir las últimas ${policy.value.preventReuse} contraseñas`
66
- : 'Sin restricción de reutilización'
67
- },
68
- {
69
- label: 'Expiración',
70
- value: policy.value.expirationDays
71
- ? `La contraseña expira cada ${policy.value.expirationDays} días`
72
- : 'Sin expiración configurada'
73
- }
74
- ]
75
- })
76
-
77
- async function loadPolicy() {
78
- try {
79
- loading.value = true
80
- errorMsg.value = ''
81
- policy.value = await passwordPolicy()
82
- } catch (error) {
83
- const err = error as Error
84
- errorMsg.value = err.message || 'No se pudo obtener la política de contraseñas'
85
- } finally {
86
- loading.value = false
87
- }
88
- }
89
-
90
- onMounted(loadPolicy)
5
+ const {t} = useI18n()
91
6
  </script>
92
7
 
93
8
  <template>
@@ -96,71 +11,13 @@ onMounted(loadPolicy)
96
11
  <v-col cols="12" md="8" lg="6">
97
12
  <v-card variant="elevated">
98
13
  <v-card-title class="pa-4">
99
- Política de contraseñas
14
+ {{ t('user.passwordPolicy.title') }}
100
15
  </v-card-title>
101
-
102
- <v-card-text v-if="loading">
103
- <div class="text-medium-emphasis">Cargando política...</div>
16
+ <v-card-text>
17
+ <PasswordPolicySummary variant="full" />
104
18
  </v-card-text>
105
-
106
- <v-card-text v-else-if="errorMsg">
107
- <v-alert type="error" variant="tonal">
108
- {{ errorMsg }}
109
- </v-alert>
110
- </v-card-text>
111
-
112
- <template v-else-if="policy">
113
- <v-card-text class="pb-2">
114
- Requisitos que debe cumplir una contraseña válida.
115
- </v-card-text>
116
-
117
- <v-card-text class="pt-0">
118
- <v-list lines="two">
119
- <v-list-item
120
- v-for="rule in rules"
121
- :key="rule.label"
122
- :prepend-icon="rule.icon"
123
- >
124
- <v-list-item-title>{{ rule.label }}</v-list-item-title>
125
- <template #append>
126
- <v-chip
127
- :color="rule.enabled ? 'success' : 'default'"
128
- size="small"
129
- variant="tonal"
130
- >
131
- {{ rule.enabled ? 'Requerido' : 'Opcional' }}
132
- </v-chip>
133
- </template>
134
- </v-list-item>
135
- </v-list>
136
- </v-card-text>
137
-
138
- <v-divider />
139
-
140
- <v-card-text>
141
- <v-row>
142
- <v-col
143
- v-for="item in metadata"
144
- :key="item.label"
145
- cols="12"
146
- sm="6"
147
- >
148
- <v-sheet class="info-box pa-4 fill-height" rounded>
149
- <div class="text-caption text-medium-emphasis">{{ item.label }}</div>
150
- <div class="text-body-1">{{ item.value }}</div>
151
- </v-sheet>
152
- </v-col>
153
- </v-row>
154
- </v-card-text>
155
- </template>
156
19
  </v-card>
157
20
  </v-col>
158
21
  </v-row>
159
22
  </v-container>
160
23
  </template>
161
-
162
- <style scoped>
163
- .info-box {
164
- border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
165
- }
166
- </style>