From 33c93684725463f5b13fdc95c1bef1abe286b51d Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:28:35 +0500 Subject: [PATCH 1/3] Refactoring auth --- package.json | 1 + pnpm-lock.yaml | 74 +++++++++++++++++++ .../api-utlities/utilities/oriyo.client.ts | 10 +++ src/app/api/auth/login/route.ts | 49 +++++------- 4 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 src/app/api-utlities/utilities/oriyo.client.ts diff --git a/package.json b/package.json index a04c324..04d3507 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-toast": "^1.2.11", "@reduxjs/toolkit": "^2.7.0", "aos": "^2.3.4", + "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a54e8a0..43ec80f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: aos: specifier: ^2.3.4 version: 2.3.4 + axios: + specifier: ^1.9.0 + version: 1.9.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1306,6 +1309,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1314,6 +1320,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1385,6 +1394,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1441,6 +1454,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -1711,10 +1728,23 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2090,6 +2120,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -2307,6 +2345,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3792,12 +3833,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.3: {} + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -3873,6 +3924,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -3927,6 +3982,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + detect-libc@2.0.3: {} detect-node-es@1.1.0: {} @@ -4352,10 +4409,19 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -4708,6 +4774,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@10.0.1: dependencies: brace-expansion: 2.0.1 @@ -4877,6 +4949,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} diff --git a/src/app/api-utlities/utilities/oriyo.client.ts b/src/app/api-utlities/utilities/oriyo.client.ts new file mode 100644 index 0000000..291fa2c --- /dev/null +++ b/src/app/api-utlities/utilities/oriyo.client.ts @@ -0,0 +1,10 @@ +import { Axios } from 'axios'; + +const oriyoClient = new Axios({ + baseURL: process.env.ORIOYO_API_ENDPOINT || '', + headers: { + 'Content-type': 'application/json', + }, +}); + +export default oriyoClient; diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 774e8e7..df541d9 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,43 +1,28 @@ import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; -import { LoginData } from '@/entities/auth/model/types'; +import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; -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 }); - } +export const POST = async (req: NextRequest) => { + const validatedBody = z + .object({ + phoneNumber: z.string().min(9).max(9), + cardNumber: z.string().nonempty(), + type: z.enum(['corporate', 'bonus']), + }) + .parse(req.body); try { - const loginRes = await fetch( - `https://test.oriyo.tj/api/client/login?type=${type}&phone=${phoneNumber}&uid=${cardNumber}`, - { - method: 'GET', + const oriyoResponse = await oriyoClient.get('/client/login', { + params: { + type: validatedBody.type, + phone: validatedBody.phoneNumber, + uid: validatedBody.cardNumber, }, - ); + }); - if (!loginRes.ok) { - return NextResponse.json( - { error: 'Error during login' }, - { status: 400 }, - ); - } + const { token } = oriyoResponse.data; - const data = (await loginRes.json()) as LoginData; - - const token = data.token; if (!token) { return NextResponse.json({ error: 'No auth token' }, { status: 401 }); } From efae331aaf7bf51533506387fba15698472ab8e3 Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Thu, 1 May 2025 01:16:11 +0500 Subject: [PATCH 2/3] Added authentication support --- package.json | 1 + pnpm-lock.yaml | 21 +++++++ src/app/api/auth/login/route.ts | 28 +++++----- .../middlewares/error-handler.middleware.ts | 18 ++++++ src/app/api/pages/main/route.ts | 8 ++- src/entities/auth/api/login.api.ts | 25 ++++----- .../auth/model/contracts/login.contract.ts | 2 +- .../model/validation}/login-form.schema.ts | 0 .../auth/login-form/ui/login-form.tsx | 19 +++---- src/pages-templates/login/index.tsx | 56 ++++++++++++++++++- src/shared/api/base-api.ts | 21 ++++++- src/shared/store/root-reducer.ts | 3 - 12 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 src/app/api/middlewares/error-handler.middleware.ts rename src/{features/auth/login-form/model => entities/auth/model/validation}/login-form.schema.ts (100%) diff --git a/package.json b/package.json index 04d3507..6bedcc6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cookies-next": "^5.1.0", "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ec80f..531d1c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: clsx: specifier: ^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: specifier: ^4.1.0 version: 4.1.0 @@ -1401,6 +1404,16 @@ packages: concat-map@0.0.1: 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: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3930,6 +3943,14 @@ snapshots: 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: dependencies: path-key: 3.1.1 diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index df541d9..a51a83c 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -3,14 +3,16 @@ import { z } from 'zod'; import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; -export const POST = async (req: NextRequest) => { - const validatedBody = z - .object({ - phoneNumber: z.string().min(9).max(9), - cardNumber: z.string().nonempty(), - type: z.enum(['corporate', 'bonus']), - }) - .parse(req.body); +import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema'; + +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +const routeHandler = async (req: NextRequest) => { + const body = await req.json(); + + const validatedBody = loginFormSchema + .merge(z.object({ type: z.enum(['bonus', 'corporate']) })) + .parse(body); try { 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) { - return NextResponse.json({ error: 'No auth token' }, { status: 401 }); + return NextResponse.json({ error: 'Credentials error' }, { status: 401 }); } const response = NextResponse.json({ success: true }); - response.cookies.set('token', token, { - httpOnly: true, + response.cookies.set(`${validatedBody.type}__token`, token, { path: '/', maxAge: 2 * 60 * 60, - secure: process.env.NODE_ENV === 'production', }); return response; @@ -42,3 +42,5 @@ export const POST = async (req: NextRequest) => { return NextResponse.json({ error: 'Server error' }, { status: 500 }); } }; + +export const POST = validationErrorHandler(routeHandler); diff --git a/src/app/api/middlewares/error-handler.middleware.ts b/src/app/api/middlewares/error-handler.middleware.ts new file mode 100644 index 0000000..a3046e8 --- /dev/null +++ b/src/app/api/middlewares/error-handler.middleware.ts @@ -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 }, + ); + } + }; diff --git a/src/app/api/pages/main/route.ts b/src/app/api/pages/main/route.ts index f17286f..deb8f1b 100644 --- a/src/app/api/pages/main/route.ts +++ b/src/app/api/pages/main/route.ts @@ -7,7 +7,9 @@ import { import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request'; 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); return new Response( @@ -21,4 +23,6 @@ export async function GET(request: Request) { headers: { 'Content-Type': 'application/json' }, }, ); -} +}; + +export const GET = validationErrorHandler(routeHandler); diff --git a/src/entities/auth/api/login.api.ts b/src/entities/auth/api/login.api.ts index 99ad4ed..5760186 100644 --- a/src/entities/auth/api/login.api.ts +++ b/src/entities/auth/api/login.api.ts @@ -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'; -export const loginAPI = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: '/api' }), - endpoints: (build) => ({ - login: build.query({ +export const authenticationApi = baseAPI.injectEndpoints({ + endpoints: (builder) => ({ + login: builder.query({ query: (data) => { - const params = new URLSearchParams({ - type: data.type, - phoneNumber: data.phoneNumber, - cardNumber: data.cardNumber, - }).toString(); - 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; diff --git a/src/entities/auth/model/contracts/login.contract.ts b/src/entities/auth/model/contracts/login.contract.ts index cb3a3da..b315099 100644 --- a/src/entities/auth/model/contracts/login.contract.ts +++ b/src/entities/auth/model/contracts/login.contract.ts @@ -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 { success: boolean; diff --git a/src/features/auth/login-form/model/login-form.schema.ts b/src/entities/auth/model/validation/login-form.schema.ts similarity index 100% rename from src/features/auth/login-form/model/login-form.schema.ts rename to src/entities/auth/model/validation/login-form.schema.ts diff --git a/src/features/auth/login-form/ui/login-form.tsx b/src/features/auth/login-form/ui/login-form.tsx index 30d7044..1af10a3 100644 --- a/src/features/auth/login-form/ui/login-form.tsx +++ b/src/features/auth/login-form/ui/login-form.tsx @@ -19,7 +19,10 @@ import { } from '@/shared/shadcn-ui/form'; 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 { type: 'bonus' | 'corporate'; @@ -40,16 +43,12 @@ export const LoginForm = ({ type }: LoginFormProps) => { }); const onSubmit = async (data: LoginFormData) => { - try { - await login({ ...data, type }).unwrap(); + await login({ ...data, type }).unwrap(); - toast.success('Logged in successfully!'); - router.push( - type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', - ); - } catch (error) { - toast.error('An error occured during login'); - } + toast.success('Logged in successfully!'); + router.push( + type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', + ); }; return ( diff --git a/src/pages-templates/login/index.tsx b/src/pages-templates/login/index.tsx index 23046b4..c248838 100644 --- a/src/pages-templates/login/index.tsx +++ b/src/pages-templates/login/index.tsx @@ -1,11 +1,14 @@ 'use client'; +import { deleteCookie, getCookie } from 'cookies-next'; import { Building2, Fuel, User } from 'lucide-react'; import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; import { LoginForm } from '@/features/auth/login-form'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; +import { Button } from '@/shared/shadcn-ui/button'; import { Card, CardContent, @@ -39,6 +42,13 @@ const tabs = [ export default function LoginPage() { 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 (
@@ -55,7 +65,12 @@ export default function LoginPage() {
- + {tabs.map((tab) => { return ( @@ -71,6 +86,45 @@ export default function LoginPage() { {tabs.map((tab) => { + const tabCookieName = `${tab.type}__token`; + + const authenticationCookie = getCookie(tabCookieName); + + if (authenticationCookie) { + return ( + + + + {t(tab.title)} + + + + + + + + + + ); + } + return ( diff --git a/src/shared/api/base-api.ts b/src/shared/api/base-api.ts index 5ecaa52..71616da 100644 --- a/src/shared/api/base-api.ts +++ b/src/shared/api/base-api.ts @@ -1,4 +1,5 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { toast } from 'sonner'; const baseQuery = fetchBaseQuery({ baseUrl: process.env.NEXT_PUBLIC_API_URL, @@ -15,7 +16,25 @@ export const TAGS = { export const baseAPI = createApi({ 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), endpoints: () => ({}), }); diff --git a/src/shared/store/root-reducer.ts b/src/shared/store/root-reducer.ts index 56b999c..1b1a8cd 100644 --- a/src/shared/store/root-reducer.ts +++ b/src/shared/store/root-reducer.ts @@ -1,10 +1,7 @@ import { combineReducers } from '@reduxjs/toolkit'; -import { loginAPI } from '@/entities/auth/api/login.api'; - import { baseAPI } from '@/shared/api/base-api'; export const rootReducer = combineReducers({ [baseAPI.reducerPath]: baseAPI.reducer, - [loginAPI.reducerPath]: loginAPI.reducer, }); From 3142023c7938a06276df488622840fafe894624a Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Thu, 1 May 2025 01:45:12 +0500 Subject: [PATCH 3/3] Loading bonus clients from API --- src/app/api/auth/login/route.ts | 14 +- src/app/api/bonus/info/route.ts | 31 ++++ src/entities/bonus/api/bonus.api.ts | 17 +++ .../model/types/bonus-client-info.type.ts | 8 + .../(dashboard)/customer-dashboard/index.tsx | 137 +++++++++--------- 5 files changed, 133 insertions(+), 74 deletions(-) create mode 100644 src/app/api/bonus/info/route.ts create mode 100644 src/entities/bonus/api/bonus.api.ts create mode 100644 src/entities/bonus/model/types/bonus-client-info.type.ts diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index a51a83c..7dbe591 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -23,7 +23,7 @@ const routeHandler = async (req: NextRequest) => { }, }); - const { token } = JSON.parse(oriyoResponse.data); + const { token, card_id } = JSON.parse(oriyoResponse.data); if (!token) { return NextResponse.json({ error: 'Credentials error' }, { status: 401 }); @@ -31,10 +31,14 @@ const routeHandler = async (req: NextRequest) => { const response = NextResponse.json({ success: true }); - response.cookies.set(`${validatedBody.type}__token`, token, { - path: '/', - maxAge: 2 * 60 * 60, - }); + response.cookies.set( + `${validatedBody.type}__token`, + JSON.stringify({ token, card_id }), + { + path: '/', + maxAge: 2 * 60 * 60, + }, + ); return response; } catch (error) { diff --git a/src/app/api/bonus/info/route.ts b/src/app/api/bonus/info/route.ts new file mode 100644 index 0000000..efb8011 --- /dev/null +++ b/src/app/api/bonus/info/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; + +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +export const routeHandler = async (req: NextRequest) => { + const bonusTokenData = req.cookies.get('bonus__token'); + + if (!bonusTokenData) { + return NextResponse.json( + { error: 'User does not have access' }, + { status: 401 }, + ); + } + + const { card_id, token } = JSON.parse(bonusTokenData.value); + + const oriyoResponse = await oriyoClient.get('/client/info', { + params: { + card_id, + token, + }, + }); + + return new Response(oriyoResponse.data, { + headers: { 'Content-Type': 'application/json' }, + }); +}; + +export const GET = validationErrorHandler(routeHandler); diff --git a/src/entities/bonus/api/bonus.api.ts b/src/entities/bonus/api/bonus.api.ts new file mode 100644 index 0000000..b8d31bf --- /dev/null +++ b/src/entities/bonus/api/bonus.api.ts @@ -0,0 +1,17 @@ +import { baseAPI } from '@/shared/api/base-api'; + +import { ClientInfo } from '../model/types/bonus-client-info.type'; + +export const bonusApi = baseAPI.injectEndpoints({ + endpoints: (builder) => ({ + fetchMyBonusInfo: builder.query({ + query: () => { + return { + url: '/bonus/info', + }; + }, + }), + }), +}); + +export const { useFetchMyBonusInfoQuery } = bonusApi; diff --git a/src/entities/bonus/model/types/bonus-client-info.type.ts b/src/entities/bonus/model/types/bonus-client-info.type.ts new file mode 100644 index 0000000..dd7138f --- /dev/null +++ b/src/entities/bonus/model/types/bonus-client-info.type.ts @@ -0,0 +1,8 @@ +export interface ClientInfo { + card_id: number; + fullname: string; + cardno: string; + reg_date: string; + end_date: string; + bonuses: string; +} diff --git a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx index d4181a1..2e0a319 100644 --- a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx @@ -2,6 +2,8 @@ import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react'; +import { useFetchMyBonusInfoQuery } from '@/entities/bonus/api/bonus.api'; + import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; import { @@ -14,20 +16,11 @@ import { import { TransactionsTable } from '@/widgets/transactions-table'; -// Sample customer data -const customerData = { - firstName: 'Алишер', - lastName: 'Рахмонов', - passportNumber: 'A12345678', - bonusPoints: 1250, - cardNumber: '5678-9012-3456-7890', - expiryDate: '12/2025', - registrationDate: '15.06.2020', -}; - export function CustomerDashboard() { const { t } = useTextController(); + const { data, isLoading } = useFetchMyBonusInfoQuery({}); + return (
@@ -41,35 +34,44 @@ export function CustomerDashboard() {
- {/* Bonus Card */} - - - - {t('customer.bonusCard.title')} - - - {t('customer.bonusCard.description')} - - - -
-

- {customerData.bonusPoints} -

-

- {t('customer.bonusCard.points')} -

-
-
-
- - {t('customer.bonusCard.validUntil')} -
- -
-
+ {!data || isLoading ? ( + // TODO: Bunyod please add loader here + <>Loader here + ) : ( + <> + + + + {t('customer.bonusCard.title')} + + + {t('customer.bonusCard.description')} + + + +
+

{data.bonuses}

+

+ {t('customer.bonusCard.points')} +

+
+
+
+ + + {t('customer.bonusCard.validUntil')}{' '} + {new Date(data.end_date).toLocaleDateString('en-GB')} + +
+ +
+
+ + )}
+ + {/* Bonus Card */} {/* Customer Card */} @@ -79,40 +81,37 @@ export function CustomerDashboard() { -
-
-
-

- {t('customer.infoCard.regDateLabel')} -

-

- {customerData.firstName} {customerData.lastName} -

+ {!data || isLoading ? ( + // TODO: Bunyod please add loader here + <>Loader here + ) : ( +
+
+
+

+ {t('customer.infoCard.regDateLabel')} +

+

{data.fullname}

+
+
+

+ {t('customer.infoCard.regDateLabel')} +

+

+ {new Date(data.reg_date).toLocaleDateString('en-GB')} +

+
-
-

- {t('customer.infoCard.regDateLabel')} -

-

- {customerData.registrationDate} -

+
+
+

+ {t('customer.infoCard.cardNumberLabel')} +

+

{data.cardno}

+
-
-
-

- {t('customer.infoCard.cardNumberLabel')} -

-

{customerData.cardNumber}

-
-
-

- {t('customer.infoCard.expiryDateLabel')} -

-

{customerData.expiryDate}

-
-
-
+ )}