login.vue 5.15 KB
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import { resolvePostLoginPath } from '~/utils/post-login'

definePageMeta({
  layout: false
})

const route = useRoute()
const toast = useToast()
const { t } = useAppI18n()
const { login } = useAuth()

const isSubmitting = ref(false)
const errorMessage = ref('')

const buildSchema = () => z.object({
  username: z.string().min(1, t('login.validation.usernameRequired')),
  password: z.string().min(1, t('login.validation.passwordRequired'))
})

const formSchema = computed(buildSchema)

type LoginFormSchema = z.output<ReturnType<typeof buildSchema>>

const formState = reactive<LoginFormSchema>({
  username: '',
  password: ''
})

const demoAccounts = computed(() => [{
  username: 'admin',
  password: '123456',
  label: t('login.demo.admin')
}, {
  username: 'operator',
  password: '123456',
  label: t('login.demo.operator')
}])

function fillDemoAccount(username: string, password: string) {
  formState.username = username
  formState.password = password
  errorMessage.value = ''
}

function getRedirectPath(roles?: string[]) {
  return resolvePostLoginPath(route.query.redirect, roles)
}

function resolveErrorMessage(errorCode: string | null, fallback: string) {
  if (errorCode === 'VALIDATION_ERROR') {
    return t('login.errors.validation')
  }

  if (errorCode === 'INVALID_CREDENTIALS') {
    return t('login.errors.invalidCredentials')
  }

  return fallback
}

async function onSubmit(event: FormSubmitEvent<LoginFormSchema>) {
  isSubmitting.value = true
  errorMessage.value = ''

  try {
    const result = await login(event.data)

    if (!result.success || !result.token || !result.user) {
      errorMessage.value = resolveErrorMessage(result.errorCode, result.message)
      return
    }

    toast.add({
      title: t('login.toast.successTitle'),
      description: t('login.toast.successDescription', { name: result.user.name }),
      color: 'success',
      icon: 'i-lucide-check'
    })

    await navigateTo(getRedirectPath(result.user.roles))
  } catch {
    errorMessage.value = t('login.errors.requestFailed')
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <main class="min-h-screen bg-gradient-to-br from-default via-elevated/30 to-primary/10">
    <div class="mx-auto grid min-h-screen max-w-6xl items-center gap-8 px-4 py-10 lg:grid-cols-2 lg:px-8">
      <section class="space-y-5">
        <UBadge color="primary" variant="soft" class="rounded-full">
          {{ t('login.badge') }}
        </UBadge>
        <h1 class="text-3xl font-semibold text-highlighted sm:text-4xl">
          {{ t('login.title') }}
        </h1>
        <p class="text-toned text-base leading-7">
          {{ t('login.description') }}
        </p>
        <div class="space-y-3">
          <p class="text-sm font-medium text-muted">
            {{ t('login.demo.title') }}
          </p>
          <div class="flex flex-wrap gap-2">
            <UButton
              v-for="account in demoAccounts"
              :key="account.username"
              color="neutral"
              variant="soft"
              size="sm"
              @click="fillDemoAccount(account.username, account.password)"
            >
              {{ account.label }}
            </UButton>
          </div>
        </div>
      </section>

      <section>
        <UCard class="w-full shadow-sm ring-1 ring-default">
          <template #header>
            <div class="space-y-1">
              <h2 class="text-xl font-semibold text-highlighted">
                {{ t('login.form.title') }}
              </h2>
              <p class="text-sm text-muted">
                {{ t('login.form.description') }}
              </p>
            </div>
          </template>

          <UForm
            :schema="formSchema"
            :state="formState"
            class="space-y-5"
            @submit="onSubmit"
          >
            <UFormField name="username" :label="t('login.form.username')" required>
              <UInput
                v-model="formState.username"
                size="xl"
                class="w-full"
                :placeholder="t('login.form.usernamePlaceholder')"
                autocomplete="username"
              />
            </UFormField>

            <UFormField name="password" :label="t('login.form.password')" required>
              <UInput
                v-model="formState.password"
                size="xl"
                class="w-full"
                :placeholder="t('login.form.passwordPlaceholder')"
                type="password"
                autocomplete="current-password"
              />
            </UFormField>

            <UAlert
              v-if="errorMessage"
              color="error"
              variant="soft"
              icon="i-lucide-circle-alert"
              :title="t('login.errors.title')"
              :description="errorMessage"
            />

            <UButton
              type="submit"
              color="primary"
              block
              size="xl"
              :loading="isSubmitting"
              :label="t('login.form.submit')"
            />
          </UForm>
        </UCard>
      </section>
    </div>
  </main>
</template>