feat: make auth logic
This commit is contained in:
parent
c989b2b7a4
commit
935b7f72e5
59
src/app/api/auth/login/route.ts
Normal file
59
src/app/api/auth/login/route.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { LoginData } from '@/entities/auth/model/types';
|
||||||
|
|
||||||
|
export const GET = async (req: NextRequest) => {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Method is not supported' },
|
||||||
|
{ status: 405 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = req.nextUrl;
|
||||||
|
|
||||||
|
const phoneNumber = searchParams.get('phoneNumber');
|
||||||
|
const cardNumber = searchParams.get('cardNumber');
|
||||||
|
const type = searchParams.get('type');
|
||||||
|
|
||||||
|
if (!phoneNumber || !cardNumber || !type) {
|
||||||
|
return NextResponse.json({ error: 'Bad request' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loginRes = await fetch(
|
||||||
|
`https://test.oriyo.tj/api/client/login?type=${type}&phone=${phoneNumber}&uid=${cardNumber}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!loginRes.ok) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Error during login' },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await loginRes.json()) as LoginData;
|
||||||
|
|
||||||
|
const token = data.token;
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: 'No auth token' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = NextResponse.json({ success: true });
|
||||||
|
|
||||||
|
response.cookies.set('token', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
maxAge: 2 * 60 * 60,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('login error:', error);
|
||||||
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -17,11 +17,30 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/shared/shadcn-ui/tabs';
|
} from '@/shared/shadcn-ui/tabs';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: 'Бонусный клиент',
|
||||||
|
type: 'bonus' as const,
|
||||||
|
title: 'Вход для бонусных клиентов',
|
||||||
|
description:
|
||||||
|
'Введите номер телефона и номер бонусной карты для входа в личный кабинет.',
|
||||||
|
Icon: User,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Корпоративный клиент',
|
||||||
|
type: 'corporate' as const,
|
||||||
|
title: 'Вход для корпоративных клиентов',
|
||||||
|
description:
|
||||||
|
'Введите номер телефона и номер корпоративной карты для входа в личный кабинет.',
|
||||||
|
Icon: Building2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col'>
|
<div className='flex min-h-screen flex-col'>
|
||||||
<main className='flex-1'>
|
<main className='flex-1'>
|
||||||
<div className='container max-w-6xl py-16'>
|
<div className='container mx-auto max-w-6xl px-2 py-16'>
|
||||||
<div className='mb-12 flex flex-col items-center text-center'>
|
<div className='mb-12 flex flex-col items-center text-center'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
<Fuel className='h-6 w-6 text-red-600' />
|
<Fuel className='h-6 w-6 text-red-600' />
|
||||||
@ -35,46 +54,37 @@ export default function LoginPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mx-auto max-w-md'>
|
<div className='mx-auto max-w-lg'>
|
||||||
<Tabs defaultValue='bonus' className='w-full'>
|
<Tabs defaultValue='bonus' className='w-full'>
|
||||||
<TabsList className='mb-8 grid w-full grid-cols-2'>
|
<TabsList className='mb-8 flex flex-col sm:flex-row w-full h-fit'>
|
||||||
<TabsTrigger value='bonus' className='text-base'>
|
{tabs.map((tab) => {
|
||||||
<User className='mr-2 h-4 w-4' /> Бонусный клиент
|
return (
|
||||||
</TabsTrigger>
|
<TabsTrigger
|
||||||
<TabsTrigger value='corporate' className='text-base'>
|
key={tab.label}
|
||||||
<Building2 className='mr-2 h-4 w-4' /> Корпоративный клиент
|
value={tab.type}
|
||||||
</TabsTrigger>
|
className='text-base w-full'
|
||||||
|
>
|
||||||
|
<tab.Icon className='mr-2 h-4 w-4' /> {tab.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value='bonus'>
|
{tabs.map((tab) => {
|
||||||
<Card>
|
return (
|
||||||
<CardHeader>
|
<TabsContent key={tab.label} value={tab.type}>
|
||||||
<CardTitle>Вход для бонусных клиентов</CardTitle>
|
<Card>
|
||||||
<CardDescription>
|
<CardHeader>
|
||||||
Введите номер телефона и номер бонусной карты для входа в
|
<CardTitle>{tab.title}</CardTitle>
|
||||||
личный кабинет.
|
<CardDescription>{tab.description}</CardDescription>
|
||||||
</CardDescription>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent className='space-y-4'>
|
||||||
<CardContent className='space-y-4'>
|
<LoginForm type={tab.type} />
|
||||||
<LoginForm />
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</TabsContent>
|
||||||
</TabsContent>
|
);
|
||||||
|
})}
|
||||||
<TabsContent value='corporate'>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Вход для корпоративных клиентов</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Введите номер телефона и номер корпоративной карты для
|
|
||||||
входа в личный кабинет.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='space-y-4'>
|
|
||||||
<LoginForm />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className='mt-8 text-center text-sm text-gray-500'>
|
<div className='mt-8 text-center text-sm text-gray-500'>
|
||||||
|
|||||||
24
src/entities/auth/api/login.api.ts
Normal file
24
src/entities/auth/api/login.api.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
|
import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
|
||||||
|
|
||||||
|
export const loginAPI = createApi({
|
||||||
|
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
|
||||||
|
endpoints: (build) => ({
|
||||||
|
login: build.query<LoginResponse, LoginParams>({
|
||||||
|
query: (data) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: data.type,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
cardNumber: data.cardNumber,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: `/auth/login?${params}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useLazyLoginQuery } = loginAPI;
|
||||||
1
src/entities/auth/index.ts
Normal file
1
src/entities/auth/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useLazyLoginQuery } from './api/login.api';
|
||||||
9
src/entities/auth/model/contracts/login.contract.ts
Normal file
9
src/entities/auth/model/contracts/login.contract.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { LoginFormData } from '@/features/auth/login-form/model/login-form.schema';
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginParams extends LoginFormData {
|
||||||
|
type: 'bonus' | 'corporate';
|
||||||
|
}
|
||||||
7
src/entities/auth/model/types/index.ts
Normal file
7
src/entities/auth/model/types/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface LoginData {
|
||||||
|
card_id: number;
|
||||||
|
created_at: string;
|
||||||
|
phone: string;
|
||||||
|
token: string;
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
@ -6,13 +6,15 @@ export const loginFormSchema = z.object({
|
|||||||
.trim()
|
.trim()
|
||||||
.regex(/^[0-9+\-() ]*$/, {
|
.regex(/^[0-9+\-() ]*$/, {
|
||||||
message:
|
message:
|
||||||
'Phone number can only contain numbers, spaces, and the following symbols: + - ( )',
|
'Номер телефона может содержать только цифры, пробелы и следующие символы: + - ( )',
|
||||||
})
|
})
|
||||||
.refine((val) => !val || val.length >= 5, {
|
.min(5, 'Номер телефона слишком короткий. Введите полный номер телефона')
|
||||||
message:
|
.max(13, 'Номер телефона не может быть длиннее 13 символов'),
|
||||||
'Phone number is too short. Please enter a complete phone number',
|
cardNumber: z
|
||||||
}),
|
.string()
|
||||||
cardNumber: z.string().min(16).trim(),
|
.min(6, 'Неверный номер карты. Введите полный номер карты')
|
||||||
|
.max(20, 'Номер карты не может быть длиннее 20 символов')
|
||||||
|
.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LoginFormData = z.infer<typeof loginFormSchema>;
|
export type LoginFormData = z.infer<typeof loginFormSchema>;
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { useLazyLoginQuery } from '@/entities/auth';
|
||||||
|
|
||||||
|
import { SubmitButton } from '@/shared/components/submit-button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -20,11 +22,12 @@ import { LoginFormData, loginFormSchema } from '../model/login-form.schema';
|
|||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
// onSubmit: (data: any) => Promise<void>;
|
// onSubmit: (data: any) => Promise<void>;
|
||||||
|
type: 'bonus' | 'corporate';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoginForm = ({}: LoginFormProps) => {
|
export const LoginForm = ({ type }: LoginFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// const [login, results] = useLoginMutation();
|
const [login, { isLoading: isLoginLoading }] = useLazyLoginQuery();
|
||||||
|
|
||||||
const form = useForm<LoginFormData>({
|
const form = useForm<LoginFormData>({
|
||||||
resolver: zodResolver(loginFormSchema),
|
resolver: zodResolver(loginFormSchema),
|
||||||
@ -35,23 +38,29 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: LoginFormData) => {
|
const onSubmit = async (data: LoginFormData) => {
|
||||||
// const response = await login(data).unwrap();
|
try {
|
||||||
// const user = response.data;
|
const response = await login({ ...data, type }).unwrap();
|
||||||
// dispatch(
|
// const user = response.data;
|
||||||
// setCredentials({
|
// dispatch(
|
||||||
// user: {
|
// setCredentials({
|
||||||
// accessToken: user.accessToken,
|
// user: {
|
||||||
// affiliateId: user.affiliateId,
|
// accessToken: user.accessToken,
|
||||||
// email: user.email,
|
// affiliateId: user.affiliateId,
|
||||||
// id: user.id,
|
// email: user.email,
|
||||||
// role: user.role,
|
// id: user.id,
|
||||||
// username: user.username,
|
// role: user.role,
|
||||||
// },
|
// username: user.username,
|
||||||
// }),
|
// },
|
||||||
// );
|
// }),
|
||||||
toast.success('Logged in successfully!');
|
// );
|
||||||
|
toast.success('Logged in successfully!');
|
||||||
|
|
||||||
router.push('/customer-dashboard');
|
router.push(
|
||||||
|
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('An error occured during login');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,7 +70,7 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name='phoneNumber'
|
name='phoneNumber'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className='flex flex-col'>
|
||||||
<FormLabel>Номер телефона</FormLabel>
|
<FormLabel>Номер телефона</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@ -79,7 +88,7 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name='cardNumber'
|
name='cardNumber'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className='flex flex-col'>
|
||||||
<FormLabel>Номер карты</FormLabel>
|
<FormLabel>Номер карты</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@ -92,16 +101,14 @@ export const LoginForm = ({}: LoginFormProps) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<SubmitButton
|
||||||
// isLoading={results.isLoading}
|
isLoading={isLoginLoading}
|
||||||
// title='Login'
|
|
||||||
type='submit'
|
type='submit'
|
||||||
className='w-full'
|
className='w-full'
|
||||||
// variant={'default'}
|
disabled={isLoginLoading}
|
||||||
// disabled={loading}
|
|
||||||
>
|
>
|
||||||
Войти
|
Войти
|
||||||
</Button>
|
</SubmitButton>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
36
src/shared/components/submit-button.tsx
Normal file
36
src/shared/components/submit-button.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Loader2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Button, type ButtonProps } from '@/shared/shadcn-ui/button';
|
||||||
|
|
||||||
|
interface SubmitButtonProps extends ButtonProps {
|
||||||
|
title?: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubmitButton = ({
|
||||||
|
title = 'Отправить',
|
||||||
|
size = 'default',
|
||||||
|
type = 'submit',
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: SubmitButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
type={type}
|
||||||
|
size={size}
|
||||||
|
className={className}
|
||||||
|
disabled={isLoading || disabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2Icon className='animate-spin' />
|
||||||
|
) : (
|
||||||
|
(props.children ?? title)
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,10 @@
|
|||||||
import { combineReducers } from '@reduxjs/toolkit';
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { loginAPI } from '@/entities/auth/api/login.api';
|
||||||
|
|
||||||
import { baseAPI } from '@/shared/api/base-api';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
[baseAPI.reducerPath]: baseAPI.reducer,
|
[baseAPI.reducerPath]: baseAPI.reducer,
|
||||||
|
[loginAPI.reducerPath]: loginAPI.reducer,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user