From 26b8516f48925ab5428284dba79dddf718346d52 Mon Sep 17 00:00:00 2001 From: Programmer DATCH Date: Thu, 28 Dec 2023 22:50:13 +0200 Subject: [PATCH] added assets API and fixed user model --- .nvmrc | 1 + package-lock.json | 21 +- package.json | 15 +- .../20231213132332_updated/migration.sql | 42 --- .../20231213150650_clear/migration.sql | 18 -- .../migrations/20231228135037_1/migration.sql | 184 ++++++++++++++ prisma/schema.prisma | 130 ++++++++-- src/controllers/assetController.ts | 95 +++++++ src/controllers/index.ts | 1 + src/controllers/userController.ts | 36 ++- src/middleware/validation.ts | 2 +- src/models/Asset.ts | 96 +++++++ src/models/User.ts | 29 ++- src/models/UserMapping.ts | 83 ++++++ src/models/index.ts | 3 + src/routes/assets/assetRoute.ts | 22 ++ src/routes/auth/authRoute.ts | 9 +- src/routes/index.ts | 8 +- src/services/assetService.ts | 240 ++++++++++++++++++ src/services/tokenService.ts | 16 +- src/services/userService.ts | 96 +++++-- src/validations/assetValidation.ts | 57 +++++ src/validations/index.ts | 1 + src/validations/userValidation.ts | 41 ++- tests/data/assetRegister.ts | 109 ++++++++ tests/data/userRegister.ts | 28 ++ tests/models/AssetCategory.ts | 9 + tests/models/AssetStockSummary.ts | 14 + tests/models/Location.ts | 18 ++ tests/models/OccupationAddress.ts | 8 + tests/models/Role.ts | 6 + tests/models/Staff.ts | 20 ++ tests/models/models.ts | 69 +++++ tsconfig.json | 2 +- 34 files changed, 1360 insertions(+), 169 deletions(-) create mode 100644 .nvmrc delete mode 100644 prisma/migrations/20231213132332_updated/migration.sql delete mode 100644 prisma/migrations/20231213150650_clear/migration.sql create mode 100644 prisma/migrations/20231228135037_1/migration.sql create mode 100644 src/controllers/assetController.ts create mode 100644 src/models/Asset.ts create mode 100644 src/models/UserMapping.ts create mode 100644 src/models/index.ts create mode 100644 src/routes/assets/assetRoute.ts create mode 100644 src/services/assetService.ts create mode 100644 src/validations/assetValidation.ts create mode 100644 tests/data/assetRegister.ts create mode 100644 tests/data/userRegister.ts create mode 100644 tests/models/AssetCategory.ts create mode 100644 tests/models/AssetStockSummary.ts create mode 100644 tests/models/Location.ts create mode 100644 tests/models/OccupationAddress.ts create mode 100644 tests/models/Role.ts create mode 100644 tests/models/Staff.ts create mode 100644 tests/models/models.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..1607b71 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +V18.17.1 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3e3791a..7af541b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.7.0", - "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/joi": "^17.2.3", "@types/node": "^20.10.0", @@ -145,13 +144,13 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz", "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==", - "dev": true + "devOptional": true }, "node_modules/@prisma/engines": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz", "integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/debug": "5.7.0", @@ -164,13 +163,13 @@ "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz", "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==", - "dev": true + "devOptional": true }, "node_modules/@prisma/fetch-engine": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz", "integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==", - "dev": true, + "devOptional": true, "dependencies": { "@prisma/debug": "5.7.0", "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", @@ -190,7 +189,7 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz", "integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==", - "dev": true, + "devOptional": true, "dependencies": { "@prisma/debug": "5.7.0" } @@ -283,14 +282,6 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, - "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -1842,7 +1833,7 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz", "integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "5.7.0" diff --git a/package.json b/package.json index 437f1bc..5889564 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.7.0", - "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/joi": "^17.2.3", "@types/node": "^20.10.0", @@ -38,24 +37,24 @@ }, "devDependencies": { "@prisma/migrate": "^5.7.0", - "@types/nodemailer": "^6.4.14", - "@types/passport": "^1.0.16", - "@types/passport-jwt": "^3.0.13", "@types/bcryptjs": "^2.4.6", "@types/compression": "^1.7.5", "@types/cors": "^2.8.17", "@types/morgan": "^1.9.9", + "@types/nodemailer": "^6.4.14", + "@types/passport": "^1.0.16", + "@types/passport-jwt": "^3.0.13", "@types/xss-filters": "^0.0.30", "dotenv": "^16.3.1", - "nodemon": "^3.0.1", - "prisma": "^5.7.0", - "typescript": "^5.3.2", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", "nodemailer": "^6.9.7", + "nodemon": "^3.0.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "pg": "^8.11.3", - "ts-node": "^10.9.1" + "prisma": "^5.7.0", + "ts-node": "^10.9.1", + "typescript": "^5.3.2" } } diff --git a/prisma/migrations/20231213132332_updated/migration.sql b/prisma/migrations/20231213132332_updated/migration.sql deleted file mode 100644 index 9b26dbf..0000000 --- a/prisma/migrations/20231213132332_updated/migration.sql +++ /dev/null @@ -1,42 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "names" TEXT NOT NULL, - "phone" TEXT NOT NULL, - "email" TEXT NOT NULL, - "gender" TEXT NOT NULL, - "nid" TEXT NOT NULL, - "marital_status" TEXT NOT NULL, - "nationality" TEXT NOT NULL, - "img" TEXT, - "password" TEXT NOT NULL, - "code" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "test" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "test_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ResetPasswordTokens" ( - "id" SERIAL NOT NULL, - "token" TEXT NOT NULL, - "tokenUserEmail" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ResetPasswordTokens_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20231213150650_clear/migration.sql b/prisma/migrations/20231213150650_clear/migration.sql deleted file mode 100644 index 0c141a5..0000000 --- a/prisma/migrations/20231213150650_clear/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateEnum -CREATE TYPE "TokenType" AS ENUM ('ACCESS', 'RESET_PASSWORD'); - --- CreateTable -CREATE TABLE "Token" ( - "id" SERIAL NOT NULL, - "token" TEXT NOT NULL, - "type" "TokenType" NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - "blacklisted" BOOLEAN NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "userId" INTEGER NOT NULL, - - CONSTRAINT "Token_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20231228135037_1/migration.sql b/prisma/migrations/20231228135037_1/migration.sql new file mode 100644 index 0000000..588f7fb --- /dev/null +++ b/prisma/migrations/20231228135037_1/migration.sql @@ -0,0 +1,184 @@ +-- CreateEnum +CREATE TYPE "TokenType" AS ENUM ('ACCESS', 'RESET_PASSWORD'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "names" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "is_line_manager" BOOLEAN NOT NULL, + "occupation_address_id" INTEGER NOT NULL, + "report_to" TEXT, + "role_id" INTEGER NOT NULL, + "custom_access" TEXT[], + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "access" TEXT[], + + CONSTRAINT "Role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OccupationAddress" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "parent_id" INTEGER, + + CONSTRAINT "OccupationAddress_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Location" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "status" TEXT NOT NULL, + "building_id" TEXT NOT NULL, + "room_id" TEXT NOT NULL, + + CONSTRAINT "Location_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Building" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Building_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Room" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Room_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ResetPasswordTokens" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "tokenUserEmail" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ResetPasswordTokens_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Token" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "type" "TokenType" NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "blacklisted" BOOLEAN NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "Token_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Asset" ( + "asset_id" TEXT NOT NULL, + "category_id" TEXT NOT NULL, + "brand_id" TEXT NOT NULL, + "stock_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "purchase_order_number" TEXT NOT NULL, + "value" DOUBLE PRECISION NOT NULL, + "life_span_years" INTEGER NOT NULL, + "date_in" TEXT NOT NULL, + + CONSTRAINT "Asset_pkey" PRIMARY KEY ("asset_id") +); + +-- CreateTable +CREATE TABLE "AssetCategory" ( + "asset_category_id" TEXT NOT NULL, + "category_name" TEXT NOT NULL, + + CONSTRAINT "AssetCategory_pkey" PRIMARY KEY ("asset_category_id") +); + +-- CreateTable +CREATE TABLE "Specification" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "values" TEXT[], + "category_id" TEXT NOT NULL, + + CONSTRAINT "Specification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Stock" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Stock_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Brand" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Brand_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Supplier" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Supplier_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "ResetPasswordTokens_tokenUserEmail_key" ON "ResetPasswordTokens"("tokenUserEmail"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "Role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_occupation_address_id_fkey" FOREIGN KEY ("occupation_address_id") REFERENCES "OccupationAddress"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Location" ADD CONSTRAINT "Location_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Location" ADD CONSTRAINT "Location_building_id_fkey" FOREIGN KEY ("building_id") REFERENCES "Building"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Location" ADD CONSTRAINT "Location_room_id_fkey" FOREIGN KEY ("room_id") REFERENCES "Room"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Asset" ADD CONSTRAINT "Asset_brand_id_fkey" FOREIGN KEY ("brand_id") REFERENCES "Brand"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Asset" ADD CONSTRAINT "Asset_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "AssetCategory"("asset_category_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Asset" ADD CONSTRAINT "Asset_stock_id_fkey" FOREIGN KEY ("stock_id") REFERENCES "Stock"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Asset" ADD CONSTRAINT "Asset_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "Supplier"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Specification" ADD CONSTRAINT "Specification_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "AssetCategory"("asset_category_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 02cfefe..388a864 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,37 +1,72 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - datasource db { provider = "postgresql" url = env("DATABASE_URL") } +generator client { + provider = "prisma-client-js" +} + model User { - id Int @id @default(autoincrement()) + id String @id @default(cuid()) names String phone String - email String @unique - gender String - nid String - marital_status String - nationality String - img String? + email String @unique password String - code String? + location Location[] + is_line_manager Boolean + occupation_address_id Int + report_to String? + role_id Int + custom_access String[] Token Token[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + //relations + role Role @relation(fields: [role_id], references: [id]) + occupation_address OccupationAddress @relation(fields: [occupation_address_id], references: [id]) } -model test { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + +model Role { + id Int @id @default(autoincrement()) + name String + access String[] + users User[] } + +model OccupationAddress { + id Int @id @default(autoincrement()) + name String + type String + parent_id Int? + users User[] +} + + + +model Location { + id String @id @default(uuid()) + user_id String + status String + building_id String + room_id String + //relations + user User @relation(fields: [user_id], references: [id]) + building Building @relation(fields: [building_id], references: [id]) + room Room @relation(fields: [room_id], references: [id]) +} + +model Building { + id String @id @default(uuid()) + name String + location Location[] +} + +model Room { + id String @id @default(uuid()) + name String + location Location[] +} + + model ResetPasswordTokens { id Int @id @default(autoincrement()) token String @@ -48,9 +83,58 @@ model Token { blacklisted Boolean createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) - userId Int + userId String } enum TokenType { ACCESS RESET_PASSWORD +} + + +//ASSETS +model Asset { + asset_id String @id @default(uuid()) + category_id String + brand_id String + stock_id String + supplier_id String + purchase_order_number String + value Float + life_span_years Int + date_in String + //relations + brand Brand @relation(fields: [brand_id], references: [id]) + category AssetCategory @relation(fields: [category_id], references: [asset_category_id]) + stock Stock @relation(fields: [stock_id], references: [id]) + supplier Supplier @relation(fields: [supplier_id], references: [id]) +} + +model AssetCategory { + asset_category_id String @id @default(uuid()) + category_name String + assets Asset[] + specifications Specification[] +} + +model Specification { + id String @id @default(uuid()) + name String + values String[] + category_id String + category AssetCategory @relation(fields: [category_id], references: [asset_category_id]) +} +model Stock { + id String @id @default(uuid()) + name String + assets Asset[] +} +model Brand { + id String @id @default(uuid()) + name String + assets Asset[] +} +model Supplier { + id String @id @default(uuid()) + name String + assets Asset[] } \ No newline at end of file diff --git a/src/controllers/assetController.ts b/src/controllers/assetController.ts new file mode 100644 index 0000000..219e831 --- /dev/null +++ b/src/controllers/assetController.ts @@ -0,0 +1,95 @@ +import { Request, Response } from 'express'; +import catchAsync from '../utils/catchAsync'; +import assetService from '../services/assetService'; +import { AssetData } from '../models'; +import httpStatus from 'http-status'; + + +export const getAllAssets = catchAsync(async (req: Request, res: Response) => { + return res.status(200).json(await assetService.getAllAssets()); +}); + +export const getAllAssetById = catchAsync(async (req: Request, res: Response) => { + return res.status(200).json(await assetService.getAllAssetById(req.params.id)); +}); + +export const addAsset = catchAsync(async (req: Request, res: Response) => { + const createdAsset: AssetData = await assetService.addAsset(req.body); + if(!createdAsset.status) + { + return res.status(400).json(createdAsset); + } + return res.status(201).json(createdAsset); +}); + +export const addMultipleAssets = catchAsync(async (req: Request, res: Response) => { + const createdAssets: AssetData[] = await assetService.addMultipleAssets(req.body); + let failedToCreate = 0; + for(let createdAsset of createdAssets) + { + if(!createdAsset.status) + { + failedToCreate++; + } + } + const statusCode = (failedToCreate === 0) ? 201 : (failedToCreate < createdAssets.length) ? 207 : 400; + return res.status(statusCode).json(createdAssets); +}); + +//Brand controller +export const addAssetBrand = catchAsync(async (req: Request, res: Response) => { + const createdBrand = await assetService.addAssetBrand(req.body); + if(!createdBrand) + { + return res.status(httpStatus.BAD_REQUEST).json("Brand with ID " + req.body.id + " already exists!"); + } + return res.status(httpStatus.CREATED).json(createdBrand); +}); + +export const getAssetBrand = catchAsync(async (req: Request, res: Response) => { + return res.status(httpStatus.OK).json(await assetService.getAssetBrand()); +}); + +//Stock controller +export const addAssetStock = catchAsync(async (req: Request, res: Response) => { + const createdStock = await assetService.addAssetStock(req.body); + if(!createdStock) + { + return res.status(httpStatus.BAD_REQUEST).json("Stock with ID " + req.body.id + " already exists!"); + } + return res.status(httpStatus.CREATED).json(createdStock); +}); + +export const getAssetStock = catchAsync(async (req: Request, res: Response) => { + return res.status(httpStatus.OK).json(await assetService.getAssetStock()); +}); + +//Supplier controller +export const addAssetSupplier = catchAsync(async (req: Request, res: Response) => { + const createdSupplier = await assetService.addAssetSupplier(req.body); + if(!createdSupplier) + { + return res.status(httpStatus.BAD_REQUEST).json("Supplier with ID " + req.body.id + " already exists!"); + } + return res.status(httpStatus.CREATED).json(createdSupplier); +}); + +export const getAssetSupplier = catchAsync(async (req: Request, res: Response) => { + return res.status(httpStatus.OK).json(await assetService.getAssetSupplier()); +}); + +//Category controller +export const addAssetCategory = catchAsync(async (req: Request, res: Response) => { + const createdCategory = await assetService.addAssetCategory(req.body); + if(!createdCategory) + { + return res.status(httpStatus.BAD_REQUEST).json("Category with ID " + req.body.id + " already exists!"); + } + return res.status(httpStatus.CREATED).json(createdCategory); +}); + +export const getAssetCategory = catchAsync(async (req: Request, res: Response) => { + return res.status(httpStatus.OK).json(await assetService.getAssetCategory()); +}); + +export default { addAsset, addMultipleAssets, getAllAssets, getAllAssetById, addAssetBrand, getAssetBrand, addAssetStock, getAssetStock, addAssetSupplier, getAssetSupplier, addAssetCategory, getAssetCategory }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index c554595..5f1e781 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1 +1,2 @@ export { default as UserController } from './userController'; +export { default as assetController } from './assetController'; diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 862c9c9..7cf32bf 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -12,6 +12,16 @@ import { TokenType } from '@prisma/client'; export const loginUser = catchAsync(async (req: Request, res: Response) => { const existUser = await prisma.user.findFirst({ where: { email: req.body.email }, + include: { + location: { + include: { + building: true, + room: true, + }, + }, + occupation_address: true, + role: true, + }, }); if (!existUser) { @@ -30,7 +40,7 @@ export const loginUser = catchAsync(async (req: Request, res: Response) => { res.status(200).json({ message: 'Login Successful', token: accessToken, - user: exclude(existUser, ['password', 'createdAt', 'updatedAt']), + user: exclude(existUser, ['password']), }); }); @@ -40,12 +50,22 @@ export const currentUserLogin = catchAsync( const existUser = await prisma.user.findFirst({ where: { id: user.id }, + include: { + location: { + include: { + building: true, + room: true, + }, + }, + occupation_address: true, + role: true, + }, }); res.status(200).json({ message: 'Welcome back', data: existUser - ? exclude(existUser, ['password', 'createdAt', 'updatedAt']) + ? exclude(existUser, ['password']) : null, }); } @@ -59,9 +79,9 @@ const resetPassword = catchAsync(async (req: Request, res: Response) => { try { const email = req.body.email; if (await UserService.resetPassword(email)) { - res.status(200).json({ message: "Reset password email has been sent to " + email }) + return res.status(200).json({ message: "Reset password email has been sent to " + email }) } else - res.status(400).json({ message: `Email ${email} was not found` }) + return res.status(400).json({ message: `Email ${email} was not found` }) } catch (error) { res.status(500).json({ message: "Server Error", error }) } @@ -98,7 +118,6 @@ const emailVerification = catchAsync(async (req: Request, res: Response) => { const updatePassword = catchAsync(async (req: Request, res: Response) => { try { - const token = req.params.token; const userData = TokenService.verifyToken(token, TokenType.RESET_PASSWORD); const user = await UserService.findUserById((await userData).userId) @@ -106,12 +125,11 @@ const updatePassword = catchAsync(async (req: Request, res: Response) => { const hashedPassword = await encryptPassword(newPassword); const isPasswordChanged = (user?.email) ? await UserService.changePassword(user?.email, hashedPassword) : false; if (isPasswordChanged) { - res.status(200).json({ message: 'Password changed successfully' }); + return res.status(200).json({ message: 'Password changed successfully' }); } else { - console.log(userData); - - res.status(400).json({ message: 'Password not Changed' }); + // console.log(userData); + return res.status(400).json({ message: 'Password not Changed' }); } } catch (error) { res.status(500).json({ message: "Server error", error }) diff --git a/src/middleware/validation.ts b/src/middleware/validation.ts index 28a98a5..02e68cb 100644 --- a/src/middleware/validation.ts +++ b/src/middleware/validation.ts @@ -3,7 +3,7 @@ import httpStatus from 'http-status'; import { NextFunction, Request, Response } from 'express'; import Joi from 'joi'; -const validation =(schema: Joi.ObjectSchema) => async (req:Request, res:Response, next:NextFunction) => { +const validation =(schema: Joi.ObjectSchema | Joi.ArraySchema) => async (req:Request, res:Response, next:NextFunction) => { const { error } = schema.validate(req.body, { abortEarly: false }); if (error) { return res.status(httpStatus.BAD_REQUEST).json({ diff --git a/src/models/Asset.ts b/src/models/Asset.ts new file mode 100644 index 0000000..d15f2cd --- /dev/null +++ b/src/models/Asset.ts @@ -0,0 +1,96 @@ +interface NewAssetData { + asset_id: string; + category_id: string; + brand: + { + id: string; + name: string + }; + stock_id: string; + supplier: + { + id: string; + name: string + }; + purchase_order_number: string; + value: number; + life_span_years: number; + date_in: string; + } + + interface Asset { + asset_id: string; + category: { + asset_category_id: string; + category_name: string; + specifications: { id: string, name: string, values: string []}[]; + }; + brand: { id: string; name: string }; + stock: { id: string; name: string }; + supplier: { id: string; name: string }; + purchase_order_number: string; + value: number; + life_span_years: number; + date_in: string; + } + + + interface AssetData { + status: boolean; + data: { + asset_id: string; + category: { + asset_category_id: string; + category_name: string; + specifications: { id: string, name: string, values: string []}[]; + }; + brand: { id: string; name: string }; + stock: { id: string; name: string }; + supplier: { id: string; name: string }; + purchase_order_number: string; + value: number; + life_span_years: number; + date_in: string; + } | string; + } + interface AssetSearch { + found: boolean; + data: { + asset_id: string; + category: { + asset_category_id: string; + category_name: string; + specifications: { id: string, name: string, values: string []}[]; + }; + brand: { id: string; name: string }; + stock: { id: string; name: string }; + supplier: { id: string; name: string }; + purchase_order_number: string; + value: number; + life_span_years: number; + date_in: string; + }[] | string; + } + + interface idAndName{ + id: string; + name: string; + } + + interface AssetCategory{ + id: string; + name: string; + specifications: { id: string, name: string, values: string []}[]; + } + + interface AssetCategoryData { + asset_category_id: string; + category_name: string; + specifications: { + id: string; + name: string; + values: string[]; + }[]; + } + + export {Asset, NewAssetData, AssetData, AssetSearch, idAndName, AssetCategory, AssetCategoryData}; \ No newline at end of file diff --git a/src/models/User.ts b/src/models/User.ts index cf522ac..49ab9ab 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,17 +1,28 @@ interface User { - id: number; + id: string; names: string; phone: string; email: string; - gender: string; - nid: string; - marital_status: string; - nationality: string; - img?: string | null; password: string; - code?: string | null; - createdAt: Date; - updatedAt: Date; + location: { + building: { id: string; name: string }; + room: { id: string; name: string }; + status: 'ACTIVE' | 'INACTIVE'; + }[]; + is_line_manager: boolean; + occupation_address: { + id: number; + name: string; + type: 'DEPARTMENT' | 'SCHOOL' | 'COLLEGE' | 'CAMPUS' | 'UR'; + parent_id: number | null; + }; + report_to: string; + role: { + id: number; + name: string; + access: string[]; + }; + custom_access: string[]; } export default User; \ No newline at end of file diff --git a/src/models/UserMapping.ts b/src/models/UserMapping.ts new file mode 100644 index 0000000..50fd965 --- /dev/null +++ b/src/models/UserMapping.ts @@ -0,0 +1,83 @@ +import User from "./User"; + +interface MappedLocation { + id: string; + status: string; + building: { id: string; name: string }; + room: { id: string; name: string }; + } + + interface MappedOccupationAddress { + id: number; + name: string; + type: 'DEPARTMENT' | 'SCHOOL' | 'COLLEGE' | 'CAMPUS' | 'UR'; + parent_id: number | null; + } + + interface MappedRole { + id: number; + name: string; + access: string[]; + } + + export interface MappedUser { + id: string; + names: string; + phone: string; + email: string; + password: string; + location: MappedLocation[]; + is_line_manager: boolean; + occupation_address: MappedOccupationAddress; + report_to: string | null; + role: MappedRole; + custom_access: string[]; + } + + const mapPrismaLocationToMappedLocation = (prismaLocation: any): MappedLocation => { + return { + id: prismaLocation.id, + status: prismaLocation.status, + building: { + id: prismaLocation.building.id, + name: prismaLocation.building.name, + }, + room: { + id: prismaLocation.room.id, + name: prismaLocation.room.name, + }, + }; + }; + + const mapPrismaOccupationAddressToMappedOccupationAddress = (prismaOccupationAddress: any): MappedOccupationAddress => { + return { + id: prismaOccupationAddress.id, + name: prismaOccupationAddress.name, + type: prismaOccupationAddress.type, + parent_id: prismaOccupationAddress.parent_id, + }; + }; + + const mapPrismaRoleToMappedRole = (prismaRole: any): MappedRole => { + return { + id: prismaRole.id, + name: prismaRole.name, + access: prismaRole.access, + }; + }; + + export const mapPrismaUserToMappedUser = (prismaUser: any): User => { + return { + id: prismaUser.id, + names: prismaUser.names, + phone: prismaUser.phone, + email: prismaUser.email, + password: prismaUser.password, + location: prismaUser.location.map(mapPrismaLocationToMappedLocation), + is_line_manager: prismaUser.is_line_manager, + occupation_address: mapPrismaOccupationAddressToMappedOccupationAddress(prismaUser.occupation_address), + report_to: prismaUser.report_to, + role: mapPrismaRoleToMappedRole(prismaUser.role), + custom_access: prismaUser.custom_access, + }; + }; \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..9a37d34 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,3 @@ +export { default as User } from './User'; +export * from './Asset'; +export { mapPrismaUserToMappedUser, MappedUser } from './UserMapping'; \ No newline at end of file diff --git a/src/routes/assets/assetRoute.ts b/src/routes/assets/assetRoute.ts new file mode 100644 index 0000000..9694bd4 --- /dev/null +++ b/src/routes/assets/assetRoute.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import auth from '../../middleware/auth'; +import validation from '../../middleware/validation'; +import { assetValidation } from '../../validations'; +import { assetController } from '../../controllers'; + +const routes = express.Router(); + +routes.post('/add', validation(assetValidation.oneAsset) ,assetController.addAsset); +routes.post('/addMultiple', validation(assetValidation.multipleAssets) ,assetController.addMultipleAssets); +routes.get('/all', assetController.getAllAssets); +routes.get('/id/:id', assetController.getAllAssetById); +routes.post('/brand/add', validation(assetValidation.assetBrand) ,assetController.addAssetBrand); +routes.get('/brand/all' ,assetController.getAssetBrand); +routes.post('/stock/add', validation(assetValidation.assetStock) ,assetController.addAssetStock); +routes.get('/stock/all' ,assetController.getAssetStock); +routes.post('/supplier/add', validation(assetValidation.assetStock) ,assetController.addAssetStock); +routes.get('/supplier/all' ,assetController.getAssetStock); +routes.post('/category/add', validation(assetValidation.assetCategory) ,assetController.addAssetCategory); +routes.get('/category/all' ,assetController.getAssetCategory); + +export default routes; diff --git a/src/routes/auth/authRoute.ts b/src/routes/auth/authRoute.ts index 86edceb..1231ff4 100644 --- a/src/routes/auth/authRoute.ts +++ b/src/routes/auth/authRoute.ts @@ -1,19 +1,14 @@ import express from 'express'; import { UserController } from '../../controllers'; import auth from '../../middleware/auth'; -import validate from '../../middleware/validate'; import validation from '../../middleware/validation'; import { authValidation } from '../../validations'; const routes = express.Router(); -routes.post('/login', validate(authValidation.login), UserController.loginUser); +routes.post('/login', validation(authValidation.login), UserController.loginUser); routes.post('/current', auth(), UserController.currentUserLogin); -routes.post( - '/register', - validate(authValidation.registration), - UserController.registerUser -); +routes.post('/register', validation(authValidation.registration), UserController.registerUser); routes.post("/resetPassword", validation(authValidation.emailVerification), UserController.resetPassword); routes.post("/verify-email/:email", UserController.emailVerification); routes.post("/updatePassword/:token", validation(authValidation.updatePassword), UserController.updatePassword); diff --git a/src/routes/index.ts b/src/routes/index.ts index 59a8dc9..6b187e7 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,7 @@ import express from 'express'; import authRoute from './auth/authRoute'; +import assetsRoute from './assets/assetRoute'; +import path from 'path'; const router = express.Router(); @@ -8,11 +10,15 @@ const defaultRoutes = [ path: '/auth', route: authRoute, }, + { + path: '/asset', + route: assetsRoute, + } ]; defaultRoutes.forEach((route) => { router.use(route.path, route.route); }); - + export default router; diff --git a/src/services/assetService.ts b/src/services/assetService.ts new file mode 100644 index 0000000..d820358 --- /dev/null +++ b/src/services/assetService.ts @@ -0,0 +1,240 @@ +import { PrismaClient } from '@prisma/client'; +import { NewAssetData, Asset, AssetData, AssetSearch, idAndName, AssetCategory, AssetCategoryData } from '../models'; +import catchAsync from '../utils/catchAsync'; +const prisma = new PrismaClient(); + + +const assetService = { + + async addAsset (newAssetData: NewAssetData): Promise { + if(await this.doesAssetAlreadyExist(newAssetData.asset_id)) + { + const assetError: AssetData = { + status: false, + data: `Asset with id ${newAssetData.asset_id} already exist!, try another ID` + } + return assetError; + } + if(!await this.doesCategoryExist(newAssetData.category_id)) + { + const assetError: AssetData = { + status: false, + data: `No category with id ${newAssetData.category_id}, Consider adding category first!` + } + return assetError; + } + if(!await this.doesStockExist(newAssetData.stock_id)) + { + const assetError: AssetData = { + status: false, + data: `No category with id ${newAssetData.stock_id}, Consider adding Stock first!` + } + return assetError; + } + + const createdAsset = await prisma.asset.create({ + data: { + asset_id: newAssetData.asset_id, + category: { + connect: { asset_category_id: newAssetData.category_id } }, + brand: { + connectOrCreate: { + where: { id: newAssetData.brand.id }, + create: { id: newAssetData.brand.id, name: newAssetData.brand.name } + } + }, + stock: { connect: { id: newAssetData.stock_id } }, + supplier: { + connectOrCreate: { + where: { id: newAssetData.supplier.id }, + create: { id: newAssetData.supplier.id, name: newAssetData.supplier.name } + } + }, + purchase_order_number: newAssetData.purchase_order_number, + value: newAssetData.value, + life_span_years: newAssetData.life_span_years, + date_in: newAssetData.date_in, + }, + include: { + category: { + include: { + specifications: true, + }, + }, + brand: true, + stock: true, + supplier: true, + }, + }); + + const createdAssetData: AssetData={ + status: true, + data: createdAsset + } + + return createdAssetData; + } + , + async addMultipleAssets(multipleAssetData: NewAssetData[]) : Promise + { + let createdAssets = []; + for (const asset of multipleAssetData){ + createdAssets.push(await this.addAsset(asset)); + } + return createdAssets; + } + , + + async doesCategoryExist(categoryId: string): Promise { + const category = await prisma.assetCategory.findUnique({ + where: { asset_category_id: categoryId }, + }); + return !!category; + }, + + async doesStockExist(stockId: string): Promise { + const stock = await prisma.stock.findUnique({ + where: { id: stockId }, + }); + return !!stock; + }, + + async doesAssetAlreadyExist(AssetId: string): Promise { + const asset = await prisma.asset.findUnique({ + where: { asset_id: AssetId }, + }); + return !!asset; + }, + async doesBrandAlreadyExist(brandId: string): Promise { + const brand = await prisma.brand.findUnique({ + where: { id: brandId }, + }); + return !!brand; + }, + async doesSupplierAlreadyExist(supplierId: string): Promise { + const supplier = await prisma.supplier.findUnique({ + where: { id: supplierId }, + }); + return !!supplier; + }, + + async getAllAssets(): Promise { + return(await prisma.asset.findMany({ + include: { + category: { + include: { + specifications: true, + }, + }, + brand: true, + stock: true, + supplier: true, + }, + })) + }, + async getAllAssetById(id: string): Promise { + const allAssets = await prisma.asset.findMany({ + where: { asset_id: id }, + include: { + category: { + include: { + specifications: true, + }, + }, + brand: true, + stock: true, + supplier: true, + }, + }); + + const assetsFound: AssetSearch = { + found: allAssets.length > 0, + data: (allAssets.length > 0)? allAssets : `No Asset found with ID ${id}` + } + + return assetsFound; + }, + + //Brand + + async addAssetBrand(brandData: idAndName): Promise { + if(await this.doesBrandAlreadyExist(brandData.id)) + { + return false; + } + return(await prisma.brand.create({ data: brandData })) + }, + async getAssetBrand(): Promise { + return(await prisma.brand.findMany()) + }, + + //Stock + + async addAssetStock(idAndName: idAndName): Promise { + if(await this.doesStockExist(idAndName.id)) + { + return false; + } + return(await prisma.stock.create({ data: idAndName })) + }, + async getAssetStock(): Promise { + return(await prisma.stock.findMany()) + }, + + //Supplier + async addAssetSupplier(idAndName: idAndName): Promise { + if(await this.doesSupplierAlreadyExist(idAndName.id)) + { + return false; + } + return(await prisma.supplier.create({ data: idAndName })) + }, + async getAssetSupplier(): Promise { + return(await prisma.supplier.findMany()) + }, + + + //Category + async addAssetCategory(categoryData: AssetCategory): Promise { + if(await this.doesCategoryExist(categoryData.id)) + { + return false; + } + const createdAssetCategory = await prisma.assetCategory.create({ + data: { + asset_category_id: categoryData.id, + category_name: categoryData.name, + specifications: { + create: categoryData.specifications.map(specification => ({ + id: specification.id, + name: specification.name, + values: { set: specification.values }, + })), + }, + }, + include: { + specifications: true, + }, + }); + + return { + id: createdAssetCategory.asset_category_id, + name: createdAssetCategory.category_name, + specifications: createdAssetCategory.specifications, + }; + }, + async getAssetCategory(): Promise { + const assetCategories = await prisma.assetCategory.findMany({ + include: { specifications: true }, + }); + + return assetCategories.map(category => ({ + id: category.asset_category_id, + name: category.category_name, + specifications: category.specifications, + })); + }, + +}; + +export default assetService; \ No newline at end of file diff --git a/src/services/tokenService.ts b/src/services/tokenService.ts index f703a36..5828b4e 100644 --- a/src/services/tokenService.ts +++ b/src/services/tokenService.ts @@ -9,14 +9,14 @@ import prisma from '../client'; /** * Generate token - * @param {number} userId + * @param {string} userId * @param {Moment} expires * @param {string} type * @param {string} [secret] * @returns {string} */ const generateToken = ( - userId: number, + userId: string, expires: Moment, type: TokenType, secret: string = config.jwt.secret @@ -33,7 +33,7 @@ const generateToken = ( /** * Save a token * @param {string} token - * @param {number} userId + * @param {string} userId * @param {Moment} expires * @param {string} type * @param {boolean} [blacklisted] @@ -41,7 +41,7 @@ const generateToken = ( */ const saveToken = async ( token: string, - userId: number, + userId: string, expires: Moment, type: TokenType, blacklisted: boolean = false @@ -66,7 +66,7 @@ const saveToken = async ( */ const verifyToken = async (token: string, type: TokenType): Promise => { const payload = jwt.verify(token, config.jwt.secret); - const userId = Number(payload.sub); + const userId = String(payload.sub); const tokenData = await prisma.token.findFirst({ where: { token, type, userId, blacklisted: false }, }); @@ -82,7 +82,7 @@ const verifyToken = async (token: string, type: TokenType): Promise => { * @returns {Promise} */ const generateAuthTokens = async (user: { - id: number; + id: string; }): Promise<{ access: { token: string; expires: Date } }> => { const accessTokenExpires = moment().add( config.jwt.accessExpirationMinutes, @@ -122,13 +122,13 @@ const generateResetPasswordToken = async (email: string): Promise => { 'minutes' ); const resetPasswordToken = generateToken( - user.id as number, + user.id as string, expires, TokenType.RESET_PASSWORD ); await saveToken( resetPasswordToken, - user.id as number, + user.id as string, expires, TokenType.RESET_PASSWORD ); diff --git a/src/services/userService.ts b/src/services/userService.ts index 74519ee..eb6c812 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,6 +1,6 @@ import prisma from '../client'; import * as nodemailer from 'nodemailer'; -import { User } from '@prisma/client'; +import { User, mapPrismaUserToMappedUser, MappedUser } from '../models'; import { encryptPassword } from '../utils/encryption'; interface Token { @@ -8,23 +8,54 @@ interface Token { token: string; } -const registerUser = async (userData: User): Promise => { +const registerUser = async (userData: User): Promise => { const createdUser = await prisma.user.create({ data: { names: userData.names, phone: userData.phone, email: userData.email, - gender: userData.gender, - nid: userData.nid, - marital_status: userData.marital_status, - nationality: userData.nationality, - img: userData.img, password: await encryptPassword(userData.password), - code: userData.code, + location: { + create: userData.location.map(loc => ({ + status: loc.status, + building: { + create: { + id: loc.building.id, + name: loc.building.name, + }, + }, + room: { + create: { + id: loc.room.id, + name: loc.room.name, + }, + }, + })), + }, + is_line_manager: userData.is_line_manager, + occupation_address: { + create: { + id: userData.occupation_address.id, + name: userData.occupation_address.name, + type: userData.occupation_address.type, + parent_id: userData.occupation_address.parent_id, + }, + }, + report_to: userData.report_to, + role: { + create: { + id: userData.role.id, + name: userData.role.name, + access: userData.role.access, + }, + }, + custom_access: userData.custom_access, }, }); - return createdUser; + + return findUserById(createdUser.id); }; + const changePassword = async ( userEmail: string, newPassword: string @@ -58,19 +89,52 @@ const findUserByEmail = async (email: string): Promise => { where: { email: email, }, + include: { + location: { + include: { + building: true, + room: true, + }, + }, + occupation_address: true, + role: true, + }, }); - return user; + + if (!user) { + return null; + } + + return mapPrismaUserToMappedUser(user); }; -const findUserById = async (id: number): Promise => { + + +const findUserById = async (userId: string): Promise => { const user = await prisma.user.findUnique({ where: { - id, + id: userId, + }, + include: { + location: { + include: { + building: true, + room: true, + }, + }, + occupation_address: true, + role: true, }, }); - return user; + + if (!user) { + return null; + } + + return mapPrismaUserToMappedUser(user); }; + function storeResetToken(user: User, token: string): void { const tokenUserEmail = user.email; prisma.resetPasswordTokens.create({ @@ -124,15 +188,15 @@ const sendResetEmail = async (email: string, token: string): Promise => { const transporter = nodemailer.createTransport({ service: 'gmail', auth: { - user: process.env.EMAIL, - pass: process.env.EMAIL_PASSWORD, + user: process.env.EMAIL_FROM, + pass: process.env.EMAIL_FROM_PASSWORD, }, }); const mailOptions = { from: process.env.EMAIL, to: email, - subject: 'Password Reset', + subject: 'BinaryHub AMS - Password Reset', html: `

Hi
Please enter the 6-digit code below on the email verification page:

${token}

Remember, beware of scams and keep this one-time verification code confidential.
Thanks,

`, diff --git a/src/validations/assetValidation.ts b/src/validations/assetValidation.ts new file mode 100644 index 0000000..7a0d9f7 --- /dev/null +++ b/src/validations/assetValidation.ts @@ -0,0 +1,57 @@ +import Joi from 'joi'; + +const specificationSchema = Joi.object({ + name: Joi.string().required(), + values: Joi.string().required(), + category_id: Joi.string().required(), +}); + +const oneAsset = Joi.object({ + asset_id: Joi.string().required(), + category_id: Joi.string().required(), + brand: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() + }).required(), + stock_id: Joi.string().required(), + supplier: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() + }).required(), + purchase_order_number: Joi.string().required(), + value: Joi.number().required(), + life_span_years: Joi.number().required(), + date_in: Joi.string().required(), +}); + +const assetBrand = Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() +}).required(); + +const assetCategory = Joi.object({ + id: Joi.string().required(), + name: Joi.string().required(), + specifications: Joi.array().items( + Joi.object({ + id: Joi.string().required(), + name: Joi.string().required(), + values: Joi.array().items(Joi.string()).required() + }) + ).required() +}).required(); + + +const assetStock = Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() +}).required(); + +const assetSupplier = Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() +}).required(); + +const multipleAssets = Joi.array().items(oneAsset); + +export default { oneAsset, multipleAssets, assetBrand, assetCategory, assetStock, assetSupplier }; \ No newline at end of file diff --git a/src/validations/index.ts b/src/validations/index.ts index 4f12b07..0531423 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -1 +1,2 @@ export { default as authValidation } from './userValidation'; +export { default as assetValidation } from './assetValidation'; diff --git a/src/validations/userValidation.ts b/src/validations/userValidation.ts index 37bee5b..c61d5bb 100644 --- a/src/validations/userValidation.ts +++ b/src/validations/userValidation.ts @@ -1,23 +1,42 @@ import Joi from 'joi'; -const login = { - body: Joi.object({ +const login = Joi.object({ email: Joi.string().email().required(), password: Joi.string().required(), - }), -}; + }); + const registration = Joi.object({ names: Joi.string().required(), phone: Joi.string().required(), email: Joi.string().email().required(), - gender: Joi.string().required(), - nid: Joi.string().required(), - martial_status: Joi.string().required(), - nationality: Joi.string().required(), - img: Joi.string().allow(null), - password: Joi.string().min(6).required(), - code: Joi.string().allow(null), + password: Joi.string().required(), + location: Joi.array().items(Joi.object({ + building: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required(), + }).required(), + room: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required(), + }).required(), + status: Joi.string().valid('ACTIVE', 'INACTIVE').required(), + })).required(), + is_line_manager: Joi.boolean().required(), + occupation_address: Joi.object({ + id: Joi.number().required(), + name: Joi.string().required(), + type: Joi.string().valid('DEPARTMENT', 'SCHOOL', 'COLLEGE', 'CAMPUS', 'UR').required(), + parent_id: Joi.number().allow(null).required(), + }).required(), + report_to: Joi.string().allow(null).required(), + role: Joi.object({ + id: Joi.number().required(), + name: Joi.string().required(), + access: Joi.array().items(Joi.string()).required(), + }).required(), + custom_access: Joi.array().items(Joi.string()).required(), }); + const emailVerification = Joi.object({ email: Joi.string().email().required(), }); diff --git a/tests/data/assetRegister.ts b/tests/data/assetRegister.ts new file mode 100644 index 0000000..a2e449d --- /dev/null +++ b/tests/data/assetRegister.ts @@ -0,0 +1,109 @@ +const assetExample = { + "asset_id": "abc123", + "category": { "id": 1, "name": "Laptop" }, + "brand": { "id": 1, "name": "HP" }, + "stock": { "id": "xyz789", "name": "IT Department" }, + "supplier": { "id": "123456", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO123", + "value": 1200, + "life_span_years": 4, + "date_in": "2022-12-01", + "specifications": [ + { "name": "processor", "values": "Intel Core i7" }, + { "name": "memory", "values": "16GB RAM" }, + { "name": "storage", "values": "512GB SSD" }, + { "name": "screen_size", "values": "15.6 inches" } + ] + } + + + +const manyAssetsExample = [ + { + "asset_id": "desktop1", + "category": { "id": 2, "name": "Desktop" }, + "brand": { "id": 2, "name": "Dell" }, + "stock": { "id": "stock1", "name": "IT Department" }, + "supplier": { "id": "supplier1", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO456", + "value": 800, + "life_span_years": 5, + "date_in": "2022-12-05", + "specifications": [ + { "name": "processor", "values": "Intel Core i5" }, + { "name": "memory", "values": "8GB RAM" }, + { "name": "storage", "values": "1TB HDD" }, + { "name": "graphics_card", "values": "NVIDIA GeForce GTX 1650" } + ] + }, + { + "asset_id": "desktop2", + "category": { "id": 2, "name": "Desktop" }, + "brand": { "id": 3, "name": "HP" }, + "stock": { "id": "stock2", "name": "IT Department" }, + "supplier": { "id": "supplier2", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO457", + "value": 900, + "life_span_years": 6, + "date_in": "2022-12-08", + "specifications": [ + { "name": "processor", "values": "AMD Ryzen 7" }, + { "name": "memory", "values": "12GB RAM" }, + { "name": "storage", "values": "512GB SSD" }, + { "name": "graphics_card", "values": "AMD Radeon RX 5500 XT" } + ] + }, + { + "asset_id": "desktop3", + "category": { "id": 2, "name": "Desktop" }, + "brand": { "id": 2, "name": "Dell" }, + "stock": { "id": "stock1", "name": "IT Department" }, + "supplier": { "id": "supplier1", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO458", + "value": 850, + "life_span_years": 5, + "date_in": "2022-12-10", + "specifications": [ + { "name": "processor", "values": "Intel Core i7" }, + { "name": "memory", "values": "16GB RAM" }, + { "name": "storage", "values": "256GB SSD + 1TB HDD" }, + { "name": "graphics_card", "values": "NVIDIA GeForce GTX 1660 Ti" } + ] + }, + { + "asset_id": "desktop4", + "category": { "id": 2, "name": "Desktop" }, + "brand": { "id": 4, "name": "Lenovo" }, + "stock": { "id": "stock3", "name": "IT Department" }, + "supplier": { "id": "supplier3", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO459", + "value": 750, + "life_span_years": 4, + "date_in": "2022-12-12", + "specifications": [ + { "name": "processor", "values": "AMD Ryzen 5" }, + { "name": "memory", "values": "8GB RAM" }, + { "name": "storage", "values": "512GB SSD" }, + { "name": "graphics_card", "values": "Integrated AMD Radeon Graphics" } + ] + }, + { + "asset_id": "desktop5", + "category": { "id": 2, "name": "Desktop" }, + "brand": { "id": 3, "name": "HP" }, + "stock": { "id": "stock2", "name": "IT Department" }, + "supplier": { "id": "supplier2", "name": "Tech Supplier Inc" }, + "purchase_order_number": "PO460", + "value": 950, + "life_span_years": 7, + "date_in": "2022-12-15", + "specifications": [ + { "name": "processor", "values": "Intel Core i9" }, + { "name": "memory", "values": "32GB RAM" }, + { "name": "storage", "values": "1TB SSD" }, + { "name": "graphics_card", "values": "NVIDIA GeForce RTX 3080" } + ] + } + ] + + \ No newline at end of file diff --git a/tests/data/userRegister.ts b/tests/data/userRegister.ts new file mode 100644 index 0000000..3ea8b1d --- /dev/null +++ b/tests/data/userRegister.ts @@ -0,0 +1,28 @@ +const userExample = { + id: "12345", + names: "John Doe", + phone: "123456789", + email: "john.doe@example.com", + password: "hashedPassword", + location: [ + { + building: { id: "building1", name: "Office Building 1" }, + room: { id: "room101", name: "Meeting Room 101" }, + status: "ACTIVE", + }, + ], + is_line_manager: true, + occupation_address: { + id: 1, + name: "Asset Manager", + type: "UR", + parent_id: null, + }, + report_to: "Manager", + role: { + id: 1, + name: "Manager", + access: ["read", "write"], + }, + custom_access: ["custom_permission1", "custom_permission2"], + }; \ No newline at end of file diff --git a/tests/models/AssetCategory.ts b/tests/models/AssetCategory.ts new file mode 100644 index 0000000..3e84adc --- /dev/null +++ b/tests/models/AssetCategory.ts @@ -0,0 +1,9 @@ +interface AssetCategory { + asset_category_id: string; + category_name: string; + specifications: { + name: string; + values: string[]; + }[]; + } + export default AssetCategory; \ No newline at end of file diff --git a/tests/models/AssetStockSummary.ts b/tests/models/AssetStockSummary.ts new file mode 100644 index 0000000..8728dbf --- /dev/null +++ b/tests/models/AssetStockSummary.ts @@ -0,0 +1,14 @@ +interface AssetStockSummary { + category: { id: number; name: string }; + subcategory: { id: number; name: string }; + brand: { id: number; name: string }; + stock: { id: string; name: string }; + assets: { + asset_id: string; + specifications: { + name: string; + values: string; + }[]; + }[]; + } + export default AssetStockSummary; \ No newline at end of file diff --git a/tests/models/Location.ts b/tests/models/Location.ts new file mode 100644 index 0000000..61eca1e --- /dev/null +++ b/tests/models/Location.ts @@ -0,0 +1,18 @@ +interface Location { + id: string; + building: Building; + room: Room; + status: 'ACTIVE' | 'INACTIVE'; + } + +export default Location; + +interface Building { + id: string; + name: string; +} + +interface Room { + id: string; + name: string; +} \ No newline at end of file diff --git a/tests/models/OccupationAddress.ts b/tests/models/OccupationAddress.ts new file mode 100644 index 0000000..277e91d --- /dev/null +++ b/tests/models/OccupationAddress.ts @@ -0,0 +1,8 @@ +interface OccupationAddress { + id: number; + name: string; + type: 'DEPARTMENT' | 'SCHOOL' | 'COLLEGE' | 'CAMPUS' | 'UR'; + parent_id: number | null; + } + +export default OccupationAddress; \ No newline at end of file diff --git a/tests/models/Role.ts b/tests/models/Role.ts new file mode 100644 index 0000000..1f0f2c4 --- /dev/null +++ b/tests/models/Role.ts @@ -0,0 +1,6 @@ +interface Role { + role_id: number; + name: string; + access: string[]; +} + export default Role; \ No newline at end of file diff --git a/tests/models/Staff.ts b/tests/models/Staff.ts new file mode 100644 index 0000000..7f2e4bd --- /dev/null +++ b/tests/models/Staff.ts @@ -0,0 +1,20 @@ +interface Staff { + staff_id: string; + names: string; + phone: string; + email: string; + password: string; + position_id: string; + location: { + building: { id: string; name: string }; + room: { id: string; name: string }; + status: 'ACTIVE' | 'INACTIVE'; + }[]; + is_line_manager: boolean; + occupation_address_id: string; + report_to: string; + role_id: number; + custom_access: string[]; + } + + export default Staff; \ No newline at end of file diff --git a/tests/models/models.ts b/tests/models/models.ts new file mode 100644 index 0000000..d5b067a --- /dev/null +++ b/tests/models/models.ts @@ -0,0 +1,69 @@ +// interface User { +// id: string; +// names: string; +// phone: string; +// email: string; +// password: string; +// position_id: string; +// location: { +// building: { id: string; name: string }; +// room: { id: string; name: string }; +// status: 'ACTIVE' | 'INACTIVE'; +// }[]; +// is_line_manager: boolean; +// occupation_address_id: string; +// report_to: string; +// role_id: number; +// custom_access: string[]; +// } + +// interface Role { +// role_id: number; +// name: string; +// access: string[]; +// } + +// interface occupation_address { +// id: number; +// name: string; +// type: 'DEPARTMENT' | 'SCHOOL' | 'COLLEGE' | 'CAMPUS' | 'UR'; +// parent_id: number | null; +// } + +// interface AssetCategory { +// asset_category_id: string; +// category_name: string; +// specifications: { +// name: string; +// values: string[]; +// }[]; +// } +// interface Asset { +// asset_id: string; +// category: { id: number; name: string }; +// subcategory: { id: number; name: string }; +// brand: { id: number; name: string }; +// stock: { id: string; name: string }; +// supplier: { id: string; name: string }; +// purchase_order_number: string; +// value: number; +// life_span_years: number; +// date_in: string; +// specifications: { +// name: string; //processor, serial_number +// values: string; //i5, 7938783794 +// }[]; +// } +// interface AssetStockSummary { +// category: { id: number; name: string }; +// subcategory: { id: number; name: string }; +// brand: { id: number; name: string }; +// stock: { id: string; name: string }; +// assets: { +// asset_id: string; +// specifications: { +// name: string; +// values: string; +// }[]; +// }[]; +// } diff --git a/tsconfig.json b/tsconfig.json index efddd81..02a2e23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "noImplicitAny": true, "removeComments": true }, - "include": ["src"], + "include": ["src", "tests/models/AssetCategory.ts"], "exclude": ["dist", "prisma", "node_modules"] }