security.vue 3.9 KB
<script setup lang="ts">
import * as z from 'zod'
import type { FormError, FormSubmitEvent } from '@nuxt/ui'

const { t } = useAppI18n()
const toast = useToast()
const isPasswordSubmitting = ref(false)
const isDeleteSubmitting = ref(false)
const settingsApi = useSettingsApi()

const buildPasswordSchema = () => z.object({
  current: z.string().min(8, t('settings.security.password.validation.minLength')),
  new: z.string().min(8, t('settings.security.password.validation.minLength'))
})

const passwordSchema = computed(buildPasswordSchema)

type PasswordSchema = z.output<ReturnType<typeof buildPasswordSchema>>

const password = reactive<Partial<PasswordSchema>>({
  current: '',
  new: ''
})

const validate = (state: Partial<PasswordSchema>): FormError[] => {
  const errors: FormError[] = []
  if (state.current && state.new && state.current === state.new) {
    errors.push({ name: 'new', message: t('settings.security.password.validation.different') })
  }
  return errors
}

async function onPasswordSubmit(event: FormSubmitEvent<PasswordSchema>) {
  isPasswordSubmitting.value = true

  try {
    const result = await settingsApi.updatePassword(event.data)

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    password.current = ''
    password.new = ''

    toast.add({
      title: t('settings.profile.toastTitle'),
      description: result.message,
      icon: 'i-lucide-check',
      color: 'success'
    })
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    isPasswordSubmitting.value = false
  }
}

async function onDeleteAccount() {
  isDeleteSubmitting.value = true

  try {
    const result = await settingsApi.deleteAccount()

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    toast.add({
      title: t('settings.profile.toastTitle'),
      description: result.message,
      icon: 'i-lucide-check',
      color: 'success'
    })
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    isDeleteSubmitting.value = false
  }
}
</script>

<template>
  <UPageCard
    :title="t('settings.security.password.title')"
    :description="t('settings.security.password.description')"
    variant="subtle"
  >
    <UForm
      :schema="passwordSchema"
      :state="password"
      :validate="validate"
      class="flex flex-col gap-4 max-w-xs"
      @submit="onPasswordSubmit"
    >
      <UFormField name="current">
        <UInput
          v-model="password.current"
          type="password"
          :placeholder="t('settings.security.password.currentPlaceholder')"
          class="w-full"
        />
      </UFormField>

      <UFormField name="new">
        <UInput
          v-model="password.new"
          type="password"
          :placeholder="t('settings.security.password.newPlaceholder')"
          class="w-full"
        />
      </UFormField>

      <UButton
        :label="t('settings.security.password.update')"
        class="w-fit"
        type="submit"
        :loading="isPasswordSubmitting"
      />
    </UForm>
  </UPageCard>

  <UPageCard
    :title="t('settings.security.account.title')"
    :description="t('settings.security.account.description')"
    class="bg-linear-to-tl from-error/10 from-5% to-default"
  >
    <template #footer>
      <UButton
        :label="t('settings.security.account.delete')"
        color="error"
        :loading="isDeleteSubmitting"
        @click="onDeleteAccount"
      />
    </template>
  </UPageCard>
</template>