From 4da24e78742aa4f1392555116fb91777e3aea3b2 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Thu, 14 Aug 2025 22:28:16 +0100 Subject: [PATCH 01/33] created skeleton flow --- .env.example | 2 + .gitignore | 2 + USAGE.md | 2 + package.json | 59 + src/.gitignore | 5 + src/constants/index.ts | 31 + src/controllers/auth.controller.ts | 14 + src/middlewares/asyncHandler.ts | 6 + src/middlewares/errorHandler.ts | 11 + src/middlewares/index.ts | 0 .../20250813111215_init/migration.sql | 14 + src/prisma/migrations/migration_lock.toml | 3 + src/prisma/schema.prisma | 24 + src/routes/auth.routes.ts | 10 + src/routes/index.ts | 10 + src/server.ts | 45 + src/services/auth.service.ts | 11 + src/utils/ApiResponse.ts | 9 + src/utils/AppError.ts | 11 + src/utils/globalErrorHandler.ts | 31 + tsconfig.json | 29 + yarn.lock | 1648 +++++++++++++++++ 22 files changed, 1977 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 USAGE.md create mode 100644 package.json create mode 100644 src/.gitignore create mode 100644 src/constants/index.ts create mode 100644 src/controllers/auth.controller.ts create mode 100644 src/middlewares/asyncHandler.ts create mode 100644 src/middlewares/errorHandler.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/prisma/migrations/20250813111215_init/migration.sql create mode 100644 src/prisma/migrations/migration_lock.toml create mode 100644 src/prisma/schema.prisma create mode 100644 src/routes/auth.routes.ts create mode 100644 src/routes/index.ts create mode 100644 src/server.ts create mode 100644 src/services/auth.service.ts create mode 100644 src/utils/ApiResponse.ts create mode 100644 src/utils/AppError.ts create mode 100644 src/utils/globalErrorHandler.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8f6e87b --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NODE_ENV="" +DATABASE_URL="" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..06969de --- /dev/null +++ b/USAGE.md @@ -0,0 +1,2 @@ +documentation how to use the api goes here... +- the database linked to this project will expire on September 13, 2025. diff --git a/package.json b/package.json new file mode 100644 index 0000000..4d8ed6c --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "backend-test", + "version": "1.0.0", + "description": "backend-coding-exercise", + "main": "src/server.ts", + "repository": "https://github.com/Dunsin-cyber/backend-test.git", + "author": "Dunsin ", + "license": "MIT", + "private": false, + "scripts": { + "clean": "rimraf dist", + "prestart": "node dist/constants/index.js", + "dev": "npm run db:deploy && npm run db:init && nodemon --legacy-watch -r tsconfig-paths/register src/server.ts", + "build": "npm run clean && tsc && cp src/docs/index.yaml dist/docs/index.yaml && cp -r src/prisma/ dist/prisma/", + "dummy": "npm run db:migrate", + "db:deploy": "cd ./src && npx prisma migrate deploy", + "db:reset": "cd ./src && npx prisma migrate reset --force", + "db:migrate": "cd ./src && npx prisma migrate dev --name init", + "db:init": "cd ./src && npx prisma generate", + "start": "node dist/server.js", + "start:prod": "npm run db:deploy:prod && npm run start", + "db:deploy:prod": "cd ./dist && npx prisma migrate deploy", + "docker:dev": "docker-compose -f docker-compose.dev.yml up --build" + }, + "_moduleAliases": { + "@": "./src" + }, + "dependencies": { + "@prisma/client": "^6.14.0", + "@prisma/extension-accelerate": "^2.0.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-session": "^1.18.1", + "jsonwebtoken": "^9.0.2", + "module-alias": "^2.2.3", + "morgan": "^1.10.0", + "tsconfig-paths": "^4.2.0", + "ulid": "^3.0.0" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-session": "^1.18.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/module-alias": "^2.0.4", + "@types/morgan": "^1.9.9", + "@types/node": "^22.13.10", + "@types/yamljs": "^0.2.34", + "nodemon": "^3.1.9", + "prisma": "^6.14.0", + "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.2" + } +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..126419d --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,5 @@ +node_modules +# Keep environment variables out of version control +.env + +/src/generated/prisma diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..f34d92b --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,31 @@ +import dotenv from 'dotenv'; +// import { CipherKey } from 'crypto'; +dotenv.config(); + + +// MAKES SURE THAT ALL VARIABLES ARE IN ENV BEFPRE APP STARTS +// ? DATABASE_URL won't be exported because the new format has a "postgres" prifix which +// ? doesnt parse wellif imported through this format +const requiredEnvVars = [ + 'NODE_ENV', +]; + +const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]); + +if (missingEnvVars.length > 0) { + console.error(`❌ Missing required environment variables: ${missingEnvVars.join(', ')}`); + process.exit(1); +} + + + +// ! SYSTEM CREDENTIALS +const NODE_ENV = process.env.NODE_ENV +const PORT = process.env.PORT || 3000; + + +export const config = { + NODE_ENV, + PORT, + +} \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..20c41b3 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,14 @@ +import { asyncHandler } from '@/middlewares/asyncHandler'; +import { ApiResponse } from '@/utils/ApiResponse'; +import { NextFunction, Request, Response } from 'express'; +import { createUser } from '@/services/auth.service'; + + +export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + // TODO: user input validation + const user = await createUser(req.body); + // Send access token and user data + return res.status(200).json(new ApiResponse("success", user)); + +}) + diff --git a/src/middlewares/asyncHandler.ts b/src/middlewares/asyncHandler.ts new file mode 100644 index 0000000..a9a6a3a --- /dev/null +++ b/src/middlewares/asyncHandler.ts @@ -0,0 +1,6 @@ +import { NextFunction, Request, Response } from "express"; + +export const asyncHandler = (fn: Function) => + (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts new file mode 100644 index 0000000..719841f --- /dev/null +++ b/src/middlewares/errorHandler.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from "express"; +import { ApiResponse } from "@/utils/ApiResponse"; +import { AppError } from "@/utils/AppError"; + +export const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => { + console.error(err.message); + const statusCode = err.statusCode || 500; + const message = err.message /* || "Internal Server Error" */; + + res.status(statusCode).json(new ApiResponse("fail", message)); +}; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/prisma/migrations/20250813111215_init/migration.sql b/src/prisma/migrations/20250813111215_init/migration.sql new file mode 100644 index 0000000..e6f03db --- /dev/null +++ b/src/prisma/migrations/20250813111215_init/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "public"."User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); diff --git a/src/prisma/migrations/migration_lock.toml b/src/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma new file mode 100644 index 0000000..19eddd7 --- /dev/null +++ b/src/prisma/schema.prisma @@ -0,0 +1,24 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" + // output = "../" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + email String @unique + password String + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts new file mode 100644 index 0000000..8706a05 --- /dev/null +++ b/src/routes/auth.routes.ts @@ -0,0 +1,10 @@ +import express from 'express'; +import { handleCreateAcc } from "@/controllers/auth.controller" +const router = express.Router(); + + + +router.post('/create', handleCreateAcc) + + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..39ea203 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,10 @@ +import express from 'express'; +import authRoutes from './auth.routes'; + + +const router = express.Router(); + + +router.use("/auth", authRoutes); + +export default router \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..7161618 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,45 @@ +import 'module-alias/register'; +import express from 'express'; +import cors from 'cors'; +import V1Routes from '@/routes/index'; +import { errorHandler } from "@/middlewares/errorHandler"; +import morgan from "morgan" +import { config } from "@/constants/index" +import { AppError } from "@/utils/AppError"; +import cookieParser from "cookie-parser"; + + +const app = express(); + + +const corsOptions = { + origin: "*", + credentials: true, // Allow credentials (cookies) + optionsSuccessStatus: 200 // Some legacy browsers choke on 204 +}; + +app.use(cors(corsOptions)); +app.use(express.json()); +app.use(morgan('dev')) +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); + + + + +app.use('/api/v1', V1Routes); + +// 404 Handler +app.all('*', (req, res, next) => { + next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404)); +}); + +app.use(errorHandler) + + + +app.listen(config.PORT, () => { + // Your application code here + console.log('Application started with config Loaded up✅'); + console.log(`Server running on port ${config.PORT}`); +}); \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..67faee7 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,11 @@ +import { PrismaClient, User } from '@prisma/client' + +const prisma = new PrismaClient() + + +export const createUser = async (data: User) => { + const user = await prisma.user.create({ + data + }); + return user +} diff --git a/src/utils/ApiResponse.ts b/src/utils/ApiResponse.ts new file mode 100644 index 0000000..c1af413 --- /dev/null +++ b/src/utils/ApiResponse.ts @@ -0,0 +1,9 @@ +export class ApiResponse { + status: "success" | "fail"; + data: T | string; + + constructor(status: "success" | "fail", data: T | string) { + this.status = status; + this.data = data; + } +} diff --git a/src/utils/AppError.ts b/src/utils/AppError.ts new file mode 100644 index 0000000..d242814 --- /dev/null +++ b/src/utils/AppError.ts @@ -0,0 +1,11 @@ +export class AppError extends Error { + public statusCode: number; + public status: "fail" | "error"; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.status = statusCode < 500 ? "fail" : "error"; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/utils/globalErrorHandler.ts b/src/utils/globalErrorHandler.ts new file mode 100644 index 0000000..e81d957 --- /dev/null +++ b/src/utils/globalErrorHandler.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; + +const globalErrorHandler = ( + err: any, + req: Request, + res: Response, + next: NextFunction +) => { + // Default error values + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + + // Handle operational errors (e.g., invalid input, missing resource) + if (err.isOperational) { + res.status(err.statusCode).json({ + status: err.status, + message: err.message, + }); + } else { + // Log the error for debugging + console.error('ERROR 💥', err); + + // Send a generic response for non-operational errors + res.status(500).json({ + status: 'error', + message: 'Something went wrong!', + }); + } +}; + +export default globalErrorHandler; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d740d20 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "./", // Base directory for imports + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "src/**/*" + ], + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..9f2b6fd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1648 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@prisma/client@^6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-6.14.0.tgz#52b6aa169bb596a1aa9cab9a158a03765ffea68b" + integrity sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw== + +"@prisma/config@6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/config/-/config-6.14.0.tgz#33f820a182c5eaa85dd6dd705c84d878c978a666" + integrity sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ== + dependencies: + c12 "3.1.0" + deepmerge-ts "7.1.5" + effect "3.16.12" + empathic "2.0.0" + +"@prisma/debug@6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.14.0.tgz#9dbf781cf2b2c942b9fc7eec6eba22f9015f1a8c" + integrity sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug== + +"@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49": + version "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz#ecca349e575cc473b3c9b95d9493b07fdccea001" + integrity sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA== + +"@prisma/engines@6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-6.14.0.tgz#b43c850e942db79e9d3923c020b891671b30da57" + integrity sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w== + dependencies: + "@prisma/debug" "6.14.0" + "@prisma/engines-version" "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49" + "@prisma/fetch-engine" "6.14.0" + "@prisma/get-platform" "6.14.0" + +"@prisma/extension-accelerate@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@prisma/extension-accelerate/-/extension-accelerate-2.0.2.tgz#2a8afac940413fb814b50ce9015dac049a783503" + integrity sha512-yZK6/k7uOEFpEsKoZezQS1CKDboPtBCQ0NyI70e1Un8tDiRgg80iWGyjsJmRpps2ZIut3MroHP+dyR3wVKh8lA== + +"@prisma/fetch-engine@6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz#f0cf3df2b1bfe65bb0904fb7da06b25dddea6f4b" + integrity sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg== + dependencies: + "@prisma/debug" "6.14.0" + "@prisma/engines-version" "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49" + "@prisma/get-platform" "6.14.0" + +"@prisma/get-platform@6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-6.14.0.tgz#fc86665a299b8aa9c6b1546dff847a2289854dfb" + integrity sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww== + dependencies: + "@prisma/debug" "6.14.0" + +"@standard-schema/spec@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" + integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/body-parser@*": + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie-parser@^1.4.8": + version "1.4.9" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.9.tgz#f0e79c766a58ee7369a52e7509b3840222f68ed2" + integrity sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g== + +"@types/cors@^2.8.17": + version "2.8.19" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" + integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^5.0.0": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-session@^1.18.1": + version "1.18.2" + resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.18.2.tgz#778dc3296da9aa97d5bf8e42358a54c52a230317" + integrity sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg== + dependencies: + "@types/express" "*" + +"@types/express@*", "@types/express@^5.0.0": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + +"@types/jsonwebtoken@^9.0.9": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" + integrity sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA== + dependencies: + "@types/ms" "*" + "@types/node" "*" + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/module-alias@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/module-alias/-/module-alias-2.0.4.tgz#c6a784be6bc2ff5424889e23084ac001454d5f00" + integrity sha512-5+G/QXO/DvHZw60FjvbDzO4JmlD/nG5m2/vVGt25VN1eeP3w2bCoks1Wa7VuptMPM1TxJdx6RjO70N9Fw0nZPA== + +"@types/morgan@^1.9.9": + version "1.9.10" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.10.tgz#725c15d95a5e6150237524cd713bc2d68f9edf1a" + integrity sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA== + dependencies: + "@types/node" "*" + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node@*": + version "24.2.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.2.1.tgz#83e41543f0a518e006594bb394e2cd961de56727" + integrity sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ== + dependencies: + undici-types "~7.10.0" + +"@types/node@^22.13.10": + version "22.17.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.1.tgz#484a755050497ebc3b37ff5adb7470f2e3ea5f5b" + integrity sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA== + dependencies: + undici-types "~6.21.0" + +"@types/qs@*": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/yamljs@^0.2.34": + version "0.2.34" + resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.34.tgz#c10b1f31b173f2cc93342f27b0796c2eb5b3ae84" + integrity sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +c12@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/c12/-/c12-3.1.0.tgz#9e237970e1d3b74ebae51d25945cb59664c12c89" + integrity sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw== + dependencies: + chokidar "^4.0.3" + confbox "^0.2.2" + defu "^6.1.4" + dotenv "^16.6.1" + exsolve "^1.0.7" + giget "^2.0.0" + jiti "^2.4.2" + ohash "^2.0.11" + pathe "^2.0.3" + perfect-debounce "^1.0.0" + pkg-types "^2.2.0" + rc9 "^2.1.2" + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confbox@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + +consola@^3.2.3, consola@^3.4.0, consola@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie-signature@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookie@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +deepmerge-ts@7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz#ff818564007f5c150808d2b7b732cac83aa415ab" + integrity sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw== + +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destr@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dotenv@^16.4.7, dotenv@^16.6.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +effect@3.16.12: + version "3.16.12" + resolved "https://registry.yarnpkg.com/effect/-/effect-3.16.12.tgz#3762f745846cfa4905512e397e17f683438addbe" + integrity sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg== + dependencies: + "@standard-schema/spec" "^1.0.0" + fast-check "^3.23.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +empathic@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/empathic/-/empathic-2.0.0.tgz#71d3c2b94fad49532ef98a6c34be0386659f6131" + integrity sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express-session@^1.18.1: + version "1.18.2" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.2.tgz#34db6252611b57055e877036eea09b4453dec5d8" + integrity sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.7" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.1.0" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + +express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +exsolve@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e" + integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw== + +fast-check@^3.23.1: + version "3.23.2" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-3.23.2.tgz#0129f1eb7e4f500f58e8290edc83c670e4a574a2" + integrity sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A== + dependencies: + pure-rand "^6.1.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +foreground-child@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +giget@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/giget/-/giget-2.0.0.tgz#395fc934a43f9a7a29a29d55b99f23e30c14f195" + integrity sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA== + dependencies: + citty "^0.1.6" + consola "^3.4.0" + defu "^6.1.4" + node-fetch-native "^1.6.6" + nypm "^0.6.0" + pathe "^2.0.3" + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^11.0.0: + version "11.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" + integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== + dependencies: + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.0.3" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + +jiti@^2.4.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.5.1.tgz#bd099c1c2be1c59bbea4e5adcd127363446759d0" + integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== + +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9" + integrity sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lru-cache@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117" + integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +module-alias@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.3.tgz#ec2e85c68973bda6ab71ce7c93b763ec96053221" + integrity sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q== + +morgan@^1.10.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.1.tgz#4e02e6a4465a48e26af540191593955d17f61570" + integrity sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.1.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-fetch-native@^1.6.6: + version "1.6.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== + +nodemon@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nypm@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.6.1.tgz#4905b419641073de25ef0f19fb47c5658ada0c35" + integrity sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w== + dependencies: + citty "^0.1.6" + consola "^3.4.2" + pathe "^2.0.3" + pkg-types "^2.2.0" + tinyexec "^1.0.1" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +ohash@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" + integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-types@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.2.0.tgz#049bf404f82a66c465200149457acf0c5fb0fb2d" + integrity sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ== + dependencies: + confbox "^0.2.2" + exsolve "^1.0.7" + pathe "^2.0.3" + +prisma@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-6.14.0.tgz#818a7c721c269c7e8360ad5ab7bb94ab4d749007" + integrity sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA== + dependencies: + "@prisma/config" "6.14.0" + "@prisma/engines" "6.14.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pure-rand@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc9@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.1.2.tgz#6282ff638a50caa0a91a31d76af4a0b9cbd1080d" + integrity sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg== + dependencies: + defu "^6.1.4" + destr "^2.0.3" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rimraf@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e" + integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A== + dependencies: + glob "^11.0.0" + package-json-from-dist "^1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.5.3, semver@^7.5.4: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tinyexec@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" + integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.8.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== + +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + +ulid@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ulid/-/ulid-3.0.1.tgz#6fae1779938843f476702946931122d127cf3243" + integrity sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 20d5b03415d52e5f1a1d04c9c5d1abf3b5f11cb4 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 10:46:02 +0100 Subject: [PATCH 02/33] added api documantation --- USAGE.md | 3 + package.json | 12 ++- src/routes/auth.routes.ts | 24 ++++- src/server.ts | 7 +- src/utils/swagger.ts | 29 +++++ tsconfig.json | 7 +- yarn.lock | 216 +++++++++++++++++++++++++++++++++++++- 7 files changed, 287 insertions(+), 11 deletions(-) create mode 100644 src/utils/swagger.ts diff --git a/USAGE.md b/USAGE.md index 06969de..5a16bea 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,2 +1,5 @@ documentation how to use the api goes here... - the database linked to this project will expire on September 13, 2025. +- create a env file and put in the necessary values you see in env.exmaple there, after this, +- usage, run yarn install, and start up projects with yarn dev. +- to see api docusmentation esure the server is running then , go to this url http://localhost:[PORT]/api-docs/ diff --git a/package.json b/package.json index 4d8ed6c..4dc5f12 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "jsonwebtoken": "^9.0.2", "module-alias": "^2.2.3", "morgan": "^1.10.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "tsconfig-paths": "^4.2.0", "ulid": "^3.0.0" }, @@ -49,11 +51,17 @@ "@types/module-alias": "^2.0.4", "@types/morgan": "^1.9.9", "@types/node": "^22.13.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "@types/yamljs": "^0.2.34", - "nodemon": "^3.1.9", + "nodemon": "^3.1.10", "prisma": "^6.14.0", "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.8.2" + }, + "engines": { + "node": "22.14.0", + "yarn": "1.22.19" } -} +} \ No newline at end of file diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 8706a05..2e057b0 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -3,7 +3,29 @@ import { handleCreateAcc } from "@/controllers/auth.controller" const router = express.Router(); - +/** + * @swagger + * /api/auth/create: + * post: + * summary: Creates a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * password: + * type: string + * name: + * type: string + * responses: + * 200: + * description: Login successful + */ router.post('/create', handleCreateAcc) diff --git a/src/server.ts b/src/server.ts index 7161618..052fc46 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,8 @@ import morgan from "morgan" import { config } from "@/constants/index" import { AppError } from "@/utils/AppError"; import cookieParser from "cookie-parser"; +import swaggerUi from "swagger-ui-express"; +import swaggerSpec from "@/utils/swagger"; const app = express(); @@ -27,7 +29,8 @@ app.use(cookieParser()); -app.use('/api/v1', V1Routes); +app.use('/api', V1Routes); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // 404 Handler app.all('*', (req, res, next) => { @@ -39,7 +42,7 @@ app.use(errorHandler) app.listen(config.PORT, () => { - // Your application code here console.log('Application started with config Loaded up✅'); console.log(`Server running on port ${config.PORT}`); + console.log(`API documentation available at 📝📚 http://localhost:${config.PORT}/api-docs`); }); \ No newline at end of file diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts new file mode 100644 index 0000000..89c1aeb --- /dev/null +++ b/src/utils/swagger.ts @@ -0,0 +1,29 @@ +import swaggerJSDoc from "swagger-jsdoc"; +import path from 'path'; +import { config } from "@/constants/index" + + + + +const swaggerDefinition = { + openapi: "3.0.0", + info: { + title: "Paritie Backend Test", + version: "1.0.0", + description: "API documentation", + }, + servers: [ + { + url: `http://localhost:${config.PORT}`, + }, + ], +}; + +const options = { + swaggerDefinition, + apis: [path.join(__dirname, "../routes/**/*.ts")], +}; + +const swaggerSpec = swaggerJSDoc(options); + +export default swaggerSpec; diff --git a/tsconfig.json b/tsconfig.json index d740d20..78d5af2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { - "target": "ES2020", - "module": "commonjs", + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, "outDir": "./dist", "rootDir": "./src", "strict": true, - "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, diff --git a/yarn.lock b/yarn.lock index 9f2b6fd..da0c140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,38 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -51,6 +83,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@prisma/client@^6.14.0": version "6.14.0" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-6.14.0.tgz#52b6aa169bb596a1aa9cab9a158a03765ffea68b" @@ -107,6 +144,11 @@ dependencies: "@prisma/debug" "6.14.0" +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + "@standard-schema/spec@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c" @@ -190,6 +232,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/jsonwebtoken@^9.0.9": version "9.0.10" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz#a7932a47177dcd4283b6146f3bd5c26d82647f09" @@ -261,6 +308,19 @@ "@types/node" "*" "@types/send" "*" +"@types/swagger-jsdoc@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz#bb4f60f3a5f103818e022f2e29ff8935113fb83d" + integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== + +"@types/swagger-ui-express@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz#3c0e0bf2543c7efb500eaa081bfde6d92f88096c" + integrity sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g== + dependencies: + "@types/express" "*" + "@types/serve-static" "*" + "@types/yamljs@^0.2.34": version "0.2.34" resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.34.tgz#c10b1f31b173f2cc93342f27b0796c2eb5b3ae84" @@ -321,6 +381,11 @@ arg@^4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -420,6 +485,11 @@ call-bound@^1.0.2: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + chokidar@^3.5.2: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -461,6 +531,16 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -587,6 +667,13 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dotenv@^16.4.7, dotenv@^16.6.1: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" @@ -673,6 +760,11 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -779,6 +871,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -832,6 +929,18 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^11.0.0: version "11.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" @@ -889,7 +998,15 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -inherits@2.0.4: +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -945,6 +1062,13 @@ jiti@^2.4.2: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.5.1.tgz#bd099c1c2be1c59bbea4e5adcd127363446759d0" integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" @@ -983,6 +1107,11 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -993,6 +1122,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -1013,6 +1147,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -1072,7 +1211,7 @@ minimatch@^10.0.3: dependencies: "@isaacs/brace-expansion" "^5.0.0" -minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1125,7 +1264,7 @@ node-fetch-native@^1.6.6: resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== -nodemon@^3.1.9: +nodemon@^3.1.10: version "3.1.10" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== @@ -1191,6 +1330,13 @@ on-headers@~1.1.0: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" @@ -1201,6 +1347,11 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -1507,6 +1658,39 @@ supports-color@^5.5.0: dependencies: has-flag "^3.0.0" +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.27.1" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz#556e77c659752e99621ac61ad5ef6cb0832279e7" + integrity sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA== + dependencies: + "@scarf/scarf" "=1.4.0" + +swagger-ui-express@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz#fb8c1b781d2793a6bd2f8a205a3f4bd6fa020dd8" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + tinyexec@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.1.tgz#70c31ab7abbb4aea0a24f55d120e5990bfa1e0b1" @@ -1612,6 +1796,11 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +validator@^13.7.0: + version "13.15.15" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.15.15.tgz#246594be5671dc09daa35caec5689fcd18c6e7e4" + integrity sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -1642,7 +1831,28 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0" From 12dce25f4b5e60843b960f644e0369415403b730 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 11:14:47 +0100 Subject: [PATCH 03/33] add JWT --- .env.example | 2 ++ package.json | 5 ++-- src/constants/index.ts | 10 +++++++- src/controllers/auth.controller.ts | 34 ++++++++++++++++++++++--- src/routes/auth.routes.ts | 20 ++++++++++++--- src/services/auth.service.ts | 16 +++++++++++- src/utils/index.ts | 40 ++++++++++++++++++++++++++++++ yarn.lock | 5 ++++ 8 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 src/utils/index.ts diff --git a/.env.example b/.env.example index 8f6e87b..e04b1c7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ NODE_ENV="" DATABASE_URL="" +JWT_REFRESH_SECRET="" +JWT_SECRET="" \ No newline at end of file diff --git a/package.json b/package.json index 4dc5f12..d26017c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dependencies": { "@prisma/client": "^6.14.0", "@prisma/extension-accelerate": "^2.0.2", + "bcryptjs": "^3.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "crypto": "^1.0.1", @@ -62,6 +63,6 @@ }, "engines": { "node": "22.14.0", - "yarn": "1.22.19" + "yarn": "^1.22.19" } -} \ No newline at end of file +} diff --git a/src/constants/index.ts b/src/constants/index.ts index f34d92b..a2f94d3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -8,6 +8,8 @@ dotenv.config(); // ? doesnt parse wellif imported through this format const requiredEnvVars = [ 'NODE_ENV', + 'JWT_REFRESH_SECRET', + 'JWT_SECRET' ]; const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]); @@ -19,13 +21,19 @@ if (missingEnvVars.length > 0) { -// ! SYSTEM CREDENTIALS +// ? SYSTEM CREDENTIALS const NODE_ENV = process.env.NODE_ENV const PORT = process.env.PORT || 3000; +// ? JWT CREDENTIALS +const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; +const JWT_SECRET = process.env.JWT_SECRET; + export const config = { NODE_ENV, PORT, + JWT_REFRESH_SECRET, + JWT_SECRET } \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 20c41b3..4200046 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,14 +1,42 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; +import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; import { createUser } from '@/services/auth.service'; +import utils from '@/utils/index'; +import jwt from 'jsonwebtoken'; +import { config } from '@/constants'; export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - // TODO: user input validation + const validInput = utils.validateCreateUserInput(req.body); + if (!validInput) { + throw (new AppError("Invalid input data", 400)); + } const user = await createUser(req.body); - // Send access token and user data - return res.status(200).json(new ApiResponse("success", user)); + + + const accessToken = jwt.sign( + { userId: user.id }, + config.JWT_SECRET!, + { expiresIn: "15m" } + ); + + const refreshToken = jwt.sign( + { userId: user.id }, + config.JWT_REFRESH_SECRET!, + { expiresIn: "7d" } + ); + + // Set refresh token as an HTTP-only cookie + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + secure: config.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + return res.status(200).json(new ApiResponse("success", {user, accessToken})); }) diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 2e057b0..557c29e 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -3,7 +3,7 @@ import { handleCreateAcc } from "@/controllers/auth.controller" const router = express.Router(); -/** +/** * @swagger * /api/auth/create: * post: @@ -16,16 +16,28 @@ const router = express.Router(); * schema: * type: object * properties: + * name: + * type: string + * description: Must be at least 2 characters long, only letters and spaces allowed + * minLength: 2 + * pattern: '^[A-Za-z ]+$' + * example: Dunsin * email: * type: string + * description: Must be a valid email address + * format: email + * example: dunsin@example.com * password: * type: string - * name: - * type: string + * description: Must be at least 8 characters long, contain one uppercase letter, one lowercase letter, and one number + * minLength: 8 + * pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$' + * example: StrongPass123 * responses: * 200: - * description: Login successful + * description: User created successfully */ + router.post('/create', handleCreateAcc) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 67faee7..e5cc470 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,11 +1,25 @@ import { PrismaClient, User } from '@prisma/client' +import utils from '@/utils/index'; const prisma = new PrismaClient() +// TODO: check if user exists first to thrpw a better error message export const createUser = async (data: User) => { + const { password } = data; + const hashedPassword = await utils.hashPassword(password); + const user = await prisma.user.create({ - data + data: { + ...data, password: hashedPassword + }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true + } }); return user } diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e4dc56e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,40 @@ +import { hash, compare } from 'bcryptjs'; +const utils = { + hashPassword: async (password: string): Promise => { + const saltRounds = 10; + return await hash(password, saltRounds); + }, + decryptPassword: async (hashedPassword: string, password: string): Promise => { + return await compare(password, hashedPassword); + }, + validEmail: (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }, + validPassword: (password: string): boolean => { + // Example validation: at least 8 characters, one uppercase, one lowercase, one number + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$/; + return passwordRegex.test(password); + }, + validName: (name: string): boolean => { + // Example validation: at least 2 characters, only letters and spaces + const nameRegex = /^[A-Za-z\s]{2,}$/; + return nameRegex.test(name); + }, + validateCreateUserInput: (data: { email: string; password: string; name: string }): boolean => { + return utils.validEmail(data.email) && utils.validPassword(data.password) && utils.validName(data.name); + } + + +}; + + + + + + + + + + +export default utils \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index da0c140..6c543c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -403,6 +403,11 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" +bcryptjs@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.2.tgz#caadcca1afefe372ed6e20f86db8e8546361c1ca" + integrity sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" From 0d132a8e383c511ef405235bb292e489454a66ab Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 11:32:27 +0100 Subject: [PATCH 04/33] feat: signup and login complete --- src/controllers/auth.controller.ts | 37 +++++++++++++++++++++++++++++- src/routes/auth.routes.ts | 33 +++++++++++++++++++++++++- src/services/auth.service.ts | 23 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 4200046..e5738b6 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -2,7 +2,7 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; -import { createUser } from '@/services/auth.service'; +import { createUser, getUser } from '@/services/auth.service'; import utils from '@/utils/index'; import jwt from 'jsonwebtoken'; import { config } from '@/constants'; @@ -40,3 +40,38 @@ export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, }) + +export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + //TODO: check if both params are wrong too + if (!utils.validPassword(req.body.password) || !utils.validEmail(req.body.email)) { + throw (new AppError("Invalid input data", 400)); + } + + + const user = await getUser(req.body); + + + const accessToken = jwt.sign( + { userId: user.id }, + config.JWT_SECRET!, + { expiresIn: "15m" } + ); + + const refreshToken = jwt.sign( + { userId: user.id }, + config.JWT_REFRESH_SECRET!, + { expiresIn: "7d" } + ); + + // Set refresh token as an HTTP-only cookie + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + secure: config.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + return res.status(200).json(new ApiResponse("success", { user, accessToken })); + +}) + diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 557c29e..f191999 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { handleCreateAcc } from "@/controllers/auth.controller" +import { handleCreateAcc, handleLoginAcc } from "@/controllers/auth.controller" const router = express.Router(); @@ -40,5 +40,36 @@ const router = express.Router(); router.post('/create', handleCreateAcc) +/** + * @swagger + * /api/auth/login: + * post: + * summary: Login user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * description: Must be a valid email address + * format: email + * example: dunsin@example.com + * password: + * type: string + * description: Must be at least 8 characters long, contain one uppercase letter, one lowercase letter, and one number + * minLength: 8 + * pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$' + * example: StrongPass123 + * responses: + * 200: + * description: User created successfully + */ + +router.post('/login', handleLoginAcc) + export default router; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index e5cc470..f0cc603 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,5 +1,6 @@ import { PrismaClient, User } from '@prisma/client' import utils from '@/utils/index'; +import { AppError } from '@/utils/AppError'; const prisma = new PrismaClient() @@ -23,3 +24,25 @@ export const createUser = async (data: User) => { }); return user } + + + +export const getUser = async (data: { email: string; password: string }) => { + const userWithPassword = await prisma.user.findUnique({ + where: { email: data.email }, + }); + + if (!userWithPassword) { + throw new AppError("Invalid email or password", 401); + } + + + const isMatch = await utils.decryptPassword(userWithPassword.password, data.password); + if (!isMatch) { + throw new AppError("Invalid email or password",401); + } + + + const { password, ...safeUser } = userWithPassword; + return safeUser; +}; From 99ad67b8be9748e12956c353ba1769b62f4a3bc8 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 13:53:34 +0100 Subject: [PATCH 05/33] add new migration, middleware, controllers and serices --- USAGE.md | 2 + package.json | 2 +- src/controllers/transaction.controller.ts | 46 ++++++++ src/middlewares/index.ts | 28 +++++ .../migration.sql | 48 ++++++++ src/prisma/schema.prisma | 43 +++++++- src/routes/auth.routes.ts | 4 +- src/routes/index.ts | 4 + src/routes/transaction.routes.ts | 80 ++++++++++++++ src/services/auth.service.ts | 35 +++++- src/services/transaction.service.ts | 103 ++++++++++++++++++ src/types/express/index.d.ts | 13 +++ src/utils/index.ts | 7 +- 13 files changed, 402 insertions(+), 13 deletions(-) create mode 100644 src/controllers/transaction.controller.ts create mode 100644 src/prisma/migrations/20250815104938_add_other_models/migration.sql create mode 100644 src/routes/transaction.routes.ts create mode 100644 src/services/transaction.service.ts create mode 100644 src/types/express/index.d.ts diff --git a/USAGE.md b/USAGE.md index 5a16bea..f34fe9d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -3,3 +3,5 @@ documentation how to use the api goes here... - create a env file and put in the necessary values you see in env.exmaple there, after this, - usage, run yarn install, and start up projects with yarn dev. - to see api docusmentation esure the server is running then , go to this url http://localhost:[PORT]/api-docs/ +- every user has a default 100,000 balcne in theor wallet, that they can use th=i make trancations +- send funds to beneficiary by email diff --git a/package.json b/package.json index d26017c..d1efab0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dummy": "npm run db:migrate", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", - "db:migrate": "cd ./src && npx prisma migrate dev --name init", + "db:migrate": "cd ./src && npx prisma migrate dev --name add_other_models", "db:init": "cd ./src && npx prisma generate", "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts new file mode 100644 index 0000000..ac22457 --- /dev/null +++ b/src/controllers/transaction.controller.ts @@ -0,0 +1,46 @@ +import { asyncHandler } from '@/middlewares/asyncHandler'; +import { ApiResponse } from '@/utils/ApiResponse'; +import { AppError } from '@/utils/AppError'; +import { NextFunction, Request, Response } from 'express'; +import { setTransactionPIN, createDonation } from '@/services/transaction.service'; +import { getUserPrivateFn } from '@/services/auth.service'; + +import utils from '@/utils/index'; +import { User } from '@prisma/client'; + + + +export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const validInput = utils.validPIN(req.body); + if (!validInput) { + throw (new AppError("Invalid pin format", 400)); + } + + const user = (req as Request & { user?: User }).user! + await setTransactionPIN(user.id, req.body); + + + return res.status(200).json(new ApiResponse("success", "Pin created successfully")); + +}) + + +export const handleCreateDonation = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + //TODO: check if both params are wrong too + const { amount, beneficiaryEmail } = req.body + if (!amount || !utils.validEmail(beneficiaryEmail)) { + throw (new AppError("Invalid input data", 400)); + } + + const beneficiary = await getUserPrivateFn(req.body.beneficiaryEmail); + + + + const user = (req as Request & { user?: User }).user! + const donation = await createDonation(user.id, beneficiary.id, req.body.amount); + + + return res.status(200).json(new ApiResponse("success", donation)); + +}) + diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index e69de29..6abe629 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -0,0 +1,28 @@ +import { AppError } from "@/utils/AppError"; +import { NextFunction, Request, Response } from "express"; +import { asyncHandler } from "./asyncHandler"; +import jwt from "jsonwebtoken"; +import { config } from "@/constants"; +import { getUserById } from "@/services/auth.service"; +import { User } from '@prisma/client'; + + +export const ensureAuthenticated = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.split(" ")[1]; + console.log("token", token) + + if (!token) { + throw new AppError("Not authenticated", 401); + } + + const decoded = jwt.verify(token, config.JWT_SECRET!) as { userId: string }; + const user = await getUserById(decoded.userId) + if (user) { + // req.user = user; + (req as Request & {user: User}).user = user; + next(); + } else { + throw new AppError("User does not exist", 401); + + } +}) \ No newline at end of file diff --git a/src/prisma/migrations/20250815104938_add_other_models/migration.sql b/src/prisma/migrations/20250815104938_add_other_models/migration.sql new file mode 100644 index 0000000..9cfc391 --- /dev/null +++ b/src/prisma/migrations/20250815104938_add_other_models/migration.sql @@ -0,0 +1,48 @@ +-- AlterTable +ALTER TABLE "public"."User" ADD COLUMN "transactionPIN" TEXT; + +-- CreateTable +CREATE TABLE "public"."Wallet" ( + "balance" DOUBLE PRECISION NOT NULL DEFAULT 100000, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Wallet_pkey" PRIMARY KEY ("userId") +); + +-- CreateTable +CREATE TABLE "public"."Donation" ( + "id" TEXT NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "donorId" TEXT NOT NULL, + "beneficiaryId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Donation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Wallet_userId_key" ON "public"."Wallet"("userId"); + +-- CreateIndex +CREATE INDEX "Wallet_userId_idx" ON "public"."Wallet"("userId"); + +-- CreateIndex +CREATE INDEX "Donation_donorId_idx" ON "public"."Donation"("donorId"); + +-- CreateIndex +CREATE INDEX "Donation_beneficiaryId_idx" ON "public"."Donation"("beneficiaryId"); + +-- CreateIndex +CREATE INDEX "User_email_idx" ON "public"."User"("email"); + +-- AddForeignKey +ALTER TABLE "public"."Wallet" ADD CONSTRAINT "Wallet_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Donation" ADD CONSTRAINT "Donation_donorId_fkey" FOREIGN KEY ("donorId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Donation" ADD CONSTRAINT "Donation_beneficiaryId_fkey" FOREIGN KEY ("beneficiaryId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 19eddd7..5f52370 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -6,7 +6,6 @@ generator client { provider = "prisma-client-js" - // output = "../" } datasource db { @@ -15,10 +14,44 @@ datasource db { } model User { - id String @id @default(uuid()) - email String @unique - password String - name String + id String @id @default(uuid()) + email String @unique + password String + name String + transactionPIN String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + wallet Wallet? + donations Donation[] + received Donation[] @relation("UserReceivedDonations") + + @@index([email]) +} + +model Wallet { + balance Float @default(100000) + userId String @id @unique + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([userId]) +} + +model Donation { + id String @id @default(uuid()) + amount Float + donorId String + beneficiaryId String + donor User @relation(fields: [donorId], references: [id]) + beneficiary User @relation("UserReceivedDonations", fields: [beneficiaryId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([donorId]) + @@index([beneficiaryId]) } diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index f191999..3abd099 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -34,7 +34,7 @@ const router = express.Router(); * pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$' * example: StrongPass123 * responses: - * 200: + * 201: * description: User created successfully */ @@ -66,7 +66,7 @@ router.post('/create', handleCreateAcc) * example: StrongPass123 * responses: * 200: - * description: User created successfully + * description: Login successfully */ router.post('/login', handleLoginAcc) diff --git a/src/routes/index.ts b/src/routes/index.ts index 39ea203..fd72fff 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,10 +1,14 @@ import express from 'express'; import authRoutes from './auth.routes'; +import transactionRoutes from './transaction.routes'; +import { ensureAuthenticated } from "@/middlewares/index" + const router = express.Router(); router.use("/auth", authRoutes); +router.use("/tx", ensureAuthenticated, transactionRoutes) export default router \ No newline at end of file diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts new file mode 100644 index 0000000..690f948 --- /dev/null +++ b/src/routes/transaction.routes.ts @@ -0,0 +1,80 @@ +import express from 'express'; +import { handleCreateTxPIN, handleCreateDonation } from "@/controllers/transaction.controller" +const router = express.Router(); + + +/** + * @swagger + * /api/tx/pin: + * post: + * summary: Creates a new user + * tags: [Donation] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * pin: + * type: string + * description: Must be exactly 4 characters long, contain only numbers + * minLength: 4 + * example: 1234 + * responses: + * 200: + * description: Tx Pin created successfully + */ + +router.post('/pin', handleCreateTxPIN) + + +/** + * @swagger + * /api/tx/create-donation: + * post: + * summary: Create a donation transaction + * tags: [Donation] + * parameters: + * - in: query + * name: transactionPin + * schema: + * type: string + * minLength: 4 + * maxLength: 6 + * required: true + * description: User's 4–6 digit transaction PIN + * example: "1234" + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - amount + * - beneficiaryId + * properties: + * amount: + * type: number + * description: Amount to donate + * example: 50.0 + * beneficiaryId: + * type: string + * description: ID of the beneficiary receiving the donation + * example: "clxyz123abc456def789ghi0" + * responses: + * 201: + * description: Donation created successfully + * 400: + * description: Invalid request payload or parameters + * 401: + * description: Unauthorized — invalid transaction PIN + * 500: + * description: Internal server error + */ + + +router.post('/create-donation', handleCreateDonation) + +export default router; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index f0cc603..f1ae313 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -35,14 +35,41 @@ export const getUser = async (data: { email: string; password: string }) => { if (!userWithPassword) { throw new AppError("Invalid email or password", 401); } - - const isMatch = await utils.decryptPassword(userWithPassword.password, data.password); if (!isMatch) { - throw new AppError("Invalid email or password",401); + throw new AppError("Invalid email or password", 401); } + const { password, ...safeUser } = userWithPassword; + return safeUser; +}; - const { password, ...safeUser } = userWithPassword; + + +export const getUserPrivateFn = async (email: string) => { + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + throw new AppError("user does not exist", 401); + } + + const { password, transactionPIN, ...safeUser } = user; return safeUser; }; + + +export const getUserById = async (id: string) => { + const user = await prisma.user.findUnique({ + where: { + id + }, + include: { + wallet: true, + donations: true, + received: true + } + }); + return user +} diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts new file mode 100644 index 0000000..cc4cfa9 --- /dev/null +++ b/src/services/transaction.service.ts @@ -0,0 +1,103 @@ + +import { PrismaClient, User } from '@prisma/client' +import utils from '@/utils/index'; +import { AppError } from '@/utils/AppError'; + + +const prisma = new PrismaClient() + + +export const setTransactionPIN = async (userId: string, pin: string) => { + const hashedPin = await utils.hashPassword(pin); + return await prisma.user.update({ + where: { id: userId }, + data: { transactionPIN: hashedPin } + }); +}; + + + +//MAIN FUNCTION +export const createDonation = async ( + donorId: string, + beneficiaryId: string, + amount: number +) => { + if (amount <= 0) { + throw new AppError("Donation amount must be greater than 0", 400); + } + + await prisma.$transaction(async (tx) => { + + const donorWallet = await tx.wallet.findUnique({ + where: { userId: donorId }, + select: { balance: true } + }); + + if (!donorWallet) { + throw new AppError("Donor wallet not found", 404); + } + + if (donorWallet.balance < amount) { + throw new AppError("Insufficient balance to make donation", 400); + } + + const beneficiaryWallet = await tx.wallet.findUnique({ + where: { userId: beneficiaryId }, + select: { balance: true } + }); + + if (!beneficiaryWallet) { + throw new AppError("Beneficiary wallet not found", 404); + } + + + await tx.donation.create({ + data: { + amount, + donorId, + beneficiaryId + } + }); + + + await tx.wallet.update({ + where: { userId: donorId }, + data: { balance: { decrement: amount } } + }); + + + await tx.wallet.update({ + where: { userId: beneficiaryId }, + data: { balance: { increment: amount } } + }); + }); +}; + + + + +export const countUserDonations = async (userId: string) => { + return prisma.donation.count({ + where: { donorId: userId } + }); +}; + + +export const donationsInPeriod = async (userId: string, start: Date, end: Date) => { + return prisma.donation.findMany({ + where: { + donorId: userId, + createdAt: { gte: start, lte: end } + } + }); +}; + + + +export const getDonation = async (donationId: string) => { + return prisma.donation.findUnique({ + where: { id: donationId }, + include: { beneficiary: true, donor: true } + }); +}; diff --git a/src/types/express/index.d.ts b/src/types/express/index.d.ts new file mode 100644 index 0000000..8986dfa --- /dev/null +++ b/src/types/express/index.d.ts @@ -0,0 +1,13 @@ +import { User, Wallet } from "@prisma/client"; + +type UserWithWallet = User & { wallet?: Wallet | null }; + +declare global { + namespace Express { + interface Request { + user?: UserWithWallet; + } + } +} + +export { }; diff --git a/src/utils/index.ts b/src/utils/index.ts index e4dc56e..bcb3f9b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,7 +23,12 @@ const utils = { }, validateCreateUserInput: (data: { email: string; password: string; name: string }): boolean => { return utils.validEmail(data.email) && utils.validPassword(data.password) && utils.validName(data.name); - } + }, + validPIN: (pin: number): boolean => { + // Validates that pin is exactly 4 digits + const pinRegex = /^\d{4}$/; + return pinRegex.test(pin.toString()); + }, }; From 15acc49768cf6d5a1c74c475bb669e716822fb2b Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 13:56:28 +0100 Subject: [PATCH 06/33] chore: api docs --- src/routes/transaction.routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 690f948..3089ff4 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -7,7 +7,7 @@ const router = express.Router(); * @swagger * /api/tx/pin: * post: - * summary: Creates a new user + * summary: Creates a transaction PIN * tags: [Donation] * requestBody: * required: true From 9cb0ce48815f2084eb413d978c6b3ff05fd09043 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 15 Aug 2025 14:05:30 +0100 Subject: [PATCH 07/33] added little TODOs --- src/routes/transaction.routes.ts | 12 ++++++------ src/services/transaction.service.ts | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 3089ff4..1329778 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -43,7 +43,7 @@ router.post('/pin', handleCreateTxPIN) * minLength: 4 * maxLength: 6 * required: true - * description: User's 4–6 digit transaction PIN + * description: User's 4 digit transaction PIN * example: "1234" * requestBody: * required: true @@ -53,23 +53,23 @@ router.post('/pin', handleCreateTxPIN) * type: object * required: * - amount - * - beneficiaryId + * - beneficiaryEmail * properties: * amount: * type: number * description: Amount to donate * example: 50.0 - * beneficiaryId: + * beneficiaryEmail: * type: string - * description: ID of the beneficiary receiving the donation - * example: "clxyz123abc456def789ghi0" + * description: Email of the beneficiary receiving the donation + * example: "dunsin@exmaple.com" * responses: * 201: * description: Donation created successfully * 400: * description: Invalid request payload or parameters * 401: - * description: Unauthorized — invalid transaction PIN + * description: Unaut horized — invalid transaction PIN * 500: * description: Internal server error */ diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index cc4cfa9..6cb4f9e 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -27,6 +27,8 @@ export const createDonation = async ( throw new AppError("Donation amount must be greater than 0", 400); } + // TODO: check for transcation pin if user has created one or if it matches + await prisma.$transaction(async (tx) => { const donorWallet = await tx.wallet.findUnique({ From 216b9764e2a5cdf16b7d50663aadb2c1bbd024cc Mon Sep 17 00:00:00 2001 From: Dunsin Date: Sat, 16 Aug 2025 23:51:57 +0100 Subject: [PATCH 08/33] little mod --- src/services/transaction.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 6cb4f9e..0cce890 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -24,7 +24,7 @@ export const createDonation = async ( amount: number ) => { if (amount <= 0) { - throw new AppError("Donation amount must be greater than 0", 400); + throw new AppError("Donation amount must be greater than 0 :(", 400); } // TODO: check for transcation pin if user has created one or if it matches From 52b0322183c17e861bd699dd4f43184f9756ddf4 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Sun, 17 Aug 2025 07:22:41 +0100 Subject: [PATCH 09/33] feat: create pin --- src/controllers/transaction.controller.ts | 4 ++-- src/routes/transaction.routes.ts | 8 ++++++-- src/services/transaction.service.ts | 4 ++-- src/utils/swagger.ts | 9 +++++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index ac22457..76181aa 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -11,13 +11,13 @@ import { User } from '@prisma/client'; export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - const validInput = utils.validPIN(req.body); + const validInput = utils.validPIN(req.body.pin); if (!validInput) { throw (new AppError("Invalid pin format", 400)); } const user = (req as Request & { user?: User }).user! - await setTransactionPIN(user.id, req.body); + await setTransactionPIN(user.id, req.body.pin); return res.status(200).json(new ApiResponse("success", "Pin created successfully")); diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 1329778..be1471b 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -9,6 +9,8 @@ const router = express.Router(); * post: * summary: Creates a transaction PIN * tags: [Donation] + * security: + * - bearerAuth: [] * requestBody: * required: true * content: @@ -17,8 +19,8 @@ const router = express.Router(); * type: object * properties: * pin: - * type: string - * description: Must be exactly 4 characters long, contain only numbers + * type: number + * description: Must be exactly 4 digits long * minLength: 4 * example: 1234 * responses: @@ -35,6 +37,8 @@ router.post('/pin', handleCreateTxPIN) * post: * summary: Create a donation transaction * tags: [Donation] + * security: + * - bearerAuth: [] * parameters: * - in: query * name: transactionPin diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 0cce890..2b3681a 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -7,8 +7,8 @@ import { AppError } from '@/utils/AppError'; const prisma = new PrismaClient() -export const setTransactionPIN = async (userId: string, pin: string) => { - const hashedPin = await utils.hashPassword(pin); +export const setTransactionPIN = async (userId: string, pin: number) => { + const hashedPin = await utils.hashPassword(pin.toString()); return await prisma.user.update({ where: { id: userId }, data: { transactionPIN: hashedPin } diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts index 89c1aeb..0daa75a 100644 --- a/src/utils/swagger.ts +++ b/src/utils/swagger.ts @@ -12,6 +12,15 @@ const swaggerDefinition = { version: "1.0.0", description: "API documentation", }, + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, servers: [ { url: `http://localhost:${config.PORT}`, From 5774ec2a00d6475b15f12d198ea3471216b19903 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Sun, 17 Aug 2025 23:48:19 +0100 Subject: [PATCH 10/33] fix: update donate method --- src/controllers/transaction.controller.ts | 9 +++++++-- src/services/auth.service.ts | 8 +++----- src/services/transaction.service.ts | 11 ++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 76181aa..21d6545 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -28,16 +28,21 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response export const handleCreateDonation = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { //TODO: check if both params are wrong too const { amount, beneficiaryEmail } = req.body - if (!amount || !utils.validEmail(beneficiaryEmail)) { + const txPIN = req.query.transactionPin as string; + if ((typeof amount != "number") || !utils.validEmail(beneficiaryEmail)) { throw (new AppError("Invalid input data", 400)); } + if (!txPIN || !utils.validPIN(+txPIN)) { + throw (new AppError("Invalid transaction PIN", 401)); + } + const beneficiary = await getUserPrivateFn(req.body.beneficiaryEmail); const user = (req as Request & { user?: User }).user! - const donation = await createDonation(user.id, beneficiary.id, req.body.amount); + const donation = await createDonation(user, beneficiary.id, amount, +txPIN); return res.status(200).json(new ApiResponse("success", donation)); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index f1ae313..5e805e8 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -39,7 +39,7 @@ export const getUser = async (data: { email: string; password: string }) => { if (!isMatch) { throw new AppError("Invalid email or password", 401); } - const { password, ...safeUser } = userWithPassword; + const { password, transactionPIN, ...safeUser } = userWithPassword; return safeUser; }; @@ -52,11 +52,9 @@ export const getUserPrivateFn = async (email: string) => { }); if (!user) { - throw new AppError("user does not exist", 401); + throw new AppError("Beneficiary does not exist", 401); } - - const { password, transactionPIN, ...safeUser } = user; - return safeUser; + return user; }; diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 2b3681a..2cac626 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -19,20 +19,25 @@ export const setTransactionPIN = async (userId: string, pin: number) => { //MAIN FUNCTION export const createDonation = async ( - donorId: string, + donor: User, beneficiaryId: string, - amount: number + amount: number, + txPIN: number ) => { if (amount <= 0) { throw new AppError("Donation amount must be greater than 0 :(", 400); } // TODO: check for transcation pin if user has created one or if it matches + if(!utils.decryptPassword(donor.transactionPIN,txPIN.toString())) { + throw new AppError("Invalid transaction PIN", 401); + } + await prisma.$transaction(async (tx) => { const donorWallet = await tx.wallet.findUnique({ - where: { userId: donorId }, + where: { userId: donor.id }, select: { balance: true } }); From d927145621e2aa8345889f0469f9dff905e96798 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 09:59:38 +0100 Subject: [PATCH 11/33] feat: donate service complete --- src/controllers/transaction.controller.ts | 3 +++ src/services/auth.service.ts | 26 +++++++++++++++-------- src/services/transaction.service.ts | 14 +++++++----- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 21d6545..c955de4 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -42,6 +42,9 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo const user = (req as Request & { user?: User }).user! + if (!user.transactionPIN) { + throw new AppError("Please set a Transaction PIN first", 401); + } const donation = await createDonation(user, beneficiary.id, amount, +txPIN); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 5e805e8..43febcf 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -12,17 +12,19 @@ export const createUser = async (data: User) => { const user = await prisma.user.create({ data: { - ...data, password: hashedPassword + ...data, password: hashedPassword, + wallet: { + create: {} + } }, - select: { - id: true, - email: true, - name: true, - createdAt: true, - updatedAt: true + include: { + wallet: true, + donations: true, + received: true } }); - return user + const { password: _, transactionPIN, ...safeUser } = user; + return safeUser } @@ -30,6 +32,11 @@ export const createUser = async (data: User) => { export const getUser = async (data: { email: string; password: string }) => { const userWithPassword = await prisma.user.findUnique({ where: { email: data.email }, + include: { + wallet: true, + donations: true, + received: true + } }); if (!userWithPassword) { @@ -39,7 +46,7 @@ export const getUser = async (data: { email: string; password: string }) => { if (!isMatch) { throw new AppError("Invalid email or password", 401); } - const { password, transactionPIN, ...safeUser } = userWithPassword; + const { password, transactionPIN, ...safeUser } = userWithPassword; return safeUser; }; @@ -47,6 +54,7 @@ export const getUser = async (data: { email: string; password: string }) => { export const getUserPrivateFn = async (email: string) => { + console.log("getUserPrivateFn called with email:", email); const user = await prisma.user.findUnique({ where: { email }, }); diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 2cac626..f34b4d4 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -29,12 +29,12 @@ export const createDonation = async ( } // TODO: check for transcation pin if user has created one or if it matches - if(!utils.decryptPassword(donor.transactionPIN,txPIN.toString())) { + if(!utils.decryptPassword(donor.transactionPIN!,txPIN.toString())) { throw new AppError("Invalid transaction PIN", 401); } - await prisma.$transaction(async (tx) => { + const data = await prisma.$transaction(async (tx) => { const donorWallet = await tx.wallet.findUnique({ where: { userId: donor.id }, @@ -59,17 +59,17 @@ export const createDonation = async ( } - await tx.donation.create({ + const donated = await tx.donation.create({ data: { amount, - donorId, + donorId: donor.id, beneficiaryId } }); await tx.wallet.update({ - where: { userId: donorId }, + where: { userId: donor.id }, data: { balance: { decrement: amount } } }); @@ -78,7 +78,11 @@ export const createDonation = async ( where: { userId: beneficiaryId }, data: { balance: { increment: amount } } }); + + return donated }); + + return data }; From f369b8c83ff2daba4c9ef54904f655f4a2968442 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 11:38:17 +0100 Subject: [PATCH 12/33] task complete - done with necessary endpoints --- src/controllers/transaction.controller.ts | 46 +++++++++++++- src/routes/transaction.routes.ts | 77 ++++++++++++++++++++++- src/services/transaction.service.ts | 21 ++++++- 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index c955de4..1ef86b5 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -2,7 +2,7 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; -import { setTransactionPIN, createDonation } from '@/services/transaction.service'; +import { setTransactionPIN, createDonation, getUserDonations, donationsInPeriod, getDonationById } from '@/services/transaction.service'; import { getUserPrivateFn } from '@/services/auth.service'; import utils from '@/utils/index'; @@ -52,3 +52,47 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo }) + +export const handleGetUserDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const user = (req as Request & { user?: User }).user! + if (!user) { + throw new AppError("User not found", 404); + } + + const donations = await getUserDonations(user.id); + + return res.status(200).json(new ApiResponse("success", donations)); +}) + + +export const handleFilterDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const user = (req as Request & { user?: User }).user! + + const { start, end } = req.query; + if (!start || !end) { + throw new AppError("Start and end dates are required", 400); + } + + const startDate = new Date(start as string); + const endDate = new Date(end as string); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new AppError("Invalid date format", 400); + } + + const donations = await donationsInPeriod(user.id, startDate, endDate); + + return res.status(200).json(new ApiResponse("success", donations)); +} +) + +export const handleDonationDetails = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const donationId = req.params.id; + if (!donationId) { + throw new AppError("Donation ID is required", 400); + } + + const donation = await getDonationById(donationId); + + return res.status(200).json(new ApiResponse("success", donation)); +}); \ No newline at end of file diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index be1471b..d9fc39e 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { handleCreateTxPIN, handleCreateDonation } from "@/controllers/transaction.controller" +import { handleCreateTxPIN, handleCreateDonation, handleGetUserDonations, handleFilterDonations, handleDonationDetails } from "@/controllers/transaction.controller" const router = express.Router(); @@ -81,4 +81,79 @@ router.post('/pin', handleCreateTxPIN) router.post('/create-donation', handleCreateDonation) +/** + * @swagger + * /api/tx/my-donations: + * get: + * summary: Get all donations made by the user + * tags: [Donation] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of donations made by the user + * 401: + * description: Unauthorized + */ + +router.get("/my-donations", handleGetUserDonations); + + +/** + * @swagger + * /api/tx/filter-donations: + * get: + * summary: Filter donations made by the user within a date range + * tags: [Donation] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: start + * schema: + * type: string + * format: date-time + * required: true + * description: Start date for filtering donations + * example: "2023-01-01T00:00:00Z" + * - in: query + * name: end + * schema: + * type: string + * format: date-time + * required: true + * description: End date for filtering donations + * example: "2025-12-31T23:59:59Z" + * responses: + * 200: + * description: Filtered list of donations made by the user + */ +router.get("/filter-donations", handleFilterDonations ); + +// Allow a user view a single donation made to a fellow user (beneficiary) +/** + * @swagger + * /api/tx/donation/{id}: + * get: + * summary: Get details of a specific donation by ID + * tags: [Donation] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the donation to retrieve + * responses: + * 200: + * description: Donation details retrieved successfully + * 404: + * description: Donation not found + */ +router.get("/donation/:id", handleDonationDetails ) + + + export default router; diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index f34b4d4..06e17a5 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -95,20 +95,35 @@ export const countUserDonations = async (userId: string) => { }; -export const donationsInPeriod = async (userId: string, start: Date, end: Date) => { +export const getUserDonations = async (userId: string) => { return prisma.donation.findMany({ + where: { donorId: userId }, + include: { beneficiary: true } + }); +}; + +export const donationsInPeriod = async (userId: string, start: Date, end: Date) => { + const data = prisma.donation.findMany({ where: { donorId: userId, createdAt: { gte: start, lte: end } } }); + if (!data) { + throw new AppError("No donations found in this period", 404); + } + return data; }; -export const getDonation = async (donationId: string) => { - return prisma.donation.findUnique({ +export const getDonationById = async (donationId: string) => { + const data = prisma.donation.findUnique({ where: { id: donationId }, include: { beneficiary: true, donor: true } }); + if (!data) { + throw new AppError("Donation not found", 404); + } + return data; }; From b77ddddb47cf3864c97ce7ef7681b57bd41fa42b Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 12:21:28 +0100 Subject: [PATCH 13/33] feat: add pagination --- .env.example | 3 +- USAGE.md | 29 ++++++++++--- src/controllers/transaction.controller.ts | 9 ++-- src/routes/transaction.routes.ts | 38 +++++++++++++++- src/services/transaction.service.ts | 53 ++++++++++++++++++----- src/utils/pagintion.ts | 45 +++++++++++++++++++ 6 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 src/utils/pagintion.ts diff --git a/.env.example b/.env.example index e04b1c7..06e60fa 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ NODE_ENV="" DATABASE_URL="" JWT_REFRESH_SECRET="" -JWT_SECRET="" \ No newline at end of file +JWT_SECRET="" +PORT="" \ No newline at end of file diff --git a/USAGE.md b/USAGE.md index f34fe9d..d28afd4 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,7 +1,26 @@ -documentation how to use the api goes here... -- the database linked to this project will expire on September 13, 2025. -- create a env file and put in the necessary values you see in env.exmaple there, after this, -- usage, run yarn install, and start up projects with yarn dev. -- to see api docusmentation esure the server is running then , go to this url http://localhost:[PORT]/api-docs/ + - every user has a default 100,000 balcne in theor wallet, that they can use th=i make trancations - send funds to beneficiary by email +- auto reverse fund s is built in in create Donation endpoint, so if any of the process fials or something goes wrong, fnds are automtivally reveresed +pagination is included, +to use this prohject simole follow these steps +git clone +yarn install +cp env.example env +go to any postgres database provvider and creatd a database publicily accessible, you should have a result like "postgresqk://...", set that valie as uout +DATABASE_URL, also fill in all the necessary things in thre .env/.env.exmaple file like node_env, jwt_secret, etc + +then run yarn dev. +check the console for logs and sintructions in where to access the docs and the now deployed sercie, you shuld see these logs + +```bash +[nodemon] starting `ts-node -r tsconfig-paths/register src/server.ts` +Application started with config Loaded up✅ +Server running on port [PORT] +API documentation available at 📝📚 http://localhost:[port]/api-docs +``` + +that measn everuthing is working, you are good to go, to the docusmtation endpoint and test it out + +howeverm yo test a dpeloyed verison of this serive out, visit -> but this mght break from septoemner ot mught be slow, this is becuae both the database probvider and hosting serie are free plans, and might shi=ut down anytme from now +- the database linked to this project will expire on September 13, 2025. diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 1ef86b5..b24b3ec 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -59,7 +59,9 @@ export const handleGetUserDonations = asyncHandler(async (req: Request, res: Res throw new AppError("User not found", 404); } - const donations = await getUserDonations(user.id); + const { limit, page } = req.query; + + const donations = await getUserDonations(user.id, page as string, limit as string); return res.status(200).json(new ApiResponse("success", donations)); }) @@ -68,11 +70,10 @@ export const handleGetUserDonations = asyncHandler(async (req: Request, res: Res export const handleFilterDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const user = (req as Request & { user?: User }).user! - const { start, end } = req.query; + const { start, end, limit, page } = req.query; if (!start || !end) { throw new AppError("Start and end dates are required", 400); } - const startDate = new Date(start as string); const endDate = new Date(end as string); @@ -80,7 +81,7 @@ export const handleFilterDonations = asyncHandler(async (req: Request, res: Resp throw new AppError("Invalid date format", 400); } - const donations = await donationsInPeriod(user.id, startDate, endDate); + const donations = await donationsInPeriod(user.id, startDate, endDate, page as string, limit as string); return res.status(200).json(new ApiResponse("success", donations)); } diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index d9fc39e..ab35981 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -89,6 +89,23 @@ router.post('/create-donation', handleCreateDonation) * tags: [Donation] * security: * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Page number for pagination + * example: 1 + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Number of results per page + * example: 10 * responses: * 200: * description: List of donations made by the user @@ -124,13 +141,30 @@ router.get("/my-donations", handleGetUserDonations); * required: true * description: End date for filtering donations * example: "2025-12-31T23:59:59Z" + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Page number for pagination + * example: 1 + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Number of results per page + * example: 10 * responses: * 200: * description: Filtered list of donations made by the user */ + router.get("/filter-donations", handleFilterDonations ); -// Allow a user view a single donation made to a fellow user (beneficiary) + /** * @swagger * /api/tx/donation/{id}: @@ -152,7 +186,7 @@ router.get("/filter-donations", handleFilterDonations ); * 404: * description: Donation not found */ -router.get("/donation/:id", handleDonationDetails ) +router.get("/donation/:id", handleDonationDetails) diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 06e17a5..73bcf27 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -2,7 +2,7 @@ import { PrismaClient, User } from '@prisma/client' import utils from '@/utils/index'; import { AppError } from '@/utils/AppError'; - +import { paginate } from '@/utils/pagintion'; const prisma = new PrismaClient() @@ -29,7 +29,7 @@ export const createDonation = async ( } // TODO: check for transcation pin if user has created one or if it matches - if(!utils.decryptPassword(donor.transactionPIN!,txPIN.toString())) { + if (!utils.decryptPassword(donor.transactionPIN!, txPIN.toString())) { throw new AppError("Invalid transaction PIN", 401); } @@ -59,7 +59,7 @@ export const createDonation = async ( } - const donated = await tx.donation.create({ + const donated = await tx.donation.create({ data: { amount, donorId: donor.id, @@ -95,19 +95,50 @@ export const countUserDonations = async (userId: string) => { }; -export const getUserDonations = async (userId: string) => { - return prisma.donation.findMany({ - where: { donorId: userId }, - include: { beneficiary: true } - }); +export const getUserDonations = async (userId: string, page?: string, limit?: string) => { + var page_; + var limit_; + if (page) { + page_ = +page; + + } + if (limit) { + limit_ = +limit; + + } + + return await paginate({ + model: 'donation', + where: { + donorId: userId, + }, + include: { beneficiary: true }, + page: page_, + limit: limit_ + }) }; -export const donationsInPeriod = async (userId: string, start: Date, end: Date) => { - const data = prisma.donation.findMany({ +export const donationsInPeriod = async (userId: string, start: Date, end: Date, page?: string, limit?: string) => { + var page_; + var limit_; + if (page) { + page_ = +page; + + } + if (limit) { + limit_ = +limit; + + } + const data = await paginate({ + model: 'donation', where: { donorId: userId, createdAt: { gte: start, lte: end } - } + }, + orderBy: { createdAt: 'desc' }, + include: { beneficiary: true }, + page: page_, + limit: limit_ }); if (!data) { throw new AppError("No donations found in this period", 404); diff --git a/src/utils/pagintion.ts b/src/utils/pagintion.ts new file mode 100644 index 0000000..80d6d38 --- /dev/null +++ b/src/utils/pagintion.ts @@ -0,0 +1,45 @@ +import { PrismaClient } from "@prisma/client"; +const prisma = new PrismaClient(); + +type PaginationParams = { + model: keyof PrismaClient; + page?: number; + limit?: number; + where?: any; + orderBy?: any; + include?: any; +}; + +export const paginate = async ({ + model, + page = 1, + limit = 10, + where, + orderBy, + include, +}: PaginationParams) => { + const skip = (page - 1) * limit; + + const prismaModel = prisma[model] as any; + + const [data, total] = await Promise.all([ + prismaModel.findMany({ + skip, + take: limit, + where, + orderBy, + include, + }), + prismaModel.count({ where }), + ]); + + return { + data, + pagination: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; +}; From f7b4e334847f3ebd02762bd4ed1327a407f585f0 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 13:00:08 +0100 Subject: [PATCH 14/33] add docker start file for render --- .gitignore | 1 + Dockerfile | 35 +++++++++++++++++++++++++++++++++++ USAGE.md | 4 ++++ package.json | 6 ++---- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index 37d7e73..e6367a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .env +dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7207c30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Use Node.js as the base image +FROM node:18-alpine + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN yarn install + +# Copy the rest of the application code +COPY . . + +# Generate Prisma Client +RUN npm run db:init + +# Build the TypeScript project +RUN npm run build + +# Declare a build-time variable +ARG DATABASE_URL + +# Set it as an environment variable +ENV DATABASE_URL=${DATABASE_URL} + +# Run migrations +RUN npm run db:deploy:prod + +# Expose the application port +EXPOSE 3000 + +# Start the application +CMD ["node", "dist/server.js"] \ No newline at end of file diff --git a/USAGE.md b/USAGE.md index d28afd4..920430b 100644 --- a/USAGE.md +++ b/USAGE.md @@ -24,3 +24,7 @@ that measn everuthing is working, you are good to go, to the docusmtation endpoi howeverm yo test a dpeloyed verison of this serive out, visit -> but this mght break from septoemner ot mught be slow, this is becuae both the database probvider and hosting serie are free plans, and might shi=ut down anytme from now - the database linked to this project will expire on September 13, 2025. + + +to call the endpoints, you have to be authenticatedm, creating an account or logigign in will ie you an accessToken, this access token that rxpires in 15 mins, after getting thisw accesstoken, go to th authorixe button on th e top right corner of the wagger coumentation user insterface, and put in the vlaue, and ghen click authorie, t now you can now access proitected routes simoley by calling he endpoint in the UI + diff --git a/package.json b/package.json index d1efab0..c05358d 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,14 @@ "clean": "rimraf dist", "prestart": "node dist/constants/index.js", "dev": "npm run db:deploy && npm run db:init && nodemon --legacy-watch -r tsconfig-paths/register src/server.ts", - "build": "npm run clean && tsc && cp src/docs/index.yaml dist/docs/index.yaml && cp -r src/prisma/ dist/prisma/", - "dummy": "npm run db:migrate", + "build": "npm run clean && tsc && cp -r src/prisma/ dist/prisma/", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", "db:migrate": "cd ./src && npx prisma migrate dev --name add_other_models", "db:init": "cd ./src && npx prisma generate", "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", - "db:deploy:prod": "cd ./dist && npx prisma migrate deploy", - "docker:dev": "docker-compose -f docker-compose.dev.yml up --build" + "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, "_moduleAliases": { "@": "./src" From ec54fd1847c74808161fc1b4e1cffc48f952d3d5 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 13:14:23 +0100 Subject: [PATCH 15/33] update node version in dockerfile --- Dockerfile | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7207c30..5339207 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use Node.js as the base image -FROM node:18-alpine +FROM node:20.14.0 # Set the working directory WORKDIR /app diff --git a/package.json b/package.json index c05358d..50d77fb 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "typescript": "^5.8.2" }, "engines": { - "node": "22.14.0", + "node": "^20.14.0", "yarn": "^1.22.19" } } From 4c0ab90c82f7eefcb793644e2f92edb051029dc7 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 14:45:14 +0100 Subject: [PATCH 16/33] fixed module aliases and update usage.md --- USAGE.md | 108 +++++++++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/USAGE.md b/USAGE.md index 920430b..e0f555d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,30 +1,106 @@ -- every user has a default 100,000 balcne in theor wallet, that they can use th=i make trancations -- send funds to beneficiary by email -- auto reverse fund s is built in in create Donation endpoint, so if any of the process fials or something goes wrong, fnds are automtivally reveresed -pagination is included, -to use this prohject simole follow these steps -git clone + +# Paritie Skill Test – Wallet & Donation Service + +This project implements a simple wallet and donation API with built-in transaction safety, pagination, and authentication. + +--- + +## 🚀 Features + +* **Default Wallet Balance**: Every user automatically gets a wallet with a starting balance of **100,000 units**. +* **Send Funds by Email**: Users can send funds to beneficiaries using their registered email address. +* **Automatic Transaction Rollback**: Built-in safety mechanism in the **create donation** endpoint ensures that if any part of the process fails, funds are **automatically reversed**. +* **Pagination Support**: All resource fetch endpoints (users, donations, wallets) include pagination for efficient data retrieval. +* **Authentication**: Secure login & signup flow. Every request to protected endpoints requires a valid JWT access token. + +--- + +## 🛠️ Getting Started + +Follow these steps to set up the project locally: + +```bash +# 1. Clone the repository +git clone https://github.com/Dunsin-cyber/backend-test/tree/main + +# 2. Install dependencies yarn install -cp env.example env -go to any postgres database provvider and creatd a database publicily accessible, you should have a result like "postgresqk://...", set that valie as uout -DATABASE_URL, also fill in all the necessary things in thre .env/.env.exmaple file like node_env, jwt_secret, etc -then run yarn dev. -check the console for logs and sintructions in where to access the docs and the now deployed sercie, you shuld see these logs +# 3. Copy environment variables +cp .env.example .env +``` + +### ⚙️ Environment Setup + +You need a PostgreSQL database connection string. +Create a database from any **Postgres provider** and copy the connection URL. +It should look like this: + +``` +postgresql://USER:PASSWORD@HOST:PORT/DATABASE +``` + +Set this value in your `.env` file as `DATABASE_URL`. +Also fill in other required values like: + +* `NODE_ENV` +* `JWT_SECRET` +* `PORT` etc + +--- + +## ▶️ Running the Project + +Start the development server with: + +```bash +yarn dev +``` + +If everything is working, you’ll see logs like this: ```bash [nodemon] starting `ts-node -r tsconfig-paths/register src/server.ts` Application started with config Loaded up✅ Server running on port [PORT] -API documentation available at 📝📚 http://localhost:[port]/api-docs +API documentation available at 📝📚 http://localhost:[PORT]/api-docs ``` -that measn everuthing is working, you are good to go, to the docusmtation endpoint and test it out +Now you can open the documentation link in your browser to test the APIs. + +--- + +## 🌍 Deployed Service + +A hosted version of this service is available at: + +* **Backend URL** → [https://backend-test-21ij.onrender.com](https://backend-test-21ij.onrender.com) +* **API Docs** → [https://backend-test-21ij.onrender.com/api-docs](https://backend-test-21ij.onrender.com/api-docs) + +⚠️ **Note**: + +* The hosting and database are on **free plans** and may be slow or unavailable. +* The database linked to this project will **expire on September 13, 2025**. + +--- + +## 🔑 Authentication + +Most endpoints are **protected**. To access them: + +1. **Create an account** or **login** to get an `accessToken`. +2. The token is valid for **15 minutes**. +3. In the API docs (`/api-docs`), click the **Authorize** button (top right). +4. Paste the token into the input field and click **Authorize**. +5. You can now access protected routes directly from the Swagger UI. -howeverm yo test a dpeloyed verison of this serive out, visit -> but this mght break from septoemner ot mught be slow, this is becuae both the database probvider and hosting serie are free plans, and might shi=ut down anytme from now -- the database linked to this project will expire on September 13, 2025. +--- +## ✅ Summary -to call the endpoints, you have to be authenticatedm, creating an account or logigign in will ie you an accessToken, this access token that rxpires in 15 mins, after getting thisw accesstoken, go to th authorixe button on th e top right corner of the wagger coumentation user insterface, and put in the vlaue, and ghen click authorie, t now you can now access proitected routes simoley by calling he endpoint in the UI +* Every user starts with **100,000 units** in their wallet. +* Transactions are **safe with auto-reversal** on failure. +* **Pagination** makes large data queries efficient. +* Fully documented and easy to test using Swagger. diff --git a/package.json b/package.json index 50d77fb..3bf9721 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, "_moduleAliases": { - "@": "./src" + "@": "./dist" }, "dependencies": { "@prisma/client": "^6.14.0", From fe605d039d35851ee71304a3b0f019ce99113edc Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 14:57:30 +0100 Subject: [PATCH 17/33] updated uasge.md --- USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index e0f555d..115fa2d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -76,7 +76,7 @@ Now you can open the documentation link in your browser to test the APIs. A hosted version of this service is available at: * **Backend URL** → [https://backend-test-21ij.onrender.com](https://backend-test-21ij.onrender.com) -* **API Docs** → [https://backend-test-21ij.onrender.com/api-docs](https://backend-test-21ij.onrender.com/api-docs) +* **API Docs** → http://localhost:[PORT]/api-docs ⚠️ **Note**: From fd04b8aaa711f0ac1604c1963eeced4d6fb31e15 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 18 Aug 2025 15:13:13 +0100 Subject: [PATCH 18/33] update moduleAliases --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3bf9721..50d77fb 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, "_moduleAliases": { - "@": "./dist" + "@": "./src" }, "dependencies": { "@prisma/client": "^6.14.0", From 6ec14c0d8f2060ebe42297850acb7643f6d15a18 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 12:18:20 +0100 Subject: [PATCH 19/33] fix: remove database migration from dev script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 50d77fb..f198879 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "clean": "rimraf dist", "prestart": "node dist/constants/index.js", - "dev": "npm run db:deploy && npm run db:init && nodemon --legacy-watch -r tsconfig-paths/register src/server.ts", + "dev": "nodemon --legacy-watch -r tsconfig-paths/register src/server.ts", "build": "npm run clean && tsc && cp -r src/prisma/ dist/prisma/", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", @@ -60,7 +60,7 @@ "typescript": "^5.8.2" }, "engines": { - "node": "^20.14.0", + "node": "^22.14.0", "yarn": "^1.22.19" } } From 5d50fb497ab55ecf3300663758fe86262f55151c Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 12:35:09 +0100 Subject: [PATCH 20/33] fix: show generic error message if from ORM and only return needed data --- src/controllers/auth.controller.ts | 2 +- src/middlewares/errorHandler.ts | 4 ++-- src/services/auth.service.ts | 14 +++----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index e5738b6..5c2b403 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -13,7 +13,7 @@ export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, if (!validInput) { throw (new AppError("Invalid input data", 400)); } - const user = await createUser(req.body); + const user = await createUser({...req.body, email: req.body.email.toLowerCase()}); const accessToken = jwt.sign( diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 719841f..69dd42e 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,11 +1,11 @@ import { Request, Response, NextFunction } from "express"; import { ApiResponse } from "@/utils/ApiResponse"; import { AppError } from "@/utils/AppError"; +import { Prisma } from "@prisma/client"; export const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => { - console.error(err.message); const statusCode = err.statusCode || 500; - const message = err.message /* || "Internal Server Error" */; + const message = err instanceof Prisma.PrismaClientKnownRequestError || Prisma.PrismaClientUnknownRequestError ? "Somehting went wrong" : err.message; res.status(statusCode).json(new ApiResponse("fail", message)); }; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 43febcf..0110a82 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -16,13 +16,9 @@ export const createUser = async (data: User) => { wallet: { create: {} } - }, - include: { - wallet: true, - donations: true, - received: true } }); + // TODO: show a more readable error if error happens const { password: _, transactionPIN, ...safeUser } = user; return safeUser } @@ -32,11 +28,7 @@ export const createUser = async (data: User) => { export const getUser = async (data: { email: string; password: string }) => { const userWithPassword = await prisma.user.findUnique({ where: { email: data.email }, - include: { - wallet: true, - donations: true, - received: true - } + }); if (!userWithPassword) { @@ -65,7 +57,7 @@ export const getUserPrivateFn = async (email: string) => { return user; }; - +//only used internally export const getUserById = async (id: string) => { const user = await prisma.user.findUnique({ where: { From 8eb288031fd06af36f17a56e2254f1c0879c9d68 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 13:15:56 +0100 Subject: [PATCH 21/33] fix: add remaining fix suggested during demo call --- module-alias.js | 7 +++++ package.json | 5 +--- src/constants/index.ts | 2 +- src/controllers/transaction.controller.ts | 9 +++--- src/middlewares/errorHandler.ts | 23 +++++++++++++-- src/routes/transaction.routes.ts | 19 +++++------- src/server.ts | 1 + src/services/auth.service.ts | 2 -- src/services/transaction.service.ts | 18 +++++++----- src/utils/index.ts | 10 +++---- src/utils/prismaErrors.ts | 35 +++++++++++++++++++++++ 11 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 module-alias.js create mode 100644 src/utils/prismaErrors.ts diff --git a/module-alias.js b/module-alias.js new file mode 100644 index 0000000..c5d010d --- /dev/null +++ b/module-alias.js @@ -0,0 +1,7 @@ +const moduleAlias = require("module-alias"); +const path = require("path"); + +const isProd = process.env.NODE_ENV === "production"; +const basePath = isProd ? "./dist" : "./src"; + +moduleAlias.addAlias("@", path.join(__dirname, basePath)); diff --git a/package.json b/package.json index f198879..8699f19 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,6 @@ "start:prod": "npm run db:deploy:prod && npm run start", "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, - "_moduleAliases": { - "@": "./src" - }, "dependencies": { "@prisma/client": "^6.14.0", "@prisma/extension-accelerate": "^2.0.2", @@ -63,4 +60,4 @@ "node": "^22.14.0", "yarn": "^1.22.19" } -} +} \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index a2f94d3..d1ebedc 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -35,5 +35,5 @@ export const config = { PORT, JWT_REFRESH_SECRET, JWT_SECRET - + } \ No newline at end of file diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index b24b3ec..339b66a 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -27,13 +27,12 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response export const handleCreateDonation = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { //TODO: check if both params are wrong too - const { amount, beneficiaryEmail } = req.body - const txPIN = req.query.transactionPin as string; + const { amount, beneficiaryEmail, transactionPin } = req.body if ((typeof amount != "number") || !utils.validEmail(beneficiaryEmail)) { throw (new AppError("Invalid input data", 400)); } - if (!txPIN || !utils.validPIN(+txPIN)) { + if (!transactionPin || !utils.validPIN(transactionPin)) { throw (new AppError("Invalid transaction PIN", 401)); } @@ -43,9 +42,9 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo const user = (req as Request & { user?: User }).user! if (!user.transactionPIN) { - throw new AppError("Please set a Transaction PIN first", 401); + throw new AppError("Please set a Transaction PIN first", 400); } - const donation = await createDonation(user, beneficiary.id, amount, +txPIN); + const donation = await createDonation(user, beneficiary.id, amount, transactionPin); return res.status(200).json(new ApiResponse("success", donation)); diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 69dd42e..0d4423d 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -2,10 +2,27 @@ import { Request, Response, NextFunction } from "express"; import { ApiResponse } from "@/utils/ApiResponse"; import { AppError } from "@/utils/AppError"; import { Prisma } from "@prisma/client"; +import { prismaErrorMap } from "@/utils/prismaErrors"; -export const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => { - const statusCode = err.statusCode || 500; - const message = err instanceof Prisma.PrismaClientKnownRequestError || Prisma.PrismaClientUnknownRequestError ? "Somehting went wrong" : err.message; +export const errorHandler = ( + err: AppError | Error, + req: Request, + res: Response, + next: NextFunction +) => { + let statusCode = (err as AppError).statusCode || 500; + let message = err.message; + + if (err instanceof Prisma.PrismaClientKnownRequestError) { + // If Prisma gives us a code, check our map + message = prismaErrorMap[err.code] || "Database error occurred."; + } else if ( + err instanceof Prisma.PrismaClientUnknownRequestError || + err instanceof Prisma.PrismaClientInitializationError || + err instanceof Prisma.PrismaClientRustPanicError + ) { + message = "Internal database error. Please try again later."; + } res.status(statusCode).json(new ApiResponse("fail", message)); }; diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index ab35981..2785b21 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -20,9 +20,9 @@ const router = express.Router(); * properties: * pin: * type: number - * description: Must be exactly 4 digits long + * description: Must be exactly 4 or 6digits long * minLength: 4 - * example: 1234 + * example: 123456 * responses: * 200: * description: Tx Pin created successfully @@ -39,16 +39,6 @@ router.post('/pin', handleCreateTxPIN) * tags: [Donation] * security: * - bearerAuth: [] - * parameters: - * - in: query - * name: transactionPin - * schema: - * type: string - * minLength: 4 - * maxLength: 6 - * required: true - * description: User's 4 digit transaction PIN - * example: "1234" * requestBody: * required: true * content: @@ -67,6 +57,11 @@ router.post('/pin', handleCreateTxPIN) * type: string * description: Email of the beneficiary receiving the donation * example: "dunsin@exmaple.com" + * transactionPin: + * type: string + * description: User's 4 0r 6 digit transaction PIN + * example: "123456" + * required: true * responses: * 201: * description: Donation created successfully diff --git a/src/server.ts b/src/server.ts index 052fc46..46dcb63 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,5 @@ import 'module-alias/register'; +import "../module-alias"; import express from 'express'; import cors from 'cors'; import V1Routes from '@/routes/index'; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0110a82..8f6d69f 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -18,7 +18,6 @@ export const createUser = async (data: User) => { } } }); - // TODO: show a more readable error if error happens const { password: _, transactionPIN, ...safeUser } = user; return safeUser } @@ -46,7 +45,6 @@ export const getUser = async (data: { email: string; password: string }) => { export const getUserPrivateFn = async (email: string) => { - console.log("getUserPrivateFn called with email:", email); const user = await prisma.user.findUnique({ where: { email }, }); diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 73bcf27..eb53a37 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -18,18 +18,20 @@ export const setTransactionPIN = async (userId: string, pin: number) => { //MAIN FUNCTION +// ------------------------------------- START ----------------------- + export const createDonation = async ( donor: User, beneficiaryId: string, amount: number, - txPIN: number + txPIN: string ) => { if (amount <= 0) { throw new AppError("Donation amount must be greater than 0 :(", 400); } // TODO: check for transcation pin if user has created one or if it matches - if (!utils.decryptPassword(donor.transactionPIN!, txPIN.toString())) { + if(!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { throw new AppError("Invalid transaction PIN", 401); } @@ -67,7 +69,7 @@ export const createDonation = async ( } }); - +// -------------------------STARTS HERE await tx.wallet.update({ where: { userId: donor.id }, data: { balance: { decrement: amount } } @@ -79,13 +81,16 @@ export const createDonation = async ( data: { balance: { increment: amount } } }); + // -------------------------STARTS HERE + + return donated }); return data }; - +// ------------------------------------- END ----------------------- export const countUserDonations = async (userId: string) => { @@ -112,7 +117,6 @@ export const getUserDonations = async (userId: string, page?: string, limit?: st where: { donorId: userId, }, - include: { beneficiary: true }, page: page_, limit: limit_ }) @@ -136,7 +140,6 @@ export const donationsInPeriod = async (userId: string, start: Date, end: Date, createdAt: { gte: start, lte: end } }, orderBy: { createdAt: 'desc' }, - include: { beneficiary: true }, page: page_, limit: limit_ }); @@ -151,10 +154,11 @@ export const donationsInPeriod = async (userId: string, start: Date, end: Date, export const getDonationById = async (donationId: string) => { const data = prisma.donation.findUnique({ where: { id: donationId }, - include: { beneficiary: true, donor: true } + // include: { beneficiary: true, donor: true } }); if (!data) { throw new AppError("Donation not found", 404); } return data; }; +- \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index bcb3f9b..e93836f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,7 +9,7 @@ const utils = { }, validEmail: (email: string): boolean => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); + return emailRegex.test(email.toLocaleLowerCase()); }, validPassword: (password: string): boolean => { // Example validation: at least 8 characters, one uppercase, one lowercase, one number @@ -24,10 +24,10 @@ const utils = { validateCreateUserInput: (data: { email: string; password: string; name: string }): boolean => { return utils.validEmail(data.email) && utils.validPassword(data.password) && utils.validName(data.name); }, - validPIN: (pin: number): boolean => { - // Validates that pin is exactly 4 digits - const pinRegex = /^\d{4}$/; - return pinRegex.test(pin.toString()); + + validPIN: (pin: string): boolean => { + const pinRegex = /^(\d{4}|\d{6})$/; + return pinRegex.test(pin); }, diff --git a/src/utils/prismaErrors.ts b/src/utils/prismaErrors.ts new file mode 100644 index 0000000..587330f --- /dev/null +++ b/src/utils/prismaErrors.ts @@ -0,0 +1,35 @@ +import { Prisma } from "@prisma/client"; + +export const prismaErrorMap: Record = { + // Known request errors + P2000: "The value you provided for a field is too long.", + P2001: "The record you are looking for does not exist.", + P2002: "Unique constraint violation. The value already exists.", + P2003: "Foreign key constraint failed on this field.", + P2004: "A constraint failed on the database.", + P2005: "Invalid value stored in the database for this field.", + P2006: "The provided value is not valid for the field type.", + P2007: "Data validation error.", + P2008: "Failed to parse the query.", + P2009: "Failed to validate the query.", + P2010: "Raw query failed. Please check the query.", + P2011: "Null constraint violation on a required field.", + P2012: "Missing required value.", + P2013: "Missing required argument for field.", + P2014: "Relation violation: a related record is missing.", + P2015: "A related record could not be found.", + P2016: "Query interpretation error.", + P2017: "Records for relation are not connected.", + P2018: "Required connected records not found.", + P2019: "Input error.", + P2020: "Value out of range for the field.", + P2021: "The table does not exist in the database.", + P2022: "The column does not exist in the database.", + P2023: "Inconsistent column data.", + P2024: "Timeout reached while querying the database.", + P2025: "The record you are trying to update/delete does not exist.", + P2026: "Unsupported feature for the database engine.", + P2027: "Multiple errors occurred during query execution.", + P2028: "Transaction API error.", + +}; From 60cc8b39ff5e8d1c60ce5c29661be5f9cef24dc8 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 13:35:31 +0100 Subject: [PATCH 22/33] fix: add modifications suggested during demo call --- module-alias.js | 2 +- package.json | 7 +++++-- src/middlewares/errorHandler.ts | 2 +- src/{utils => middlewares}/prismaErrors.ts | 0 src/server.ts | 1 - tsconfig.json | 3 ++- 6 files changed, 9 insertions(+), 6 deletions(-) rename src/{utils => middlewares}/prismaErrors.ts (100%) diff --git a/module-alias.js b/module-alias.js index c5d010d..9951b90 100644 --- a/module-alias.js +++ b/module-alias.js @@ -2,6 +2,6 @@ const moduleAlias = require("module-alias"); const path = require("path"); const isProd = process.env.NODE_ENV === "production"; -const basePath = isProd ? "./dist" : "./src"; +const basePath = isProd ? "dist" : "src"; moduleAlias.addAlias("@", path.join(__dirname, basePath)); diff --git a/package.json b/package.json index 8699f19..1954d8d 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,13 @@ "db:reset": "cd ./src && npx prisma migrate reset --force", "db:migrate": "cd ./src && npx prisma migrate dev --name add_other_models", "db:init": "cd ./src && npx prisma generate", - "start": "node dist/server.js", + "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, + "_moduleAliases": { + "@": "./dist" + }, "dependencies": { "@prisma/client": "^6.14.0", "@prisma/extension-accelerate": "^2.0.2", @@ -60,4 +63,4 @@ "node": "^22.14.0", "yarn": "^1.22.19" } -} \ No newline at end of file +} diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 0d4423d..60bdeee 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { ApiResponse } from "@/utils/ApiResponse"; import { AppError } from "@/utils/AppError"; import { Prisma } from "@prisma/client"; -import { prismaErrorMap } from "@/utils/prismaErrors"; +import { prismaErrorMap } from "@/middlewares/prismaErrors"; export const errorHandler = ( err: AppError | Error, diff --git a/src/utils/prismaErrors.ts b/src/middlewares/prismaErrors.ts similarity index 100% rename from src/utils/prismaErrors.ts rename to src/middlewares/prismaErrors.ts diff --git a/src/server.ts b/src/server.ts index 46dcb63..052fc46 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,4 @@ import 'module-alias/register'; -import "../module-alias"; import express from 'express'; import cors from 'cors'; import V1Routes from '@/routes/index'; diff --git a/tsconfig.json b/tsconfig.json index 78d5af2..e2fd144 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ } }, "include": [ - "src/**/*" + "src/**/*", + "module-alias.js" ], "typeRoots": [ "./node_modules/@types", From e17766db1b574ed85bc7d016d42d2332cb22e88e Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 13:41:17 +0100 Subject: [PATCH 23/33] update node version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1954d8d..12f9aa7 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "typescript": "^5.8.2" }, "engines": { - "node": "^22.14.0", + "node": "^20.14.0", "yarn": "^1.22.19" } } From d6e5bc104fb3282896fbc2db0d48fe0f32c770ce Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 13:49:45 +0100 Subject: [PATCH 24/33] fix: build error fix --- src/services/transaction.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index eb53a37..aa3a9e2 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -31,7 +31,7 @@ export const createDonation = async ( } // TODO: check for transcation pin if user has created one or if it matches - if(!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { + if (!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { throw new AppError("Invalid transaction PIN", 401); } @@ -69,7 +69,7 @@ export const createDonation = async ( } }); -// -------------------------STARTS HERE + // -------------------------STARTS HERE await tx.wallet.update({ where: { userId: donor.id }, data: { balance: { decrement: amount } } @@ -153,7 +153,7 @@ export const donationsInPeriod = async (userId: string, start: Date, end: Date, export const getDonationById = async (donationId: string) => { const data = prisma.donation.findUnique({ - where: { id: donationId }, + where: { id: donationId } // include: { beneficiary: true, donor: true } }); if (!data) { @@ -161,4 +161,4 @@ export const getDonationById = async (donationId: string) => { } return data; }; -- \ No newline at end of file + From f4e866472773d0bd614024ead1b81cae97271e47 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Fri, 22 Aug 2025 13:57:06 +0100 Subject: [PATCH 25/33] change moduleAliases --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12f9aa7..bd244d3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" }, "_moduleAliases": { - "@": "./dist" + "@": "./src" }, "dependencies": { "@prisma/client": "^6.14.0", From a87801d05e27957acd4a3df1a7ec948ff8b97bfd Mon Sep 17 00:00:00 2001 From: Dunsin Date: Sun, 24 Aug 2025 16:55:34 +0100 Subject: [PATCH 26/33] fix: error message more user friendly --- src/middlewares/prismaErrors.ts | 62 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/middlewares/prismaErrors.ts b/src/middlewares/prismaErrors.ts index 587330f..2a8f339 100644 --- a/src/middlewares/prismaErrors.ts +++ b/src/middlewares/prismaErrors.ts @@ -1,35 +1,35 @@ import { Prisma } from "@prisma/client"; export const prismaErrorMap: Record = { - // Known request errors - P2000: "The value you provided for a field is too long.", - P2001: "The record you are looking for does not exist.", - P2002: "Unique constraint violation. The value already exists.", - P2003: "Foreign key constraint failed on this field.", - P2004: "A constraint failed on the database.", - P2005: "Invalid value stored in the database for this field.", - P2006: "The provided value is not valid for the field type.", - P2007: "Data validation error.", - P2008: "Failed to parse the query.", - P2009: "Failed to validate the query.", - P2010: "Raw query failed. Please check the query.", - P2011: "Null constraint violation on a required field.", - P2012: "Missing required value.", - P2013: "Missing required argument for field.", - P2014: "Relation violation: a related record is missing.", - P2015: "A related record could not be found.", - P2016: "Query interpretation error.", - P2017: "Records for relation are not connected.", - P2018: "Required connected records not found.", - P2019: "Input error.", - P2020: "Value out of range for the field.", - P2021: "The table does not exist in the database.", - P2022: "The column does not exist in the database.", - P2023: "Inconsistent column data.", - P2024: "Timeout reached while querying the database.", - P2025: "The record you are trying to update/delete does not exist.", - P2026: "Unsupported feature for the database engine.", - P2027: "Multiple errors occurred during query execution.", - P2028: "Transaction API error.", + // User-friendly, generic messages for known Prisma errors + P2000: "One of the values is too long. Please shorten the input and try again.", + P2001: "We couldn't find the item you requested.", + P2002: "An item with that value already exists. Please use a different value.", + P2003: "A related record required for this action is missing.", + P2004: "A database constraint prevented this action. Please check your input and try again.", + P2005: "There is an invalid value in the database. Please try again or contact support.", + P2006: "A provided value has the wrong type or format. Please check your input.", + P2007: "Some input failed validation. Please review and correct the data.", + P2008: "The database could not parse the request. Please try again.", + P2009: "The database rejected the request. Please verify the data and try again.", + P2010: "A low-level database operation failed. Please try again later.", + P2011: "A required field was set to null. Please provide a value for all required fields.", + P2012: "A required value is missing. Please include all required information.", + P2013: "A required argument is missing. Please include all necessary fields.", + P2014: "A related record is missing. Ensure related data exists before retrying.", + P2015: "A related item could not be found. Please verify related records exist.", + P2016: "There was a problem interpreting the request. Please try again.", + P2017: "Related records are not connected. Please ensure relationships are correct.", + P2018: "Required related records were not found. Please confirm related data exists.", + P2019: "Invalid input provided. Please check the data and try again.", + P2020: "A value is out of the allowed range. Please use a valid value.", + P2021: "The requested table or resource does not exist.", + P2022: "A required database column is missing.", + P2023: "Inconsistent data detected. Please try again or contact support if the issue continues.", + P2024: "The database request timed out. Please try again.", + P2025: "The record you're trying to update or delete was not found.", + P2026: "This operation isn't supported by the database engine.", + P2027: "Multiple database errors occurred. Please review the request and try again.", + P2028: "A database transaction failed. The operation was not completed. Please" -}; +} \ No newline at end of file From 125499a137e550b1fbcb67e14606a38fd09f8d84 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Sun, 24 Aug 2025 18:46:36 +0100 Subject: [PATCH 27/33] fix: implement fix suggested during second demo --- src/controllers/auth.controller.ts | 54 +++++++++++++++++++---- src/controllers/transaction.controller.ts | 39 ++++++++++------ src/middlewares/errorHandler.ts | 1 + src/prisma/schema.prisma | 7 +++ src/routes/auth.routes.ts | 37 +++++++++++++++- src/routes/transaction.routes.ts | 40 ++--------------- src/services/auth.service.ts | 33 ++++++-------- src/services/transaction.service.ts | 33 ++------------ src/utils/index.ts | 7 +++ 9 files changed, 142 insertions(+), 109 deletions(-) diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 5c2b403..c729737 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -2,7 +2,7 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; -import { createUser, getUser } from '@/services/auth.service'; +import { createUser, getUserByEmail } from '@/services/auth.service'; import utils from '@/utils/index'; import jwt from 'jsonwebtoken'; import { config } from '@/constants'; @@ -11,15 +11,24 @@ import { config } from '@/constants'; export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const validInput = utils.validateCreateUserInput(req.body); if (!validInput) { - throw (new AppError("Invalid input data", 400)); + throw (new AppError("Invalid input data", 400)); } - const user = await createUser({...req.body, email: req.body.email.toLowerCase()}); - + const { email, password, name } = req.body; + + + const emailExists = await getUserByEmail(email); + if (emailExists) { + throw new AppError("Email already exists", 409); + } + + + const user = await createUser({ password, name, email: utils.formatEmail(email) }); + const accessToken = jwt.sign( { userId: user.id }, config.JWT_SECRET!, - { expiresIn: "15m" } + { expiresIn: "7d" } ); const refreshToken = jwt.sign( @@ -36,25 +45,33 @@ export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); - return res.status(200).json(new ApiResponse("success", {user, accessToken})); + return res.status(200).json(new ApiResponse("success", { user, accessToken })); }) export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { //TODO: check if both params are wrong too - if (!utils.validPassword(req.body.password) || !utils.validEmail(req.body.email)) { + const { email, password } = req.body + if (!utils.validPassword(password) || !utils.validEmail(email)) { throw (new AppError("Invalid input data", 400)); } + const user = await getUserByEmail(utils.formatEmail(email)) + if (!user) { + throw new AppError("Check login Credentials", 404); + } - const user = await getUser(req.body); + const isMatch = await utils.decryptPassword(user.password, password); + if (!isMatch) { + throw new AppError("Invalid email or password", 400); + } const accessToken = jwt.sign( { userId: user.id }, config.JWT_SECRET!, - { expiresIn: "15m" } + { expiresIn: "1d" } ); const refreshToken = jwt.sign( @@ -63,6 +80,7 @@ export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, n { expiresIn: "7d" } ); + console.log(refreshToken); // Set refresh token as an HTTP-only cookie res.cookie("refreshToken", refreshToken, { httpOnly: true, @@ -75,3 +93,21 @@ export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, n }) + +export const handleRefreshToken = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const refreshToken = req.cookies.refreshToken; + if (!refreshToken) throw new AppError("Refresh token missing", 401); + const decoded = jwt.verify(refreshToken, config.JWT_REFRESH_SECRET!) as { userId: string };; + console.log(decoded) + const newAccessToken = jwt.sign({ userId: decoded.userId }, config.JWT_SECRET!, { expiresIn: "1d" }); + + res.status(200).json(new ApiResponse("success", { accessToken: newAccessToken })); + +}) + + + +export const handleLogout = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + res.clearCookie("refreshToken"); + res.status(200).json(new ApiResponse("success", "Logged out")); +}) \ No newline at end of file diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 339b66a..465bfae 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -2,7 +2,7 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; -import { setTransactionPIN, createDonation, getUserDonations, donationsInPeriod, getDonationById } from '@/services/transaction.service'; +import { setTransactionPIN, createDonation, donationsInPeriod, getDonationById } from '@/services/transaction.service'; import { getUserPrivateFn } from '@/services/auth.service'; import utils from '@/utils/index'; @@ -36,14 +36,24 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo throw (new AppError("Invalid transaction PIN", 401)); } - const beneficiary = await getUserPrivateFn(req.body.beneficiaryEmail); + const beneficiary = await getUserPrivateFn(utils.formatEmail(beneficiaryEmail)); + + if (!beneficiary) { + throw new AppError("Beneficiary does not exist", 404); + } + const user = (req as Request & { user?: User }).user! if (!user.transactionPIN) { throw new AppError("Please set a Transaction PIN first", 400); } + + if (utils.formatEmail(beneficiaryEmail) === user.email) { + throw new AppError("You cannot Donate to Self", 401); + } + const donation = await createDonation(user, beneficiary.id, amount, transactionPin); @@ -51,18 +61,8 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo }) - +//! DEPRECATED TO FAVOUR handleFilterDonations export const handleGetUserDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - const user = (req as Request & { user?: User }).user! - if (!user) { - throw new AppError("User not found", 404); - } - - const { limit, page } = req.query; - - const donations = await getUserDonations(user.id, page as string, limit as string); - - return res.status(200).json(new ApiResponse("success", donations)); }) @@ -82,17 +82,28 @@ export const handleFilterDonations = asyncHandler(async (req: Request, res: Resp const donations = await donationsInPeriod(user.id, startDate, endDate, page as string, limit as string); + if (!donations) { + throw new AppError("No donations found in this period", 404); + + } + + return res.status(200).json(new ApiResponse("success", donations)); } ) +// TODO: chck if it is a valid uuid export const handleDonationDetails = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const donationId = req.params.id; if (!donationId) { throw new AppError("Donation ID is required", 400); } - const donation = await getDonationById(donationId); + const donation = await getDonationById(donationId); + + if (!donation) { + throw new AppError("Donation not found", 404); + } return res.status(200).json(new ApiResponse("success", donation)); }); \ No newline at end of file diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index 60bdeee..3540340 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -12,6 +12,7 @@ export const errorHandler = ( ) => { let statusCode = (err as AppError).statusCode || 500; let message = err.message; + console.log(err.message) if (err instanceof Prisma.PrismaClientKnownRequestError) { // If Prisma gives us a code, check our map diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 5f52370..1ecc030 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -55,3 +55,10 @@ model Donation { @@index([donorId]) @@index([beneficiaryId]) } + +// TODO ADD TRANSACTIOJN MODEL AND RELATION TO DONATION +// model Transaction { +// id +// prev balacne +// balacne afeter tx +// } \ No newline at end of file diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 3abd099..c832395 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { handleCreateAcc, handleLoginAcc } from "@/controllers/auth.controller" +import { handleCreateAcc, handleLoginAcc, handleRefreshToken, handleLogout } from "@/controllers/auth.controller" const router = express.Router(); @@ -72,4 +72,39 @@ router.post('/create', handleCreateAcc) router.post('/login', handleLoginAcc) +/** + * @swagger + * /api/auth/refresh-token: + * post: + * summary: Refresh access token using refresh token cookie + * tags: [Auth] + * parameters: + * - in: cookie + * name: refreshToken + * schema: + * type: string + * required: true + * description: HTTP-only refresh token cookie + * responses: + * 200: + * description: New access token returned + * 401: + * description: Refresh token missing or invalid + */ +router.post("/refresh-token", handleRefreshToken); + + + +/** + * @swagger + * /api/auth/logout: + * get: + * summary: Logout (clears refresh token cookie) + * tags: [Auth] + * responses: + * 200: + * description: User logged out and refresh token cookie cleared + */ +router.get("/logout", handleLogout); + export default router; diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 2785b21..1d1ccfb 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -19,7 +19,7 @@ const router = express.Router(); * type: object * properties: * pin: - * type: number + * type: string * description: Must be exactly 4 or 6digits long * minLength: 4 * example: 123456 @@ -76,44 +76,10 @@ router.post('/pin', handleCreateTxPIN) router.post('/create-donation', handleCreateDonation) -/** - * @swagger - * /api/tx/my-donations: - * get: - * summary: Get all donations made by the user - * tags: [Donation] - * security: - * - bearerAuth: [] - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * minimum: 1 - * required: false - * description: Page number for pagination - * example: 1 - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * required: false - * description: Number of results per page - * example: 10 - * responses: - * 200: - * description: List of donations made by the user - * 401: - * description: Unauthorized - */ - -router.get("/my-donations", handleGetUserDonations); - /** * @swagger - * /api/tx/filter-donations: + * /api/tx/my-donations: * get: * summary: Filter donations made by the user within a date range * tags: [Donation] @@ -157,7 +123,7 @@ router.get("/my-donations", handleGetUserDonations); * description: Filtered list of donations made by the user */ -router.get("/filter-donations", handleFilterDonations ); +router.get("/my-donations", handleFilterDonations); /** diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 8f6d69f..f3d9f71 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -4,12 +4,17 @@ import { AppError } from '@/utils/AppError'; const prisma = new PrismaClient() +type CreateUserT = { + email: string; + password: string; + name: string; +} -// TODO: check if user exists first to thrpw a better error message -export const createUser = async (data: User) => { +export const createUser = async (data: CreateUserT) => { const { password } = data; const hashedPassword = await utils.hashPassword(password); + const user = await prisma.user.create({ data: { ...data, password: hashedPassword, @@ -24,22 +29,14 @@ export const createUser = async (data: User) => { -export const getUser = async (data: { email: string; password: string }) => { - const userWithPassword = await prisma.user.findUnique({ - where: { email: data.email }, +export const getUserByEmail = async (email: string) => { + const user = await prisma.user.findUnique({ + where: { email } + }) - }); + return user +} - if (!userWithPassword) { - throw new AppError("Invalid email or password", 401); - } - const isMatch = await utils.decryptPassword(userWithPassword.password, data.password); - if (!isMatch) { - throw new AppError("Invalid email or password", 401); - } - const { password, transactionPIN, ...safeUser } = userWithPassword; - return safeUser; -}; @@ -49,9 +46,7 @@ export const getUserPrivateFn = async (email: string) => { where: { email }, }); - if (!user) { - throw new AppError("Beneficiary does not exist", 401); - } + return user; }; diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index aa3a9e2..20dcb6c 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -19,7 +19,7 @@ export const setTransactionPIN = async (userId: string, pin: number) => { //MAIN FUNCTION // ------------------------------------- START ----------------------- - +// TODO : disable send to self export const createDonation = async ( donor: User, beneficiaryId: string, @@ -30,7 +30,6 @@ export const createDonation = async ( throw new AppError("Donation amount must be greater than 0 :(", 400); } - // TODO: check for transcation pin if user has created one or if it matches if (!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { throw new AppError("Invalid transaction PIN", 401); } @@ -100,28 +99,8 @@ export const countUserDonations = async (userId: string) => { }; -export const getUserDonations = async (userId: string, page?: string, limit?: string) => { - var page_; - var limit_; - if (page) { - page_ = +page; - - } - if (limit) { - limit_ = +limit; - - } - - return await paginate({ - model: 'donation', - where: { - donorId: userId, - }, - page: page_, - limit: limit_ - }) -}; +// TODO: filter my dmy and merge it into getUserDonations export const donationsInPeriod = async (userId: string, start: Date, end: Date, page?: string, limit?: string) => { var page_; var limit_; @@ -143,9 +122,6 @@ export const donationsInPeriod = async (userId: string, start: Date, end: Date, page: page_, limit: limit_ }); - if (!data) { - throw new AppError("No donations found in this period", 404); - } return data; }; @@ -156,9 +132,8 @@ export const getDonationById = async (donationId: string) => { where: { id: donationId } // include: { beneficiary: true, donor: true } }); - if (!data) { - throw new AppError("Donation not found", 404); - } + return data; }; + diff --git a/src/utils/index.ts b/src/utils/index.ts index e93836f..6a09ba9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -29,6 +29,13 @@ const utils = { const pinRegex = /^(\d{4}|\d{6})$/; return pinRegex.test(pin); }, + formatEmail: (email: string) => { + let formatted = email.trim(); + formatted = formatted.toLowerCase(); + // Collapse multiple spaces inside (shouldn’t normally exist but just in case) + formatted = formatted.replace(/\s+/g, ""); + return formatted; + } }; From 7a4fb336729afb3aff1b1cb1d48d7e80b39abf05 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 25 Aug 2025 06:09:51 +0100 Subject: [PATCH 28/33] add transaction schema --- src/controllers/transaction.controller.ts | 11 ++-- .../migration.sql | 54 +++++++++++++++++++ .../migration.sql | 8 +++ src/prisma/schema.prisma | 54 +++++++++++++++---- src/services/transaction.service.ts | 53 +++++++++++++++--- 5 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 src/prisma/migrations/20250824190139_add_transaction_model/migration.sql create mode 100644 src/prisma/migrations/20250825050302_remove_transaction_id_from_donation_modal/migration.sql diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 465bfae..dfadb21 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -4,7 +4,7 @@ import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; import { setTransactionPIN, createDonation, donationsInPeriod, getDonationById } from '@/services/transaction.service'; import { getUserPrivateFn } from '@/services/auth.service'; - +import { validate as uuidValidate } from 'uuid'; import utils from '@/utils/index'; import { User } from '@prisma/client'; @@ -20,7 +20,7 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response await setTransactionPIN(user.id, req.body.pin); - return res.status(200).json(new ApiResponse("success", "Pin created successfully")); + return res.status(201).json(new ApiResponse("success", "Pin created successfully")); }) @@ -57,7 +57,7 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo const donation = await createDonation(user, beneficiary.id, amount, transactionPin); - return res.status(200).json(new ApiResponse("success", donation)); + return res.status(201).json(new ApiResponse("success", donation)); }) @@ -98,7 +98,10 @@ export const handleDonationDetails = asyncHandler(async (req: Request, res: Resp if (!donationId) { throw new AppError("Donation ID is required", 400); } - + const validUUID = uuidValidate(donationId); + if (!validUUID) { + throw new AppError("Invalid donation ID format", 400); + } const donation = await getDonationById(donationId); if (!donation) { diff --git a/src/prisma/migrations/20250824190139_add_transaction_model/migration.sql b/src/prisma/migrations/20250824190139_add_transaction_model/migration.sql new file mode 100644 index 0000000..a3a415f --- /dev/null +++ b/src/prisma/migrations/20250824190139_add_transaction_model/migration.sql @@ -0,0 +1,54 @@ +/* + Warnings: + + - Added the required column `transactionId` to the `Donation` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "public"."TransactionType" AS ENUM ('DONATION', 'AIRTIME_PURCHASE', 'WITHDRAWAL', 'DEPOSIT', 'TRANSFER'); + +-- AlterTable +ALTER TABLE "public"."Donation" ADD COLUMN "transactionId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "public"."Transaction" ( + "id" TEXT NOT NULL, + "type" "public"."TransactionType" NOT NULL, + "reference" TEXT, + "description" TEXT, + "donationId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."TransactionEntry" ( + "id" TEXT NOT NULL, + "transactionId" TEXT NOT NULL, + "walletId" TEXT NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "balanceBefore" DOUBLE PRECISION NOT NULL, + "balanceAfter" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "TransactionEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Transaction_donationId_key" ON "public"."Transaction"("donationId"); + +-- CreateIndex +CREATE INDEX "TransactionEntry_transactionId_idx" ON "public"."TransactionEntry"("transactionId"); + +-- CreateIndex +CREATE INDEX "TransactionEntry_walletId_idx" ON "public"."TransactionEntry"("walletId"); + +-- AddForeignKey +ALTER TABLE "public"."Transaction" ADD CONSTRAINT "Transaction_donationId_fkey" FOREIGN KEY ("donationId") REFERENCES "public"."Donation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TransactionEntry" ADD CONSTRAINT "TransactionEntry_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "public"."Transaction"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TransactionEntry" ADD CONSTRAINT "TransactionEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "public"."Wallet"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20250825050302_remove_transaction_id_from_donation_modal/migration.sql b/src/prisma/migrations/20250825050302_remove_transaction_id_from_donation_modal/migration.sql new file mode 100644 index 0000000..e2cd59c --- /dev/null +++ b/src/prisma/migrations/20250825050302_remove_transaction_id_from_donation_modal/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `transactionId` on the `Donation` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "public"."Donation" DROP COLUMN "transactionId"; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 1ecc030..58874e8 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -35,6 +35,8 @@ model Wallet { userId String @id @unique user User @relation(fields: [userId], references: [id]) + transactionEntries TransactionEntry[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -42,12 +44,13 @@ model Wallet { } model Donation { - id String @id @default(uuid()) + id String @id @default(uuid()) amount Float donorId String beneficiaryId String - donor User @relation(fields: [donorId], references: [id]) - beneficiary User @relation("UserReceivedDonations", fields: [beneficiaryId], references: [id]) + donor User @relation(fields: [donorId], references: [id]) + beneficiary User @relation("UserReceivedDonations", fields: [beneficiaryId], references: [id]) + transaction Transaction? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -56,9 +59,42 @@ model Donation { @@index([beneficiaryId]) } -// TODO ADD TRANSACTIOJN MODEL AND RELATION TO DONATION -// model Transaction { -// id -// prev balacne -// balacne afeter tx -// } \ No newline at end of file +model Transaction { + id String @id @default(uuid()) + type TransactionType + reference String? // e.g. external reference, donationId, airtimeId + description String? + + entries TransactionEntry[] + + donation Donation? @relation(fields: [donationId], references: [id]) + donationId String? @unique + + createdAt DateTime @default(now()) +} + +model TransactionEntry { + id String @id @default(uuid()) + transactionId String + transaction Transaction @relation(fields: [transactionId], references: [id]) + + walletId String + wallet Wallet @relation(fields: [walletId], references: [userId]) + + amount Float // positive for credit, negative for debit + balanceBefore Float + balanceAfter Float + + createdAt DateTime @default(now()) + + @@index([transactionId]) + @@index([walletId]) +} + +enum TransactionType { + DONATION + AIRTIME_PURCHASE + WITHDRAWAL + DEPOSIT + TRANSFER +} diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 20dcb6c..f0d2c52 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -36,6 +36,7 @@ export const createDonation = async ( const data = await prisma.$transaction(async (tx) => { + // ? 1 - GET DONAOR AND BENEFICIARY WALLETS const donorWallet = await tx.wallet.findUnique({ where: { userId: donor.id }, @@ -59,8 +60,8 @@ export const createDonation = async ( throw new AppError("Beneficiary wallet not found", 404); } - - const donated = await tx.donation.create({ + // ? 2 -CREATE DONATION RECORD + const donation = await tx.donation.create({ data: { amount, donorId: donor.id, @@ -68,22 +69,60 @@ export const createDonation = async ( } }); - // -------------------------STARTS HERE - await tx.wallet.update({ + + //? 3 - CREATE TRANSACTION RECORD + + const transaction = await tx.transaction.create({ + data: { + type: "DONATION", + description: `Donation of N${amount} from ${donor.id} to ${beneficiaryId}`, + donationId: donation.id + } + }) + + + //? 4 - UPDATE WALLET BALANCE + const updatedDonorWallet = await tx.wallet.update({ where: { userId: donor.id }, data: { balance: { decrement: amount } } }); - await tx.wallet.update({ + const updatedBeneficiaryWallet = await tx.wallet.update({ where: { userId: beneficiaryId }, data: { balance: { increment: amount } } }); - // -------------------------STARTS HERE + //? 5 - CREATE TRANSCATION ENTRY ROWS + + const debitEntry = await tx.transactionEntry.create({ + data: { + transactionId: transaction.id, + walletId: donor.id, + amount: -amount, + balanceBefore: donorWallet.balance, + balanceAfter: updatedDonorWallet.balance + } + }) + + const creditEntry = await tx.transactionEntry.create({ + data: { + transactionId: transaction.id, + walletId: beneficiaryId, + amount: +amount, + balanceBefore: beneficiaryWallet.balance, + balanceAfter: updatedBeneficiaryWallet.balance + } + }) + - return donated + + return { + donation, + transaction, + entries: [debitEntry, creditEntry] + } }); return data From d5671f1db2e1857328d508b6a10cc7d6e94bf777 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 25 Aug 2025 06:32:53 +0100 Subject: [PATCH 29/33] fix: seperate transactions from donation files --- src/controllers/donation.controller.ts | 97 +++++++++++++ src/controllers/transaction.controller.ts | 103 ++----------- src/controllers/user.controller.ts | 12 ++ src/routes/donation.routes.ts | 129 +++++++++++++++++ src/routes/index.ts | 5 +- src/routes/transaction.routes.ts | 121 +--------------- src/routes/user.routes.ts | 26 ++++ src/services/donation.service.ts | 167 +++++++++++++++++++++ src/services/transaction.service.ts | 168 +--------------------- 9 files changed, 451 insertions(+), 377 deletions(-) create mode 100644 src/controllers/donation.controller.ts create mode 100644 src/controllers/user.controller.ts create mode 100644 src/routes/donation.routes.ts create mode 100644 src/routes/user.routes.ts create mode 100644 src/services/donation.service.ts diff --git a/src/controllers/donation.controller.ts b/src/controllers/donation.controller.ts new file mode 100644 index 0000000..2fa84cb --- /dev/null +++ b/src/controllers/donation.controller.ts @@ -0,0 +1,97 @@ +import { asyncHandler } from '@/middlewares/asyncHandler'; +import { ApiResponse } from '@/utils/ApiResponse'; +import { AppError } from '@/utils/AppError'; +import { NextFunction, Request, Response } from 'express'; +import { createDonation, donationsInPeriod, getDonationById } from '@/services/donation.service'; +import { getUserPrivateFn } from '@/services/auth.service'; +import { validate as uuidValidate } from 'uuid'; +import utils from '@/utils/index'; +import { User } from '@prisma/client'; + + + +export const handleCreateDonation = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + //TODO: check if both params are wrong too + const { amount, beneficiaryEmail, transactionPin } = req.body + if ((typeof amount != "number") || !utils.validEmail(beneficiaryEmail)) { + throw (new AppError("Invalid input data", 400)); + } + + if (!transactionPin || !utils.validPIN(transactionPin)) { + throw (new AppError("Invalid transaction PIN", 401)); + } + + + + const beneficiary = await getUserPrivateFn(utils.formatEmail(beneficiaryEmail)); + + if (!beneficiary) { + throw new AppError("Beneficiary does not exist", 404); + } + + + const user = (req as Request & { user?: User }).user! + if (!user.transactionPIN) { + throw new AppError("Please set a Transaction PIN first", 400); + } + + if (utils.formatEmail(beneficiaryEmail) === user.email) { + throw new AppError("You cannot Donate to Self", 401); + } + + const donation = await createDonation(user, beneficiary.id, amount, transactionPin); + + + return res.status(201).json(new ApiResponse("success", donation)); + +}) + +//! DEPRECATED TO FAVOUR handleFilterDonations +export const handleGetUserDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { +}) + + +export const handleFilterDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const user = (req as Request & { user?: User }).user! + + const { start, end, limit, page } = req.query; + if (!start || !end) { + throw new AppError("Start and end dates are required", 400); + } + const startDate = new Date(start as string); + const endDate = new Date(end as string); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new AppError("Invalid date format", 400); + } + + const donations = await donationsInPeriod(user.id, startDate, endDate, page as string, limit as string); + + if (!donations) { + throw new AppError("No donations found in this period", 404); + + } + + + return res.status(200).json(new ApiResponse("success", donations)); +} +) + +// TODO: chck if it is a valid uuid +export const handleDonationDetails = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const donationId = req.params.id; + if (!donationId) { + throw new AppError("Donation ID is required", 400); + } + const validUUID = uuidValidate(donationId); + if (!validUUID) { + throw new AppError("Invalid donation ID format", 400); + } + const donation = await getDonationById(donationId); + + if (!donation) { + throw new AppError("Donation not found", 404); + } + + return res.status(200).json(new ApiResponse("success", donation)); +}); \ No newline at end of file diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index dfadb21..1cf69e9 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -1,12 +1,12 @@ -import { asyncHandler } from '@/middlewares/asyncHandler'; -import { ApiResponse } from '@/utils/ApiResponse'; -import { AppError } from '@/utils/AppError'; -import { NextFunction, Request, Response } from 'express'; -import { setTransactionPIN, createDonation, donationsInPeriod, getDonationById } from '@/services/transaction.service'; -import { getUserPrivateFn } from '@/services/auth.service'; -import { validate as uuidValidate } from 'uuid'; -import utils from '@/utils/index'; -import { User } from '@prisma/client'; +import { asyncHandler } from "@/middlewares/asyncHandler"; +import { setTransactionPIN } from "@/services/transaction.service"; +import utils from "@/utils"; +import { ApiResponse } from "@/utils/ApiResponse"; +import { AppError } from "@/utils/AppError"; +import { User } from "@prisma/client"; +import { NextFunction, Request, Response } from "express"; + + @@ -25,88 +25,7 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response }) -export const handleCreateDonation = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - //TODO: check if both params are wrong too - const { amount, beneficiaryEmail, transactionPin } = req.body - if ((typeof amount != "number") || !utils.validEmail(beneficiaryEmail)) { - throw (new AppError("Invalid input data", 400)); - } - - if (!transactionPin || !utils.validPIN(transactionPin)) { - throw (new AppError("Invalid transaction PIN", 401)); - } - - - - const beneficiary = await getUserPrivateFn(utils.formatEmail(beneficiaryEmail)); - - if (!beneficiary) { - throw new AppError("Beneficiary does not exist", 404); - } - - - const user = (req as Request & { user?: User }).user! - if (!user.transactionPIN) { - throw new AppError("Please set a Transaction PIN first", 400); - } - - if (utils.formatEmail(beneficiaryEmail) === user.email) { - throw new AppError("You cannot Donate to Self", 401); - } - - const donation = await createDonation(user, beneficiary.id, amount, transactionPin); - - - return res.status(201).json(new ApiResponse("success", donation)); - -}) - -//! DEPRECATED TO FAVOUR handleFilterDonations -export const handleGetUserDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { -}) - - -export const handleFilterDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - const user = (req as Request & { user?: User }).user! - - const { start, end, limit, page } = req.query; - if (!start || !end) { - throw new AppError("Start and end dates are required", 400); - } - const startDate = new Date(start as string); - const endDate = new Date(end as string); - - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - throw new AppError("Invalid date format", 400); - } - - const donations = await donationsInPeriod(user.id, startDate, endDate, page as string, limit as string); - - if (!donations) { - throw new AppError("No donations found in this period", 404); - - } - +export const handleGetUserTransactions = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - return res.status(200).json(new ApiResponse("success", donations)); -} -) - -// TODO: chck if it is a valid uuid -export const handleDonationDetails = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { - const donationId = req.params.id; - if (!donationId) { - throw new AppError("Donation ID is required", 400); - } - const validUUID = uuidValidate(donationId); - if (!validUUID) { - throw new AppError("Invalid donation ID format", 400); - } - const donation = await getDonationById(donationId); - - if (!donation) { - throw new AppError("Donation not found", 404); - } - return res.status(200).json(new ApiResponse("success", donation)); -}); \ No newline at end of file +}) \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..8d6f300 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,12 @@ +import { asyncHandler } from '@/middlewares/asyncHandler'; +import { ApiResponse } from '@/utils/ApiResponse'; +import { NextFunction, Request, Response } from 'express'; +import { User } from '@prisma/client'; + + + +export const handleGetUser = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const user = (req as Request & { user?: User }).user! + return res.status(200).json(new ApiResponse("success", user)); + +}) diff --git a/src/routes/donation.routes.ts b/src/routes/donation.routes.ts new file mode 100644 index 0000000..2880266 --- /dev/null +++ b/src/routes/donation.routes.ts @@ -0,0 +1,129 @@ +import express from 'express'; +import { handleCreateDonation, handleGetUserDonations, handleFilterDonations, handleDonationDetails } from "@/controllers/donation.controller" +const router = express.Router(); + + + + +/** + * @swagger + * /api/donation/create: + * post: + * summary: Create a donation transaction + * tags: [Donation] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - amount + * - beneficiaryEmail + * properties: + * amount: + * type: number + * description: Amount to donate + * example: 50.0 + * beneficiaryEmail: + * type: string + * description: Email of the beneficiary receiving the donation + * example: "dunsin@exmaple.com" + * transactionPin: + * type: string + * description: User's 4 0r 6 digit transaction PIN + * example: "123456" + * required: true + * responses: + * 201: + * description: Donation created successfully + * 400: + * description: Invalid request payload or parameters + * 401: + * description: Unaut horized — invalid transaction PIN + * 500: + * description: Internal server error + */ + + +router.post('/create', handleCreateDonation) + + +/** + * @swagger + * /api/donation/my-donations: + * get: + * summary: Get donations made by the user within a date range + * tags: [Donation] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: start + * schema: + * type: string + * format: date-time + * required: true + * description: Start date for filtering donations + * example: "2023-01-01T00:00:00Z" + * - in: query + * name: end + * schema: + * type: string + * format: date-time + * required: true + * description: End date for filtering donations + * example: "2025-12-31T23:59:59Z" + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Page number for pagination + * example: 1 + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Number of results per page + * example: 10 + * responses: + * 200: + * description: list of donations made by the user + */ + +router.get("/my-donations", handleFilterDonations); + + +/** + * @swagger + * /api/donation/{id}: + * get: + * summary: Get details of a specific donation by ID + * tags: [Donation] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The ID of the donation to retrieve + * responses: + * 200: + * description: Donation details retrieved successfully + * 404: + * description: Donation not found + */ +router.get("/:id", handleDonationDetails) + + + + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index fd72fff..8bc063a 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,7 +2,8 @@ import express from 'express'; import authRoutes from './auth.routes'; import transactionRoutes from './transaction.routes'; import { ensureAuthenticated } from "@/middlewares/index" - +import userRoutes from "./user.routes" +import donationRoutes from "./donation.routes" const router = express.Router(); @@ -10,5 +11,7 @@ const router = express.Router(); router.use("/auth", authRoutes); router.use("/tx", ensureAuthenticated, transactionRoutes) +router.use("/donation", ensureAuthenticated, donationRoutes) +router.use("/user", ensureAuthenticated, userRoutes) export default router \ No newline at end of file diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 1d1ccfb..d32f2b4 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { handleCreateTxPIN, handleCreateDonation, handleGetUserDonations, handleFilterDonations, handleDonationDetails } from "@/controllers/transaction.controller" +import { handleCreateTxPIN, handleGetUserTransactions } from "@/controllers/transaction.controller" const router = express.Router(); @@ -30,124 +30,7 @@ const router = express.Router(); router.post('/pin', handleCreateTxPIN) - -/** - * @swagger - * /api/tx/create-donation: - * post: - * summary: Create a donation transaction - * tags: [Donation] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - amount - * - beneficiaryEmail - * properties: - * amount: - * type: number - * description: Amount to donate - * example: 50.0 - * beneficiaryEmail: - * type: string - * description: Email of the beneficiary receiving the donation - * example: "dunsin@exmaple.com" - * transactionPin: - * type: string - * description: User's 4 0r 6 digit transaction PIN - * example: "123456" - * required: true - * responses: - * 201: - * description: Donation created successfully - * 400: - * description: Invalid request payload or parameters - * 401: - * description: Unaut horized — invalid transaction PIN - * 500: - * description: Internal server error - */ - - -router.post('/create-donation', handleCreateDonation) - - -/** - * @swagger - * /api/tx/my-donations: - * get: - * summary: Filter donations made by the user within a date range - * tags: [Donation] - * security: - * - bearerAuth: [] - * parameters: - * - in: query - * name: start - * schema: - * type: string - * format: date-time - * required: true - * description: Start date for filtering donations - * example: "2023-01-01T00:00:00Z" - * - in: query - * name: end - * schema: - * type: string - * format: date-time - * required: true - * description: End date for filtering donations - * example: "2025-12-31T23:59:59Z" - * - in: query - * name: page - * schema: - * type: integer - * minimum: 1 - * required: false - * description: Page number for pagination - * example: 1 - * - in: query - * name: limit - * schema: - * type: integer - * minimum: 1 - * required: false - * description: Number of results per page - * example: 10 - * responses: - * 200: - * description: Filtered list of donations made by the user - */ - -router.get("/my-donations", handleFilterDonations); - - -/** - * @swagger - * /api/tx/donation/{id}: - * get: - * summary: Get details of a specific donation by ID - * tags: [Donation] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: string - * description: The ID of the donation to retrieve - * responses: - * 200: - * description: Donation details retrieved successfully - * 404: - * description: Donation not found - */ -router.get("/donation/:id", handleDonationDetails) +router.get("/", handleGetUserTransactions) diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100644 index 0000000..308cb51 --- /dev/null +++ b/src/routes/user.routes.ts @@ -0,0 +1,26 @@ +import express from 'express'; +import { handleGetUser } from "@/controllers/user.controller" +const router = express.Router(); + + +/** + * @swagger + * /api/user: + * post: + * summary: Return logged in User + * tags: [User] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Logged in User + */ + +router.post('/', handleGetUser) + + + + + + +export default router; diff --git a/src/services/donation.service.ts b/src/services/donation.service.ts new file mode 100644 index 0000000..0c9568e --- /dev/null +++ b/src/services/donation.service.ts @@ -0,0 +1,167 @@ + +import { PrismaClient, User } from '@prisma/client' +import utils from '@/utils/index'; +import { AppError } from '@/utils/AppError'; +import { paginate } from '@/utils/pagintion'; + +const prisma = new PrismaClient() + +//MAIN FUNCTION +// ------------------------------------- START ----------------------- +// TODO : disable send to self +export const createDonation = async ( + donor: User, + beneficiaryId: string, + amount: number, + txPIN: string +) => { + if (amount <= 0) { + throw new AppError("Donation amount must be greater than 0 :(", 400); + } + + if (!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { + throw new AppError("Invalid transaction PIN", 401); + } + + + const data = await prisma.$transaction(async (tx) => { + // ? 1 - GET DONAOR AND BENEFICIARY WALLETS + + const donorWallet = await tx.wallet.findUnique({ + where: { userId: donor.id }, + select: { balance: true } + }); + + if (!donorWallet) { + throw new AppError("Donor wallet not found", 404); + } + + if (donorWallet.balance < amount) { + throw new AppError("Insufficient balance to make donation", 400); + } + + const beneficiaryWallet = await tx.wallet.findUnique({ + where: { userId: beneficiaryId }, + select: { balance: true } + }); + + if (!beneficiaryWallet) { + throw new AppError("Beneficiary wallet not found", 404); + } + + // ? 2 -CREATE DONATION RECORD + const donation = await tx.donation.create({ + data: { + amount, + donorId: donor.id, + beneficiaryId + } + }); + + + //? 3 - CREATE TRANSACTION RECORD + + const transaction = await tx.transaction.create({ + data: { + type: "DONATION", + description: `Donation of N${amount} from ${donor.id} to ${beneficiaryId}`, + donationId: donation.id + } + }) + + + //? 4 - UPDATE WALLET BALANCE + const updatedDonorWallet = await tx.wallet.update({ + where: { userId: donor.id }, + data: { balance: { decrement: amount } } + }); + + + const updatedBeneficiaryWallet = await tx.wallet.update({ + where: { userId: beneficiaryId }, + data: { balance: { increment: amount } } + }); + + //? 5 - CREATE TRANSCATION ENTRY ROWS + + const debitEntry = await tx.transactionEntry.create({ + data: { + transactionId: transaction.id, + walletId: donor.id, + amount: -amount, + balanceBefore: donorWallet.balance, + balanceAfter: updatedDonorWallet.balance + } + }) + + const creditEntry = await tx.transactionEntry.create({ + data: { + transactionId: transaction.id, + walletId: beneficiaryId, + amount: +amount, + balanceBefore: beneficiaryWallet.balance, + balanceAfter: updatedBeneficiaryWallet.balance + } + }) + + + + + return { + donation, + transaction, + entries: [debitEntry, creditEntry] + } + }); + + return data +}; + +// ------------------------------------- END ----------------------- + + +export const countUserDonations = async (userId: string) => { + return prisma.donation.count({ + where: { donorId: userId } + }); +}; + + + +// TODO: filter my dmy and merge it into getUserDonations +export const donationsInPeriod = async (userId: string, start: Date, end: Date, page?: string, limit?: string) => { + var page_; + var limit_; + if (page) { + page_ = +page; + + } + if (limit) { + limit_ = +limit; + + } + const data = await paginate({ + model: 'donation', + where: { + donorId: userId, + createdAt: { gte: start, lte: end } + }, + orderBy: { createdAt: 'desc' }, + page: page_, + limit: limit_ + }); + return data; +}; + + + +export const getDonationById = async (donationId: string) => { + const data = prisma.donation.findUnique({ + where: { id: donationId } + // include: { beneficiary: true, donor: true } + }); + + return data; +}; + + diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index f0d2c52..def6159 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -1,8 +1,7 @@ +import utils from "@/utils"; +import { PrismaClient } from "@prisma/client"; + -import { PrismaClient, User } from '@prisma/client' -import utils from '@/utils/index'; -import { AppError } from '@/utils/AppError'; -import { paginate } from '@/utils/pagintion'; const prisma = new PrismaClient() @@ -15,164 +14,3 @@ export const setTransactionPIN = async (userId: string, pin: number) => { }); }; - - -//MAIN FUNCTION -// ------------------------------------- START ----------------------- -// TODO : disable send to self -export const createDonation = async ( - donor: User, - beneficiaryId: string, - amount: number, - txPIN: string -) => { - if (amount <= 0) { - throw new AppError("Donation amount must be greater than 0 :(", 400); - } - - if (!await utils.decryptPassword(donor.transactionPIN!, txPIN)) { - throw new AppError("Invalid transaction PIN", 401); - } - - - const data = await prisma.$transaction(async (tx) => { - // ? 1 - GET DONAOR AND BENEFICIARY WALLETS - - const donorWallet = await tx.wallet.findUnique({ - where: { userId: donor.id }, - select: { balance: true } - }); - - if (!donorWallet) { - throw new AppError("Donor wallet not found", 404); - } - - if (donorWallet.balance < amount) { - throw new AppError("Insufficient balance to make donation", 400); - } - - const beneficiaryWallet = await tx.wallet.findUnique({ - where: { userId: beneficiaryId }, - select: { balance: true } - }); - - if (!beneficiaryWallet) { - throw new AppError("Beneficiary wallet not found", 404); - } - - // ? 2 -CREATE DONATION RECORD - const donation = await tx.donation.create({ - data: { - amount, - donorId: donor.id, - beneficiaryId - } - }); - - - //? 3 - CREATE TRANSACTION RECORD - - const transaction = await tx.transaction.create({ - data: { - type: "DONATION", - description: `Donation of N${amount} from ${donor.id} to ${beneficiaryId}`, - donationId: donation.id - } - }) - - - //? 4 - UPDATE WALLET BALANCE - const updatedDonorWallet = await tx.wallet.update({ - where: { userId: donor.id }, - data: { balance: { decrement: amount } } - }); - - - const updatedBeneficiaryWallet = await tx.wallet.update({ - where: { userId: beneficiaryId }, - data: { balance: { increment: amount } } - }); - - //? 5 - CREATE TRANSCATION ENTRY ROWS - - const debitEntry = await tx.transactionEntry.create({ - data: { - transactionId: transaction.id, - walletId: donor.id, - amount: -amount, - balanceBefore: donorWallet.balance, - balanceAfter: updatedDonorWallet.balance - } - }) - - const creditEntry = await tx.transactionEntry.create({ - data: { - transactionId: transaction.id, - walletId: beneficiaryId, - amount: +amount, - balanceBefore: beneficiaryWallet.balance, - balanceAfter: updatedBeneficiaryWallet.balance - } - }) - - - - - return { - donation, - transaction, - entries: [debitEntry, creditEntry] - } - }); - - return data -}; - -// ------------------------------------- END ----------------------- - - -export const countUserDonations = async (userId: string) => { - return prisma.donation.count({ - where: { donorId: userId } - }); -}; - - - -// TODO: filter my dmy and merge it into getUserDonations -export const donationsInPeriod = async (userId: string, start: Date, end: Date, page?: string, limit?: string) => { - var page_; - var limit_; - if (page) { - page_ = +page; - - } - if (limit) { - limit_ = +limit; - - } - const data = await paginate({ - model: 'donation', - where: { - donorId: userId, - createdAt: { gte: start, lte: end } - }, - orderBy: { createdAt: 'desc' }, - page: page_, - limit: limit_ - }); - return data; -}; - - - -export const getDonationById = async (donationId: string) => { - const data = prisma.donation.findUnique({ - where: { id: donationId } - // include: { beneficiary: true, donor: true } - }); - - return data; -}; - - From e1b3b31f63dfca4c2b7d6b89776fa6c774fdb6eb Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 25 Aug 2025 14:29:04 +0100 Subject: [PATCH 30/33] feat: create deopsit transaction on accounr creation from sydtem account --- USAGE.md | 5 + package.json | 15 ++- src/controllers/auth.controller.ts | 11 ++- src/controllers/donation.controller.ts | 10 +- src/controllers/transaction.controller.ts | 18 +++- src/controllers/user.controller.ts | 5 +- src/middlewares/prismaErrors.ts | 2 +- .../migration.sql | 2 + src/prisma/schema.prisma | 2 +- src/prisma/seed.ts | 34 +++++++ src/routes/auth.routes.ts | 1 + src/routes/transaction.routes.ts | 12 +++ src/routes/user.routes.ts | 23 ++++- src/services/auth.service.ts | 98 +++++++++++++++---- src/services/donation.service.ts | 4 - src/services/transaction.service.ts | 35 +++++++ yarn.lock | 5 + 17 files changed, 240 insertions(+), 42 deletions(-) create mode 100644 src/prisma/migrations/20250825120344_set_default_wallet_balance_to_0/migration.sql create mode 100644 src/prisma/seed.ts diff --git a/USAGE.md b/USAGE.md index 115fa2d..94ee5e1 100644 --- a/USAGE.md +++ b/USAGE.md @@ -50,6 +50,8 @@ Also fill in other required values like: --- + + ## ▶️ Running the Project Start the development server with: @@ -58,6 +60,9 @@ Start the development server with: yarn dev ``` +Run the seed data +`yarn run db:seed` to create a paritie system account + If everything is working, you’ll see logs like this: ```bash diff --git a/package.json b/package.json index bd244d3..f5af147 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,15 @@ "build": "npm run clean && tsc && cp -r src/prisma/ dist/prisma/", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", - "db:migrate": "cd ./src && npx prisma migrate dev --name add_other_models", + "db:migrate": "cd ./src && npx prisma migrate dev --name set_default_walletBalance_to_0", "db:init": "cd ./src && npx prisma generate", "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", - "db:deploy:prod": "cd ./dist && npx prisma migrate deploy" + "db:deploy:prod": "cd ./dist && npx prisma migrate deploy", + "db:seed": "npx prisma db seed --schema ./src/prisma/schema.prisma" + }, + "prisma": { + "seed": "ts-node ./src/prisma/seed.ts" }, "_moduleAliases": { "@": "./src" @@ -39,7 +43,8 @@ "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "tsconfig-paths": "^4.2.0", - "ulid": "^3.0.0" + "ulid": "^3.0.0", + "uuid": "^11.1.0" }, "devDependencies": { "@types/cookie-parser": "^1.4.8", @@ -60,7 +65,7 @@ "typescript": "^5.8.2" }, "engines": { - "node": "^20.14.0", + "node": "^22.14.0", "yarn": "^1.22.19" } -} +} \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index c729737..a839333 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -2,7 +2,7 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { AppError } from '@/utils/AppError'; import { NextFunction, Request, Response } from 'express'; -import { createUser, getUserByEmail } from '@/services/auth.service'; +import { createUser, getUserByEmail, getUserPrivateFn } from '@/services/auth.service'; import utils from '@/utils/index'; import jwt from 'jsonwebtoken'; import { config } from '@/constants'; @@ -23,6 +23,9 @@ export const handleCreateAcc = asyncHandler(async (req: Request, res: Response, const user = await createUser({ password, name, email: utils.formatEmail(email) }); + if (!user) { + throw new AppError("Error creating an Account, please try again", 500); + } const accessToken = jwt.sign( @@ -57,7 +60,7 @@ export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, n throw (new AppError("Invalid input data", 400)); } - const user = await getUserByEmail(utils.formatEmail(email)) + const user = await getUserPrivateFn(utils.formatEmail(email)) if (!user) { throw new AppError("Check login Credentials", 404); } @@ -67,6 +70,8 @@ export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, n throw new AppError("Invalid email or password", 400); } + const safeUser = await getUserByEmail(utils.formatEmail(email)) + const accessToken = jwt.sign( { userId: user.id }, @@ -89,7 +94,7 @@ export const handleLoginAcc = asyncHandler(async (req: Request, res: Response, n maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); - return res.status(200).json(new ApiResponse("success", { user, accessToken })); + return res.status(200).json(new ApiResponse("success", { user: safeUser, accessToken })); }) diff --git a/src/controllers/donation.controller.ts b/src/controllers/donation.controller.ts index 2fa84cb..93941e2 100644 --- a/src/controllers/donation.controller.ts +++ b/src/controllers/donation.controller.ts @@ -17,12 +17,14 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo throw (new AppError("Invalid input data", 400)); } + const user = (req as Request & { user?: User }).user! + if (!user.transactionPIN) { + throw new AppError("Please set a Transaction PIN first", 400); + } if (!transactionPin || !utils.validPIN(transactionPin)) { throw (new AppError("Invalid transaction PIN", 401)); } - - const beneficiary = await getUserPrivateFn(utils.formatEmail(beneficiaryEmail)); if (!beneficiary) { @@ -30,10 +32,6 @@ export const handleCreateDonation = asyncHandler(async (req: Request, res: Respo } - const user = (req as Request & { user?: User }).user! - if (!user.transactionPIN) { - throw new AppError("Please set a Transaction PIN first", 400); - } if (utils.formatEmail(beneficiaryEmail) === user.email) { throw new AppError("You cannot Donate to Self", 401); diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index 1cf69e9..c07fbc6 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -1,5 +1,5 @@ import { asyncHandler } from "@/middlewares/asyncHandler"; -import { setTransactionPIN } from "@/services/transaction.service"; +import { setTransactionPIN, getUserTransactions, getBalanceFromTxs } from "@/services/transaction.service"; import utils from "@/utils"; import { ApiResponse } from "@/utils/ApiResponse"; import { AppError } from "@/utils/AppError"; @@ -22,10 +22,24 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response return res.status(201).json(new ApiResponse("success", "Pin created successfully")); -}) +}); export const handleGetUserTransactions = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const user = (req as Request & { user?: User }).user! + const txs = await getUserTransactions(user.id); + + return res.status(200).json(new ApiResponse("success", txs)); + + +}) + +export const handleGetUserBalance = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + + const user = (req as Request & { user?: User }).user! + const txs = await getBalanceFromTxs(user.id); + + return res.status(200).json(new ApiResponse("success", txs)); }) \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 8d6f300..5193ba3 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -2,11 +2,14 @@ import { asyncHandler } from '@/middlewares/asyncHandler'; import { ApiResponse } from '@/utils/ApiResponse'; import { NextFunction, Request, Response } from 'express'; import { User } from '@prisma/client'; +import { getUserByEmail } from '@/services/auth.service'; export const handleGetUser = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const user = (req as Request & { user?: User }).user! - return res.status(200).json(new ApiResponse("success", user)); + + const fulUserDetails = await getUserByEmail(user.email) + return res.status(200).json(new ApiResponse("success", fulUserDetails)); }) diff --git a/src/middlewares/prismaErrors.ts b/src/middlewares/prismaErrors.ts index 2a8f339..d1164cd 100644 --- a/src/middlewares/prismaErrors.ts +++ b/src/middlewares/prismaErrors.ts @@ -30,6 +30,6 @@ export const prismaErrorMap: Record = { P2025: "The record you're trying to update or delete was not found.", P2026: "This operation isn't supported by the database engine.", P2027: "Multiple database errors occurred. Please review the request and try again.", - P2028: "A database transaction failed. The operation was not completed. Please" + P2028: "The operation was not completed. Please try again." } \ No newline at end of file diff --git a/src/prisma/migrations/20250825120344_set_default_wallet_balance_to_0/migration.sql b/src/prisma/migrations/20250825120344_set_default_wallet_balance_to_0/migration.sql new file mode 100644 index 0000000..745604c --- /dev/null +++ b/src/prisma/migrations/20250825120344_set_default_wallet_balance_to_0/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."Wallet" ALTER COLUMN "balance" SET DEFAULT 0; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 58874e8..0f5dd5e 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -31,7 +31,7 @@ model User { } model Wallet { - balance Float @default(100000) + balance Float @default(0) userId String @id @unique user User @relation(fields: [userId], references: [id]) diff --git a/src/prisma/seed.ts b/src/prisma/seed.ts new file mode 100644 index 0000000..4acbfa1 --- /dev/null +++ b/src/prisma/seed.ts @@ -0,0 +1,34 @@ +import { PrismaClient } from "@prisma/client"; +import * as bcrypt from "bcryptjs"; + +const prisma = new PrismaClient(); + +async function main() { + const hashedPassword = await bcrypt.hash("dummy", 10); + + await prisma.user.upsert({ + where: { id: "system" }, + update: {}, + create: { + id: "system", + email: "system@paritiebackendtest.com", + password: hashedPassword, + name: "Paritie Account", + wallet: { + create: { + balance: 1_000_000_000, // your treasury balance + }, + }, + }, + }); + + console.log("✅Paritie System wallet initialized"); +} + +main() + .then(() => prisma.$disconnect()) + .catch((e) => { + console.error(e); + prisma.$disconnect(); + process.exit(1); + }); diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index c832395..69263f1 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -84,6 +84,7 @@ router.post('/login', handleLoginAcc) * schema: * type: string * required: true + * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJkNzEwYzI0NC1kMzRmLTQ4OGEtYWIyYS03NDUzMDIxYzIyODgiLCJpYXQiOjE3NTYxMTUxMDQsImV4cCI6MTc1NjcxOTkwNH0.eedeTOuvLma-vAwKtVp4Wco1t0MFV8lCmQXExduXp8g * description: HTTP-only refresh token cookie * responses: * 200: diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index d32f2b4..4b7c969 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -30,6 +30,18 @@ const router = express.Router(); router.post('/pin', handleCreateTxPIN) +/** + * @swagger + * /api/tx/: + * get: + * summary: Get all transactions made by the user + * tags: [Transaction] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: List of transactions made by the user + */ router.get("/", handleGetUserTransactions) diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts index 308cb51..318d13a 100644 --- a/src/routes/user.routes.ts +++ b/src/routes/user.routes.ts @@ -1,12 +1,28 @@ import express from 'express'; import { handleGetUser } from "@/controllers/user.controller" +import { handleGetUserBalance } from '@/controllers/transaction.controller'; const router = express.Router(); + +/** + * @swagger + * /api/user/balance: + * get: + * summary: Return calcualted balance from transactions + * tags: [User] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: user balance from transactions + */ + + /** * @swagger * /api/user: - * post: + * get: * summary: Return logged in User * tags: [User] * security: @@ -16,7 +32,10 @@ const router = express.Router(); * description: Logged in User */ -router.post('/', handleGetUser) +router.get('/', handleGetUser) + + +router.get("/balance", handleGetUserBalance) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index f3d9f71..6de1f28 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -14,29 +14,93 @@ export const createUser = async (data: CreateUserT) => { const { password } = data; const hashedPassword = await utils.hashPassword(password); - - const user = await prisma.user.create({ - data: { - ...data, password: hashedPassword, - wallet: { - create: {} - } + const SYSTEM_WALLET_ID = "system"; + const amount = 100_000; + + + return await prisma.$transaction(async (tx) => { + // 1. Create user + wallet + const user = await tx.user.create({ + data: { + ...data, + password: hashedPassword, + wallet: { create: { balance: 0 } }, + }, + select: { id: true, name: true, email: true, wallet: true }, + }); + + // Fetch system wallet transaction + const systemWallet = await prisma.wallet.findUniqueOrThrow({ + where: { userId: SYSTEM_WALLET_ID }, + }); + + if (systemWallet.balance < amount) { + throw new Error("❌ System wallet has insufficient balance"); } + // 2. Create transaction + const transaction = await tx.transaction.create({ + data: { + type: "DEPOSIT", + description: "Initial funding", + }, + }); + + // 3. Update balances in parallel + const [updatedSystemWallet, updatedUserWallet] = await Promise.all([ + tx.wallet.update({ + where: { userId: systemWallet.userId }, + data: { balance: { decrement: amount } }, + }), + tx.wallet.update({ + where: { userId: user.id }, + data: { balance: { increment: amount } }, + }), + ]); + + // 4. Transaction entries (batch insert) + await tx.transactionEntry.createMany({ + data: [ + { + transactionId: transaction.id, + walletId: systemWallet.userId, + amount: -amount, + balanceBefore: systemWallet.balance, + balanceAfter: updatedSystemWallet.balance, + }, + { + transactionId: transaction.id, + walletId: user.id, + amount: amount, + balanceBefore: user.wallet!.balance, + balanceAfter: updatedUserWallet.balance, + }, + ], + }); + + const {wallet, ...userWithoutWallet} = user; + return userWithoutWallet; + }, { + timeout: 15000, // 15s instead of 5s }); - const { password: _, transactionPIN, ...safeUser } = user; - return safeUser -} +}; export const getUserByEmail = async (email: string) => { const user = await prisma.user.findUnique({ - where: { email } - }) - - return user -} + where: { email }, + select: { + id: true, + email: true, + name: true, + wallet: true, + donations: true, + received: true, + }, + }); + return user; +}; @@ -46,11 +110,11 @@ export const getUserPrivateFn = async (email: string) => { where: { email }, }); - + return user; }; -//only used internally +//only used internally in middlerware export const getUserById = async (id: string) => { const user = await prisma.user.findUnique({ where: { diff --git a/src/services/donation.service.ts b/src/services/donation.service.ts index 0c9568e..783af43 100644 --- a/src/services/donation.service.ts +++ b/src/services/donation.service.ts @@ -6,9 +6,6 @@ import { paginate } from '@/utils/pagintion'; const prisma = new PrismaClient() -//MAIN FUNCTION -// ------------------------------------- START ----------------------- -// TODO : disable send to self export const createDonation = async ( donor: User, beneficiaryId: string, @@ -117,7 +114,6 @@ export const createDonation = async ( return data }; -// ------------------------------------- END ----------------------- export const countUserDonations = async (userId: string) => { diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index def6159..6dd738f 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -14,3 +14,38 @@ export const setTransactionPIN = async (userId: string, pin: number) => { }); }; + + +export const getUserTransactions = async (userId: string) => { + const transactions = await prisma.transaction.findMany({ + where: { + entries: { + some: { + wallet: { + userId + } + } + } + }, + include: { + entries: true, + donation: true, + } + }); + + return transactions; +} + + +export const getBalanceFromTxs = async (userId: string) => { + const result = await prisma.transactionEntry.aggregate({ + _sum: { + amount: true, + }, + where: { + walletId: userId, + }, + }); + + return result._sum.amount ?? 0; // default to 0 if null +}; diff --git a/yarn.lock b/yarn.lock index 6c543c5..1bf1487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1796,6 +1796,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" From 0d4d0c252eba3b54147ab41f91c70af2c5b975fd Mon Sep 17 00:00:00 2001 From: Dunsin Date: Mon, 25 Aug 2025 15:38:39 +0100 Subject: [PATCH 31/33] feat: add pagination to get tranactions --- src/controllers/donation.controller.ts | 26 ++++++++----- src/controllers/transaction.controller.ts | 21 ++++++++++- src/routes/donation.routes.ts | 20 +++++----- src/routes/transaction.routes.ts | 33 ++++++++++++++++ src/services/donation.service.ts | 37 +++++++++--------- src/services/transaction.service.ts | 41 +++++++++++++------- src/utils/index.ts | 46 +++++++++++++++++++++++ 7 files changed, 172 insertions(+), 52 deletions(-) diff --git a/src/controllers/donation.controller.ts b/src/controllers/donation.controller.ts index 93941e2..1695f41 100644 --- a/src/controllers/donation.controller.ts +++ b/src/controllers/donation.controller.ts @@ -52,18 +52,26 @@ export const handleGetUserDonations = asyncHandler(async (req: Request, res: Res export const handleFilterDonations = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const user = (req as Request & { user?: User }).user! - const { start, end, limit, page } = req.query; - if (!start || !end) { - throw new AppError("Start and end dates are required", 400); + const { from, to, limit, page } = req.query; + let startDate + let endDate; + + if (from) { + startDate = utils.dataParser(from as string) + } + if (to) { + endDate = utils.dataParser(to as string) } - const startDate = new Date(start as string); - const endDate = new Date(end as string); - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - throw new AppError("Invalid date format", 400); + if ((startDate && !endDate) || (!startDate && endDate)) { + throw new AppError("There must be both start and end date", 401); } - const donations = await donationsInPeriod(user.id, startDate, endDate, page as string, limit as string); + const date = { start: startDate?.start, end: endDate?.end } + + + + const donations = await donationsInPeriod(user.id, date, page as string, limit as string); if (!donations) { throw new AppError("No donations found in this period", 404); @@ -75,7 +83,7 @@ export const handleFilterDonations = asyncHandler(async (req: Request, res: Resp } ) -// TODO: chck if it is a valid uuid + export const handleDonationDetails = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const donationId = req.params.id; if (!donationId) { diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index c07fbc6..de85660 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -28,7 +28,26 @@ export const handleCreateTxPIN = asyncHandler(async (req: Request, res: Response export const handleGetUserTransactions = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { const user = (req as Request & { user?: User }).user! - const txs = await getUserTransactions(user.id); + + const { from, to, limit, page } = req.query; + let startDate + let endDate; + + if (from) { + startDate = utils.dataParser(from as string) + } + if (to) { + endDate = utils.dataParser(to as string) + } + + if ((startDate && !endDate) || (!startDate && endDate)) { + throw new AppError("There must be both start and end date", 401); + } + + const date = { start: startDate?.start, end: endDate?.end } + + + const txs = await getUserTransactions(user.id, date, page as string, limit as string); return res.status(200).json(new ApiResponse("success", txs)); diff --git a/src/routes/donation.routes.ts b/src/routes/donation.routes.ts index 2880266..49d1a3f 100644 --- a/src/routes/donation.routes.ts +++ b/src/routes/donation.routes.ts @@ -61,21 +61,21 @@ router.post('/create', handleCreateDonation) * - bearerAuth: [] * parameters: * - in: query - * name: start + * name: from * schema: * type: string - * format: date-time - * required: true - * description: Start date for filtering donations - * example: "2023-01-01T00:00:00Z" + * minimum: 1 + * required: false + * description: start date + * example: "01/02/2024" * - in: query - * name: end + * name: to * schema: * type: string - * format: date-time - * required: true - * description: End date for filtering donations - * example: "2025-12-31T23:59:59Z" + * minimum: 1 + * required: false + * description: end date + * example: "12/2025" * - in: query * name: page * schema: diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 4b7c969..7f573a4 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -38,6 +38,39 @@ router.post('/pin', handleCreateTxPIN) * tags: [Transaction] * security: * - bearerAuth: [] + * parameters: + * - in: query + * name: from + * schema: + * type: string + * minimum: 1 + * required: false + * description: start date + * example: "01/02/2024" + * - in: query + * name: to + * schema: + * type: string + * minimum: 1 + * required: false + * description: end date + * example: "12/2025" + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Page number for pagination + * example: 1 + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * required: false + * description: Number of results per page + * example: 10 * responses: * 200: * description: List of transactions made by the user diff --git a/src/services/donation.service.ts b/src/services/donation.service.ts index 783af43..1b47083 100644 --- a/src/services/donation.service.ts +++ b/src/services/donation.service.ts @@ -124,29 +124,28 @@ export const countUserDonations = async (userId: string) => { -// TODO: filter my dmy and merge it into getUserDonations -export const donationsInPeriod = async (userId: string, start: Date, end: Date, page?: string, limit?: string) => { - var page_; - var limit_; - if (page) { - page_ = +page; +export const donationsInPeriod = async ( + userId: string, + date?: { start?: Date; end?: Date }, + page?: string, + limit?: string +) => { + const where: any = { donorId: userId }; + if ((date && date.start) && date.start) { + where.createdAt = { + gte: date.start, + lte: date.end, + }; } - if (limit) { - limit_ = +limit; - } - const data = await paginate({ - model: 'donation', - where: { - donorId: userId, - createdAt: { gte: start, lte: end } - }, - orderBy: { createdAt: 'desc' }, - page: page_, - limit: limit_ + return paginate({ + model: "donation", + where, + orderBy: { createdAt: "desc" }, + page: page ? +page : undefined, + limit: limit ? +limit : undefined, }); - return data; }; diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 6dd738f..58ec596 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -1,4 +1,5 @@ import utils from "@/utils"; +import { paginate } from "@/utils/pagintion"; import { PrismaClient } from "@prisma/client"; @@ -16,24 +17,38 @@ export const setTransactionPIN = async (userId: string, pin: number) => { -export const getUserTransactions = async (userId: string) => { - const transactions = await prisma.transaction.findMany({ - where: { - entries: { - some: { - wallet: { - userId - } +export const getUserTransactions = async (userId: string, date?: { start?: Date; end?: Date }, + page?: string, + limit?: string) => { + + const where: any = { + entries: { + some: { + wallet: { + userId } } - }, - include: { - entries: true, - donation: true, } + }; + + if ((date && date.start) && date.start) { + where.createdAt = { + gte: date.start, + lte: date.end, + }; + } + + + return paginate({ + model: "transaction", + where, + orderBy: { createdAt: "desc" }, + page: page ? +page : undefined, + limit: limit ? +limit : undefined, + include: { entries: true, donation: true } }); - return transactions; + } diff --git a/src/utils/index.ts b/src/utils/index.ts index 6a09ba9..35f92a6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,7 @@ import { hash, compare } from 'bcryptjs'; +import { AppError } from './AppError'; + + const utils = { hashPassword: async (password: string): Promise => { const saltRounds = 10; @@ -35,9 +38,52 @@ const utils = { // Collapse multiple spaces inside (shouldn’t normally exist but just in case) formatted = formatted.replace(/\s+/g, ""); return formatted; + }, + dataParser: (dmy: string) => { + if (!dmy || typeof dmy !== "string") { + throw new AppError("Date input required", 400); + } + + // Normalize: trim spaces, replace "-" with "/", collapse multiple slashes + const normalized = dmy.trim().replace(/-/g, "/").replace(/\s+/g, "").replace(/\/+/g, "/"); + const dmyParts = normalized.split("/").filter(Boolean); + + let start: Date; + let end: Date; + + if (dmyParts.length === 3) { + // dd/mm/yyyy + const [day, month, year] = dmyParts.map(Number); + if (isNaN(day) || isNaN(month) || isNaN(year)) { + throw new AppError("Invalid date numbers", 400); + } + start = new Date(year, month - 1, day, 0, 0, 0, 0); + end = new Date(year, month - 1, day, 23, 59, 59, 999); + } else if (dmyParts.length === 2) { + // mm/yyyy + const [month, year] = dmyParts.map(Number); + if (isNaN(month) || isNaN(year)) { + throw new AppError("Invalid month/year numbers", 400); + } + start = new Date(year, month - 1, 1, 0, 0, 0, 0); + end = new Date(year, month, 0, 23, 59, 59, 999); + } else if (dmyParts.length === 1) { + // yyyy + const year = Number(dmyParts[0]); + if (isNaN(year)) { + throw new AppError("Invalid year number", 400); + } + start = new Date(year, 0, 1, 0, 0, 0, 0); + end = new Date(year, 11, 31, 23, 59, 59, 999); + } else { + throw new AppError("Invalid date format. Use dd/mm/yyyy, mm/yyyy, or yyyy", 400); + } + + return {start, end} ; } + }; From c2dcceb0728928db75b454667d545b974a09b238 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Wed, 27 Aug 2025 14:29:36 +0100 Subject: [PATCH 32/33] feat: add transactionEntry relaton to user model --- package.json | 2 +- .../migration.sql | 14 ++++++++++++++ src/prisma/schema.prisma | 11 ++++++++--- src/services/auth.service.ts | 2 ++ src/services/donation.service.ts | 2 ++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/prisma/migrations/20250827132435_add_relation_btwn_user/migration.sql diff --git a/package.json b/package.json index f5af147..a2ba52c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "npm run clean && tsc && cp -r src/prisma/ dist/prisma/", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", - "db:migrate": "cd ./src && npx prisma migrate dev --name set_default_walletBalance_to_0", + "db:migrate": "cd ./src && npx prisma migrate dev --name add_relation_btwn_user_and_transactionEntry", "db:init": "cd ./src && npx prisma generate", "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", diff --git a/src/prisma/migrations/20250827132435_add_relation_btwn_user/migration.sql b/src/prisma/migrations/20250827132435_add_relation_btwn_user/migration.sql new file mode 100644 index 0000000..885ac19 --- /dev/null +++ b/src/prisma/migrations/20250827132435_add_relation_btwn_user/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Added the required column `userId` to the `TransactionEntry` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."TransactionEntry" ADD COLUMN "userId" TEXT NOT NULL; + +-- CreateIndex +CREATE INDEX "TransactionEntry_userId_idx" ON "public"."TransactionEntry"("userId"); + +-- AddForeignKey +ALTER TABLE "public"."TransactionEntry" ADD CONSTRAINT "TransactionEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 0f5dd5e..16fac14 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -4,6 +4,7 @@ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init +// ? background tasks and queues using redis generator client { provider = "prisma-client-js" } @@ -23,9 +24,10 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - wallet Wallet? - donations Donation[] - received Donation[] @relation("UserReceivedDonations") + wallet Wallet? + donations Donation[] + received Donation[] @relation("UserReceivedDonations") + transactions TransactionEntry[] @@index([email]) } @@ -79,7 +81,9 @@ model TransactionEntry { transaction Transaction @relation(fields: [transactionId], references: [id]) walletId String + userId String wallet Wallet @relation(fields: [walletId], references: [userId]) + user User @relation(fields: [userId], references: [id]) amount Float // positive for credit, negative for debit balanceBefore Float @@ -89,6 +93,7 @@ model TransactionEntry { @@index([transactionId]) @@index([walletId]) + @@index([userId]) } enum TransactionType { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 6de1f28..f3ea4a7 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -63,6 +63,7 @@ export const createUser = async (data: CreateUserT) => { { transactionId: transaction.id, walletId: systemWallet.userId, + userId: systemWallet.userId, amount: -amount, balanceBefore: systemWallet.balance, balanceAfter: updatedSystemWallet.balance, @@ -70,6 +71,7 @@ export const createUser = async (data: CreateUserT) => { { transactionId: transaction.id, walletId: user.id, + userId: user.id, amount: amount, balanceBefore: user.wallet!.balance, balanceAfter: updatedUserWallet.balance, diff --git a/src/services/donation.service.ts b/src/services/donation.service.ts index 1b47083..344fae4 100644 --- a/src/services/donation.service.ts +++ b/src/services/donation.service.ts @@ -85,6 +85,7 @@ export const createDonation = async ( data: { transactionId: transaction.id, walletId: donor.id, + userId: donor.id, amount: -amount, balanceBefore: donorWallet.balance, balanceAfter: updatedDonorWallet.balance @@ -95,6 +96,7 @@ export const createDonation = async ( data: { transactionId: transaction.id, walletId: beneficiaryId, + userId: beneficiaryId, amount: +amount, balanceBefore: beneficiaryWallet.balance, balanceAfter: updatedBeneficiaryWallet.balance From 43236362663ffe01101cbbb6e91d794a10f8d332 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Thu, 28 Aug 2025 14:18:27 +0100 Subject: [PATCH 33/33] feat: get a transaction endpint --- package.json | 2 +- src/controllers/transaction.controller.ts | 22 ++++++++++++++++++- src/prisma/schema.prisma | 8 +++---- src/routes/transaction.routes.ts | 26 ++++++++++++++++++++++- src/services/auth.service.ts | 3 ++- src/services/transaction.service.ts | 14 ++++++++++++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a2ba52c..37cab2a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "npm run clean && tsc && cp -r src/prisma/ dist/prisma/", "db:deploy": "cd ./src && npx prisma migrate deploy", "db:reset": "cd ./src && npx prisma migrate reset --force", - "db:migrate": "cd ./src && npx prisma migrate dev --name add_relation_btwn_user_and_transactionEntry", + "db:migrate": "cd ./src && npx prisma migrate dev --name rename_field_to_transactionEntries", "db:init": "cd ./src && npx prisma generate", "start": "node dist/server.js", "start:prod": "npm run db:deploy:prod && npm run start", diff --git a/src/controllers/transaction.controller.ts b/src/controllers/transaction.controller.ts index de85660..e6886a3 100644 --- a/src/controllers/transaction.controller.ts +++ b/src/controllers/transaction.controller.ts @@ -1,10 +1,11 @@ import { asyncHandler } from "@/middlewares/asyncHandler"; -import { setTransactionPIN, getUserTransactions, getBalanceFromTxs } from "@/services/transaction.service"; +import { setTransactionPIN, getUserTransactions, getBalanceFromTxs, getATransaction } from "@/services/transaction.service"; import utils from "@/utils"; import { ApiResponse } from "@/utils/ApiResponse"; import { AppError } from "@/utils/AppError"; import { User } from "@prisma/client"; import { NextFunction, Request, Response } from "express"; +import { validate as uuidValidate } from 'uuid'; @@ -61,4 +62,23 @@ export const handleGetUserBalance = asyncHandler(async (req: Request, res: Respo return res.status(200).json(new ApiResponse("success", txs)); +}) + +export const handleGetATransaction = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + + const txId = req.params.txId + + const validTxId = uuidValidate(txId); + if (!validTxId) { + throw new AppError("Invalid transaction ID format", 400); + } + + const tx = await getATransaction(txId); + + if (!tx) { + throw new AppError("Transaction Entry not found", 404); + } + + return res.status(200).json(new ApiResponse("success", tx)); + }) \ No newline at end of file diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 16fac14..eb6f144 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -24,10 +24,10 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - wallet Wallet? - donations Donation[] - received Donation[] @relation("UserReceivedDonations") - transactions TransactionEntry[] + wallet Wallet? + donations Donation[] + received Donation[] @relation("UserReceivedDonations") + transactionEntries TransactionEntry[] @@index([email]) } diff --git a/src/routes/transaction.routes.ts b/src/routes/transaction.routes.ts index 7f573a4..cc6ab8e 100644 --- a/src/routes/transaction.routes.ts +++ b/src/routes/transaction.routes.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { handleCreateTxPIN, handleGetUserTransactions } from "@/controllers/transaction.controller" +import { handleCreateTxPIN, handleGetATransaction, handleGetUserTransactions } from "@/controllers/transaction.controller" const router = express.Router(); @@ -78,5 +78,29 @@ router.post('/pin', handleCreateTxPIN) router.get("/", handleGetUserTransactions) +/** + * @swagger + * /api/tx/{txId}: + * get: + * summary: Get a transaction by Id + * tags: [Transaction] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: txId + * required: true + * schema: + * type: string + * description: The ID of the transaction to retrieve + * example: 5dad3afe-a870-491d-b640-958daf3459f0 + * responses: + * 200: + * description: A transaction detail with it's entries + */ + +router.get("/:txId", handleGetATransaction) + + export default router; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index f3ea4a7..e866f4c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -79,7 +79,7 @@ export const createUser = async (data: CreateUserT) => { ], }); - const {wallet, ...userWithoutWallet} = user; + const { wallet, ...userWithoutWallet } = user; return userWithoutWallet; }, { timeout: 15000, // 15s instead of 5s @@ -98,6 +98,7 @@ export const getUserByEmail = async (email: string) => { wallet: true, donations: true, received: true, + transactionEntries: true }, }); diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 58ec596..3105e26 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -64,3 +64,17 @@ export const getBalanceFromTxs = async (userId: string) => { return result._sum.amount ?? 0; // default to 0 if null }; + + +export const getATransaction = async (txId: string) => { + const tx = await prisma.transaction.findUnique({ + where: { + id: txId + }, + include: { + entries: true + } + }) + + return tx; +} \ No newline at end of file