@drax/identity-vue 3.65.0 → 3.67.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/README.md +131 -0
- package/package.json +5 -5
- package/src/components/IdentityLogin/IdentityLogin.vue +4 -0
- package/src/components/IdentityRegistration/IdentityRegistration.vue +84 -8
- package/src/components/IdentityVerificationResend/IdentityVerificationResend.vue +87 -0
- package/src/components/SwitchTenant/SwitchTenant.vue +2 -2
- package/src/composables/setupAuthSession.ts +2 -1
- package/src/composables/useAuth.ts +9 -1
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -72,6 +72,137 @@ Focused tests:
|
|
|
72
72
|
npm exec --workspace @drax/identity-vue -- vitest run test/auth-session.spec.ts test/auth-session.integration.spec.ts test/components/__tests__/IdentityLogin.spec.ts
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Registro con creación de tenant (opcional, REST)
|
|
76
|
+
|
|
77
|
+
En el **backend**:
|
|
78
|
+
|
|
79
|
+
```dotenv
|
|
80
|
+
DRAX_REGISTRATION_TENANT_ENABLED=true
|
|
81
|
+
DRAX_DEFAULT_ROLE=Member
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Cargá `LoadIdentityConfigFromEnv()`, registrá `UserRoutes` y creá previamente el rol
|
|
85
|
+
configurado. Sin la flag, o con `false`, el registro no crea tenants. No se necesita
|
|
86
|
+
una variable Vite: `IdentityRegistration`, usado por `RegistrationPage`, consulta
|
|
87
|
+
`GET /api/auth/registration-options` → `{tenantRequired: boolean}`. Mientras no pueda
|
|
88
|
+
cargar estas opciones, bloquea el envío y permite reintentar.
|
|
89
|
+
|
|
90
|
+
Si está activo, el formulario exige un nombre de organización y envía
|
|
91
|
+
`tenantName` en `POST /api/users/register`. El backend crea un tenant **nuevo** y
|
|
92
|
+
asigna el usuario a ese tenant; rechaza nombres existentes con HTTP 422,
|
|
93
|
+
`tenantName: validation.unique`. No vincula visitantes a tenants existentes. Los
|
|
94
|
+
nombres se recortan y la unicidad es exacta y sensible a mayúsculas, igual que el
|
|
95
|
+
índice MongoDB existente. SQLite también aplica un índice único, incluyendo bases
|
|
96
|
+
ya creadas; si hay duplicados previos, hay que resolverlos manualmente antes de
|
|
97
|
+
inicializar. No se borran ni renombran datos automáticamente.
|
|
98
|
+
|
|
99
|
+
Para formularios propios, usá `useAuth().registrationOptions()` y
|
|
100
|
+
`useAuth().register({...datos, tenantName})`. El rol y el ID de tenant los determina
|
|
101
|
+
el servidor; enviar `role`, `tenant` o `active` no los sobreescribe. El usuario
|
|
102
|
+
continúa inactivo hasta la verificación existente por email/teléfono; no hay
|
|
103
|
+
login automático. Si falla la creación del usuario antes de persistir, se elimina
|
|
104
|
+
el tenant recién creado; si ya se persistió o falla después el email, se conserva
|
|
105
|
+
la asociación. No es una transacción distribuida entre bases/servicios.
|
|
106
|
+
|
|
107
|
+
Desplegá backend y frontend compatibles: el formulario actualizado requiere el
|
|
108
|
+
nuevo endpoint incluso cuando la flag está apagada. El registro GraphQL no está
|
|
109
|
+
implementado; su provider devuelve `tenantRequired: false` para las opciones.
|
|
110
|
+
|
|
111
|
+
## Verificación de email y reenvío (REST)
|
|
112
|
+
|
|
113
|
+
`register(form)` conserva `{success, message}` y ahora devuelve el tipo público
|
|
114
|
+
`IRegistrationResponse` de `@drax/identity-front`, con
|
|
115
|
+
`verificationEmailSent?: boolean`. `true` indica aceptación por SMTP, **no entrega**;
|
|
116
|
+
`false` indica que la cuenta se creó pero el correo quedó pendiente. La ausencia
|
|
117
|
+
mantiene el comportamiento anterior. Un fallo SMTP ya no implica un error de
|
|
118
|
+
registro ni habilita la cuenta: no hay login automático.
|
|
119
|
+
|
|
120
|
+
`IdentityRegistration` muestra una advertencia si el valor es `false` y mantiene
|
|
121
|
+
el mensaje de éxito existente para `true` o ausente. Ofrece un botón de acceso al
|
|
122
|
+
reenvío (`auth.verificationResendTitle`), con el email prellenado, tanto desde el
|
|
123
|
+
formulario como después del éxito/advertencia o de un error (duplicado o red).
|
|
124
|
+
Al abrirlo, el reenvío **reemplaza** el formulario o el resultado del registro y
|
|
125
|
+
oculta el botón de acceso. «Volver al registro» (`auth.backToRegistration`) restaura
|
|
126
|
+
el estado anterior, conservando inputs, email, tenant y resultado de éxito/advertencia.
|
|
127
|
+
No es necesario crear otra cuenta ni agregar rutas.
|
|
128
|
+
|
|
129
|
+
El acceso al reenvío está **solo en registro**, separado del formulario principal
|
|
130
|
+
con un margen superior. `IdentityLogin` conserva los enlaces de registro y de
|
|
131
|
+
recuperación de contraseña según sus props, pero no muestra el acceso a verificación.
|
|
132
|
+
El reenvío solicita email explícitamente. Hay un único formulario visible, sin
|
|
133
|
+
formularios simultáneos ni anidados, y un único título de reenvío:
|
|
134
|
+
`auth.verificationFormTitle` («Account verification» / «Verificación de cuenta»).
|
|
135
|
+
|
|
136
|
+
El componente reutilizable está exportado:
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<script setup lang="ts">
|
|
140
|
+
import {IdentityVerificationResend} from '@drax/identity-vue'
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<IdentityVerificationResend email="persona@example.com" />
|
|
145
|
+
</template>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
La prop `email` es opcional y editable. La prop opcional `backLabel?: string`
|
|
149
|
+
muestra un botón de vuelta de tipo `button` que emite `back`; está deshabilitado
|
|
150
|
+
mientras se procesa la solicitud. El componente padre decide qué restaurar al
|
|
151
|
+
recibir ese evento. Sin `backLabel`, el uso standalone conserva un único botón de
|
|
152
|
+
envío. El botón submit queda alineado a la derecha mediante un `v-spacer` anterior.
|
|
153
|
+
Formularios propios pueden usar `useAuth().resendVerificationEmail(email)` o
|
|
154
|
+
`AuthSystem.resendVerificationEmail(email)`.
|
|
155
|
+
REST hace `POST /api/users/verification/resend` con `{email}`. Tanto este endpoint
|
|
156
|
+
como el registro son anónimos: no envían `Authorization`, no disparan el preflight
|
|
157
|
+
de refresh y no borran las cabeceras del cliente compartido.
|
|
158
|
+
|
|
159
|
+
El resultado de reenvío es siempre neutral para cuentas inexistentes, verificadas,
|
|
160
|
+
limitadas o con fallo SMTP: `{success: true, message: 'If an account is pending
|
|
161
|
+
verification, activation instructions will be sent.'}`. La UI usa traducciones
|
|
162
|
+
es/en neutrales, sin confirmar existencia de la cuenta ni envío o entrega del
|
|
163
|
+
correo. Valida el email, muestra errores de red/`ClientError` y bloquea el envío
|
|
164
|
+
durante la solicitud. No impone una cuenta regresiva fija: el cooldown lo decide
|
|
165
|
+
el servidor (60 segundos por defecto, configurable). GraphQL rechaza esta operación
|
|
166
|
+
explícitamente como no implementada; no se crea una query ni mutación ficticia.
|
|
167
|
+
Los providers personalizados anteriores pueden omitir el método opcional de
|
|
168
|
+
`IAuthProvider`; sus APIs previas siguen funcionando y `AuthSystem` rechaza el
|
|
169
|
+
reenvío con un error explícito si el provider no lo soporta.
|
|
170
|
+
|
|
171
|
+
### Backend: vencimiento, límites y recuperación de cuentas anteriores
|
|
172
|
+
|
|
173
|
+
Configuración opcional, cargada con `LoadIdentityConfigFromEnv()`:
|
|
174
|
+
|
|
175
|
+
```dotenv
|
|
176
|
+
DRAX_EMAIL_VERIFICATION_TTL_SECONDS=86400
|
|
177
|
+
DRAX_EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS=60
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Ambos valores deben ser segundos enteros positivos. El código es de un solo uso y
|
|
181
|
+
vence a las 24 horas por defecto. Reenviar reutiliza un código vigente sin extender
|
|
182
|
+
su vencimiento; rota el código únicamente si venció o si el registro anterior no
|
|
183
|
+
tenía vencimiento. Los enlaces antiguos sin vencimiento ya no activan cuentas:
|
|
184
|
+
para recuperarlas, solicitá un reenvío con el email original desde login/registro.
|
|
185
|
+
No hace falta borrar ni volver a crear usuario o tenant. Solo son elegibles cuentas
|
|
186
|
+
con `origin: 'Registry'` y email sin verificar; el reenvío no altera sus datos ni
|
|
187
|
+
reactiva cuentas verificadas que hayan sido desactivadas administrativamente.
|
|
188
|
+
|
|
189
|
+
El cooldown por email se reclama atómicamente en la base de datos, también cuando
|
|
190
|
+
SMTP falla. Se admiten 10 intentos de reenvío por IP cada 15 minutos por proceso,
|
|
191
|
+
con memoria acotada; incluso los emails inexistentes consumen ese límite. En un
|
|
192
|
+
despliegue multiinstancia, agregá un rate limiter compartido/gateway y configurá
|
|
193
|
+
correctamente la IP de cliente detrás del proxy. No hay cola ni reintentos
|
|
194
|
+
automáticos de correo: el usuario solicita el reenvío. La persistencia SQLite se
|
|
195
|
+
actualiza de forma aditiva al inicializar el repositorio; no se borran registros.
|
|
196
|
+
|
|
197
|
+
Actualizá backend, `identity-share`, `identity-front` e `identity-vue` de forma
|
|
198
|
+
coherente y desplegá backend antes de la UI nueva. En el monorepo compilá primero
|
|
199
|
+
`identity-share` para que los tipos utilizados por los demás paquetes estén al día.
|
|
200
|
+
|
|
201
|
+
Pruebas enfocadas: `test/verification-resend.spec.ts`, `test/registration.spec.ts`,
|
|
202
|
+
`test/components/__tests__/IdentityLogin.spec.ts`,
|
|
203
|
+
`test/auth-session.integration.spec.ts` y
|
|
204
|
+
`../identity-front/test/verification-resend.test.ts`.
|
|
205
|
+
|
|
75
206
|
## Recommended IDE Setup
|
|
76
207
|
|
|
77
208
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.67.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"@drax/common-vue": "^3.65.0",
|
|
23
23
|
"@drax/crud-front": "^3.58.0",
|
|
24
24
|
"@drax/crud-share": "^3.65.0",
|
|
25
|
-
"@drax/crud-vue": "^3.
|
|
26
|
-
"@drax/identity-front": "^3.
|
|
27
|
-
"@drax/identity-share": "^3.
|
|
25
|
+
"@drax/crud-vue": "^3.66.0",
|
|
26
|
+
"@drax/identity-front": "^3.67.0",
|
|
27
|
+
"@drax/identity-share": "^3.67.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"pinia": "^3.0.4",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"vue-tsc": "^3.2.4",
|
|
50
50
|
"vuetify": "^3.11.8"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "ffe6431614727caa3940fcb03da690d50c5032db"
|
|
53
53
|
}
|
|
@@ -4,6 +4,7 @@ import {useAuth} from '../../composables/useAuth.js'
|
|
|
4
4
|
import IdentityProfileView from "../IdentityProfileView/IdentityProfileView.vue";
|
|
5
5
|
import {useI18n} from "vue-i18n";
|
|
6
6
|
import IdentityRememberMe from '../IdentityRememberMe.vue'
|
|
7
|
+
|
|
7
8
|
import {isRememberMeEnabled} from '../../composables/setupAuthSession'
|
|
8
9
|
const {t, te} = useI18n()
|
|
9
10
|
|
|
@@ -22,6 +23,7 @@ const authError = ref('')
|
|
|
22
23
|
const loading = ref(false)
|
|
23
24
|
const rememberMe = ref(false)
|
|
24
25
|
|
|
26
|
+
|
|
25
27
|
const isFormValid = computed(() => username.value.trim() !== '' && password.value.trim() !== '')
|
|
26
28
|
|
|
27
29
|
const emit = defineEmits(['loginSuccess'])
|
|
@@ -60,6 +62,7 @@ function togglePasswordVisibility() {
|
|
|
60
62
|
</v-card>
|
|
61
63
|
</template>
|
|
62
64
|
|
|
65
|
+
|
|
63
66
|
<template v-else>
|
|
64
67
|
|
|
65
68
|
<v-form @submit.prevent="submitLogin">
|
|
@@ -124,6 +127,7 @@ function togglePasswordVisibility() {
|
|
|
124
127
|
</v-card>
|
|
125
128
|
</v-form>
|
|
126
129
|
|
|
130
|
+
|
|
127
131
|
</template>
|
|
128
132
|
</template>
|
|
129
133
|
|
|
@@ -1,18 +1,42 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {ref} from 'vue'
|
|
2
|
+
import {onMounted, ref} from 'vue'
|
|
3
|
+
import type {IRegistrationOptions} from '@drax/identity-front'
|
|
3
4
|
import {useAuth} from "../../composables/useAuth.js";
|
|
4
5
|
import {ClientError} from "@drax/common-front";
|
|
5
6
|
import type {IClientInputError} from "@drax/common-front";
|
|
6
7
|
import {useI18nValidation} from "@drax/common-vue";
|
|
7
8
|
import {useI18n} from "vue-i18n";
|
|
8
9
|
import {useRouter} from "vue-router"
|
|
10
|
+
import IdentityVerificationResend from '../IdentityVerificationResend/IdentityVerificationResend.vue'
|
|
9
11
|
|
|
10
12
|
const {t,te} = useI18n()
|
|
11
13
|
const {$ta} = useI18nValidation()
|
|
12
14
|
|
|
13
15
|
const router = useRouter()
|
|
14
16
|
|
|
15
|
-
const {register} = useAuth()
|
|
17
|
+
const {register, registrationOptions} = useAuth()
|
|
18
|
+
const options = ref<IRegistrationOptions | null>(null)
|
|
19
|
+
const optionsLoading = ref(true)
|
|
20
|
+
const optionsError = ref(false)
|
|
21
|
+
const tenantName = ref('')
|
|
22
|
+
const rform = ref<{validate: () => Promise<{valid: boolean}>} | null>(null)
|
|
23
|
+
|
|
24
|
+
async function loadRegistrationOptions() {
|
|
25
|
+
optionsLoading.value = true
|
|
26
|
+
optionsError.value = false
|
|
27
|
+
options.value = null
|
|
28
|
+
try {
|
|
29
|
+
const result = await registrationOptions()
|
|
30
|
+
if (typeof result?.tenantRequired !== 'boolean') throw new Error('Invalid registration options')
|
|
31
|
+
options.value = result
|
|
32
|
+
} catch {
|
|
33
|
+
optionsError.value = true
|
|
34
|
+
} finally {
|
|
35
|
+
optionsLoading.value = false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onMounted(loadRegistrationOptions)
|
|
16
40
|
|
|
17
41
|
const variant = ref<"outlined" | "plain" | "filled" | "underlined" | "solo" | "solo-inverted" | "solo-filled" | undefined>('filled')
|
|
18
42
|
|
|
@@ -29,6 +53,8 @@ const inputErrors = ref<IClientInputError|undefined>({currentPassword: [], newPa
|
|
|
29
53
|
const errorMsg = ref('')
|
|
30
54
|
const loading = ref(false)
|
|
31
55
|
const success = ref(false)
|
|
56
|
+
const verificationEmailSent = ref<boolean>()
|
|
57
|
+
const showVerificationResend = ref(false)
|
|
32
58
|
|
|
33
59
|
let passwordVisibility = ref(false)
|
|
34
60
|
let confirmPasswordVisibility = ref(false)
|
|
@@ -39,10 +65,23 @@ function confirmPasswordRule(value: string) {
|
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
async function submitRegistration() {
|
|
68
|
+
if (!options.value || optionsLoading.value || loading.value) return
|
|
69
|
+
loading.value = true
|
|
70
|
+
errorMsg.value = ''
|
|
71
|
+
inputErrors.value = undefined
|
|
42
72
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
if (options.value.tenantRequired && !tenantName.value.trim()) {
|
|
74
|
+
inputErrors.value = {tenantName: ['validation.required']}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (!(await rform.value?.validate())?.valid) return
|
|
78
|
+
const payload = {
|
|
79
|
+
...form.value,
|
|
80
|
+
...(options.value.tenantRequired ? {tenantName: tenantName.value.trim()} : {})
|
|
81
|
+
}
|
|
82
|
+
const result = await register(payload)
|
|
45
83
|
success.value = result.success
|
|
84
|
+
verificationEmailSent.value = result.verificationEmailSent
|
|
46
85
|
} catch (err) {
|
|
47
86
|
if (err instanceof ClientError) {
|
|
48
87
|
inputErrors.value = err.inputErrors
|
|
@@ -61,12 +100,18 @@ async function submitRegistration() {
|
|
|
61
100
|
|
|
62
101
|
<template>
|
|
63
102
|
<v-sheet>
|
|
103
|
+
<identity-verification-resend
|
|
104
|
+
v-if="showVerificationResend"
|
|
105
|
+
:email="form.email"
|
|
106
|
+
:back-label="t('auth.backToRegistration')"
|
|
107
|
+
@back="showVerificationResend = false"
|
|
108
|
+
/>
|
|
64
109
|
|
|
65
|
-
<template v-if="success">
|
|
110
|
+
<template v-else-if="success">
|
|
66
111
|
<v-card>
|
|
67
112
|
<v-card-text>
|
|
68
|
-
<v-alert type="success">
|
|
69
|
-
{{ t('user.events.registrationComplete') }}
|
|
113
|
+
<v-alert :type="verificationEmailSent === false ? 'warning' : 'success'">
|
|
114
|
+
{{ verificationEmailSent === false ? t('auth.registrationVerificationPending') : t('user.events.registrationComplete') }}
|
|
70
115
|
</v-alert>
|
|
71
116
|
</v-card-text>
|
|
72
117
|
<v-card-text class="text-center">
|
|
@@ -81,12 +126,29 @@ async function submitRegistration() {
|
|
|
81
126
|
<v-form ref="rform" @submit.prevent="submitRegistration">
|
|
82
127
|
<v-card variant="elevated">
|
|
83
128
|
<v-card-title class="pa-4">{{ t('user.action.register') }}</v-card-title>
|
|
129
|
+
<v-card-text v-if="optionsLoading">
|
|
130
|
+
<v-alert type="info">{{ t('auth.registrationOptionsLoading') }}</v-alert>
|
|
131
|
+
</v-card-text>
|
|
132
|
+
<v-card-text v-if="optionsError">
|
|
133
|
+
<v-alert type="error">{{ t('auth.registrationOptionsError') }}</v-alert>
|
|
134
|
+
<v-btn @click="loadRegistrationOptions">{{ t('auth.registrationOptionsRetry') }}</v-btn>
|
|
135
|
+
</v-card-text>
|
|
84
136
|
<v-card-text v-if="errorMsg">
|
|
85
137
|
<v-alert type="error">
|
|
86
138
|
{{ te(errorMsg) ?t(errorMsg) : errorMsg }}
|
|
87
139
|
</v-alert>
|
|
88
140
|
</v-card-text>
|
|
89
141
|
<v-card-text>
|
|
142
|
+
<v-text-field
|
|
143
|
+
v-if="options?.tenantRequired"
|
|
144
|
+
id="tenant-name-input"
|
|
145
|
+
v-model="tenantName"
|
|
146
|
+
:label="t('auth.registrationTenantName')"
|
|
147
|
+
:variant="variant"
|
|
148
|
+
required
|
|
149
|
+
:rules="[(v: string) => !!v?.trim() || t('validation.required')]"
|
|
150
|
+
:error-messages="$ta(inputErrors?.tenantName)"
|
|
151
|
+
></v-text-field>
|
|
90
152
|
<v-text-field
|
|
91
153
|
id="name-input"
|
|
92
154
|
:label="t('user.field.name')"
|
|
@@ -167,7 +229,7 @@ async function submitRegistration() {
|
|
|
167
229
|
color="grey"
|
|
168
230
|
variant="text"
|
|
169
231
|
id="submit-button"
|
|
170
|
-
type="
|
|
232
|
+
type="button"
|
|
171
233
|
href="/login"
|
|
172
234
|
>
|
|
173
235
|
{{ t('auth.login') }}
|
|
@@ -179,6 +241,7 @@ async function submitRegistration() {
|
|
|
179
241
|
id="submit-button"
|
|
180
242
|
type="submit"
|
|
181
243
|
:loading="loading"
|
|
244
|
+
:disabled="!options || optionsLoading || loading"
|
|
182
245
|
>
|
|
183
246
|
{{ t('action.sent') }}
|
|
184
247
|
</v-btn>
|
|
@@ -186,7 +249,20 @@ async function submitRegistration() {
|
|
|
186
249
|
</v-card>
|
|
187
250
|
</v-form>
|
|
188
251
|
</template>
|
|
252
|
+
|
|
253
|
+
|
|
189
254
|
</v-sheet>
|
|
255
|
+
|
|
256
|
+
<div v-if="!showVerificationResend" class="mt-2 text-center">
|
|
257
|
+
<v-btn
|
|
258
|
+
type="button"
|
|
259
|
+
variant="text"
|
|
260
|
+
color="secondary"
|
|
261
|
+
@click="showVerificationResend = true"
|
|
262
|
+
>
|
|
263
|
+
{{ t('auth.verificationResendTitle') }}
|
|
264
|
+
</v-btn>
|
|
265
|
+
</div>
|
|
190
266
|
</template>
|
|
191
267
|
|
|
192
268
|
<style scoped lang="sass">
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {ref, watch} from 'vue'
|
|
3
|
+
import {ClientError} from '@drax/common-front'
|
|
4
|
+
import type {IClientInputError} from '@drax/common-front'
|
|
5
|
+
import {useI18nValidation} from '@drax/common-vue'
|
|
6
|
+
import {useI18n} from 'vue-i18n'
|
|
7
|
+
import {useAuth} from '../../composables/useAuth.js'
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{email?: string, backLabel?: string}>(), {email: ''})
|
|
10
|
+
const emit = defineEmits<{back: []}>()
|
|
11
|
+
const {t, te} = useI18n()
|
|
12
|
+
const {$ta} = useI18nValidation()
|
|
13
|
+
const {resendVerificationEmail} = useAuth()
|
|
14
|
+
const email = ref(props.email)
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
const success = ref(false)
|
|
17
|
+
const errorMsg = ref('')
|
|
18
|
+
const inputErrors = ref<IClientInputError>()
|
|
19
|
+
|
|
20
|
+
watch(() => props.email, value => { email.value = value })
|
|
21
|
+
watch(email, () => {
|
|
22
|
+
success.value = false
|
|
23
|
+
errorMsg.value = ''
|
|
24
|
+
inputErrors.value = undefined
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
async function submitResend() {
|
|
28
|
+
if (loading.value) return
|
|
29
|
+
success.value = false
|
|
30
|
+
errorMsg.value = ''
|
|
31
|
+
inputErrors.value = undefined
|
|
32
|
+
const address = email.value.trim()
|
|
33
|
+
if (!address || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)) {
|
|
34
|
+
inputErrors.value = {email: [address ? 'auth.verificationEmailInvalid' : 'validation.required']}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
loading.value = true
|
|
38
|
+
try {
|
|
39
|
+
const result = await resendVerificationEmail(address)
|
|
40
|
+
if (!result.success) throw new Error('auth.verificationResendError')
|
|
41
|
+
// A neutral response does not confirm an account exists or that mail was sent.
|
|
42
|
+
success.value = true
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof ClientError) inputErrors.value = error.inputErrors
|
|
45
|
+
errorMsg.value = error instanceof Error ? error.message : 'auth.verificationResendError'
|
|
46
|
+
} finally {
|
|
47
|
+
loading.value = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<v-form @submit.prevent="submitResend" novalidate>
|
|
54
|
+
<v-card variant="flat">
|
|
55
|
+
<v-card-title>{{ t('auth.verificationFormTitle') }}</v-card-title>
|
|
56
|
+
<v-card-text>
|
|
57
|
+
<v-alert v-if="success" type="success" role="status" class="mb-4">
|
|
58
|
+
{{ t('auth.verificationResendNeutral') }}
|
|
59
|
+
</v-alert>
|
|
60
|
+
<v-alert v-if="errorMsg" type="error" role="alert" class="mb-4">
|
|
61
|
+
{{ te(errorMsg) ? t(errorMsg) : errorMsg }}
|
|
62
|
+
</v-alert>
|
|
63
|
+
<v-text-field
|
|
64
|
+
v-model="email"
|
|
65
|
+
name="verification-email"
|
|
66
|
+
type="email"
|
|
67
|
+
autocomplete="email"
|
|
68
|
+
:label="t('user.field.email')"
|
|
69
|
+
prepend-inner-icon="mdi-email"
|
|
70
|
+
variant="outlined"
|
|
71
|
+
required
|
|
72
|
+
:disabled="loading"
|
|
73
|
+
:error-messages="$ta(inputErrors?.email)"
|
|
74
|
+
/>
|
|
75
|
+
</v-card-text>
|
|
76
|
+
<v-card-actions>
|
|
77
|
+
<v-btn v-if="backLabel" type="button" variant="text" :disabled="loading" @click="emit('back')">
|
|
78
|
+
{{ backLabel }}
|
|
79
|
+
</v-btn>
|
|
80
|
+
<v-spacer />
|
|
81
|
+
<v-btn type="submit" color="primary" variant="tonal" :loading="loading" :disabled="loading">
|
|
82
|
+
{{ t('auth.verificationResend') }}
|
|
83
|
+
</v-btn>
|
|
84
|
+
</v-card-actions>
|
|
85
|
+
</v-card>
|
|
86
|
+
</v-form>
|
|
87
|
+
</template>
|
|
@@ -6,7 +6,7 @@ import {useAuthStore} from "../../stores/AuthStore"
|
|
|
6
6
|
import {useI18n} from "vue-i18n"
|
|
7
7
|
|
|
8
8
|
const authStore = useAuthStore()
|
|
9
|
-
const {switchTenant, hasPermission} = useAuth()
|
|
9
|
+
const {switchTenant, hasPermission, isAuthenticated} = useAuth()
|
|
10
10
|
|
|
11
11
|
const {t} = useI18n()
|
|
12
12
|
const tenant = ref();
|
|
@@ -19,7 +19,7 @@ async function changeTenant() {
|
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<template>
|
|
22
|
-
<v-toolbar v-if="hasPermission('user:switchTenant')">
|
|
22
|
+
<v-toolbar v-if="isAuthenticated() && hasPermission('user:switchTenant')">
|
|
23
23
|
<v-toolbar-title>
|
|
24
24
|
<b>{{t('tenant.current')}}:</b> {{authStore.authUser?.tenant ? authStore.authUser?.tenant?.name : '-'}}
|
|
25
25
|
</v-toolbar-title>
|
|
@@ -162,7 +162,8 @@ export async function setupAuthSession(): Promise<void> {
|
|
|
162
162
|
}
|
|
163
163
|
syncAuthHeaders(store)
|
|
164
164
|
client.setBeforeRequest?.(async (url) => {
|
|
165
|
-
if (/\/api\/auth\/(login|refresh|logout)\/?(?:[?#]|$)/.test(url)) return
|
|
165
|
+
if (/\/api\/auth\/(login|refresh|logout|registration-options)\/?(?:[?#]|$)/.test(url)) return
|
|
166
|
+
if (/\/api\/users\/(register|verification\/resend)\/?(?:[?#]|$)/.test(url)) return
|
|
166
167
|
// The profile fetch uses the fresh JWT and must not await its own refresh promise.
|
|
167
168
|
if (fetchingUser.has(store) && /\/api\/auth\/me\/?(?:[?#]|$)/.test(url)) return
|
|
168
169
|
await refreshAuthSession(store)
|
|
@@ -81,6 +81,14 @@ export function useAuth() {
|
|
|
81
81
|
return await authSystem.recoveryPasswordComplete(recoveryCode, newPassword)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const registrationOptions = async () => {
|
|
85
|
+
return await authSystem.registrationOptions()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resendVerificationEmail = async (email: string) => {
|
|
89
|
+
return await authSystem.resendVerificationEmail(email)
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
const register = async (form: any) => {
|
|
85
93
|
return await authSystem.register(form)
|
|
86
94
|
}
|
|
@@ -147,7 +155,7 @@ export function useAuth() {
|
|
|
147
155
|
isAuthenticated, fetchAuthUser,
|
|
148
156
|
changeOwnPassword, changeAvatar, passwordPolicy,
|
|
149
157
|
recoveryPasswordRequest, recoveryPasswordComplete,
|
|
150
|
-
register, switchTenant
|
|
158
|
+
register, registrationOptions, resendVerificationEmail, switchTenant
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export {setupAuthSession, refreshAuthSession, isRememberMeEnabled} from './composables/setupAuthSession'
|
|
2
2
|
export {default as IdentityRememberMe} from './components/IdentityRememberMe.vue'
|
|
3
|
+
export {default as IdentityVerificationResend} from './components/IdentityVerificationResend/IdentityVerificationResend.vue'
|
|
3
4
|
|
|
4
5
|
import IdentityLogin from "./components/IdentityLogin/IdentityLogin.vue";
|
|
5
6
|
import IdentityProfileAvatar from "./components/IdentityProfileAvatar/IdentityProfileAvatar.vue";
|