From dd72be5ad88cf81fb2512cc2f78a52673d999f44 Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Sat, 3 May 2025 13:45:40 +0500 Subject: [PATCH] added authorization middleware & pagination for transactions --- .../{utilities => clients}/oriyo.client.ts | 4 + .../{utilities => clients}/taylor.client.ts | 0 src/app/api-utlities/utilities/get-params.ts | 11 ++ src/app/api/auth/login/route.ts | 2 +- src/app/api/bonus/info/route.ts | 26 ++-- src/app/api/bonus/transactions/route.ts | 44 ++---- src/app/api/corporate/info/route.ts | 22 ++- src/app/api/corporate/transactions/route.ts | 46 ++++++ src/app/api/middlewares/auth.middleware.ts | 32 +++++ src/entities/bonus/api/bonus.api.ts | 9 +- .../model/types/bonus-client-info.type.ts | 26 ---- src/entities/corporate/api/corporate.api.ts | 21 ++- src/entities/transactions/model/types.ts | 25 ++++ .../(dashboard)/corporate-dashboard/index.tsx | 109 ++++----------- .../(dashboard)/customer-dashboard/index.tsx | 26 +++- src/widgets/pagination.tsx | 118 ++++++++++++++++ src/widgets/transactions-table.tsx | 132 ++++++++++++++++-- 17 files changed, 463 insertions(+), 190 deletions(-) rename src/app/api-utlities/{utilities => clients}/oriyo.client.ts (72%) rename src/app/api-utlities/{utilities => clients}/taylor.client.ts (100%) create mode 100644 src/app/api-utlities/utilities/get-params.ts create mode 100644 src/app/api/corporate/transactions/route.ts create mode 100644 src/app/api/middlewares/auth.middleware.ts create mode 100644 src/entities/transactions/model/types.ts create mode 100644 src/widgets/pagination.tsx diff --git a/src/app/api-utlities/utilities/oriyo.client.ts b/src/app/api-utlities/clients/oriyo.client.ts similarity index 72% rename from src/app/api-utlities/utilities/oriyo.client.ts rename to src/app/api-utlities/clients/oriyo.client.ts index 291fa2c..4a22683 100644 --- a/src/app/api-utlities/utilities/oriyo.client.ts +++ b/src/app/api-utlities/clients/oriyo.client.ts @@ -5,6 +5,10 @@ const oriyoClient = new Axios({ headers: { 'Content-type': 'application/json', }, + + transformResponse: (response) => { + return JSON.parse(response); + }, }); export default oriyoClient; diff --git a/src/app/api-utlities/utilities/taylor.client.ts b/src/app/api-utlities/clients/taylor.client.ts similarity index 100% rename from src/app/api-utlities/utilities/taylor.client.ts rename to src/app/api-utlities/clients/taylor.client.ts diff --git a/src/app/api-utlities/utilities/get-params.ts b/src/app/api-utlities/utilities/get-params.ts new file mode 100644 index 0000000..2b5cbae --- /dev/null +++ b/src/app/api-utlities/utilities/get-params.ts @@ -0,0 +1,11 @@ +import { NextRequest } from 'next/server'; + +export const getParams = (request: NextRequest) => + Array.from(request.nextUrl.searchParams.entries()).reduce( + (pr, cr) => { + pr[cr[0]] = cr[1]; + + return pr; + }, + {} as Record, + ); diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 8772735..c042d92 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; +import oriyoClient from '@/app/api-utlities/clients/oriyo.client'; import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema'; diff --git a/src/app/api/bonus/info/route.ts b/src/app/api/bonus/info/route.ts index d7a8178..a6ed56e 100644 --- a/src/app/api/bonus/info/route.ts +++ b/src/app/api/bonus/info/route.ts @@ -1,20 +1,13 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { NextRequest } from 'next/server'; -import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; +import oriyoClient from '@/app/api-utlities/clients/oriyo.client'; +import { authorizationMiddleware } from '../../middlewares/auth.middleware'; import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; -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 routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => { + const { card_id, token } = JSON.parse(requestCookie.value); const oriyoResponse = await oriyoClient.get('/client/info', { params: { @@ -23,9 +16,12 @@ const routeHandler = async (req: NextRequest) => { }, }); - return new Response(oriyoResponse.data, { + return new Response(JSON.stringify(oriyoResponse.data), { headers: { 'Content-Type': 'application/json' }, }); }; -export const GET = validationErrorHandler(routeHandler); +export const GET = authorizationMiddleware( + validationErrorHandler(routeHandler), + 'bonus__token', +); diff --git a/src/app/api/bonus/transactions/route.ts b/src/app/api/bonus/transactions/route.ts index 310e78a..d710705 100644 --- a/src/app/api/bonus/transactions/route.ts +++ b/src/app/api/bonus/transactions/route.ts @@ -1,8 +1,11 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { NextRequest } from 'next/server'; import { z } from 'zod'; -import oriyoClient from '@/app/api-utlities/utilities/oriyo.client'; +import oriyoClient from '@/app/api-utlities/clients/oriyo.client'; +import { getParams } from '@/app/api-utlities/utilities/get-params'; +import { authorizationMiddleware } from '../../middlewares/auth.middleware'; import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; const validatedSchema = z.object({ @@ -12,28 +15,10 @@ const validatedSchema = z.object({ page: z.coerce.number(), }); -const routeHandler = async (req: NextRequest) => { - const bonusTokenData = req.cookies.get('bonus__token'); +const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => { + const validatedRequest = validatedSchema.parse(getParams(req)); - if (!bonusTokenData) { - return NextResponse.json( - { error: 'User does not have access' }, - { status: 401 }, - ); - } - - const params = Array.from(req.nextUrl.searchParams.entries()).reduce( - (pr, cr) => { - pr[cr[0]] = cr[1]; - - return pr; - }, - {} as Record, - ); - - const validatedRequest = validatedSchema.parse(params); - - const { card_id, token } = JSON.parse(bonusTokenData.value); + const { card_id, token } = JSON.parse(requestCookie.value); const oriyoResponse = await oriyoClient.get('/client/transactions', { params: { @@ -49,15 +34,14 @@ const routeHandler = async (req: NextRequest) => { }, }); - const parsedResponse = JSON.parse(oriyoResponse.data); + if (oriyoResponse.data.error) throw oriyoResponse.data; - if (parsedResponse.error) { - return NextResponse.json({ message: 'Fetch error' }, { status: 400 }); - } - - return new Response(oriyoResponse.data, { + return new Response(JSON.stringify(oriyoResponse.data), { headers: { 'Content-Type': 'application/json' }, }); }; -export const GET = validationErrorHandler(routeHandler); +export const GET = authorizationMiddleware( + validationErrorHandler(routeHandler), + 'bonus__token', +); diff --git a/src/app/api/corporate/info/route.ts b/src/app/api/corporate/info/route.ts index b7e23ff..575d652 100644 --- a/src/app/api/corporate/info/route.ts +++ b/src/app/api/corporate/info/route.ts @@ -1,23 +1,19 @@ import { omit } from 'lodash'; -import { NextRequest, NextResponse } from 'next/server'; +import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { NextRequest } from 'next/server'; +import { authorizationMiddleware } from '../../middlewares/auth.middleware'; import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; -const routeHandler = async (req: NextRequest) => { - const bonusTokenData = req.cookies.get('corporate__token'); - - if (!bonusTokenData) { - return NextResponse.json( - { error: 'User does not have access' }, - { status: 401 }, - ); - } - - const parsedData = JSON.parse(bonusTokenData.value); +const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => { + const parsedData = JSON.parse(requestCookie.value); return new Response(JSON.stringify(omit(parsedData, 'token')), { headers: { 'Content-Type': 'application/json' }, }); }; -export const GET = validationErrorHandler(routeHandler); +export const GET = authorizationMiddleware( + validationErrorHandler(routeHandler), + 'corporate__token', +); diff --git a/src/app/api/corporate/transactions/route.ts b/src/app/api/corporate/transactions/route.ts new file mode 100644 index 0000000..f61ef9a --- /dev/null +++ b/src/app/api/corporate/transactions/route.ts @@ -0,0 +1,46 @@ +import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'; +import { NextRequest } from 'next/server'; +import { z } from 'zod'; + +import oriyoClient from '@/app/api-utlities/clients/oriyo.client'; +import { getParams } from '@/app/api-utlities/utilities/get-params'; + +import { authorizationMiddleware } from '../../middlewares/auth.middleware'; +import { validationErrorHandler } from '../../middlewares/error-handler.middleware'; + +const validatedSchema = z.object({ + start_date: z.string().optional(), + end_date: z.string().optional(), + limit: z.coerce.number(), + page: z.coerce.number(), +}); + +const routeHandler = async (req: NextRequest, requestCookie: RequestCookie) => { + const validatedRequest = validatedSchema.parse(getParams(req)); + + const { group_id, token } = JSON.parse(requestCookie.value); + + const oriyoResponse = await oriyoClient.get('/client/transactions', { + params: { + group_id, + token, + limit: validatedRequest.limit, + page: validatedRequest.page, + type: 'corporate', + sort: 'id', + direction: 'desc', + start_date: validatedRequest.start_date, + end_date: validatedRequest.end_date, + }, + }); + + if (oriyoResponse.data.error) throw oriyoResponse.data; + + return new Response(JSON.stringify(oriyoResponse.data), { + headers: { 'Content-Type': 'application/json' }, + }); +}; + +export const GET = validationErrorHandler( + authorizationMiddleware(routeHandler, 'corporate__token'), +); diff --git a/src/app/api/middlewares/auth.middleware.ts b/src/app/api/middlewares/auth.middleware.ts new file mode 100644 index 0000000..352ca76 --- /dev/null +++ b/src/app/api/middlewares/auth.middleware.ts @@ -0,0 +1,32 @@ +import { has } from 'lodash'; +import { NextRequest, NextResponse } from 'next/server'; + +export const authorizationMiddleware = + (handler: Function, authorizationTokenKey: string) => + async (req: NextRequest, ...args: any[]) => { + const requestedToken = req.cookies.get(authorizationTokenKey); + + if (!requestedToken) { + return NextResponse.json( + { error: 'User does not have access' }, + { status: 401 }, + ); + } + + try { + return await handler(req, requestedToken, ...args); + } catch (error) { + if (has(error, 'code') && error.code === 401) { + const response = NextResponse.json( + { message: 'Authorization session was timed out' }, + { status: 401 }, + ); + + response.cookies.delete(authorizationTokenKey); + + return response; + } + + throw error; + } + }; diff --git a/src/entities/bonus/api/bonus.api.ts b/src/entities/bonus/api/bonus.api.ts index b83c712..5bdc1a9 100644 --- a/src/entities/bonus/api/bonus.api.ts +++ b/src/entities/bonus/api/bonus.api.ts @@ -1,10 +1,11 @@ -import { baseAPI } from '@/shared/api/base-api'; - import { - ClientInfo, TransactionRequest, TransactionResponse, -} from '../model/types/bonus-client-info.type'; +} from '@/entities/transactions/model/types'; + +import { baseAPI } from '@/shared/api/base-api'; + +import { ClientInfo } from '../model/types/bonus-client-info.type'; export const bonusApi = baseAPI.injectEndpoints({ endpoints: (builder) => ({ diff --git a/src/entities/bonus/model/types/bonus-client-info.type.ts b/src/entities/bonus/model/types/bonus-client-info.type.ts index bc50a6c..dd7138f 100644 --- a/src/entities/bonus/model/types/bonus-client-info.type.ts +++ b/src/entities/bonus/model/types/bonus-client-info.type.ts @@ -6,29 +6,3 @@ export interface ClientInfo { end_date: string; bonuses: string; } - -export interface TransactionResponse { - transactions: Transaction[]; - card_id: string; - current_page: number; - limit: number; - total_records: number; - total_pages: number; -} - -export interface Transaction { - id: number; - date_create: string; - station: string; - product_name: string; - amount: string; - price_real: string; - sum_real: string; -} - -export interface TransactionRequest { - start_date?: string; - end_date?: string; - page: number; - limit: number; -} diff --git a/src/entities/corporate/api/corporate.api.ts b/src/entities/corporate/api/corporate.api.ts index 32426ac..f66beda 100644 --- a/src/entities/corporate/api/corporate.api.ts +++ b/src/entities/corporate/api/corporate.api.ts @@ -1,3 +1,8 @@ +import { + TransactionRequest, + TransactionResponse, +} from '@/entities/transactions/model/types'; + import { baseAPI } from '@/shared/api/base-api'; import { CorporateInfoResponse } from '../model/types/corporate-client-info.type'; @@ -11,7 +16,21 @@ export const corporateApi = baseAPI.injectEndpoints({ }; }, }), + fetchCorporateTransactions: builder.query< + TransactionResponse, + TransactionRequest + >({ + query: (request) => { + return { + url: '/corporate/transactions', + params: request, + }; + }, + }), }), }); -export const { useFetchMyCorporateInfoQuery } = corporateApi; +export const { + useFetchMyCorporateInfoQuery, + useFetchCorporateTransactionsQuery, +} = corporateApi; diff --git a/src/entities/transactions/model/types.ts b/src/entities/transactions/model/types.ts new file mode 100644 index 0000000..46fb6c8 --- /dev/null +++ b/src/entities/transactions/model/types.ts @@ -0,0 +1,25 @@ +export interface TransactionResponse { + transactions: Transaction[]; + card_id: string; + current_page: number; + limit: number; + total_records: number; + total_pages: number; +} + +export interface Transaction { + id: number; + date_create: string; + station: string; + product_name: string; + amount: string; + price_real: string; + sum_real: string; +} + +export interface TransactionRequest { + start_date?: string; + end_date?: string; + page: number; + limit: number; +} diff --git a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx index 062b643..977b5fc 100644 --- a/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/corporate-dashboard/index.tsx @@ -1,11 +1,14 @@ 'use client'; import { deleteCookie } from 'cookies-next'; -import { subMonths } from 'date-fns'; import { Building2, LogOut, Wallet } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; -import { useFetchMyCorporateInfoQuery } from '@/entities/corporate/api/corporate.api'; +import { + useFetchCorporateTransactionsQuery, + useFetchMyCorporateInfoQuery, +} from '@/entities/corporate/api/corporate.api'; +import { TransactionRequest } from '@/entities/transactions/model/types'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { Button } from '@/shared/shadcn-ui/button'; @@ -19,88 +22,23 @@ import { import { TransactionsTable } from '@/widgets/transactions-table'; -// import { CardsList } from '@/widgets/cards-list'; - -// Sample company data -const companyData = { - companyName: 'ООО «ТаджикТранс»', - numberOfCards: 12, - fund: 25000, - overdraft: 5000, - totalFund: 30000, - registrationDate: '10.03.2019', -}; - -// Sample transaction data -const generateTransactions = () => { - const stations = [ - 'АЗС Душанбе-Центр', - 'АЗС Душанбе-Запад', - 'АЗС Душанбе-Восток', - 'АЗС Худжанд', - 'АЗС Куляб', - ]; - - const products = [ - { name: 'ДТ', price: 8.5 }, - { name: 'АИ-92', price: 9.2 }, - { name: 'АИ-95', price: 10.5 }, - { name: 'Z-100 Power', price: 11.8 }, - { name: 'Пропан', price: 6.3 }, - ]; - - const transactions = []; - - // Generate 50 random transactions over the last 6 months - for (let i = 0; i < 50; i++) { - const date = subMonths(new Date(), Math.random() * 6); - const station = stations[Math.floor(Math.random() * stations.length)]; - const product = products[Math.floor(Math.random() * products.length)]; - const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters - const cost = product.price; - const total = quantity * cost; - - transactions.push({ - id: i + 1, - date, - station, - product: product.name, - quantity, - cost, - total, - }); - } - - // Sort by date (newest first) - return transactions.sort((a, b) => b.date.getTime() - a.date.getTime()); -}; - -const transactions = generateTransactions(); - export function CorporateDashboard() { - const [startDate, setStartDate] = useState( - subMonths(new Date(), 1), - ); - const [endDate, setEndDate] = useState(new Date()); - const [filteredTransactions, setFilteredTransactions] = - useState(transactions); - - // Filter transactions by date range - const filterTransactions = () => { - if (!startDate || !endDate) return; - - const filtered = transactions.filter((transaction) => { - const transactionDate = new Date(transaction.date); - return transactionDate >= startDate && transactionDate <= endDate; - }); - - setFilteredTransactions(filtered); - }; - const { t } = useTextController(); const { data, isLoading } = useFetchMyCorporateInfoQuery({}); + const [request, setTransactionFetchRequest] = useState({ + limit: 10, + page: 1, + }); + + const { data: transactionsResponse, refetch } = + useFetchCorporateTransactionsQuery(request); + + useEffect(() => { + refetch(); + }, [request]); + return (
@@ -221,11 +159,12 @@ export function CorporateDashboard() {
- {/* */} - - {/* Transactions */} - - + {transactionsResponse && ( + + )} diff --git a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx index ea7d221..7a0ad8d 100644 --- a/src/pages-templates/(dashboard)/customer-dashboard/index.tsx +++ b/src/pages-templates/(dashboard)/customer-dashboard/index.tsx @@ -2,8 +2,13 @@ import { deleteCookie } from 'cookies-next'; import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react'; +import { useEffect, useState } from 'react'; -import { useFetchMyBonusInfoQuery } from '@/entities/bonus/api/bonus.api'; +import { + useFetchBonusTransactionsQuery, + useFetchMyBonusInfoQuery, +} from '@/entities/bonus/api/bonus.api'; +import { TransactionRequest } from '@/entities/transactions/model/types'; import Loader from '@/shared/components/loader'; import { useTextController } from '@/shared/language/hooks/use-text-controller'; @@ -21,8 +26,20 @@ import { TransactionsTable } from '@/widgets/transactions-table'; export function CustomerDashboard() { const { t } = useTextController(); + const [request, setTransactionFetchRequest] = useState({ + limit: 10, + page: 1, + }); + const { data, isLoading } = useFetchMyBonusInfoQuery({}); + const { data: transactionsResponse, refetch } = + useFetchBonusTransactionsQuery(request); + + useEffect(() => { + refetch(); + }, [request]); + return (
@@ -131,7 +148,12 @@ export function CustomerDashboard() {
- + {transactionsResponse && ( + + )} diff --git a/src/widgets/pagination.tsx b/src/widgets/pagination.tsx new file mode 100644 index 0000000..972a33e --- /dev/null +++ b/src/widgets/pagination.tsx @@ -0,0 +1,118 @@ +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; +import * as React from 'react'; + +import { cn } from '@/shared/lib/utils'; +import { ButtonProps, buttonVariants } from '@/shared/shadcn-ui/button'; + +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +