Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/pages/profile/ui/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ProfilePageSkeleton } from './ProfilePage.skeleton';
import { ProfileIdentityCard } from './ProfileIdentityCard';
import { ProfileSecurityCard } from './ProfileSecurityCard';
import { ProfileNotificationsCard } from './ProfileNotificationsCard';
import { SignOut } from './SignOut';

interface ProfilePageProps {
className?: string;
Expand Down Expand Up @@ -38,6 +39,7 @@ function ProfilePage({ className }: ProfilePageProps) {
}
return (
<div className={cn('mx-auto w-full max-w-5xl space-y-6 pb-6', className)}>
<SignOut />
<ProfileIdentityCard />

<div className="grid items-start gap-4 lg:grid-cols-2">
Expand Down
39 changes: 39 additions & 0 deletions src/pages/profile/ui/SignOut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { LogOut } from 'lucide-react';
import { ComponentProps } from 'react';
import { Button } from 'shared/ui';
import { AccessToken, signout } from 'shared/api';
import { routes } from 'shared/config';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { currentUserQueryKey } from '../model/queries/use-current-user';

function SignOut(props: Omit<ComponentProps<typeof Button>, 'children'>) {
const router = useRouter();
const queryClient = useQueryClient();

const signoutMutation = useMutation({
mutationFn: signout,
onSuccess: (response) => {
AccessToken.clear();
queryClient.removeQueries({ queryKey: currentUserQueryKey });
router.replace(routes.auth.signin());
toast.success(response.message || 'Вы вышли из аккаунта');
},
});

return (
<Button
className="text-destructive"
variant="link"
onClick={() => signoutMutation.mutate()}
disabled={signoutMutation.isPending}
{...props}
>
Выйти
<LogOut className="size-4" />
</Button>
);
}

export { SignOut };
13 changes: 13 additions & 0 deletions src/pages/signin/model/schemas/sign-in-form-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod';

const MIN_PASS_LENGTH = 8;
const MAX_PASS_LENGTH = 32;

export const SigninFormSchema = z.object({
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`)
.max(MAX_PASS_LENGTH, 'Слишком длинный пароль'),
});
11 changes: 8 additions & 3 deletions src/pages/signin/ui/SigninForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Controller, type FieldPath, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { SigninBody, SigninFormSchema, SigninResponse } from '../model/schemas/login-schema';
import { SigninFormSchema } from '../model/schemas/sign-in-form-schema';
import {
Button,
Card,
Expand All @@ -23,8 +23,13 @@ import { cn } from 'shared/lib/utils';
import { routes } from 'shared/config';
import * as z from 'zod';
import { useMutation } from '@tanstack/react-query';
import { signin } from '../model/services/signin';
import { extractValidationIssues, ValidationIssue } from 'shared/api';
import {
extractValidationIssues,
signin,
SigninBody,
SigninResponse,
ValidationIssue,
} from 'shared/api';

type FSchema = z.infer<typeof SigninFormSchema>;
type BSchema = z.infer<typeof SigninBody>;
Expand Down
5 changes: 5 additions & 0 deletions src/pages/signup/model/schemas/confirm-form-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from 'zod';

export const ConfirmFormSchema = z.object({
code: z.string().min(6, 'Обязательное поле'),
});
25 changes: 25 additions & 0 deletions src/pages/signup/model/schemas/signup-form-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from 'zod';

const MIN_PASS_LENGTH = 8;
const MAX_PASS_LENGTH = 32;

export const SignupFormSchema = z
.object({
name: z
.string()
.trim()
.min(1, 'Обязательное поле')
.min(2, 'Слишком короткое имя')
.max(100, 'Слишком длинное имя'),
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`)
.max(MAX_PASS_LENGTH, 'Слишком длинный пароль'),
confirmPassword: z.string().min(1, 'Обязательное поле'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'],
});
21 changes: 0 additions & 21 deletions src/pages/signup/model/services/confirm.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/pages/signup/model/utils/field-name-mapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FieldPath } from 'react-hook-form';
import { SignupBody, SignupFormSchema } from '../schemas/signup-schema';
import { SignupBody } from 'shared/api';
import { SignupFormSchema } from '../schemas/signup-form-schema';
import { z } from 'zod';

export const fieldNameMapper = (
Expand Down
20 changes: 13 additions & 7 deletions src/pages/signup/ui/OTPForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@ import {
InputOTPSlot,
Spinner,
} from 'shared/ui';
import { ConfirmBody, ConfirmFormSchema, ConfirmResponse } from '../model/schemas/confirm-schema';
import { ConfirmFormSchema } from '../model/schemas/confirm-form-schema';
import { cn } from 'shared/lib/utils';
import { z } from 'zod';
import { useMutation } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { GlobalErrorResponseType, isAxiosValidationError } from 'shared/api';
import { confirm } from '../model/services/confirm';
import {
GlobalErrorResponseType,
isAxiosValidationError,
signupConfirm,
SignupConfirmBody,
SignupConfirmResponse,
} from 'shared/api';
import { REGEXP_ONLY_DIGITS } from 'input-otp';
import { ComponentProps } from 'react';

type FSchema = z.infer<typeof ConfirmFormSchema>;
type BSchema = z.infer<typeof ConfirmBody>;
type RSchema = z.infer<typeof ConfirmResponse>;
type BSchema = z.infer<typeof SignupConfirmBody>;
type RSchema = z.infer<typeof SignupConfirmResponse>;

interface OTPFormProps extends Omit<React.ComponentProps<'form'>, 'children'> {
interface OTPFormProps extends Omit<ComponentProps<'form'>, 'children'> {
email: string;
onSuccess?: (body: BSchema, res: RSchema) => void;
autoFocusCode?: boolean;
Expand All @@ -46,7 +52,7 @@ export function OTPForm({
}: OTPFormProps) {
const sendConfirm = useMutation({
mutationFn: (data: BSchema) => {
return confirm(data);
return signupConfirm(data);
},
meta: {
skipGlobalValidationToast: true,
Expand Down
11 changes: 8 additions & 3 deletions src/pages/signup/ui/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,21 @@ import {
Link,
Spinner,
} from 'shared/ui';
import { SignupBody, SignupFormSchema, SignupResponse } from '../model/schemas/signup-schema';
import { SignupFormSchema } from '../model/schemas/signup-form-schema';
import { cn } from 'shared/lib/utils';
import { routes } from 'shared/config';
import { z } from 'zod';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { signup } from '../model/services/signup';
import { fieldNameMapper } from '../model/utils/field-name-mapper';
import { prepareFullName } from '../model/utils/prepare-fullname';
import { extractValidationIssues, ValidationIssue } from 'shared/api';
import {
extractValidationIssues,
signup,
SignupBody,
SignupResponse,
ValidationIssue,
} from 'shared/api';

type FSchema = z.infer<typeof SignupFormSchema>;
type BSchema = z.infer<typeof SignupBody>;
Expand Down
8 changes: 8 additions & 0 deletions src/shared/api/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { signin } from './services/sign-in';
export { signup } from './services/sign-up';
export { signupConfirm } from './services/sign-up-confirm';
export { signout } from './services/sign-out';
export { SigninBody, SigninResponse } from './schemas/sign-in-schema';
export { SignoutResponse } from './schemas/sign-out-schema';
export { SignupConfirmBody, SignupConfirmResponse } from './schemas/sign-up-confirm-schema';
export { SignupBody, SignupResponse } from './schemas/sign-up-schema';
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { z } from 'zod';

const MIN_PASS_LENGTH = 8;
const MAX_PASS_LENGTH = 32;

export const SigninBody = z
.object({
email: z.email().describe('Email пользователя'),
Expand All @@ -15,12 +12,3 @@ export const SigninResponse = z.object({
token: z.string().describe('Новый access token (JWT)'),
message: z.string().optional().describe('Дополнительное сообщение (опционально)'),
});

export const SigninFormSchema = z.object({
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`)
.max(MAX_PASS_LENGTH, 'Слишком длинный пароль'),
});
6 changes: 6 additions & 0 deletions src/shared/api/auth/schemas/sign-out-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from 'zod';

export const SignoutResponse = z.object({
success: z.boolean().describe('Статус операции'),
message: z.string().optional().describe('Сообщение для пользователя'),
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { z } from 'zod';

export const ConfirmBody = z
export const SignupConfirmBody = z
.object({
email: z.email().describe('Email пользователя, на который был отправлен код'),
code: z.string().min(6).max(6).describe('6-значный OTP код подтверждения'),
})
.describe('Схема верификации OTP кода');

export const ConfirmResponse = z.object({
export const SignupConfirmResponse = z.object({
success: z.boolean().describe('Успешное подтверждение'),
token: z.string().describe('Token (JWT)'),
message: z.string().optional().describe('Дополнительное сообщение (опционально)'),
});

export const ConfirmFormSchema = z.object({
code: z.string().min(6, 'Обязательное поле'),
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,3 @@ export const SignupResponse = z.object({
success: z.boolean().describe('Статус операции'),
message: z.string().optional().describe('Сообщение для пользователя'),
});

export const SignupFormSchema = z
.object({
name: z
.string()
.trim()
.min(1, 'Обязательное поле')
.min(2, 'Слишком короткое имя')
.max(100, 'Слишком длинное имя'),
email: z.string().min(1, 'Обязательное поле').check(z.email('Неверный формат email')),
password: z
.string()
.min(1, 'Обязательное поле')
.min(MIN_PASS_LENGTH, `Минимум ${MIN_PASS_LENGTH} символов`)
.max(MAX_PASS_LENGTH, 'Слишком длинный пароль'),
confirmPassword: z.string().min(1, 'Обязательное поле'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'],
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { api } from 'shared/api';
import { SigninBody, SigninResponse } from '../schemas/login-schema';
import { z } from 'zod';
import { instance } from '../../instance';
import { SigninBody, SigninResponse } from '../schemas/sign-in-schema';

export function signin(data: z.infer<typeof SigninBody>): Promise<z.infer<typeof SigninResponse>> {
return api(
return instance(
{
url: '/auth/sign-in',
method: 'POST',
Expand Down
17 changes: 17 additions & 0 deletions src/shared/api/auth/services/sign-out.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';
import { instance } from '../../instance';
import { SignoutResponse } from '../schemas/sign-out-schema';

export function signout(): Promise<z.infer<typeof SignoutResponse>> {
return instance(
{
url: '/auth/sign-out',
method: 'POST',
},
{
contracts: {
response: SignoutResponse,
},
}
);
}
22 changes: 22 additions & 0 deletions src/shared/api/auth/services/sign-up-confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';
import { SignupConfirmBody, SignupConfirmResponse } from '../schemas/sign-up-confirm-schema';
import { api } from 'shared/api';

export function signupConfirm(
data: z.infer<typeof SignupConfirmBody>
): Promise<z.infer<typeof SignupConfirmResponse>> {
return api(
{
url: '/auth/sign-up/confirm',
method: 'POST',
data: data,
skipAuthRefresh: true,
},
{
contracts: {
body: SignupConfirmBody,
response: SignupConfirmResponse,
},
}
);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { api } from 'shared/api';
import { SignupBody, SignupResponse } from '../schemas/signup-schema';
import { z } from 'zod';
import { instance } from '../../instance';
import { SignupBody, SignupResponse } from '../schemas/sign-up-schema';

export function signup(data: z.infer<typeof SignupBody>): Promise<z.infer<typeof SignupResponse>> {
return api(
return instance(
{
url: '/auth/sign-up',
method: 'POST',
Expand Down
1 change: 1 addition & 0 deletions src/shared/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export {
export { type GlobalErrorResponseType } from './errors';
export { AccessToken } from './token';
export { queryClient } from './query-client';
export * from './auth';
6 changes: 2 additions & 4 deletions src/shared/api/token/refresh-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';
import { RefreshTokenResponse } from './response-schema';
import { AccessToken } from './access-token';
import { GlobalErrorResponseType } from '../errors';
import { signout } from '../auth';

declare module 'axios' {
export interface AxiosRequestConfig {
Expand Down Expand Up @@ -85,10 +86,7 @@ export const refreshInterceptor = (instance: AxiosInstance) => {
} catch (refreshError) {
processQueue(refreshError);
try {
await instance.request({
url: '/auth/sign-out',
method: 'POST',
});
await signout();
} catch (err) {
return Promise.reject(err);
}
Expand Down
Loading