Appearance
Auth Feature Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Implement full authentication flow — login, registration, email verification, password reset — with middleware, plugin, and Nuxt UI forms.
Architecture: Session-based auth with PHP backend. CSRF tokens via cookie. Auth state in Pinia store hydrated by plugin on app init. Middleware guards routes. Pages use Nuxt UI forms with Zod validation. Errors mapped via useApiError composable.
Tech Stack: Nuxt 4, Vue 3.5, Pinia, Nuxt UI v3, Zod, @nuxtjs/i18n
Task 1: Update User Entity Schema
Files:
- Modify:
app/entities/user/model/user.schema.ts
Step 1: Update user schema to match backend API
Replace the entire file content:
ts
import { z } from 'zod'
export const accountTypeSchema = z.enum(['personal', 'business'])
export const userSchema = z.object({
id: z.number().int().positive(),
email: z.string().email(),
display_name: z.string(),
phone: z.string().nullable(),
account_type: accountTypeSchema,
email_verified: z.boolean(),
avatar_url: z.string().nullable(),
city_id: z.number().nullable(),
district_id: z.number().nullable(),
metro_station_id: z.number().nullable(),
rating: z.string(),
reviews_count: z.number().int(),
products_count: z.number().int(),
is_active: z.boolean(),
is_admin: z.boolean(),
created_at: z.string(),
})
export const businessProfileSchema = z.object({
id: z.number().int().positive(),
company_name: z.string(),
inn: z.string().nullable(),
address: z.string().nullable(),
website: z.string().nullable(),
working_hours: z.string().nullable(),
is_verified: z.boolean(),
})
export type User = z.infer<typeof userSchema>
export type AccountType = z.infer<typeof accountTypeSchema>
export type BusinessProfile = z.infer<typeof businessProfileSchema>Key changes from old schema:
email_verified_at: z.string().nullable()→email_verified: z.boolean()phone_verified_atremoved (not in backend API response)display_namechanged fromnullable()to requiredstring()- Added:
city_id,district_id,metro_station_id,is_active,is_admin ratingchanged fromz.number()toz.string()(backend returns"0.00")
Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/entities/user/model/user.schema.ts Expected: No errors
Step 3: Commit
bash
git add app/entities/user/model/user.schema.ts
git commit -m "fix(entities): update user schema to match backend API response"Task 2: Update Auth Store
Files:
- Modify:
app/stores/auth.ts
Step 1: Rewrite auth store with actions
Replace entire file:
ts
import { defineStore } from 'pinia'
import type { User } from '~/entities/user/model/user.schema'
import type { ApiItemResponse } from '~/shared/api/types'
import { useApiClient } from '~/shared/api/client'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
loading: false,
}),
getters: {
isAuthenticated: (state) => state.user !== null,
isEmailVerified: (state) => state.user?.email_verified ?? false,
isAdmin: (state) => state.user?.is_admin ?? false,
},
actions: {
setUser(user: User | null) {
this.user = user
},
clearUser() {
this.user = null
},
async fetchUser() {
const api = useApiClient()
try {
const response = await api.get<ApiItemResponse<User>>('/auth/me')
this.user = response.data
}
catch {
this.user = null
}
},
async login(email: string, password: string) {
const api = useApiClient()
const response = await api.post<ApiItemResponse<User>>('/auth/login', { email, password })
this.user = response.data
},
async register(email: string, password: string, displayName: string) {
const api = useApiClient()
const response = await api.post<ApiItemResponse<User>>('/auth/register', {
email,
password,
display_name: displayName,
})
this.user = response.data
},
async logout() {
const api = useApiClient()
await api.post('/auth/logout')
this.user = null
},
},
})Note: Store uses useApiClient() directly (not useApi()) — store lives in app layer and doesn't need the redirect-on-401 wrapper (it handles auth itself).
Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/stores/auth.ts Expected: No errors
Step 3: Commit
bash
git add app/stores/auth.ts
git commit -m "feat(auth): add login, register, logout, fetchUser actions to auth store"Task 3: Create Auth Validation Schemas
Files:
- Create:
app/shared/schemas/auth.ts
Step 1: Create Zod schemas
ts
import { z } from 'zod'
const passwordSchema = z
.string()
.min(8, 'Минимум 8 символов')
.max(128, 'Максимум 128 символов')
.regex(/[A-Z]/, 'Минимум одна заглавная буква')
.regex(/[a-z]/, 'Минимум одна строчная буква')
.regex(/[0-9]/, 'Минимум одна цифра')
.regex(/[^a-zA-Z0-9]/, 'Минимум один спецсимвол')
.refine(
(val) => !/(.)\1{2,}/.test(val),
'Не допускается 3+ одинаковых символа подряд',
)
export const loginSchema = z.object({
email: z.string().email('Некорректный email'),
password: z.string().min(1, 'Введите пароль'),
})
export const registerSchema = z
.object({
email: z.string().email('Некорректный email'),
password: passwordSchema,
password_confirmation: z.string().min(1, 'Повторите пароль'),
display_name: z
.string()
.min(1, 'Введите имя')
.max(100, 'Максимум 100 символов'),
})
.refine((data) => data.password === data.password_confirmation, {
message: 'Пароли не совпадают',
path: ['password_confirmation'],
})
export const forgotPasswordSchema = z.object({
email: z.string().email('Некорректный email'),
})
export const resetPasswordSchema = z.object({
token: z.string().min(1),
password: passwordSchema,
})
export const verifyEmailSchema = z.object({
code: z
.string()
.length(6, 'Код должен содержать 6 цифр')
.regex(/^\d+$/, 'Только цифры'),
})
export type LoginForm = z.infer<typeof loginSchema>
export type RegisterForm = z.infer<typeof registerSchema>
export type ForgotPasswordForm = z.infer<typeof forgotPasswordSchema>
export type ResetPasswordForm = z.infer<typeof resetPasswordSchema>
export type VerifyEmailForm = z.infer<typeof verifyEmailSchema>Step 2: Add shared/schemas to auto-imports in nuxt.config.ts
In nuxt.config.ts, add 'shared/schemas' to the imports.dirs array:
ts
imports: {
dirs: [
'shared/ui',
'shared/api',
'shared/schemas', // ← ADD THIS LINE
'shared/lib',
'shared/config',
// ... rest stays the same
],
},Step 3: Verify lint passes
Run: npm run lint -- --no-fix app/shared/schemas/auth.ts Expected: No errors
Step 4: Commit
bash
git add app/shared/schemas/auth.ts nuxt.config.ts
git commit -m "feat(auth): add Zod validation schemas for auth forms"Task 4: Create useApiError Composable
Files:
- Create:
app/features/auth/composables/useApiError.ts - Remove:
app/features/auth/api/.gitkeep,app/features/auth/model/.gitkeep,app/features/auth/ui/.gitkeep(replace with actual code)
Step 1: Create composable
ts
import type { ApiError } from '~/shared/api/types'
interface FetchErrorLike {
response?: { status?: number; headers?: { get(name: string): string | null } }
data?: ApiError
}
export function useApiError() {
const globalError = ref<string | null>(null)
const fieldErrors = ref<Record<string, string>>({})
function handleError(err: unknown) {
globalError.value = null
fieldErrors.value = {}
const fetchError = err as FetchErrorLike
const apiError = fetchError.data?.error
if (!apiError) {
globalError.value = 'Ошибка соединения. Попробуйте ещё раз.'
return
}
if (fetchError.response?.status === 422 && apiError.details) {
for (const [field, messages] of Object.entries(apiError.details)) {
fieldErrors.value[field] = messages[0]
}
return
}
if (fetchError.response?.status === 429) {
const retryAfter = fetchError.response.headers?.get('Retry-After')
const seconds = retryAfter ? parseInt(retryAfter) : 60
globalError.value = `Слишком много попыток. Попробуйте через ${Math.ceil(seconds / 60)} мин.`
return
}
globalError.value = apiError.message
}
function clearErrors() {
globalError.value = null
fieldErrors.value = {}
}
return { globalError, fieldErrors, handleError, clearErrors }
}Step 2: Add features/*/composables to auto-imports in nuxt.config.ts
In nuxt.config.ts, add 'features/*/composables' to the imports.dirs array:
ts
imports: {
dirs: [
'shared/ui',
'shared/api',
'shared/schemas',
'shared/lib',
'shared/config',
'entities/*/ui',
'entities/*/model',
'entities/*/api',
'features/*/ui',
'features/*/model',
'features/*/api',
'features/*/composables', // ← ADD THIS LINE
'widgets/*/ui',
],
},Step 3: Clean up .gitkeep files
Delete:
app/features/auth/api/.gitkeepapp/features/auth/model/.gitkeepapp/features/auth/ui/.gitkeep
Step 4: Verify lint passes
Run: npm run lint -- --no-fix app/features/auth/composables/useApiError.ts Expected: No errors
Step 5: Commit
bash
git add app/features/auth/composables/useApiError.ts nuxt.config.ts
git rm app/features/auth/api/.gitkeep app/features/auth/model/.gitkeep app/features/auth/ui/.gitkeep
git commit -m "feat(auth): add useApiError composable for form error handling"Task 5: Create Auth Middleware
Files:
- Create:
app/middleware/auth.ts - Create:
app/middleware/guest.ts
Step 1: Create auth middleware
ts
export default defineNuxtRouteMiddleware(async () => {
const authStore = useAuthStore()
if (!authStore.isAuthenticated) {
await authStore.fetchUser()
}
if (!authStore.isAuthenticated) {
return navigateTo('/auth/login')
}
})Step 2: Create guest middleware
ts
export default defineNuxtRouteMiddleware(async () => {
const authStore = useAuthStore()
if (!authStore.isAuthenticated) {
await authStore.fetchUser()
}
if (authStore.isAuthenticated) {
return navigateTo('/cabinet')
}
})Step 3: Verify lint passes
Run: npm run lint -- --no-fix app/middleware/ Expected: No errors
Step 4: Commit
bash
git add app/middleware/auth.ts app/middleware/guest.ts
git commit -m "feat(auth): add auth and guest route middleware"Task 6: Create Auth Plugin
Files:
- Create:
app/plugins/auth.ts
Step 1: Create plugin
ts
export default defineNuxtPlugin(async () => {
const authStore = useAuthStore()
if (import.meta.server || !authStore.isAuthenticated) {
await authStore.fetchUser()
}
})Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/plugins/auth.ts Expected: No errors
Step 3: Commit
bash
git add app/plugins/auth.ts
git commit -m "feat(auth): add auth plugin to hydrate user on app init"Task 7: Update i18n Locale
Files:
- Modify:
i18n/locales/ru.json
Step 1: Add auth-related i18n keys
Add/update the auth section:
json
{
"common": {
"appName": "Partizap",
"search": "Найти запчасть",
"login": "Войти",
"register": "Регистрация",
"logout": "Выйти",
"favorites": "Избранное",
"addListing": "Продать запчасть",
"allRegions": "Все регионы",
"loadMore": "Загрузить ещё",
"save": "Сохранить",
"cancel": "Отмена",
"delete": "Удалить",
"edit": "Редактировать",
"back": "Назад",
"close": "Закрыть",
"or": "или",
"comingSoon": "Скоро"
},
"auth": {
"email": "Email",
"emailPlaceholder": "email@example.com",
"password": "Пароль",
"passwordPlaceholder": "Минимум 8 символов",
"confirmPassword": "Повторите пароль",
"displayName": "Имя",
"displayNamePlaceholder": "Как вас зовут",
"phone": "Мобильный телефон",
"phonePlaceholder": "+7 (___) ___-__-__",
"rememberMe": "Запомнить меня",
"forgotPassword": "Забыли пароль?",
"noAccount": "Нет аккаунта?",
"hasAccount": "Уже есть аккаунт?",
"loginAction": "Войти",
"registerAction": "Зарегистрироваться",
"loginViaSms": "Войти по SMS",
"loginTitle": "Вход",
"registerTitle": "Регистрация",
"verifyEmailTitle": "Подтверждение Email",
"verifyEmailDescription": "Введите 6-значный код, отправленный на",
"verifyAction": "Подтвердить",
"resendCode": "Отправить повторно",
"resendIn": "Отправить через {seconds}с",
"forgotPasswordTitle": "Восстановление пароля",
"forgotPasswordAction": "Отправить ссылку",
"forgotPasswordSent": "Если этот email зарегистрирован, ссылка для сброса отправлена.",
"resetPasswordTitle": "Новый пароль",
"resetPasswordAction": "Сбросить пароль",
"resetPasswordSuccess": "Пароль успешно изменён. Теперь вы можете войти.",
"backToLogin": "Вернуться ко входу",
"verificationCode": "Код подтверждения",
"newPassword": "Новый пароль",
"passwordHint": "Заглавная, строчная, цифра, спецсимвол"
},
"product": {
"price": "Цена",
"condition": "Состояние",
"showPhone": "Показать телефон",
"addToFavorites": "В избранное",
"removeFromFavorites": "Убрать из избранного",
"views": "Просмотры",
"published": "Опубликовано"
},
"catalog": {
"filters": "Фильтры",
"sort": "Сортировка",
"sortByDate": "По дате",
"sortByPriceAsc": "Сначала дешёвые",
"sortByPriceDesc": "Сначала дорогие",
"resetFilters": "Сбросить фильтры",
"noResults": "Ничего не найдено"
},
"ymm": {
"make": "Марка",
"model": "Модель",
"generation": "Поколение",
"year": "Год",
"selectMake": "Выберите марку",
"selectModel": "Выберите модель",
"selectGeneration": "Выберите поколение"
}
}Step 2: Commit
bash
git add i18n/locales/ru.json
git commit -m "feat(i18n): add auth-related Russian locale keys"Task 8: Update Default Layout (auth-aware header)
Files:
- Modify:
app/layouts/default.vue
Step 1: Make header show user state
html
<script setup lang="ts">
const authStore = useAuthStore()
const { t } = useI18n()
async function logout() {
await authStore.logout()
await navigateTo('/auth/login')
}
</script>
<template>
<div class="min-h-screen flex flex-col">
<header class="border-b border-gray-200 dark:border-gray-800">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<NuxtLink to="/">
<SharedAppLogo />
</NuxtLink>
<nav class="flex items-center gap-4">
<UButton :label="t('common.addListing')" to="/cabinet/products/new" variant="solid" />
<template v-if="authStore.isAuthenticated">
<UButton :label="t('common.favorites')" to="/cabinet/favorites" variant="ghost" />
<UButton variant="ghost" to="/cabinet">
{{ authStore.user?.display_name }}
</UButton>
<UButton :label="t('common.logout')" variant="ghost" @click="logout" />
</template>
<UButton v-else :label="t('common.login')" to="/auth/login" variant="ghost" />
</nav>
</div>
</header>
<main class="flex-1">
<slot />
</main>
<footer class="border-t border-gray-200 dark:border-gray-800 py-8">
<div class="container mx-auto px-4 text-center text-sm text-gray-500">
© {{ new Date().getFullYear() }} Partizap
</div>
</footer>
</div>
</template>Step 2: Update useApi composable redirect path
In app/composables/useApi.ts, change redirect from /auth to /auth/login:
ts
onUnauthorized: async () => {
authStore.clear()
await navigateTo('/auth/login')
},Also rename authStore.clear() → authStore.clearUser() to match new store API.
Step 3: Verify lint passes
Run: npm run lint -- --no-fix app/layouts/default.vue app/composables/useApi.ts Expected: No errors
Step 4: Commit
bash
git add app/layouts/default.vue app/composables/useApi.ts
git commit -m "feat(layout): add auth-aware header with user menu and logout"Task 9: Create Login Page
Files:
- Remove:
app/pages/auth/index.vue - Create:
app/pages/auth/login.vue
Step 1: Delete old stub and create login page
Delete app/pages/auth/index.vue.
Create app/pages/auth/login.vue:
html
<script setup lang="ts">
import { loginSchema } from '~/shared/schemas/auth'
definePageMeta({
middleware: 'guest',
layout: 'default',
})
useSeoMeta({
title: 'Вход | Partizap',
robots: 'noindex, nofollow',
})
const { t } = useI18n()
const authStore = useAuthStore()
const { globalError, handleError, clearErrors } = useApiError()
const loading = ref(false)
const state = reactive({
email: '',
password: '',
})
async function onSubmit() {
clearErrors()
loading.value = true
try {
await authStore.login(state.email, state.password)
await navigateTo('/cabinet')
}
catch (err) {
handleError(err)
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-sm">
<h1 class="text-2xl font-bold mb-6">
{{ t('auth.loginTitle') }}
</h1>
<UAlert
v-if="globalError"
color="error"
:title="globalError"
class="mb-4"
/>
<UForm :schema="loginSchema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField :label="t('auth.email')" name="email">
<UInput v-model="state.email" type="email" :placeholder="t('auth.emailPlaceholder')" />
</UFormField>
<UFormField :label="t('auth.password')" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<div class="flex items-center justify-between">
<UCheckbox :label="t('auth.rememberMe')" disabled />
<NuxtLink to="/auth/forgot-password" class="text-sm text-primary">
{{ t('auth.forgotPassword') }}
</NuxtLink>
</div>
<UButton type="submit" block :loading="loading">
{{ t('auth.loginAction') }}
</UButton>
</UForm>
<div class="my-6 flex items-center gap-3">
<div class="flex-1 border-t border-gray-200 dark:border-gray-700" />
<span class="text-sm text-gray-500">{{ t('common.or') }}</span>
<div class="flex-1 border-t border-gray-200 dark:border-gray-700" />
</div>
<UTooltip :text="t('common.comingSoon')">
<UButton variant="outline" block disabled>
{{ t('auth.loginViaSms') }}
</UButton>
</UTooltip>
<p class="mt-6 text-sm text-center">
{{ t('auth.noAccount') }}
<NuxtLink to="/auth/register" class="text-primary">
{{ t('common.register') }}
</NuxtLink>
</p>
</div>
</template>Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/pages/auth/login.vue Expected: No errors
Step 3: Commit
bash
git rm app/pages/auth/index.vue
git add app/pages/auth/login.vue
git commit -m "feat(auth): add login page with email/password form"Task 10: Create Registration Page
Files:
- Create:
app/pages/auth/register.vue
Step 1: Create registration page
html
<script setup lang="ts">
import { registerSchema } from '~/shared/schemas/auth'
definePageMeta({
middleware: 'guest',
layout: 'default',
})
useSeoMeta({
title: 'Регистрация | Partizap',
robots: 'noindex, nofollow',
})
const { t } = useI18n()
const authStore = useAuthStore()
const { globalError, handleError, clearErrors } = useApiError()
const loading = ref(false)
const state = reactive({
display_name: '',
email: '',
password: '',
password_confirmation: '',
})
async function onSubmit() {
clearErrors()
loading.value = true
try {
await authStore.register(state.email, state.password, state.display_name)
await navigateTo('/auth/verify-email')
}
catch (err) {
handleError(err)
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-sm">
<h1 class="text-2xl font-bold mb-6">
{{ t('auth.registerTitle') }}
</h1>
<UAlert
v-if="globalError"
color="error"
:title="globalError"
class="mb-4"
/>
<UForm :schema="registerSchema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField :label="t('auth.displayName')" name="display_name">
<UInput v-model="state.display_name" :placeholder="t('auth.displayNamePlaceholder')" />
</UFormField>
<UTooltip :text="t('common.comingSoon')">
<UFormField :label="t('auth.phone')" name="phone">
<UInput :placeholder="t('auth.phonePlaceholder')" disabled />
</UFormField>
</UTooltip>
<UFormField :label="t('auth.email')" name="email">
<UInput v-model="state.email" type="email" :placeholder="t('auth.emailPlaceholder')" />
</UFormField>
<UFormField :label="t('auth.password')" name="password" :hint="t('auth.passwordHint')">
<UInput v-model="state.password" type="password" :placeholder="t('auth.passwordPlaceholder')" />
</UFormField>
<UFormField :label="t('auth.confirmPassword')" name="password_confirmation">
<UInput v-model="state.password_confirmation" type="password" />
</UFormField>
<UButton type="submit" block :loading="loading">
{{ t('auth.registerAction') }}
</UButton>
</UForm>
<p class="mt-6 text-sm text-center">
{{ t('auth.hasAccount') }}
<NuxtLink to="/auth/login" class="text-primary">
{{ t('common.login') }}
</NuxtLink>
</p>
</div>
</template>Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/pages/auth/register.vue Expected: No errors
Step 3: Commit
bash
git add app/pages/auth/register.vue
git commit -m "feat(auth): add registration page with password confirmation"Task 11: Create Email Verification Page
Files:
- Create:
app/pages/auth/verify-email.vue
Step 1: Create verify-email page
html
<script setup lang="ts">
import { verifyEmailSchema } from '~/shared/schemas/auth'
definePageMeta({
middleware: 'auth',
layout: 'default',
})
useSeoMeta({
title: 'Подтверждение Email | Partizap',
robots: 'noindex, nofollow',
})
const { t } = useI18n()
const authStore = useAuthStore()
const api = useApiClient()
const { globalError, handleError, clearErrors } = useApiError()
const loading = ref(false)
const resending = ref(false)
const resendCooldown = ref(0)
let cooldownInterval: ReturnType<typeof setInterval>
const state = reactive({
code: '',
})
if (authStore.isEmailVerified) {
navigateTo('/cabinet')
}
async function onSubmit() {
clearErrors()
loading.value = true
try {
await api.post('/auth/verify-email', { code: state.code })
await authStore.fetchUser()
await navigateTo('/cabinet')
}
catch (err) {
handleError(err)
}
finally {
loading.value = false
}
}
async function resendCode() {
clearErrors()
resending.value = true
try {
await api.post('/auth/resend-verification')
resendCooldown.value = 60
cooldownInterval = setInterval(() => {
resendCooldown.value--
if (resendCooldown.value <= 0) clearInterval(cooldownInterval)
}, 1000)
}
catch (err) {
handleError(err)
}
finally {
resending.value = false
}
}
onUnmounted(() => clearInterval(cooldownInterval))
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-sm">
<h1 class="text-2xl font-bold mb-2">
{{ t('auth.verifyEmailTitle') }}
</h1>
<p class="text-gray-500 mb-6">
{{ t('auth.verifyEmailDescription') }}
<strong>{{ authStore.user?.email }}</strong>
</p>
<UAlert
v-if="globalError"
color="error"
:title="globalError"
class="mb-4"
/>
<UForm :schema="verifyEmailSchema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField :label="t('auth.verificationCode')" name="code">
<UInput
v-model="state.code"
placeholder="123456"
maxlength="6"
inputmode="numeric"
/>
</UFormField>
<UButton type="submit" block :loading="loading">
{{ t('auth.verifyAction') }}
</UButton>
</UForm>
<div class="mt-4 text-center">
<UButton
variant="ghost"
:disabled="resendCooldown > 0"
:loading="resending"
@click="resendCode"
>
{{ resendCooldown > 0 ? t('auth.resendIn', { seconds: resendCooldown }) : t('auth.resendCode') }}
</UButton>
</div>
</div>
</template>Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/pages/auth/verify-email.vue Expected: No errors
Step 3: Commit
bash
git add app/pages/auth/verify-email.vue
git commit -m "feat(auth): add email verification page with resend cooldown"Task 12: Create Forgot Password Page
Files:
- Create:
app/pages/auth/forgot-password.vue
Step 1: Create forgot-password page
html
<script setup lang="ts">
import { forgotPasswordSchema } from '~/shared/schemas/auth'
definePageMeta({
middleware: 'guest',
layout: 'default',
})
useSeoMeta({
title: 'Восстановление пароля | Partizap',
robots: 'noindex, nofollow',
})
const { t } = useI18n()
const api = useApiClient()
const { globalError, handleError, clearErrors } = useApiError()
const loading = ref(false)
const sent = ref(false)
const state = reactive({
email: '',
})
async function onSubmit() {
clearErrors()
loading.value = true
try {
await api.post('/auth/forgot-password', { email: state.email })
sent.value = true
}
catch (err) {
handleError(err)
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-sm">
<h1 class="text-2xl font-bold mb-6">
{{ t('auth.forgotPasswordTitle') }}
</h1>
<template v-if="sent">
<UAlert
color="success"
:title="t('auth.forgotPasswordSent')"
class="mb-4"
/>
<NuxtLink to="/auth/login" class="text-primary text-sm">
{{ t('auth.backToLogin') }}
</NuxtLink>
</template>
<template v-else>
<UAlert
v-if="globalError"
color="error"
:title="globalError"
class="mb-4"
/>
<UForm :schema="forgotPasswordSchema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField :label="t('auth.email')" name="email">
<UInput v-model="state.email" type="email" :placeholder="t('auth.emailPlaceholder')" />
</UFormField>
<UButton type="submit" block :loading="loading">
{{ t('auth.forgotPasswordAction') }}
</UButton>
</UForm>
</template>
</div>
</template>Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/pages/auth/forgot-password.vue Expected: No errors
Step 3: Commit
bash
git add app/pages/auth/forgot-password.vue
git commit -m "feat(auth): add forgot password page"Task 13: Create Reset Password Page
Files:
- Create:
app/pages/auth/reset-password.vue
Step 1: Create reset-password page
html
<script setup lang="ts">
import { resetPasswordSchema } from '~/shared/schemas/auth'
definePageMeta({
middleware: 'guest',
layout: 'default',
})
useSeoMeta({
title: 'Новый пароль | Partizap',
robots: 'noindex, nofollow',
})
const { t } = useI18n()
const route = useRoute()
const api = useApiClient()
const { globalError, handleError, clearErrors } = useApiError()
const loading = ref(false)
const success = ref(false)
const token = route.query.token as string
if (!token) {
navigateTo('/auth/forgot-password')
}
const state = reactive({
token,
password: '',
})
async function onSubmit() {
clearErrors()
loading.value = true
try {
await api.post('/auth/reset-password', {
token: state.token,
password: state.password,
})
success.value = true
}
catch (err) {
handleError(err)
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="container mx-auto px-4 py-16 max-w-sm">
<h1 class="text-2xl font-bold mb-6">
{{ t('auth.resetPasswordTitle') }}
</h1>
<template v-if="success">
<UAlert
color="success"
:title="t('auth.resetPasswordSuccess')"
class="mb-4"
/>
<NuxtLink to="/auth/login" class="text-primary text-sm">
{{ t('auth.backToLogin') }}
</NuxtLink>
</template>
<template v-else>
<UAlert
v-if="globalError"
color="error"
:title="globalError"
class="mb-4"
/>
<UForm :schema="resetPasswordSchema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField
:label="t('auth.newPassword')"
name="password"
:hint="t('auth.passwordHint')"
>
<UInput v-model="state.password" type="password" />
</UFormField>
<UButton type="submit" block :loading="loading">
{{ t('auth.resetPasswordAction') }}
</UButton>
</UForm>
</template>
</div>
</template>Step 2: Verify lint passes
Run: npm run lint -- --no-fix app/pages/auth/reset-password.vue Expected: No errors
Step 3: Commit
bash
git add app/pages/auth/reset-password.vue
git commit -m "feat(auth): add reset password page"Task 14: Update Routing and Final Cleanup
Files:
- Modify:
app/layouts/default.vue(already done in Task 8) - Modify:
nuxt.config.ts— updateapiBasefor dev server
Step 1: Update nuxt.config.ts apiBase
Change apiBase default to /api (same-origin proxy):
ts
runtimeConfig: {
public: {
apiBase: '/api',
},
},For local dev without nginx, set NUXT_PUBLIC_API_BASE=http://localhost:8000 in .env.
Step 2: Create .env file for local development
Create .env:
NUXT_PUBLIC_API_BASE=http://localhost:8000Step 3: Verify .env is in .gitignore
Check .gitignore contains .env. If not, add it.
Step 4: Full lint check
Run: npm run lint Expected: No errors
Step 5: Commit
bash
git add nuxt.config.ts .env
git commit -m "chore: update apiBase to /api, add .env for local dev"Task 15: Update CLAUDE.md and Verify
Files:
- Modify:
CLAUDE.md
Step 1: Update CLAUDE.md progress and structure
Update the Project Structure section to reflect new files. Update the Routing table. Mark auth task as complete in Progress.
Step 2: Run full verification
Run: npm run lint && npm run typecheck Expected: No errors
Step 3: Final commit
bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with auth feature documentation"