modified: main_dc/yalarba/easySite/easySite/app/composables/useAuth.ts

modified:   main_dc/yalarba/easySite/easySite/app/middleware/auth.ts
modified:   main_dc/yalarba/easySite/easySite/app/pages/auth/login.vue
	modified:   main_dc/yalarba/easySite/easySite/app/pages/auth/register.vue
	new file:   main_dc/yalarba/easySite/easySite/app/pages/plugins/auth.client.ts
	modified:   main_dc/yalarba/easySite/easySite/app/pages/profile/index.vue
	new file:   main_dc/yalarba/easySite/easySite/app/schemas/auth.ts
modified:   main_dc/yalarba/easySite/easySite/app/types/auth.ts
	modified:   main_dc/yalarba/easySite/easySite/package-lock.json
	modified:   main_dc/yalarba/easySite/easySite/package.json
update login register with use vee and firebase
This commit is contained in:
2025-11-14 23:55:02 +05:00
parent e5723490d4
commit 8fee46ce5c
10 changed files with 603 additions and 234 deletions
@@ -1,91 +1,99 @@
import { ref } from 'vue' // composables/useAuth.ts
import type { User, LoginForm, RegisterForm } from '~/types/auth'
interface User {
id: number
email: string
name: string
avatar?: string
}
interface LoginData {
email: string
password: string
}
interface RegisterData {
name: string
email: string
password: string
passwordConfirmation: string
}
export const useAuth = () => { export const useAuth = () => {
const user = ref<User | null>(null) const user = useState<User | null>('user', () => null)
const isAuthenticated = ref(false) const isAuthenticated = computed(() => !!user.value)
const loading = ref(false)
// Мок-функция входа const login = async (credentials: LoginForm) => {
const login = async (credentials: LoginData): Promise<User> => { loading.value = true
// В реальном приложении здесь будет запрос к API try {
return new Promise((resolve, reject) => { const response = await $fetch<{ user: User; token: string }>(
setTimeout(() => { 'https://easysite102.ru/api/auth/login',
if (credentials.email === 'user@example.com' && credentials.password === 'password') { {
const mockUser: User = { method: 'POST',
id: 1, body: credentials
email: credentials.email, }
name: 'Иван Иванов' )
user.value = response.user
// Сохраняем токен в localStorage или cookies
localStorage.setItem('auth_token', response.token)
return response
// eslint-disable-next-line no-useless-catch
} catch (error) {
throw error
} finally {
loading.value = false
}
}
const register = async (userData: RegisterForm) => {
loading.value = true
try {
const { passwordConfirm, ...registerData } = userData
const full_name = `${userData.first_name} ${userData.last_name}`
const response = await $fetch<{ user: User }>(
'https://easysite102.ru/api/auth/register',
{
method: 'POST',
body: {
...registerData,
full_name
} }
user.value = mockUser
isAuthenticated.value = true
localStorage.setItem('user', JSON.stringify(mockUser))
resolve(mockUser)
} else {
reject(new Error('Неверные учетные данные'))
} }
}, 1000) )
})
return response
// eslint-disable-next-line no-useless-catch
} catch (error) {
throw error
} finally {
loading.value = false
}
} }
// Мок-функция регистрации const logout = async () => {
const register = async (data: RegisterData): Promise<User> => { try {
return new Promise((resolve) => { await $fetch('https://easysite102.ru/api/auth/logout', {
setTimeout(() => { method: 'POST'
const mockUser: User = { })
id: Date.now(), } catch (error) {
email: data.email, console.error('Logout error:', error)
name: data.name } finally {
user.value = null
localStorage.removeItem('auth_token')
await navigateTo('/auth/login')
}
}
const checkAuth = async () => {
const token = localStorage.getItem('auth_token')
if (!token) return
try {
const response = await $fetch<{ user: User }>(
'https://easysite102.ru/api/auth/me',
{
headers: {
Authorization: `Bearer ${token}`
}
} }
user.value = mockUser )
isAuthenticated.value = true user.value = response.user
localStorage.setItem('user', JSON.stringify(mockUser)) } catch (error) {
resolve(mockUser) console.error('Auth check failed:', error)
}, 1000) localStorage.removeItem('auth_token')
})
}
// Выход
const logout = async (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
user.value = null
isAuthenticated.value = false
localStorage.removeItem('user')
resolve()
}, 500)
})
}
// Проверка авторизации при загрузке
const checkAuth = () => {
const storedUser = localStorage.getItem('user')
if (storedUser) {
user.value = JSON.parse(storedUser)
isAuthenticated.value = true
} }
} }
return { return {
user, user: readonly(user),
isAuthenticated, isAuthenticated,
loading: readonly(loading),
login, login,
register, register,
logout, logout,
@@ -1,8 +1,14 @@
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => { export default defineNuxtRouteMiddleware((to) => {
console.log(to) const { isAuthenticated } = useAuth()
const { isAuthenticated } = useAuth()
// Если маршрут требует аутентификации и пользователь не авторизован
if (!isAuthenticated) { if (to.meta.requiresAuth && !isAuthenticated.value) {
return navigateTo('/auth/login') return navigateTo('/auth/login')
} }
// Если пользователь авторизован и пытается получить доступ к страницам аутентификации
if (to.path.startsWith('/auth') && isAuthenticated.value) {
return navigateTo('/profile')
}
}) })
@@ -6,40 +6,37 @@
<div class="card-header text-center"> <div class="card-header text-center">
<h1 class="auth-title">Вход в систему</h1> <h1 class="auth-title">Вход в систему</h1>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="auth-form" @submit.prevent="handleSubmit"> <form class="auth-form" @submit="onSubmit">
<div class="form-group"> <div class="form-group">
<label class="form-label">Email</label> <label class="form-label">Email</label>
<input <input v-model="email" type="email" class="form-input" :class="{ 'error': errors.email }"
v-model="form.email" placeholder="your@email.com">
type="email" <span v-if="errors.email" class="error-message">
class="form-input" {{ errors.email }}
placeholder="your@email.com" </span>
required
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Пароль</label> <label class="form-label">Пароль</label>
<input <input v-model="password" type="password" class="form-input" :class="{ 'error': errors.password }"
v-model="form.password" placeholder="Введите пароль">
type="password" <span v-if="errors.password" class="error-message">
class="form-input" {{ errors.password }}
placeholder="Введите пароль" </span>
required
>
</div> </div>
<button type="submit" class="btn btn-primary auth-button"> <button type="submit" class="btn btn-primary auth-button" :disabled="isSubmitting || auth.loading.value">
Войти <span v-if="isSubmitting || auth.loading.value">Вход...</span>
<span v-else>Войти</span>
</button> </button>
</form> </form>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<p class="auth-footer-text"> <p class="auth-footer-text">
Нет аккаунта? Нет аккаунта?
<NuxtLink to="/auth/register" class="auth-link"> <NuxtLink to="/auth/register" class="auth-link">
Зарегистрируйтесь Зарегистрируйтесь
</NuxtLink> </NuxtLink>
@@ -55,16 +52,38 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const form = ref({ import { useField, useForm } from 'vee-validate'
email: '', import { loginSchema } from '~/schemas/auth'
password: ''
definePageMeta({
middleware: 'auth'
}) })
const handleSubmit = () => { const auth = useAuth()
// В демо-режиме просто переходим в профиль const { handleSubmit, errors, isSubmitting } = useForm({
alert('Демо-режим: вход выполнен') validationSchema: loginSchema
navigateTo('/profile') })
}
const { value: email } = useField('email')
const { value: password } = useField('password')
const onSubmit = handleSubmit(async (values) => {
try {
await auth.login(values)
await navigateTo('/profile')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error('Login error:', error)
if (error.status === 401) {
alert('Неверный email или пароль')
} else if (error.status === 422) {
alert('Неверные данные для входа')
} else {
alert('Произошла ошибка при входе. Попробуйте позже.')
}
}
})
</script> </script>
<style scoped> <style scoped>
@@ -74,7 +93,7 @@ const handleSubmit = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem 1rem; padding: 1rem;
} }
.auth-container { .auth-container {
@@ -86,9 +105,29 @@ const handleSubmit = () => {
width: 100%; width: 100%;
} }
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.card-header {
padding: 2rem 2rem 0;
}
.card-body {
padding: 1.5rem 2rem;
}
.card-footer {
padding: 0 2rem 2rem;
}
.auth-title { .auth-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@@ -98,20 +137,69 @@ const handleSubmit = () => {
gap: 1rem; gap: 1rem;
} }
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
font-size: 0.875rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
transition: all 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input.error {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
.error-message {
color: #dc2626;
font-size: 0.75rem;
margin-top: 0.25rem;
display: block;
}
.auth-button { .auth-button {
width: 100%; width: 100%;
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0.75rem;
font-size: 0.875rem;
}
.auth-button:disabled {
opacity: 0.6;
cursor: not-allowed;
} }
.auth-footer-text { .auth-footer-text {
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 0.875rem;
} }
.auth-link { .auth-link {
color: var(--primary-600); color: var(--primary-600);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
font-size: 0.875rem;
} }
.auth-link:hover { .auth-link:hover {
@@ -121,5 +209,48 @@ const handleSubmit = () => {
.home-link { .home-link {
display: inline-block; display: inline-block;
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.875rem;
}
/* Адаптивность для мобильных устройств */
@media (max-width: 640px) {
.auth-container {
max-width: 100%;
}
.card-header {
padding: 1.5rem 1.5rem 0;
}
.card-body {
padding: 1rem 1.5rem;
}
.card-footer {
padding: 0 1.5rem 1.5rem;
}
.auth-title {
font-size: 1.25rem;
}
}
/* Для очень маленьких экранов */
@media (max-width: 380px) {
.auth-page {
padding: 0.5rem;
}
.card-header {
padding: 1rem 1rem 0;
}
.card-body {
padding: 0.75rem 1rem;
}
.card-footer {
padding: 0 1rem 1rem;
}
} }
</style> </style>
@@ -8,41 +8,63 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form @submit.prevent="handleSubmit" class="auth-form"> <form @submit="onSubmit" class="auth-form">
<!-- Имя --> <div class="form-row">
<div class="form-group"> <!-- Имя -->
<label class="form-label">Имя</label> <div class="form-group">
<input v-model="form.first_name" type="text" class="form-input" placeholder="Ваше имя" required> <label class="form-label">Имя</label>
</div> <input v-model="firstName" type="text" class="form-input" :class="{ 'error': errors.first_name }"
placeholder="Ваше имя">
<span v-if="errors.first_name" class="error-message">
{{ errors.first_name }}
</span>
</div>
<!-- Фамилия --> <!-- Фамилия -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Фамилия</label> <label class="form-label">Фамилия</label>
<input v-model="form.last_name" type="text" class="form-input" placeholder="Ваша фамилия" required> <input v-model="lastName" type="text" class="form-input" :class="{ 'error': errors.last_name }"
placeholder="Ваша фамилия">
<span v-if="errors.last_name" class="error-message">
{{ errors.last_name }}
</span>
</div>
</div> </div>
<!-- Email --> <!-- Email -->
<div class="form-group"> <div class="form-group">
<label class="form-label">Email</label> <label class="form-label">Email</label>
<input v-model="form.email" type="email" class="form-input" placeholder="your@email.com" required> <input v-model="email" type="email" class="form-input" :class="{ 'error': errors.email }"
placeholder="your@email.com">
<span v-if="errors.email" class="error-message">
{{ errors.email }}
</span>
</div> </div>
<!-- Пароль --> <div class="form-row">
<div class="form-group"> <!-- Пароль -->
<label class="form-label">Пароль</label> <div class="form-group">
<input v-model="form.password" type="password" class="form-input" <label class="form-label">Пароль</label>
placeholder="Придумайте пароль (минимум 6 символов)" required minlength="6"> <input v-model="password" type="password" class="form-input" :class="{ 'error': errors.password }"
placeholder="Минимум 6 символов">
<span v-if="errors.password" class="error-message">
{{ errors.password }}
</span>
</div>
<!-- Подтверждение пароля -->
<div class="form-group">
<label class="form-label">Подтверждение</label>
<input v-model="passwordConfirm" type="password" class="form-input"
:class="{ 'error': errors.passwordConfirm }" placeholder="Повторите пароль">
<span v-if="errors.passwordConfirm" class="error-message">
{{ errors.passwordConfirm }}
</span>
</div>
</div> </div>
<!-- Подтверждение пароля --> <button type="submit" class="btn btn-primary auth-button" :disabled="isSubmitting || auth.loading.value">
<div class="form-group"> <span v-if="isSubmitting || auth.loading.value">Регистрация...</span>
<label class="form-label">Подтверждение пароля</label>
<input v-model="form.passwordConfirm" type="password" class="form-input" placeholder="Повторите пароль"
required minlength="6">
</div>
<button type="submit" class="btn btn-primary auth-button" :disabled="loading">
<span v-if="loading">Регистрация...</span>
<span v-else>Зарегистрироваться</span> <span v-else>Зарегистрироваться</span>
</button> </button>
</form> </form>
@@ -66,83 +88,44 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const loading = ref(false) import { useField, useForm } from 'vee-validate'
import { registerSchema } from '~/schemas/auth'
// Форма регистрации — теперь с отдельными полями имени и фамилии definePageMeta({
const form = ref({ middleware: 'auth'
full_name: '',
first_name: '',
last_name: '',
email: '',
password: '',
passwordConfirm: '',
}) })
const handleSubmit = async () => { const auth = useAuth()
// Валидация паролей const { handleSubmit, errors, isSubmitting } = useForm({
if (form.value.password !== form.value.passwordConfirm) { validationSchema: registerSchema
alert('Пароли не совпадают') })
return
}
// Проверка минимальной длины пароля const { value: firstName } = useField('first_name')
if (form.value.password.length < 6) { const { value: lastName } = useField('last_name')
alert('Пароль должен содержать минимум 6 символов') const { value: email } = useField('email')
return const { value: password } = useField('password')
} const { value: passwordConfirm } = useField('passwordConfirm')
loading.value = true
const onSubmit = handleSubmit(async (values) => {
try { try {
// Формируем full_name из имени и фамилии await auth.register(values)
const full_name = `${form.value.first_name.trim()} ${form.value.last_name.trim()}`
// Отправка данных на бэкенд
const response = await $fetch(`https://easysite102.ru/api/auth/register`, {
method: 'POST',
body: {
email: form.value.email,
password: form.value.password,
full_name,
first_name: form.value.first_name,
last_name: form.value.last_name
}
})
console.log("response from fetch: %s", response)
// Успешная регистрация
alert('Регистрация выполнена успешно!') alert('Регистрация выполнена успешно!')
await navigateTo('/auth/login')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error('Registration error:', error)
// Переход на страницу входа if (error.status === 409) {
navigateTo('/auth/login') alert('Пользователь с таким email уже существует')
} else if (error.status === 422) {
} catch (error: unknown) { alert('Неверные данные для регистрации')
console.error('Ошибка регистрации:', error)
if (error instanceof Error) {
if ('status' in error) {
const fetchError = error as { status?: number }
if (fetchError.status === 409) {
alert('Пользователь с таким email уже существует')
return
} else if (fetchError.status === 400) {
alert('Неверные данные для регистрации')
return
}
}
alert(`Ошибка: ${error.message}`)
} else { } else {
alert('Неизвестная ошибка при регистрации. Попробуйте позже.') alert('Произошла ошибка при регистрации. Попробуйте позже.')
} }
} finally {
loading.value = false
} }
} })
</script> </script>
<style scoped> <style scoped>
.auth-page { .auth-page {
min-height: 100vh; min-height: 100vh;
@@ -150,21 +133,41 @@ const handleSubmit = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem 1rem; padding: 1rem;
} }
.auth-container { .auth-container {
width: 100%; width: 100%;
max-width: 400px; max-width: 440px;
} }
.auth-card { .auth-card {
width: 100%; width: 100%;
} }
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.card-header {
padding: 2rem 2rem 0;
}
.card-body {
padding: 1.5rem 2rem;
}
.card-footer {
padding: 0 2rem 2rem;
}
.auth-title { .auth-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@@ -174,9 +177,57 @@ const handleSubmit = async () => {
gap: 1rem; gap: 1rem;
} }
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
font-size: 0.875rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
transition: all 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input.error {
border-color: #dc2626;
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
.error-message {
color: #dc2626;
font-size: 0.75rem;
margin-top: 0.25rem;
display: block;
}
.auth-button { .auth-button {
width: 100%; width: 100%;
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0.75rem;
font-size: 0.875rem;
} }
.auth-button:disabled { .auth-button:disabled {
@@ -187,12 +238,14 @@ const handleSubmit = async () => {
.auth-footer-text { .auth-footer-text {
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 0.875rem;
} }
.auth-link { .auth-link {
color: var(--primary-600); color: var(--primary-600);
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
font-size: 0.875rem;
} }
.auth-link:hover { .auth-link:hover {
@@ -202,30 +255,53 @@ const handleSubmit = async () => {
.home-link { .home-link {
display: inline-block; display: inline-block;
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.875rem;
} }
.form-input { /* Адаптивность для мобильных устройств */
width: 100%; @media (max-width: 640px) {
padding: 0.75rem; .auth-container {
border: 1px solid var(--border-color); max-width: 100%;
border-radius: 0.375rem; }
font-size: 1rem;
.card-header {
padding: 1.5rem 1.5rem 0;
}
.card-body {
padding: 1rem 1.5rem;
}
.card-footer {
padding: 0 1.5rem 1.5rem;
}
.form-row {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.auth-title {
font-size: 1.25rem;
}
} }
.form-input:focus { /* Для очень маленьких экранов */
outline: none; @media (max-width: 380px) {
border-color: var(--primary-500); .auth-page {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); padding: 0.5rem;
} }
.form-label { .card-header {
display: block; padding: 1rem 1rem 0;
margin-bottom: 0.5rem; }
font-weight: 500;
color: var(--text-primary);
}
.form-group { .card-body {
margin-bottom: 1rem; padding: 0.75rem 1rem;
}
.card-footer {
padding: 0 1rem 1rem;
}
} }
</style> </style>
@@ -0,0 +1,5 @@
// plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
const auth = useAuth()
await auth.checkAuth()
})
@@ -195,7 +195,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// Страница профиля доступна всем в демо-режиме definePageMeta({
middleware: 'auth',
requiresAuth: true
})
const auth = useAuth()
</script> </script>
<style scoped> <style scoped>
@@ -0,0 +1,36 @@
// schemas/auth.ts
import * as yup from 'yup'
export const loginSchema = yup.object({
email: yup
.string()
.email('Введите корректный email')
.required('Email обязателен'),
password: yup
.string()
.min(6, 'Пароль должен содержать минимум 6 символов')
.required('Пароль обязателен')
})
export const registerSchema = yup.object({
first_name: yup
.string()
.min(2, 'Имя должно содержать минимум 2 символа')
.required('Имя обязательно'),
last_name: yup
.string()
.min(2, 'Фамилия должна содержать минимум 2 символа')
.required('Фамилия обязательна'),
email: yup
.string()
.email('Введите корректный email')
.required('Email обязателен'),
password: yup
.string()
.min(6, 'Пароль должен содержать минимум 6 символов')
.required('Пароль обязателен'),
passwordConfirm: yup
.string()
.oneOf([yup.ref('password')], 'Пароли должны совпадать')
.required('Подтверждение пароля обязательно')
})
@@ -0,0 +1,22 @@
// types/auth.ts
export interface LoginForm {
email: string
password: string
}
export interface RegisterForm {
first_name: string
last_name: string
email: string
password: string
passwordConfirm: string
}
export interface User {
id: string
email: string
first_name: string
last_name: string
full_name: string
created_at: string
}
+86 -8
View File
@@ -18,8 +18,10 @@
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vee-validate": "^4.15.1",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3",
"yup": "^1.7.1"
} }
}, },
"node_modules/@ai-sdk/gateway": { "node_modules/@ai-sdk/gateway": {
@@ -5865,12 +5867,12 @@
} }
}, },
"node_modules/@vue/devtools-kit": { "node_modules/@vue/devtools-kit": {
"version": "7.7.7", "version": "7.7.8",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.8.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", "integrity": "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-shared": "^7.7.7", "@vue/devtools-shared": "^7.7.8",
"birpc": "^2.3.0", "birpc": "^2.3.0",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@@ -5886,9 +5888,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-shared": { "node_modules/@vue/devtools-shared": {
"version": "7.7.7", "version": "7.7.8",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.8.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", "integrity": "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"rfdc": "^1.4.1" "rfdc": "^1.4.1"
@@ -12611,6 +12613,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/protocols": { "node_modules/protocols": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz",
@@ -13984,6 +13992,12 @@
"b4a": "^1.6.4" "b4a": "^1.6.4"
} }
}, },
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-inflate": { "node_modules/tiny-inflate": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -14039,6 +14053,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/totalist": { "node_modules/totalist": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -14820,6 +14840,40 @@
} }
} }
}, },
"node_modules/vee-validate": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.15.1.tgz",
"integrity": "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.5.2",
"type-fest": "^4.8.3"
},
"peerDependencies": {
"vue": "^3.4.26"
}
},
"node_modules/vee-validate/node_modules/@vue/devtools-api": {
"version": "7.7.8",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.8.tgz",
"integrity": "sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.8"
}
},
"node_modules/vee-validate/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.12", "version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
@@ -15591,6 +15645,30 @@
"error-stack-parser-es": "^1.0.5" "error-stack-parser-es": "^1.0.5"
} }
}, },
"node_modules/yup": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
@@ -21,7 +21,9 @@
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vee-validate": "^4.15.1",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3",
"yup": "^1.7.1"
} }
} }