Added authentication support
This commit is contained in:
parent
33c9368472
commit
efae331aaf
@ -26,6 +26,7 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
|||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@ -56,6 +56,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cookies-next:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@ -1401,6 +1404,16 @@ packages:
|
|||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cookies-next@5.1.0:
|
||||||
|
resolution: {integrity: sha512-9Ekne+q8hfziJtnT9c1yDUBqT0eDMGgPrfPl4bpR3xwQHLTd/8gbSf6+IEkP/pjGsDZt1TGbC6emYmFYRbIXwQ==}
|
||||||
|
peerDependencies:
|
||||||
|
next: '>=15.0.0'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -3930,6 +3943,14 @@ snapshots:
|
|||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
|
cookies-next@5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|||||||
@ -3,14 +3,16 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
|
||||||
|
|
||||||
export const POST = async (req: NextRequest) => {
|
import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
|
||||||
const validatedBody = z
|
|
||||||
.object({
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
phoneNumber: z.string().min(9).max(9),
|
|
||||||
cardNumber: z.string().nonempty(),
|
const routeHandler = async (req: NextRequest) => {
|
||||||
type: z.enum(['corporate', 'bonus']),
|
const body = await req.json();
|
||||||
})
|
|
||||||
.parse(req.body);
|
const validatedBody = loginFormSchema
|
||||||
|
.merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
|
||||||
|
.parse(body);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oriyoResponse = await oriyoClient.get('/client/login', {
|
const oriyoResponse = await oriyoClient.get('/client/login', {
|
||||||
@ -21,19 +23,17 @@ export const POST = async (req: NextRequest) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { token } = oriyoResponse.data;
|
const { token } = JSON.parse(oriyoResponse.data);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return NextResponse.json({ error: 'No auth token' }, { status: 401 });
|
return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = NextResponse.json({ success: true });
|
const response = NextResponse.json({ success: true });
|
||||||
|
|
||||||
response.cookies.set('token', token, {
|
response.cookies.set(`${validatedBody.type}__token`, token, {
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: 2 * 60 * 60,
|
maxAge: 2 * 60 * 60,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -42,3 +42,5 @@ export const POST = async (req: NextRequest) => {
|
|||||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const POST = validationErrorHandler(routeHandler);
|
||||||
|
|||||||
18
src/app/api/middlewares/error-handler.middleware.ts
Normal file
18
src/app/api/middlewares/error-handler.middleware.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
|
export const validationErrorHandler =
|
||||||
|
(handler: Function) => async (req: NextRequest, res: NextResponse) => {
|
||||||
|
try {
|
||||||
|
return await handler(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError)
|
||||||
|
return NextResponse.json({ message: error.format() }, { status: 400 });
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: 'Server died for some reason' },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -7,7 +7,9 @@ import {
|
|||||||
import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request';
|
import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request';
|
||||||
import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client';
|
import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
|
||||||
|
|
||||||
|
const routeHandler = async (request: Request) => {
|
||||||
const response = await requestTaylor(mainPageRequest);
|
const response = await requestTaylor(mainPageRequest);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
@ -21,4 +23,6 @@ export async function GET(request: Request) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const GET = validationErrorHandler(routeHandler);
|
||||||
|
|||||||
@ -1,24 +1,23 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { baseAPI } from '@/shared/api/base-api';
|
||||||
|
|
||||||
import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
|
import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
|
||||||
|
|
||||||
export const loginAPI = createApi({
|
export const authenticationApi = baseAPI.injectEndpoints({
|
||||||
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
|
endpoints: (builder) => ({
|
||||||
endpoints: (build) => ({
|
login: builder.query<LoginResponse, LoginParams>({
|
||||||
login: build.query<LoginResponse, LoginParams>({
|
|
||||||
query: (data) => {
|
query: (data) => {
|
||||||
const params = new URLSearchParams({
|
|
||||||
type: data.type,
|
|
||||||
phoneNumber: data.phoneNumber,
|
|
||||||
cardNumber: data.cardNumber,
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: `/auth/login?${params}`,
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
type: data.type,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
cardNumber: data.cardNumber,
|
||||||
|
},
|
||||||
|
url: '/auth/login',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useLazyLoginQuery } = loginAPI;
|
export const { useLazyLoginQuery } = authenticationApi;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { LoginFormData } from '@/features/auth/login-form/model/login-form.schema';
|
import { LoginFormData } from '@/entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@ -19,7 +19,10 @@ import {
|
|||||||
} from '@/shared/shadcn-ui/form';
|
} from '@/shared/shadcn-ui/form';
|
||||||
import { Input } from '@/shared/shadcn-ui/input';
|
import { Input } from '@/shared/shadcn-ui/input';
|
||||||
|
|
||||||
import { LoginFormData, loginFormSchema } from '../model/login-form.schema';
|
import {
|
||||||
|
LoginFormData,
|
||||||
|
loginFormSchema,
|
||||||
|
} from '../../../../entities/auth/model/validation/login-form.schema';
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
type: 'bonus' | 'corporate';
|
type: 'bonus' | 'corporate';
|
||||||
@ -40,16 +43,12 @@ export const LoginForm = ({ type }: LoginFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: LoginFormData) => {
|
const onSubmit = async (data: LoginFormData) => {
|
||||||
try {
|
await login({ ...data, type }).unwrap();
|
||||||
await login({ ...data, type }).unwrap();
|
|
||||||
|
|
||||||
toast.success('Logged in successfully!');
|
toast.success('Logged in successfully!');
|
||||||
router.push(
|
router.push(
|
||||||
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
|
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
toast.error('An error occured during login');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { deleteCookie, getCookie } from 'cookies-next';
|
||||||
import { Building2, Fuel, User } from 'lucide-react';
|
import { Building2, Fuel, User } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { LoginForm } from '@/features/auth/login-form';
|
import { LoginForm } from '@/features/auth/login-form';
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -39,6 +42,13 @@ const tabs = [
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { t } = useTextController();
|
const { t } = useTextController();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const defaultTab = searchParams.get('tab') || 'bonus';
|
||||||
|
|
||||||
|
const handleTabChange = (tabType: string) => {
|
||||||
|
router.push(`?tab=${tabType}`, undefined, { shallow: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-col items-center justify-center'>
|
<div className='flex min-h-screen flex-col items-center justify-center'>
|
||||||
@ -55,7 +65,12 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mx-auto max-w-lg'>
|
<div className='mx-auto max-w-lg'>
|
||||||
<Tabs defaultValue='bonus' className='w-full'>
|
<Tabs
|
||||||
|
defaultValue={defaultTab}
|
||||||
|
value={defaultTab}
|
||||||
|
onValueChange={handleTabChange}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
<TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'>
|
<TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
return (
|
return (
|
||||||
@ -71,6 +86,45 @@ export default function LoginPage() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
|
const tabCookieName = `${tab.type}__token`;
|
||||||
|
|
||||||
|
const authenticationCookie = getCookie(tabCookieName);
|
||||||
|
|
||||||
|
if (authenticationCookie) {
|
||||||
|
return (
|
||||||
|
<TabsContent key={tab.label} value={tab.type}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t(tab.title)}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='flex justify-center gap-2 space-y-4'>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
tab.type === 'bonus'
|
||||||
|
? '/customer-dashboard'
|
||||||
|
: '/corporate-dashboard'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button className='flex items-center'>
|
||||||
|
Открыть
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='flex items-center gap-2'
|
||||||
|
onClick={() => {
|
||||||
|
deleteCookie(tabCookieName);
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent key={tab.label} value={tab.type}>
|
<TabsContent key={tab.label} value={tab.type}>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const baseQuery = fetchBaseQuery({
|
const baseQuery = fetchBaseQuery({
|
||||||
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||||
@ -15,7 +16,25 @@ export const TAGS = {
|
|||||||
|
|
||||||
export const baseAPI = createApi({
|
export const baseAPI = createApi({
|
||||||
reducerPath: 'baseAPI',
|
reducerPath: 'baseAPI',
|
||||||
baseQuery,
|
baseQuery: async (args, api, extraOptions) => {
|
||||||
|
const result = await baseQuery(args, api, extraOptions);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
switch (result.error.status) {
|
||||||
|
case 401:
|
||||||
|
toast.error('Login credentials error');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 500:
|
||||||
|
toast.error('Server error, please try later');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
tagTypes: Object.values(TAGS),
|
tagTypes: Object.values(TAGS),
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
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