refactor(portal): move components to shared/public-form and portal/{event,*}
- public-form/** (18 files + 7 component tests) → shared/public-form/**
This is the runtime form-renderer; goes into shared/ because it will
be reused by the organizer-app Form Builder preview (S3b).
- event/{Claimen,Informatie,Overzicht,Rooster}Tab.vue → portal/event/**
- portal/{StatusCard,EventCard,UserAvatarMenu}.vue → portal/** (no
path change — both apps had a portal/ subfolder).
- AppLoadingIndicator.vue, auth/{PasswordRequirements,MfaChallengeCard}.vue,
settings/Mfa{Disable,Email,Totp}SetupDialog.vue: portal copies
deleted as duplicates of pre-existing apps/app components (diffs
were trivial formatting only).
Inside the moved files: rewrote @form-schema/* → @/composables/forms/*
and @/components/{public-form,event/[Tab]} → new sub-zone paths.
Updated apps/app/tsconfig.json to drop the @form-schema path alias
and the packages/form-schema include path. Updated formSchema.ts to
import from @/composables/forms/types/formBuilder. Carried the
crypto polyfill from apps/portal/tests/setup.ts into
apps/app/tests/setup.ts (needed by useFormDraft tests landing in C.4).
NOTE: Some moved tests still fail because they reference portal
composables (usePublicFormSections, useFormDraft) that move in C.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormSubmissionDuplicate } from '@form-schema/types/formBuilder'
|
import type { PublicFormSubmissionDuplicate } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: PublicFormSubmissionDuplicate | null
|
data: PublicFormSubmissionDuplicate | null
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
|
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
|
||||||
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
||||||
import type { PublicFormField, PublicFormTimeSlot } from '@form-schema/types/formBuilder'
|
import type { PublicFormField, PublicFormTimeSlot } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField, runValidators } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { resolveOptionLabel } from '@form-schema/types/formBuilder'
|
import { resolveOptionLabel } from '@/composables/forms/types/formBuilder'
|
||||||
import type { OptionSpec, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { OptionSpec, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField, runValidators } from '@/composables/forms/utils/formValidation'
|
||||||
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { resolveOptionLabel } from '@form-schema/types/formBuilder'
|
import { resolveOptionLabel } from '@/composables/forms/types/formBuilder'
|
||||||
import type { OptionSpec, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { OptionSpec, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { resolveOptionLabel } from '@form-schema/types/formBuilder'
|
import { resolveOptionLabel } from '@/composables/forms/types/formBuilder'
|
||||||
import type { OptionSpec, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { OptionSpec, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -16,9 +16,9 @@ import FieldTagPicker from './FieldTagPicker.vue'
|
|||||||
import FieldText from './FieldText.vue'
|
import FieldText from './FieldText.vue'
|
||||||
import FieldTextarea from './FieldTextarea.vue'
|
import FieldTextarea from './FieldTextarea.vue'
|
||||||
import FieldUrl from './FieldUrl.vue'
|
import FieldUrl from './FieldUrl.vue'
|
||||||
import { evaluateConditionalLogic } from '@form-schema/composables/useConditionalLogic'
|
import { evaluateConditionalLogic } from '@/composables/forms/composables/useConditionalLogic'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { FormFieldDisplayWidth, FormValues, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { FormFieldDisplayWidth, FormValues, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -3,8 +3,8 @@ import draggable from 'vuedraggable'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
|
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
|
||||||
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
||||||
import type { PublicFormField, PublicFormSectionOption, SectionPriorityValue } from '@form-schema/types/formBuilder'
|
import type { PublicFormField, PublicFormSectionOption, SectionPriorityValue } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField, runValidators } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField, runValidators } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { resolveOptionLabel } from '@form-schema/types/formBuilder'
|
import { resolveOptionLabel } from '@/composables/forms/types/formBuilder'
|
||||||
import type { OptionSpec, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { OptionSpec, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
import { usePublicFormLocale } from '@/composables/publicFormInjection'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AvailableTag, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { AvailableTag, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
import { getValidatorsForField } from '@form-schema/utils/formValidation'
|
import { getValidatorsForField } from '@/composables/forms/utils/formValidation'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
field: PublicFormField
|
field: PublicFormField
|
||||||
@@ -3,16 +3,16 @@ import DuplicateSubmissionHint from './DuplicateSubmissionHint.vue'
|
|||||||
import IdentityMatchBanner from './IdentityMatchBanner.vue'
|
import IdentityMatchBanner from './IdentityMatchBanner.vue'
|
||||||
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
|
import { usePublicFormSections } from '@/composables/api/usePublicFormSections'
|
||||||
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
|
import { usePublicFormTimeSlots } from '@/composables/api/usePublicFormTimeSlots'
|
||||||
import { formatFieldValue } from '@form-schema/composables/formatFieldValue'
|
import { formatFieldValue } from '@/composables/forms/composables/formatFieldValue'
|
||||||
import type { FormStep } from '@form-schema/composables/useFormSteps'
|
import type { FormStep } from '@/composables/forms/composables/useFormSteps'
|
||||||
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
import { usePublicFormToken } from '@/composables/publicFormInjection'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type {
|
import type {
|
||||||
FormValues,
|
FormValues,
|
||||||
PublicFormField,
|
PublicFormField,
|
||||||
PublicFormSubmissionDuplicate,
|
PublicFormSubmissionDuplicate,
|
||||||
PublicFormSubmissionIdentityMatch,
|
PublicFormSubmissionIdentityMatch,
|
||||||
} from '@form-schema/types/formBuilder'
|
} from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
steps: FormStep[]
|
steps: FormStep[]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrorCode } from '@form-schema/types/formBuilder'
|
import type { FormErrorCode } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
errorCode?: FormErrorCode | string
|
errorCode?: FormErrorCode | string
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import type { FormStep } from '@form-schema/composables/useFormSteps'
|
import type { FormStep } from '@/composables/forms/composables/useFormSteps'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
steps: FormStep[]
|
steps: FormStep[]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import DuplicateSubmissionHint from '@/components/public-form/DuplicateSubmissionHint.vue'
|
import DuplicateSubmissionHint from '@/components/shared/public-form/DuplicateSubmissionHint.vue'
|
||||||
import type { PublicFormSubmissionDuplicate } from '@form-schema/types/formBuilder'
|
import type { PublicFormSubmissionDuplicate } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
function mountHint(data: PublicFormSubmissionDuplicate | null) {
|
function mountHint(data: PublicFormSubmissionDuplicate | null) {
|
||||||
return mount(DuplicateSubmissionHint, {
|
return mount(DuplicateSubmissionHint, {
|
||||||
@@ -20,9 +20,9 @@ vi.mock('@/composables/publicFormInjection', () => ({
|
|||||||
providePublicFormToken: () => {},
|
providePublicFormToken: () => {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import FieldAvailabilityPicker from '@/components/public-form/FieldAvailabilityPicker.vue'
|
import FieldAvailabilityPicker from '@/components/shared/public-form/FieldAvailabilityPicker.vue'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { PublicFormField, PublicFormTimeSlot } from '@form-schema/types/formBuilder'
|
import type { PublicFormField, PublicFormTimeSlot } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
||||||
return {
|
return {
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { computed, defineComponent, h, ref } from 'vue'
|
import { computed, defineComponent, h, ref } from 'vue'
|
||||||
import FieldCheckboxList from '@/components/public-form/FieldCheckboxList.vue'
|
import FieldCheckboxList from '@/components/shared/public-form/FieldCheckboxList.vue'
|
||||||
import FieldMultiselect from '@/components/public-form/FieldMultiselect.vue'
|
import FieldMultiselect from '@/components/shared/public-form/FieldMultiselect.vue'
|
||||||
import FieldRadio from '@/components/public-form/FieldRadio.vue'
|
import FieldRadio from '@/components/shared/public-form/FieldRadio.vue'
|
||||||
import FieldSelect from '@/components/public-form/FieldSelect.vue'
|
import FieldSelect from '@/components/shared/public-form/FieldSelect.vue'
|
||||||
import { providePublicFormLocale } from '@/composables/publicFormInjection'
|
import { providePublicFormLocale } from '@/composables/publicFormInjection'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { OptionSpec, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { OptionSpec, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
const stubs = {
|
const stubs = {
|
||||||
VRadioGroup: { name: 'VRadioGroup', template: '<div class="v-radio-group-stub"><slot/></div>' },
|
VRadioGroup: { name: 'VRadioGroup', template: '<div class="v-radio-group-stub"><slot/></div>' },
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import FieldRenderer from '@/components/public-form/FieldRenderer.vue'
|
import FieldRenderer from '@/components/shared/public-form/FieldRenderer.vue'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { PublicFormField } from '@form-schema/types/formBuilder'
|
import type { PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
function makeField(partial: Partial<PublicFormField>): PublicFormField {
|
function makeField(partial: Partial<PublicFormField>): PublicFormField {
|
||||||
return {
|
return {
|
||||||
@@ -35,9 +35,9 @@ vi.mock('vuedraggable', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import FieldSectionPriority from '@/components/public-form/FieldSectionPriority.vue'
|
import FieldSectionPriority from '@/components/shared/public-form/FieldSectionPriority.vue'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { PublicFormField, PublicFormSectionOption } from '@form-schema/types/formBuilder'
|
import type { PublicFormField, PublicFormSectionOption } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
||||||
return {
|
return {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import FieldTagPicker from '@/components/public-form/FieldTagPicker.vue'
|
import FieldTagPicker from '@/components/shared/public-form/FieldTagPicker.vue'
|
||||||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
import { FormFieldType } from '@/composables/forms/types/formBuilder'
|
||||||
import type { AvailableTag, PublicFormField } from '@form-schema/types/formBuilder'
|
import type { AvailableTag, PublicFormField } from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
||||||
return {
|
return {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import IdentityMatchBanner from '@/components/public-form/IdentityMatchBanner.vue'
|
import IdentityMatchBanner from '@/components/shared/public-form/IdentityMatchBanner.vue'
|
||||||
|
|
||||||
function mountBanner(props: { status: 'pending' | 'matched' | 'none' | null; message?: string | null }) {
|
function mountBanner(props: { status: 'pending' | 'matched' | 'none' | null; message?: string | null }) {
|
||||||
return mount(IdentityMatchBanner, {
|
return mount(IdentityMatchBanner, {
|
||||||
@@ -3,19 +3,19 @@
|
|||||||
// the enums at api/app/Enums/FormBuilder/.
|
// the enums at api/app/Enums/FormBuilder/.
|
||||||
//
|
//
|
||||||
// Shared schema types (FormFieldType, FormFieldDisplayWidth, etc.) are
|
// Shared schema types (FormFieldType, FormFieldDisplayWidth, etc.) are
|
||||||
// imported from @form-schema so portal and app stay in sync on the
|
// imported from the inlined form-schema source at composables/forms so
|
||||||
// submit-side contract. Organizer-only types (lifecycle, payloads,
|
// portal and app stay in sync on the submit-side contract. Organizer-only
|
||||||
// purpose/submission_mode enums) live here.
|
// types (lifecycle, payloads, purpose/submission_mode enums) live here.
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ConditionalLogic,
|
ConditionalLogic,
|
||||||
FormFieldDisplayWidth,
|
FormFieldDisplayWidth,
|
||||||
FormFieldType,
|
FormFieldType,
|
||||||
FormFieldValidationRules,
|
FormFieldValidationRules,
|
||||||
} from '@form-schema/types/formBuilder'
|
} from '@/composables/forms/types/formBuilder'
|
||||||
|
|
||||||
// Re-export shared field primitives so consumers of this module don't
|
// Re-export shared field primitives so consumers of this module don't
|
||||||
// need to reach into @form-schema directly.
|
// need to reach into the forms package directly.
|
||||||
export type { ConditionalLogic, FormFieldDisplayWidth, FormFieldType, FormFieldValidationRules }
|
export type { ConditionalLogic, FormFieldDisplayWidth, FormFieldType, FormFieldValidationRules }
|
||||||
|
|
||||||
// Mirrors api/app/Enums/FormBuilder/FormPurpose.php
|
// Mirrors api/app/Enums/FormBuilder/FormPurpose.php
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
// Deterministic idempotency-key generation for useFormDraft tests.
|
||||||
|
if (!globalThis.crypto) {
|
||||||
|
;(globalThis as { crypto: Crypto }).crypto = {
|
||||||
|
randomUUID: () => '00000000-0000-4000-8000-000000000000',
|
||||||
|
getRandomValues: (buf: Uint8Array) => {
|
||||||
|
for (let i = 0; i < buf.length; i++) buf[i] = 0
|
||||||
|
|
||||||
|
return buf
|
||||||
|
},
|
||||||
|
} as unknown as Crypto
|
||||||
|
}
|
||||||
|
|
||||||
// Default vue-router mock — individual tests can override with their own mock.
|
// Default vue-router mock — individual tests can override with their own mock.
|
||||||
// Page-level tests that exercise the actual router should not import this.
|
// Page-level tests that exercise the actual router should not import this.
|
||||||
vi.mock('vue-router', () => ({
|
vi.mock('vue-router', () => ({
|
||||||
|
|||||||
@@ -40,9 +40,6 @@
|
|||||||
"@validators": [
|
"@validators": [
|
||||||
"./src/@core/utils/validators"
|
"./src/@core/utils/validators"
|
||||||
],
|
],
|
||||||
"@form-schema/*": [
|
|
||||||
"../../packages/form-schema/src/*"
|
|
||||||
],
|
|
||||||
"vue": [
|
"vue": [
|
||||||
"./node_modules/vue"
|
"./node_modules/vue"
|
||||||
]
|
]
|
||||||
@@ -69,8 +66,7 @@
|
|||||||
"./src/**/*.vue",
|
"./src/**/*.vue",
|
||||||
"./themeConfig.ts",
|
"./themeConfig.ts",
|
||||||
"./auto-imports.d.ts",
|
"./auto-imports.d.ts",
|
||||||
"./components.d.ts",
|
"./components.d.ts"
|
||||||
"../../packages/form-schema/src/**/*"
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"./dist",
|
"./dist",
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const bufferValue = ref(20)
|
|
||||||
const progressValue = ref(10)
|
|
||||||
const isFallbackState = ref(false)
|
|
||||||
const interval = ref<ReturnType<typeof setInterval>>()
|
|
||||||
const showProgress = ref(false)
|
|
||||||
|
|
||||||
watch([progressValue, isFallbackState], () => {
|
|
||||||
if (progressValue.value > 80 && isFallbackState.value)
|
|
||||||
progressValue.value = 82
|
|
||||||
|
|
||||||
startBuffer()
|
|
||||||
})
|
|
||||||
|
|
||||||
function startBuffer() {
|
|
||||||
clearInterval(interval.value)
|
|
||||||
interval.value = setInterval(() => {
|
|
||||||
progressValue.value += Math.random() * (15 - 5) + 5
|
|
||||||
bufferValue.value += Math.random() * (15 - 5) + 6
|
|
||||||
}, 800)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackHandle = () => {
|
|
||||||
showProgress.value = true
|
|
||||||
progressValue.value = 10
|
|
||||||
isFallbackState.value = true
|
|
||||||
startBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveHandle = () => {
|
|
||||||
isFallbackState.value = false
|
|
||||||
progressValue.value = 100
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
clearInterval(interval.value)
|
|
||||||
progressValue.value = 0
|
|
||||||
bufferValue.value = 20
|
|
||||||
showProgress.value = false
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
fallbackHandle,
|
|
||||||
resolveHandle,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- loading state via #fallback slot -->
|
|
||||||
<div
|
|
||||||
v-if="showProgress"
|
|
||||||
class="position-fixed"
|
|
||||||
style="z-index: 9999; inset-block-start: 0; inset-inline: 0 0;"
|
|
||||||
>
|
|
||||||
<VProgressLinear
|
|
||||||
v-model="progressValue"
|
|
||||||
:buffer-value="bufferValue"
|
|
||||||
color="primary"
|
|
||||||
height="2"
|
|
||||||
bg-color="background"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useVerifyMfa, useSendMfaEmailCode } from '@/composables/api/useMfa'
|
|
||||||
import { generateDeviceFingerprint, getDeviceName } from '@/utils/deviceFingerprint'
|
|
||||||
import type { MfaMethod } from '@/types/mfa'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mfaSessionToken: string
|
|
||||||
methods: MfaMethod[]
|
|
||||||
preferredMethod: string
|
|
||||||
expiresIn: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
verified: [data: unknown]
|
|
||||||
cancelled: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const verifyMutation = useVerifyMfa()
|
|
||||||
const sendEmailMutation = useSendMfaEmailCode()
|
|
||||||
|
|
||||||
const selectedMethod = ref<string>(props.preferredMethod)
|
|
||||||
const otpCode = ref('')
|
|
||||||
const backupCode = ref('')
|
|
||||||
const trustDevice = ref(false)
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const timeLeft = ref(props.expiresIn)
|
|
||||||
|
|
||||||
const isBackupMethod = computed(() => selectedMethod.value === 'backup_code')
|
|
||||||
|
|
||||||
// Countdown timer
|
|
||||||
const countdownInterval = setInterval(() => {
|
|
||||||
timeLeft.value--
|
|
||||||
if (timeLeft.value <= 0) {
|
|
||||||
clearInterval(countdownInterval)
|
|
||||||
errorMessage.value = 'MFA-sessie verlopen. Log opnieuw in.'
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(countdownInterval)
|
|
||||||
})
|
|
||||||
|
|
||||||
const formattedTimeLeft = computed(() => {
|
|
||||||
const minutes = Math.floor(timeLeft.value / 60)
|
|
||||||
const seconds = timeLeft.value % 60
|
|
||||||
|
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const methodTabs = computed(() => {
|
|
||||||
const tabs: Array<{ value: string; title: string; icon: string }> = []
|
|
||||||
|
|
||||||
if (props.methods.includes('totp' as MfaMethod))
|
|
||||||
tabs.push({ value: 'totp', title: 'Authenticator', icon: 'tabler-device-mobile' })
|
|
||||||
|
|
||||||
if (props.methods.includes('email' as MfaMethod))
|
|
||||||
tabs.push({ value: 'email', title: 'E-mailcode', icon: 'tabler-mail' })
|
|
||||||
|
|
||||||
if (props.methods.includes('backup_code' as MfaMethod))
|
|
||||||
tabs.push({ value: 'backup_code', title: 'Backup code', icon: 'tabler-key' })
|
|
||||||
|
|
||||||
return tabs
|
|
||||||
})
|
|
||||||
|
|
||||||
function onMethodChange(method: string) {
|
|
||||||
errorMessage.value = ''
|
|
||||||
otpCode.value = ''
|
|
||||||
backupCode.value = ''
|
|
||||||
|
|
||||||
if (method === 'email')
|
|
||||||
handleSendEmailCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSendEmailCode() {
|
|
||||||
try {
|
|
||||||
await sendEmailMutation.mutateAsync(props.mfaSessionToken)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Rate limited or other error — user can try resend link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOtpFinish(code: string) {
|
|
||||||
otpCode.value = code
|
|
||||||
handleVerify()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleVerify() {
|
|
||||||
errorMessage.value = ''
|
|
||||||
const code = isBackupMethod.value ? backupCode.value : otpCode.value
|
|
||||||
|
|
||||||
if (!code) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await verifyMutation.mutateAsync({
|
|
||||||
mfa_session_token: props.mfaSessionToken,
|
|
||||||
code,
|
|
||||||
method: selectedMethod.value as MfaMethod,
|
|
||||||
trust_device: trustDevice.value || undefined,
|
|
||||||
device_fingerprint: trustDevice.value ? generateDeviceFingerprint() : undefined,
|
|
||||||
device_name: trustDevice.value ? getDeviceName() : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
emit('verified', data)
|
|
||||||
}
|
|
||||||
catch (err: unknown) {
|
|
||||||
const ax = err as { response?: { data?: { message?: string } } }
|
|
||||||
|
|
||||||
errorMessage.value = ax.response?.data?.message ?? 'Verificatie mislukt. Probeer het opnieuw.'
|
|
||||||
otpCode.value = ''
|
|
||||||
backupCode.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VCard
|
|
||||||
class="auth-card"
|
|
||||||
max-width="460"
|
|
||||||
:class="$vuetify.display.smAndUp ? 'pa-6' : 'pa-0'"
|
|
||||||
>
|
|
||||||
<VCardText>
|
|
||||||
<h4 class="text-h4 mb-1">
|
|
||||||
Tweestapsverificatie
|
|
||||||
</h4>
|
|
||||||
<p class="mb-0">
|
|
||||||
Voer je verificatiecode in om in te loggen
|
|
||||||
</p>
|
|
||||||
<VChip
|
|
||||||
v-if="timeLeft > 0"
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
variant="tonal"
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
<VIcon
|
|
||||||
start
|
|
||||||
icon="tabler-clock"
|
|
||||||
size="14"
|
|
||||||
/>
|
|
||||||
{{ formattedTimeLeft }}
|
|
||||||
</VChip>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardText>
|
|
||||||
<!-- Method tabs -->
|
|
||||||
<VTabs
|
|
||||||
v-if="methodTabs.length > 1"
|
|
||||||
v-model="selectedMethod"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
@update:model-value="(v: unknown) => onMethodChange(String(v))"
|
|
||||||
>
|
|
||||||
<VTab
|
|
||||||
v-for="tab in methodTabs"
|
|
||||||
:key="tab.value"
|
|
||||||
:value="tab.value"
|
|
||||||
>
|
|
||||||
<VIcon
|
|
||||||
:icon="tab.icon"
|
|
||||||
size="20"
|
|
||||||
class="me-1"
|
|
||||||
/>
|
|
||||||
{{ tab.title }}
|
|
||||||
</VTab>
|
|
||||||
</VTabs>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="errorMessage"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VForm @submit.prevent="handleVerify">
|
|
||||||
<VRow>
|
|
||||||
<!-- OTP input for TOTP and email methods -->
|
|
||||||
<VCol
|
|
||||||
v-if="!isBackupMethod"
|
|
||||||
cols="12"
|
|
||||||
>
|
|
||||||
<p class="text-body-2 mb-2">
|
|
||||||
{{
|
|
||||||
selectedMethod === 'totp'
|
|
||||||
? 'Voer de 6-cijferige code in uit je authenticator app'
|
|
||||||
: 'Voer de 6-cijferige code in die naar je e-mailadres is gestuurd'
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<VOtpInput
|
|
||||||
v-model="otpCode"
|
|
||||||
:disabled="verifyMutation.isPending.value || timeLeft <= 0"
|
|
||||||
type="number"
|
|
||||||
class="pa-0"
|
|
||||||
@finish="onOtpFinish"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
|
|
||||||
<!-- Backup code input -->
|
|
||||||
<VCol
|
|
||||||
v-else
|
|
||||||
cols="12"
|
|
||||||
>
|
|
||||||
<p class="text-body-2 mb-2">
|
|
||||||
Voer een van je backup codes in (formaat: XXXX-XXXX)
|
|
||||||
</p>
|
|
||||||
<AppTextField
|
|
||||||
v-model="backupCode"
|
|
||||||
placeholder="XXXX-XXXX"
|
|
||||||
autofocus
|
|
||||||
class="text-center"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
|
|
||||||
<!-- Trust device -->
|
|
||||||
<VCol cols="12">
|
|
||||||
<VCheckbox
|
|
||||||
v-model="trustDevice"
|
|
||||||
label="Onthoud dit apparaat voor 30 dagen"
|
|
||||||
density="compact"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
|
|
||||||
<!-- Verify button -->
|
|
||||||
<VCol cols="12">
|
|
||||||
<VBtn
|
|
||||||
block
|
|
||||||
type="submit"
|
|
||||||
:loading="verifyMutation.isPending.value"
|
|
||||||
:disabled="timeLeft <= 0"
|
|
||||||
>
|
|
||||||
Verifiëren
|
|
||||||
</VBtn>
|
|
||||||
</VCol>
|
|
||||||
|
|
||||||
<!-- Resend email code -->
|
|
||||||
<VCol
|
|
||||||
v-if="selectedMethod === 'email'"
|
|
||||||
cols="12"
|
|
||||||
class="text-center"
|
|
||||||
>
|
|
||||||
<div class="d-flex justify-center align-center flex-wrap">
|
|
||||||
<span class="me-1 text-body-2">Geen code ontvangen?</span>
|
|
||||||
<a
|
|
||||||
class="text-primary text-body-2"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="handleSendEmailCode"
|
|
||||||
>
|
|
||||||
Opnieuw versturen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</VCol>
|
|
||||||
|
|
||||||
<!-- Back to login -->
|
|
||||||
<VCol cols="12">
|
|
||||||
<a
|
|
||||||
class="d-flex align-center justify-center text-body-2"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="emit('cancelled')"
|
|
||||||
>
|
|
||||||
<VIcon
|
|
||||||
icon="tabler-chevron-left"
|
|
||||||
size="20"
|
|
||||||
class="me-1 flip-in-rtl"
|
|
||||||
/>
|
|
||||||
<span>Terug naar inloggen</span>
|
|
||||||
</a>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.v-otp-input {
|
|
||||||
.v-otp-input__content {
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ password: string }>()
|
|
||||||
|
|
||||||
const requirements = computed(() => [
|
|
||||||
{ label: 'Minimaal 8 tekens', met: props.password.length >= 8 },
|
|
||||||
{ label: 'Een hoofdletter', met: /[A-Z]/.test(props.password) },
|
|
||||||
{ label: 'Een kleine letter', met: /[a-z]/.test(props.password) },
|
|
||||||
{ label: 'Een cijfer', met: /[0-9]/.test(props.password) },
|
|
||||||
])
|
|
||||||
|
|
||||||
const allMet = computed(() => requirements.value.every(r => r.met))
|
|
||||||
|
|
||||||
defineExpose({ allMet })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="text-body-2 mt-2">
|
|
||||||
<div
|
|
||||||
v-for="req in requirements"
|
|
||||||
:key="req.label"
|
|
||||||
:class="req.met ? 'text-success' : 'text-medium-emphasis'"
|
|
||||||
class="d-flex align-center gap-1 mb-1"
|
|
||||||
>
|
|
||||||
<VIcon
|
|
||||||
:icon="req.met ? 'tabler-check' : 'tabler-x'"
|
|
||||||
size="14"
|
|
||||||
/>
|
|
||||||
<span>{{ req.label }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useDisableMfa } from '@/composables/api/useMfa'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
currentMethod: 'totp' | 'email' | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
disabled: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const disableMutation = useDisableMfa()
|
|
||||||
|
|
||||||
const code = ref('')
|
|
||||||
const method = ref<string>(props.currentMethod === 'totp' ? 'totp' : 'backup_code')
|
|
||||||
const errorMessage = ref('')
|
|
||||||
|
|
||||||
const isDialogOpen = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val: boolean) => emit('update:modelValue', val),
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isDialogOpen, (open) => {
|
|
||||||
if (open) {
|
|
||||||
code.value = ''
|
|
||||||
errorMessage.value = ''
|
|
||||||
method.value = props.currentMethod === 'totp' ? 'totp' : 'backup_code'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleDisable() {
|
|
||||||
errorMessage.value = ''
|
|
||||||
try {
|
|
||||||
await disableMutation.mutateAsync({ code: code.value, method: method.value })
|
|
||||||
isDialogOpen.value = false
|
|
||||||
emit('disabled')
|
|
||||||
}
|
|
||||||
catch (err: unknown) {
|
|
||||||
const ax = err as { response?: { data?: { message?: string } } }
|
|
||||||
|
|
||||||
errorMessage.value = ax.response?.data?.message ?? 'Kon MFA niet uitschakelen. Probeer het opnieuw.'
|
|
||||||
code.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog
|
|
||||||
v-model="isDialogOpen"
|
|
||||||
max-width="460"
|
|
||||||
>
|
|
||||||
<VCard>
|
|
||||||
<VCardTitle class="d-flex align-center pt-4">
|
|
||||||
<VIcon
|
|
||||||
icon="tabler-shield-off"
|
|
||||||
color="error"
|
|
||||||
class="me-2"
|
|
||||||
/>
|
|
||||||
Tweestapsverificatie uitschakelen
|
|
||||||
</VCardTitle>
|
|
||||||
|
|
||||||
<VCardText>
|
|
||||||
<VAlert
|
|
||||||
type="warning"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
Weet je zeker dat je tweestapsverificatie wilt uitschakelen? Je account wordt minder veilig.
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="errorMessage"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VForm @submit.prevent="handleDisable">
|
|
||||||
<p class="text-body-2 mb-2">
|
|
||||||
Voer je {{ currentMethod === 'totp' ? 'authenticator code' : 'backup code' }} in ter bevestiging
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<AppTextField
|
|
||||||
v-model="code"
|
|
||||||
:placeholder="currentMethod === 'totp' ? '123456' : 'XXXX-XXXX'"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
@click="isDialogOpen = false"
|
|
||||||
>
|
|
||||||
Annuleren
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
color="error"
|
|
||||||
:loading="disableMutation.isPending.value"
|
|
||||||
:disabled="!code"
|
|
||||||
@click="handleDisable"
|
|
||||||
>
|
|
||||||
Uitschakelen
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useSetupEmail, useConfirmEmail } from '@/composables/api/useMfa'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
userEmail: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
completed: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const setupMutation = useSetupEmail()
|
|
||||||
const confirmMutation = useConfirmEmail()
|
|
||||||
|
|
||||||
const step = ref(1)
|
|
||||||
const confirmCode = ref('')
|
|
||||||
const backupCodes = ref<string[]>([])
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const savedBackupCodes = ref(false)
|
|
||||||
const codeSent = ref(false)
|
|
||||||
|
|
||||||
const isDialogOpen = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val: boolean) => emit('update:modelValue', val),
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isDialogOpen, (open) => {
|
|
||||||
if (open) {
|
|
||||||
step.value = 1
|
|
||||||
errorMessage.value = ''
|
|
||||||
confirmCode.value = ''
|
|
||||||
backupCodes.value = []
|
|
||||||
savedBackupCodes.value = false
|
|
||||||
codeSent.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleSendCode() {
|
|
||||||
errorMessage.value = ''
|
|
||||||
try {
|
|
||||||
await setupMutation.mutateAsync()
|
|
||||||
codeSent.value = true
|
|
||||||
step.value = 2
|
|
||||||
}
|
|
||||||
catch (err: unknown) {
|
|
||||||
const ax = err as { response?: { data?: { message?: string } } }
|
|
||||||
|
|
||||||
errorMessage.value = ax.response?.data?.message ?? 'Kon geen code versturen. Probeer het opnieuw.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirm() {
|
|
||||||
errorMessage.value = ''
|
|
||||||
try {
|
|
||||||
const data = await confirmMutation.mutateAsync(confirmCode.value)
|
|
||||||
|
|
||||||
backupCodes.value = data.backup_codes
|
|
||||||
step.value = 3
|
|
||||||
}
|
|
||||||
catch (err: unknown) {
|
|
||||||
const ax = err as { response?: { data?: { message?: string } } }
|
|
||||||
|
|
||||||
errorMessage.value = ax.response?.data?.message ?? 'Ongeldige code. Probeer het opnieuw.'
|
|
||||||
confirmCode.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyBackupCodes() {
|
|
||||||
const text = backupCodes.value.join('\n')
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadBackupCodes() {
|
|
||||||
const text = `Crewli MFA Backup Codes\n${'='.repeat(30)}\n\n${backupCodes.value.join('\n')}\n\nBewaar deze codes op een veilige plek.\nElke code kan slechts eenmaal worden gebruikt.`
|
|
||||||
const blob = new Blob([text], { type: 'text/plain' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
|
|
||||||
a.href = url
|
|
||||||
a.download = 'crewli-backup-codes.txt'
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleComplete() {
|
|
||||||
isDialogOpen.value = false
|
|
||||||
emit('completed')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog
|
|
||||||
v-model="isDialogOpen"
|
|
||||||
max-width="520"
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<VCard>
|
|
||||||
<VCardTitle class="d-flex align-center pt-4">
|
|
||||||
<VIcon
|
|
||||||
icon="tabler-mail-code"
|
|
||||||
class="me-2"
|
|
||||||
/>
|
|
||||||
E-mailverificatie instellen
|
|
||||||
</VCardTitle>
|
|
||||||
|
|
||||||
<!-- Step 1: Send code -->
|
|
||||||
<template v-if="step === 1">
|
|
||||||
<VCardText>
|
|
||||||
<p class="text-body-1 mb-4">
|
|
||||||
We sturen een verificatiecode naar <strong>{{ userEmail }}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="errorMessage"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</VAlert>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
@click="isDialogOpen = false"
|
|
||||||
>
|
|
||||||
Annuleren
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:loading="setupMutation.isPending.value"
|
|
||||||
@click="handleSendCode"
|
|
||||||
>
|
|
||||||
Code versturen
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 2: Verify code -->
|
|
||||||
<template v-if="step === 2">
|
|
||||||
<VCardText>
|
|
||||||
<VAlert
|
|
||||||
type="success"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
>
|
|
||||||
Code verstuurd! Check je inbox.
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="errorMessage"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<p class="text-body-2 mb-2">
|
|
||||||
Voer de 6-cijferige code in
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<VOtpInput
|
|
||||||
v-model="confirmCode"
|
|
||||||
type="number"
|
|
||||||
class="pa-0"
|
|
||||||
:disabled="confirmMutation.isPending.value"
|
|
||||||
@finish="handleConfirm"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
|
||||||
<a
|
|
||||||
class="text-primary text-body-2"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="handleSendCode"
|
|
||||||
>
|
|
||||||
Code opnieuw versturen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
@click="step = 1"
|
|
||||||
>
|
|
||||||
Terug
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:loading="confirmMutation.isPending.value"
|
|
||||||
:disabled="confirmCode.length < 6"
|
|
||||||
@click="handleConfirm"
|
|
||||||
>
|
|
||||||
Verifiëren
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 3: Backup codes (same as TOTP) -->
|
|
||||||
<template v-if="step === 3">
|
|
||||||
<VCardText>
|
|
||||||
<VAlert
|
|
||||||
type="warning"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
Bewaar deze codes op een veilige plek. Je kunt ze gebruiken als je geen toegang hebt tot je e-mail.
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
|
||||||
<VChip
|
|
||||||
v-for="code in backupCodes"
|
|
||||||
:key="code"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
class="font-weight-bold text-body-1"
|
|
||||||
style="font-family: monospace;"
|
|
||||||
>
|
|
||||||
{{ code }}
|
|
||||||
</VChip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2 mb-4">
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
size="small"
|
|
||||||
prepend-icon="tabler-copy"
|
|
||||||
@click="copyBackupCodes"
|
|
||||||
>
|
|
||||||
Kopieer
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
size="small"
|
|
||||||
prepend-icon="tabler-download"
|
|
||||||
@click="downloadBackupCodes"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VCheckbox
|
|
||||||
v-model="savedBackupCodes"
|
|
||||||
label="Ik heb mijn backup codes veilig opgeslagen"
|
|
||||||
/>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:disabled="!savedBackupCodes"
|
|
||||||
@click="handleComplete"
|
|
||||||
>
|
|
||||||
Voltooien
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.v-otp-input .v-otp-input__content {
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import QRCode from 'qrcode'
|
|
||||||
import { useSetupTotp, useConfirmTotp } from '@/composables/api/useMfa'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
completed: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const setupMutation = useSetupTotp()
|
|
||||||
const confirmMutation = useConfirmTotp()
|
|
||||||
|
|
||||||
const step = ref(1)
|
|
||||||
const qrDataUrl = ref('')
|
|
||||||
const secret = ref('')
|
|
||||||
const confirmCode = ref('')
|
|
||||||
const backupCodes = ref<string[]>([])
|
|
||||||
const errorMessage = ref('')
|
|
||||||
const savedBackupCodes = ref(false)
|
|
||||||
|
|
||||||
const isDialogOpen = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val: boolean) => emit('update:modelValue', val),
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(isDialogOpen, async (open) => {
|
|
||||||
if (open) {
|
|
||||||
step.value = 1
|
|
||||||
errorMessage.value = ''
|
|
||||||
confirmCode.value = ''
|
|
||||||
backupCodes.value = []
|
|
||||||
savedBackupCodes.value = false
|
|
||||||
await startSetup()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function startSetup() {
|
|
||||||
try {
|
|
||||||
const data = await setupMutation.mutateAsync()
|
|
||||||
|
|
||||||
secret.value = data.secret
|
|
||||||
qrDataUrl.value = await QRCode.toDataURL(data.provisioning_uri, {
|
|
||||||
width: 256,
|
|
||||||
margin: 2,
|
|
||||||
color: { dark: '#000000', light: '#FFFFFF' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
errorMessage.value = 'Kon TOTP setup niet starten. Probeer het opnieuw.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConfirm() {
|
|
||||||
errorMessage.value = ''
|
|
||||||
try {
|
|
||||||
const data = await confirmMutation.mutateAsync(confirmCode.value)
|
|
||||||
|
|
||||||
backupCodes.value = data.backup_codes
|
|
||||||
step.value = 3
|
|
||||||
}
|
|
||||||
catch (err: unknown) {
|
|
||||||
const ax = err as { response?: { data?: { message?: string } } }
|
|
||||||
|
|
||||||
errorMessage.value = ax.response?.data?.message ?? 'Ongeldige code. Probeer het opnieuw.'
|
|
||||||
confirmCode.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard(text: string) {
|
|
||||||
navigator.clipboard.writeText(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyBackupCodes() {
|
|
||||||
const text = backupCodes.value.join('\n')
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadBackupCodes() {
|
|
||||||
const text = `Crewli MFA Backup Codes\n${'='.repeat(30)}\n\n${backupCodes.value.join('\n')}\n\nBewaar deze codes op een veilige plek.\nElke code kan slechts eenmaal worden gebruikt.`
|
|
||||||
const blob = new Blob([text], { type: 'text/plain' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
|
|
||||||
a.href = url
|
|
||||||
a.download = 'crewli-backup-codes.txt'
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleComplete() {
|
|
||||||
isDialogOpen.value = false
|
|
||||||
emit('completed')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog
|
|
||||||
v-model="isDialogOpen"
|
|
||||||
max-width="520"
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<VCard>
|
|
||||||
<VCardTitle class="d-flex align-center pt-4">
|
|
||||||
<VIcon
|
|
||||||
icon="tabler-shield-lock"
|
|
||||||
class="me-2"
|
|
||||||
/>
|
|
||||||
Authenticator app instellen
|
|
||||||
</VCardTitle>
|
|
||||||
|
|
||||||
<!-- Step 1: QR Code -->
|
|
||||||
<template v-if="step === 1">
|
|
||||||
<VCardText>
|
|
||||||
<p class="text-body-1 mb-4">
|
|
||||||
Scan de QR-code met je authenticator app (Google Authenticator, Authy, etc.)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="qrDataUrl"
|
|
||||||
class="d-flex justify-center mb-4"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="qrDataUrl"
|
|
||||||
alt="QR Code"
|
|
||||||
width="256"
|
|
||||||
height="256"
|
|
||||||
style="border-radius: 8px;"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="setupMutation.isPending.value"
|
|
||||||
type="info"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
QR-code wordt gegenereerd...
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VExpansionPanels variant="accordion">
|
|
||||||
<VExpansionPanel title="Kun je niet scannen? Voer deze code handmatig in:">
|
|
||||||
<VExpansionPanelText>
|
|
||||||
<AppTextField
|
|
||||||
:model-value="secret"
|
|
||||||
readonly
|
|
||||||
class="font-weight-bold"
|
|
||||||
append-inner-icon="tabler-copy"
|
|
||||||
@click:append-inner="() => copyToClipboard(secret)"
|
|
||||||
/>
|
|
||||||
</VExpansionPanelText>
|
|
||||||
</VExpansionPanel>
|
|
||||||
</VExpansionPanels>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
@click="isDialogOpen = false"
|
|
||||||
>
|
|
||||||
Annuleren
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:disabled="!qrDataUrl"
|
|
||||||
@click="step = 2"
|
|
||||||
>
|
|
||||||
Volgende
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 2: Verify code -->
|
|
||||||
<template v-if="step === 2">
|
|
||||||
<VCardText>
|
|
||||||
<p class="text-body-1 mb-4">
|
|
||||||
Open je authenticator app en voer de 6-cijferige code in
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<VAlert
|
|
||||||
v-if="errorMessage"
|
|
||||||
type="error"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
density="comfortable"
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<VOtpInput
|
|
||||||
v-model="confirmCode"
|
|
||||||
type="number"
|
|
||||||
class="pa-0"
|
|
||||||
:disabled="confirmMutation.isPending.value"
|
|
||||||
@finish="handleConfirm"
|
|
||||||
/>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
@click="step = 1"
|
|
||||||
>
|
|
||||||
Terug
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:loading="confirmMutation.isPending.value"
|
|
||||||
:disabled="confirmCode.length < 6"
|
|
||||||
@click="handleConfirm"
|
|
||||||
>
|
|
||||||
Verifiëren
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 3: Backup codes -->
|
|
||||||
<template v-if="step === 3">
|
|
||||||
<VCardText>
|
|
||||||
<VAlert
|
|
||||||
type="warning"
|
|
||||||
variant="tonal"
|
|
||||||
class="mb-4"
|
|
||||||
>
|
|
||||||
Bewaar deze codes op een veilige plek. Je kunt ze gebruiken als je geen toegang hebt tot je authenticator app.
|
|
||||||
</VAlert>
|
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
|
||||||
<VChip
|
|
||||||
v-for="code in backupCodes"
|
|
||||||
:key="code"
|
|
||||||
variant="tonal"
|
|
||||||
label
|
|
||||||
class="font-weight-bold text-body-1"
|
|
||||||
style="font-family: monospace;"
|
|
||||||
>
|
|
||||||
{{ code }}
|
|
||||||
</VChip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2 mb-4">
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
size="small"
|
|
||||||
prepend-icon="tabler-copy"
|
|
||||||
@click="copyBackupCodes"
|
|
||||||
>
|
|
||||||
Kopieer
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
variant="tonal"
|
|
||||||
size="small"
|
|
||||||
prepend-icon="tabler-download"
|
|
||||||
@click="downloadBackupCodes"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VCheckbox
|
|
||||||
v-model="savedBackupCodes"
|
|
||||||
label="Ik heb mijn backup codes veilig opgeslagen"
|
|
||||||
/>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:disabled="!savedBackupCodes"
|
|
||||||
@click="handleComplete"
|
|
||||||
>
|
|
||||||
Voltooien
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</template>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.v-otp-input .v-otp-input__content {
|
|
||||||
padding-inline: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { vi } from 'vitest'
|
|
||||||
|
|
||||||
// Deterministic idempotency-key generation for useFormDraft tests.
|
|
||||||
if (!globalThis.crypto) {
|
|
||||||
;(globalThis as { crypto: Crypto }).crypto = {
|
|
||||||
randomUUID: () => '00000000-0000-4000-8000-000000000000',
|
|
||||||
getRandomValues: (buf: Uint8Array) => {
|
|
||||||
for (let i = 0; i < buf.length; i++) buf[i] = 0
|
|
||||||
return buf
|
|
||||||
},
|
|
||||||
} as unknown as Crypto
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress Vue-router usage in isolated composable tests.
|
|
||||||
vi.mock('vue-router', () => ({
|
|
||||||
useRoute: () => ({ params: {}, query: {} }),
|
|
||||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
|
||||||
}))
|
|
||||||
Reference in New Issue
Block a user