feat: password reset, email change with verification, and password change
Password reset: multi-app support with custom notification linking to correct frontend (app/portal/admin). Email change: self-service with password confirmation and admin-initiated, both sending verification to new address with 24h expiry. Confirmation sent to old email on completion. Password change: authenticated endpoint revoking other sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useMemberList, useRemoveMember, useRevokeInvitation } from '@/composables/api/useMembers'
|
||||
import { useAdminChangeEmail } from '@/composables/api/useAccount'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { useOrganisationStore } from '@/stores/useOrganisationStore'
|
||||
import InviteMemberDialog from '@/components/members/InviteMemberDialog.vue'
|
||||
@@ -39,6 +40,14 @@ const isRevokeDialogOpen = ref(false)
|
||||
const invitationToRevoke = ref<{ id: string; email: string } | null>(null)
|
||||
const { mutate: revokeInvitation, isPending: isRevoking } = useRevokeInvitation(orgId)
|
||||
|
||||
// Change email
|
||||
const isEmailChangeDialogOpen = ref(false)
|
||||
const memberToChangeEmail = ref<Member | null>(null)
|
||||
const newMemberEmail = ref('')
|
||||
const adminEmailErrors = ref<Record<string, string>>({})
|
||||
const showEmailChangeSuccess = ref(false)
|
||||
const { mutate: adminChangeEmail, isPending: isChangingMemberEmail } = useAdminChangeEmail(orgId)
|
||||
|
||||
const showRemoveSuccess = ref(false)
|
||||
const showRevokeSuccess = ref(false)
|
||||
|
||||
@@ -115,6 +124,38 @@ function openRevokeDialog(invitation: { id: string; email: string }) {
|
||||
isRevokeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function openEmailChangeDialog(member: Member) {
|
||||
memberToChangeEmail.value = member
|
||||
newMemberEmail.value = ''
|
||||
adminEmailErrors.value = {}
|
||||
isEmailChangeDialogOpen.value = true
|
||||
}
|
||||
|
||||
function confirmEmailChange() {
|
||||
if (!memberToChangeEmail.value) return
|
||||
adminEmailErrors.value = {}
|
||||
|
||||
adminChangeEmail(
|
||||
{ userId: memberToChangeEmail.value.id, newEmail: newMemberEmail.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
isEmailChangeDialogOpen.value = false
|
||||
memberToChangeEmail.value = null
|
||||
newMemberEmail.value = ''
|
||||
showEmailChangeSuccess.value = true
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const ax = err as { response?: { data?: { errors?: Record<string, string[]> } } }
|
||||
if (ax.response?.data?.errors) {
|
||||
for (const [key, messages] of Object.entries(ax.response.data.errors)) {
|
||||
adminEmailErrors.value[key] = messages[0]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function confirmRevokeInvitation() {
|
||||
if (!invitationToRevoke.value) return
|
||||
|
||||
@@ -223,6 +264,12 @@ function confirmRevokeInvitation() {
|
||||
size="small"
|
||||
@click="openEditRole(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-mail-forward"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="openEmailChangeDialog(item)"
|
||||
/>
|
||||
<VBtn
|
||||
icon="tabler-trash"
|
||||
variant="text"
|
||||
@@ -354,6 +401,44 @@ function confirmRevokeInvitation() {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Change email dialog -->
|
||||
<VDialog
|
||||
v-model="isEmailChangeDialogOpen"
|
||||
max-width="420"
|
||||
>
|
||||
<VCard title="E-mailadres wijzigen">
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Wijzig het e-mailadres van <strong>{{ memberToChangeEmail?.full_name }}</strong>.
|
||||
Er wordt een verificatiemail verstuurd naar het nieuwe adres.
|
||||
</p>
|
||||
<AppTextField
|
||||
v-model="newMemberEmail"
|
||||
label="Nieuw e-mailadres"
|
||||
type="email"
|
||||
:error-messages="adminEmailErrors.new_email"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="isEmailChangeDialogOpen = false"
|
||||
>
|
||||
Annuleren
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="isChangingMemberEmail"
|
||||
:disabled="!newMemberEmail"
|
||||
@click="confirmEmailChange"
|
||||
>
|
||||
Verificatiemail versturen
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Success snackbars -->
|
||||
<VSnackbar
|
||||
v-model="showRemoveSuccess"
|
||||
@@ -369,5 +454,12 @@ function confirmRevokeInvitation() {
|
||||
>
|
||||
Uitnodiging ingetrokken
|
||||
</VSnackbar>
|
||||
<VSnackbar
|
||||
v-model="showEmailChangeSuccess"
|
||||
color="success"
|
||||
:timeout="4000"
|
||||
>
|
||||
Verificatiemail verstuurd
|
||||
</VSnackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user