121 lines
3.9 KiB
Vue
121 lines
3.9 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { reactive, computed, onMounted, ref } from 'vue'
|
||
|
|
import type { VNodeRef } from 'vue'
|
||
|
|
defineOptions({ name: 'LoginView' })
|
||
|
|
import { useRouter, useRoute } from 'vue-router'
|
||
|
|
import { Card, Typography, Form, Input, Button, message } from 'ant-design-vue'
|
||
|
|
import { useUserStore } from '../stores/userStore'
|
||
|
|
|
||
|
|
const router = useRouter()
|
||
|
|
const route = useRoute()
|
||
|
|
const userStore = useUserStore()
|
||
|
|
const loading = computed(() => userStore.loading)
|
||
|
|
|
||
|
|
const formRef = ref<VNodeRef | null>(null)
|
||
|
|
|
||
|
|
const formState = reactive({
|
||
|
|
email: '',
|
||
|
|
password: '',
|
||
|
|
})
|
||
|
|
|
||
|
|
const submit = async () => {
|
||
|
|
try {
|
||
|
|
await userStore.login(formState.email, formState.password)
|
||
|
|
message.success('Login successful')
|
||
|
|
const redirect = (route.query.redirect as string) || '/onboarding'
|
||
|
|
router.push(redirect)
|
||
|
|
} catch (error: unknown) {
|
||
|
|
let errorMsg = 'Login failed'
|
||
|
|
if (userStore.error) {
|
||
|
|
errorMsg = userStore.error
|
||
|
|
} else if (typeof error === 'string') {
|
||
|
|
errorMsg = error
|
||
|
|
} else if (error instanceof Error) {
|
||
|
|
errorMsg = error.message || 'Login failed'
|
||
|
|
} else if (typeof error === 'object' && error !== null) {
|
||
|
|
const errObj = error as { [k: string]: unknown }
|
||
|
|
const maybeResp = errObj?.response as unknown
|
||
|
|
if (typeof maybeResp === 'object' && maybeResp !== null) {
|
||
|
|
const respObj = maybeResp as { data?: unknown }
|
||
|
|
const data = respObj.data
|
||
|
|
if (typeof data === 'object' && data !== null) {
|
||
|
|
const detail = (data as { detail?: unknown }).detail
|
||
|
|
const msg = (data as { message?: unknown }).message
|
||
|
|
if (typeof detail === 'string') {
|
||
|
|
errorMsg = detail
|
||
|
|
} else if (typeof msg === 'string') {
|
||
|
|
errorMsg = msg
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
message.error(errorMsg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
await userStore.fetchSession()
|
||
|
|
if (userStore.isAuthenticated) {
|
||
|
|
const redirect = (route.query.redirect as string) || '/onboarding'
|
||
|
|
router.replace(redirect)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div class="auth-page">
|
||
|
|
<Card class="panel" :bordered="false">
|
||
|
|
<Typography.Title :level="3">Login</Typography.Title>
|
||
|
|
<Form :ref="formRef" layout="vertical" :model="formState" @finish="submit">
|
||
|
|
<Form.Item
|
||
|
|
label="Email"
|
||
|
|
name="email"
|
||
|
|
:rules="[
|
||
|
|
{ required: true, message: 'Enter your email' },
|
||
|
|
{
|
||
|
|
type: 'email',
|
||
|
|
message: 'Please enter a valid email',
|
||
|
|
},
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<Input
|
||
|
|
v-model:value="formState.email"
|
||
|
|
type="email"
|
||
|
|
placeholder="Email address"
|
||
|
|
:disabled="loading"
|
||
|
|
/>
|
||
|
|
</Form.Item>
|
||
|
|
<Form.Item
|
||
|
|
label="Password"
|
||
|
|
name="password"
|
||
|
|
:rules="[{ required: true, message: 'Enter your password' }]"
|
||
|
|
>
|
||
|
|
<Input.Password
|
||
|
|
v-model:value="formState.password"
|
||
|
|
placeholder="Password"
|
||
|
|
:disabled="loading"
|
||
|
|
/>
|
||
|
|
</Form.Item>
|
||
|
|
<Button type="primary" html-type="submit" block :loading="loading">Login</Button>
|
||
|
|
</Form>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.auth-page {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
min-height: 100vh;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
.panel {
|
||
|
|
max-width: 400px;
|
||
|
|
width: 100%;
|
||
|
|
background: #0f172a;
|
||
|
|
border: 1px solid #1f2937;
|
||
|
|
color: #e5e7eb;
|
||
|
|
}
|
||
|
|
</style>
|