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:
@@ -1,91 +1,99 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
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
|
||||
}
|
||||
// composables/useAuth.ts
|
||||
import type { User, LoginForm, RegisterForm } from '~/types/auth'
|
||||
|
||||
export const useAuth = () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const user = useState<User | null>('user', () => null)
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const loading = ref(false)
|
||||
|
||||
// Мок-функция входа
|
||||
const login = async (credentials: LoginData): Promise<User> => {
|
||||
// В реальном приложении здесь будет запрос к API
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (credentials.email === 'user@example.com' && credentials.password === 'password') {
|
||||
const mockUser: User = {
|
||||
id: 1,
|
||||
email: credentials.email,
|
||||
name: 'Иван Иванов'
|
||||
const login = async (credentials: LoginForm) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await $fetch<{ user: User; token: string }>(
|
||||
'https://easysite102.ru/api/auth/login',
|
||||
{
|
||||
method: 'POST',
|
||||
body: credentials
|
||||
}
|
||||
)
|
||||
|
||||
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 register = async (data: RegisterData): Promise<User> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const mockUser: User = {
|
||||
id: Date.now(),
|
||||
email: data.email,
|
||||
name: data.name
|
||||
const logout = async () => {
|
||||
try {
|
||||
await $fetch('https://easysite102.ru/api/auth/logout', {
|
||||
method: 'POST'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} 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
|
||||
localStorage.setItem('user', JSON.stringify(mockUser))
|
||||
resolve(mockUser)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
// Выход
|
||||
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
|
||||
)
|
||||
user.value = response.user
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
localStorage.removeItem('auth_token')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
user: readonly(user),
|
||||
isAuthenticated,
|
||||
loading: readonly(loading),
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// middleware/auth.ts
|
||||
export default defineNuxtRouteMiddleware((to) => {
|
||||
console.log(to)
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return navigateTo('/auth/login')
|
||||
}
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
// Если маршрут требует аутентификации и пользователь не авторизован
|
||||
if (to.meta.requiresAuth && !isAuthenticated.value) {
|
||||
return navigateTo('/auth/login')
|
||||
}
|
||||
|
||||
// Если пользователь авторизован и пытается получить доступ к страницам аутентификации
|
||||
if (to.path.startsWith('/auth') && isAuthenticated.value) {
|
||||
return navigateTo('/profile')
|
||||
}
|
||||
})
|
||||
@@ -6,40 +6,37 @@
|
||||
<div class="card-header text-center">
|
||||
<h1 class="auth-title">Вход в систему</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
<form class="auth-form" @submit.prevent="handleSubmit">
|
||||
<form class="auth-form" @submit="onSubmit">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="Введите пароль"
|
||||
required
|
||||
>
|
||||
<input v-model="password" type="password" class="form-input" :class="{ 'error': errors.password }"
|
||||
placeholder="Введите пароль">
|
||||
<span v-if="errors.password" class="error-message">
|
||||
{{ errors.password }}
|
||||
</span>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-footer text-center">
|
||||
<p class="auth-footer-text">
|
||||
Нет аккаунта?
|
||||
Нет аккаунта?
|
||||
<NuxtLink to="/auth/register" class="auth-link">
|
||||
Зарегистрируйтесь
|
||||
</NuxtLink>
|
||||
@@ -55,16 +52,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const form = ref({
|
||||
email: '',
|
||||
password: ''
|
||||
import { useField, useForm } from 'vee-validate'
|
||||
import { loginSchema } from '~/schemas/auth'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
// В демо-режиме просто переходим в профиль
|
||||
alert('Демо-режим: вход выполнен')
|
||||
navigateTo('/profile')
|
||||
}
|
||||
const auth = useAuth()
|
||||
const { handleSubmit, errors, isSubmitting } = useForm({
|
||||
validationSchema: loginSchema
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
@@ -74,7 +93,7 @@ const handleSubmit = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
@@ -86,9 +105,29 @@ const handleSubmit = () => {
|
||||
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 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -98,20 +137,69 @@ const handleSubmit = () => {
|
||||
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 {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--primary-600);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
@@ -121,5 +209,48 @@ const handleSubmit = () => {
|
||||
.home-link {
|
||||
display: inline-block;
|
||||
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>
|
||||
@@ -8,41 +8,63 @@
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||
<!-- Имя -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Имя</label>
|
||||
<input v-model="form.first_name" type="text" class="form-input" placeholder="Ваше имя" required>
|
||||
</div>
|
||||
<form @submit="onSubmit" class="auth-form">
|
||||
<div class="form-row">
|
||||
<!-- Имя -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Имя</label>
|
||||
<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">
|
||||
<label class="form-label">Фамилия</label>
|
||||
<input v-model="form.last_name" type="text" class="form-input" placeholder="Ваша фамилия" required>
|
||||
<!-- Фамилия -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Фамилия</label>
|
||||
<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>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input v-model="form.password" type="password" class="form-input"
|
||||
placeholder="Придумайте пароль (минимум 6 символов)" required minlength="6">
|
||||
<div class="form-row">
|
||||
<!-- Пароль -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Пароль</label>
|
||||
<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 class="form-group">
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
@@ -66,83 +88,44 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const loading = ref(false)
|
||||
import { useField, useForm } from 'vee-validate'
|
||||
import { registerSchema } from '~/schemas/auth'
|
||||
|
||||
// Форма регистрации — теперь с отдельными полями имени и фамилии
|
||||
const form = ref({
|
||||
full_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Валидация паролей
|
||||
if (form.value.password !== form.value.passwordConfirm) {
|
||||
alert('Пароли не совпадают')
|
||||
return
|
||||
}
|
||||
const auth = useAuth()
|
||||
const { handleSubmit, errors, isSubmitting } = useForm({
|
||||
validationSchema: registerSchema
|
||||
})
|
||||
|
||||
// Проверка минимальной длины пароля
|
||||
if (form.value.password.length < 6) {
|
||||
alert('Пароль должен содержать минимум 6 символов')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const { value: firstName } = useField('first_name')
|
||||
const { value: lastName } = useField('last_name')
|
||||
const { value: email } = useField('email')
|
||||
const { value: password } = useField('password')
|
||||
const { value: passwordConfirm } = useField('passwordConfirm')
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
try {
|
||||
// Формируем full_name из имени и фамилии
|
||||
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)
|
||||
|
||||
|
||||
// Успешная регистрация
|
||||
await auth.register(values)
|
||||
alert('Регистрация выполнена успешно!')
|
||||
await navigateTo('/auth/login')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error('Registration error:', error)
|
||||
|
||||
// Переход на страницу входа
|
||||
navigateTo('/auth/login')
|
||||
|
||||
} catch (error: unknown) {
|
||||
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}`)
|
||||
if (error.status === 409) {
|
||||
alert('Пользователь с таким email уже существует')
|
||||
} else if (error.status === 422) {
|
||||
alert('Неверные данные для регистрации')
|
||||
} else {
|
||||
alert('Неизвестная ошибка при регистрации. Попробуйте позже.')
|
||||
alert('Произошла ошибка при регистрации. Попробуйте позже.')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
@@ -150,21 +133,41 @@ const handleSubmit = async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
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 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -174,9 +177,57 @@ const handleSubmit = async () => {
|
||||
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 {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-button:disabled {
|
||||
@@ -187,12 +238,14 @@ const handleSubmit = async () => {
|
||||
.auth-footer-text {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--primary-600);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
@@ -202,30 +255,53 @@ const handleSubmit = async () => {
|
||||
.home-link {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@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;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
/* Для очень маленьких экранов */
|
||||
@media (max-width: 380px) {
|
||||
.auth-page {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.card-header {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
.card-body {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
// plugins/auth.client.ts
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const auth = useAuth()
|
||||
await auth.checkAuth()
|
||||
})
|
||||
@@ -195,7 +195,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Страница профиля доступна всем в демо-режиме
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
requiresAuth: true
|
||||
})
|
||||
|
||||
const auth = useAuth()
|
||||
</script>
|
||||
|
||||
<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
@@ -18,8 +18,10 @@
|
||||
"pinia": "^3.0.3",
|
||||
"sharp": "^0.34.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.3",
|
||||
"yup": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/gateway": {
|
||||
@@ -5865,12 +5867,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
|
||||
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
|
||||
"version": "7.7.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.8.tgz",
|
||||
"integrity": "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.7",
|
||||
"@vue/devtools-shared": "^7.7.8",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
@@ -5886,9 +5888,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
|
||||
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
|
||||
"version": "7.7.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.8.tgz",
|
||||
"integrity": "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
@@ -12611,6 +12613,12 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz",
|
||||
@@ -13984,6 +13992,12 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||
@@ -14039,6 +14053,12 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"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": {
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
@@ -15591,6 +15645,30 @@
|
||||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"pinia": "^3.0.3",
|
||||
"sharp": "^0.34.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.3",
|
||||
"yup": "^1.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user