From fd682b34363c3475872468f047d8fb10021287f7 Mon Sep 17 00:00:00 2001 From: Dennis-star1001 Date: Wed, 12 Jun 2024 12:25:13 -0700 Subject: [PATCH 1/2] modified the backend documentation file --- README.md | 171 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index e34e94e..0f4d764 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,10 @@ -

+# Inventors Website Backend -Nest Logo - -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

- -

- -NPM Version - -Package License - -NPM Downloads - -CircleCI - -Coverage - -Discord - -Backers on Open Collective - -Sponsors on Open Collective - - - -Support us - - - -

- - +This repository contains the backend code for the Inventors website. ## Description -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. +This is the inventors website backend project ## Setup the app @@ -60,37 +23,63 @@ Once you have cloned the project on your system, you will have your copy of the For the application to function properly when running it, you need to setup some necessary Environment Variables in your `.env` file 1. Create a file `.env` in your project root folder. + 2. Copy the Environment Variables from your `.env.sample` to your newly created `.env file` ``` + NODE_ENV=development + + PORT=3888 + + APP_DATABASE_URL=mongodb://localhost:27017/inventor_app?retryWrites=true&w=majority + + LOG_DATABASE_URL=mongodb://localhost:27017/inventor_log?retryWrites=true&w=majority + + JWT_SECRET= + + JWT_EXPIRES=60 + + RATE_LIMIT_REQUEST_SIZE=60 + + RATE_LIMIT_TTL=30 + ``` 3. Create a Cloudinary account with this [link](https://cloudinary.com/users/register_free) + 4. Navigate to your dashboard in your Cloudinary account and copy the following Environment Variables. ``` + //Cloudinary Environment Variables + + CLOUDINARY_NAME=[Enter your Cloudinary name here] + + CLOUDINARY_KEY=[Enter your Cloudinary key here] + + CLOUDINARY_SECRET= [Enter your Cloudinary secret here] + ``` Once you have copied your unique Environment Variables, paste them into your `.env` file as shown in the code above. @@ -99,40 +88,60 @@ Once you have copied your unique Environment Variables, paste them into your `.e ```bash + + $ npm install + + ``` ## Running the app ```bash + + # development + + $ nest start --watch + + # production mode + + $ npm run start:prod + + ``` Once the application is running, your terminal should display a message similar to: ``` + Inventor application is running on: http://[::1]:3888 + ``` Here, `3888` is the port number on which the application is running. This port number depends on the value you specify in your `.env` file under the `PORT` variable: ``` + PORT=[YOUR_PORT_NUMBER] + ``` To access the Swagger documentation for the API endpoints, open your web browser and navigate to the following URL, replacing `3888` with your specified port number: ``` + http://[::1]:[YOUR_PORT_NUMBER]/docs/api + ``` This URL will take you to the Swagger interface, where you can explore and interact with the API endpoints. @@ -156,23 +165,41 @@ The first step is to register a new user. This is required to create an account **Request Body:** ```` + ```json + { + "email": "string", + "password": "string", + "firstName": "string", + "lastName": "string", + "joinMethod": "SIGN_UP", + "location": { - "type": "Point", - "coordinates": [ - "longitude", - "latitude" - ] + +"type": "Point", + +"coordinates": [ + +"longitude", + +"latitude" + +] + }, + "deviceId": "string", + "deviceToken": "string" + } + ```` ### 2. Verify the User @@ -185,6 +212,8 @@ To verify the user using the Swagger documentation, you'll need to provide the * ``` + + ``` ### 3. Log In @@ -196,38 +225,56 @@ Once the user is verified, you can log in to obtain an authentication token. **Request Body:** ``` + { - "email": "string", - "password": "string" + +"email": "string", + +"password": "string" + } + ``` **Response:** The response will contain an authentication token, which you will use for authenticated requests. ``` + { + "access_token": "your_auth_token", + "_id": "your_userId", + "email": "your_email_address", + "firstName": "your_firstname", + "lastName": "your_lastname", + "role": [ "USER" ] + } + ``` ### 4. Add the Auth Token to Swagger To interact with the secured endpoints in the Swagger documentation, you need to add the obtained authentication token. -1. Open the Swagger documentation for your API. -2. Click on the **Authorize** button (usually a padlock icon) at the top of the page. -3. In the popup that appears, enter your authentication token in the appropriate field. The format usually is: +1. Open the Swagger documentation for your API. + +2. Click on the **Authorize** button (usually a padlock icon) at the top of the page. + +3. In the popup that appears, enter your authentication token in the appropriate field. The format usually is: ``` + Bearer your_auth_token + ``` -4. Click **Authorize** to apply the token. +4. Click **Authorize** to apply the token. With the authentication token added, you can now interact with the secured endpoints in the Swagger documentation. The token will be included in the `Authorization` header of your requests. @@ -237,35 +284,41 @@ Ensure you replace placeholder values (e.g., `your_firstname`,`your_lastname`, ` ```bash + + # unit tests + + $ npm run test + + # e2e tests + + $ npm run test:e2e + + # test coverage -$ npm run test:cov -``` -## Support +$ npm run test:cov -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). -## Stay in touch -- Author - [Kamil Myƛliwiec](https://kamilmysliwiec.com) +``` -- Website - [https://nestjs.com](https://nestjs.com/) +## Support -- Twitter - [@nestframework](https://twitter.com/nestframework) +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). ## License From ab08e4582cfe9aea9b6444901b2524f4488f95e0 Mon Sep 17 00:00:00 2001 From: DennisTemoye Date: Wed, 2 Oct 2024 10:08:58 +0100 Subject: [PATCH 2/2] testing changes --- package-lock.json | 86 ++++++++++++++++++++------------ package.json | 1 + src/shared/schema/index.ts | 8 ++- src/shared/schema/user.schema.ts | 22 ++++++++ src/users/users.controller.ts | 28 +++++++++++ src/users/users.service.ts | 81 ++++++++++++++++++++++++++---- 6 files changed, 182 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2672db..4f4c1af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ "@nestjs/mongoose": "^10.0.1", "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.1.1", "@nestjs/swagger": "^7.1.12", "@nestjs/throttler": "^5.1.2", - "@types/image-size": "^0.8.0", "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "class-transformer": "^0.5.1", @@ -27,7 +27,6 @@ "cloudinary": "^1.41.3", "date-fns": "^2.30.0", "google-auth-library": "^9.0.0", - "image-size": "^1.1.1", "jwks-rsa": "^3.1.0", "mailgun.js": "^9.3.0", "mongoose": "^7.5.2", @@ -3657,6 +3656,33 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", + "license": "MIT", + "dependencies": { + "cron": "3.1.7", + "uuid": "10.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/schematics": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", @@ -4219,15 +4245,6 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, - "node_modules/@types/image-size": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@types/image-size/-/image-size-0.8.0.tgz", - "integrity": "sha512-hMlhu25ji75dXQk2uZkN3pTJ+lWrgKr8M1fTpyyFvuu+SJZBdGa5gDm4BVNobWXHZbOU11mBj0vciYp7qOfAFg==", - "deprecated": "This is a stub types definition. image-size provides its own type definitions, so you do not need this installed.", - "dependencies": { - "image-size": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -4276,6 +4293,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -6159,6 +6182,16 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8374,20 +8407,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -10063,6 +10082,15 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/macos-release": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", @@ -11381,14 +11409,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index d71cd69..128ff96 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@nestjs/mongoose": "^10.0.1", "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.1.1", "@nestjs/swagger": "^7.1.12", "@nestjs/throttler": "^5.1.2", "bcryptjs": "^2.4.3", diff --git a/src/shared/schema/index.ts b/src/shared/schema/index.ts index 7dc85df..061269d 100644 --- a/src/shared/schema/index.ts +++ b/src/shared/schema/index.ts @@ -3,7 +3,12 @@ import { DataLog, DataLogSchema } from './data.log.schema'; import { configs } from '../configs'; import { Connection, ConnectOptions, createConnection } from 'mongoose'; import { Module } from '@nestjs/common'; -import { User, UserSchema } from './user.schema'; +import { + Notification, + NotificationSchema, + User, + UserSchema, +} from './user.schema'; import { EventSchema } from './events.schema'; // All Schema Models @@ -13,6 +18,7 @@ export * from './events.schema'; const SCHEMA_LIST = [ { name: User.name, schema: UserSchema, dbPrefix: 'APP' }, + { name: Notification.name, schema: NotificationSchema, dbPrefix: 'APP' }, { name: Event.name, schema: EventSchema, dbPrefix: 'APP' }, { name: DataLog.name, schema: DataLogSchema, dbPrefix: 'LOG' }, ]; diff --git a/src/shared/schema/user.schema.ts b/src/shared/schema/user.schema.ts index 4088970..a6e87fe 100644 --- a/src/shared/schema/user.schema.ts +++ b/src/shared/schema/user.schema.ts @@ -27,6 +27,7 @@ import { CreateUserDto } from '../dtos/create-user.dto'; import { UserInviteDto } from 'src/users/dto/user-invite.dto'; export type UserDocument = HydratedDocument; +export type NotificationDocument = HydratedDocument; @Schema({ timestamps: true }) export class User { @@ -303,3 +304,24 @@ UserSchema.statics.signUp = async function signUp( return { ...details, ...user }; }; +@Schema({ timestamps: true }) +export class Notification { + @Prop({ required: true }) + message: string; + + @Prop({ required: true }) + link: string; + + @Prop({ type: [String], default: [] }) // Array of user IDs to notify + userIds?: string[]; + + @Prop({ type: [String], default: [] }) // Array of roles to notify + roles: string[]; + + @Prop({ default: false }) // Indicates if the notification has been read + isRead: boolean; + + @Prop({ default: Date.now }) // Date created + createdAt: Date; +} +export const NotificationSchema = SchemaFactory.createForClass(Notification); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 999bf28..10d09b2 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -19,6 +19,7 @@ import { UserChangePasswordDto } from './dto/user-change-password.dto'; import { UserAddPhotoDto } from './dto/user-add-photo.dto'; import { DeactivateAccountDto } from './dto/deactivate-account.dto'; import { RequestReactivationDto } from './dto/request-reactivation.dto'; +import { Notification } from 'src/shared/schema'; @ApiTags('users') @Controller('users') export class UsersController { @@ -148,4 +149,31 @@ export class UsersController { // Optional: Use payload.message if needed return this.usersService.requestReactivation(userId); } + + //Notification controller + + @Post(':userId') + @ApiBearerAuth() + @UseGuards(JwtUsersGuard) + async createNotification( + @Param('userId') userId: string, + @Body() { message, link }: { message: string; link?: string }, + ): Promise { + // You need to pass the userId in the userIds array, and any relevant roles + const userIds = [userId]; // Assuming you want to notify only this user + const roles: string[] = []; // Adjust this as needed + + // Call the service to create the notification + return this.usersService.createNotification(message, link, userIds, roles); + } + @Get(':userId/notifications/:role') + @ApiBearerAuth() + @UseGuards(JwtUsersGuard) + async getNotifications( + @Param('userId') userId: string, + @Param('role') role: string, + ) { + // Wrap the role in an array to match the expected type + return this.usersService.getNotifications([userId], [role]); + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index ecc96d5..c6d6616 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -9,7 +9,12 @@ import { Model, Types } from 'mongoose'; import { faker } from '@faker-js/faker'; import { UserChangePasswordDto } from './dto/user-change-password.dto'; import { UserAddPhotoDto } from './dto/user-add-photo.dto'; -import { User, UserDocument } from 'src/shared/schema'; +import { + Notification, + NotificationDocument, + User, + UserDocument, +} from 'src/shared/schema'; import { BcryptUtil, CloudinaryFolders, @@ -35,8 +40,10 @@ import { VerificationStatus } from 'src/shared/interfaces/user.type'; export class UsersService { constructor( @Inject(User.name) + @Inject(Notification.name) private readonly userModel: Model, - ) { } + private readonly notificationModel: Model, + ) {} sendEmailVerificationToken(req: any, userId: string) { (this.userModel as any).sendEmailVerificationToken(req, userId); @@ -274,14 +281,11 @@ export class UsersService { } async requestVerification(req: ApiReq, userId: string) { - - const user = await this.userModel.findById(userId); if (!user) { throw new Error('User not found'); } - const currentDate = new Date(); if ( user.nextVerificationRequestDate && @@ -294,24 +298,81 @@ export class UsersService { throw new Error('User is already verified'); } - user.verificationStatus = VerificationStatus.PENDING; const nextVerificationDate = new Date(); nextVerificationDate.setMonth(nextVerificationDate.getMonth() + 3); user.nextVerificationRequestDate = nextVerificationDate; - await user.save(); } async updateStatus(userId: string, status: UserStatus): Promise { - return this.userModel.findByIdAndUpdate(userId, { status }, { new: true }).exec(); + return this.userModel + .findByIdAndUpdate(userId, { status }, { new: true }) + .exec(); } async deactivateAccount(userId: string): Promise { - return this.userModel.findByIdAndUpdate(userId, { status: UserStatus.DEACTIVATED }, { new: true }).exec(); + return this.userModel + .findByIdAndUpdate( + userId, + { status: UserStatus.DEACTIVATED }, + { new: true }, + ) + .exec(); } async requestReactivation(userId: string): Promise { - return this.userModel.findByIdAndUpdate(userId, { status: UserStatus.ACTIVE }, { new: true }).exec(); + return this.userModel + .findByIdAndUpdate(userId, { status: UserStatus.ACTIVE }, { new: true }) + .exec(); + } + + // Create a new notification for specific users, roles, or as a general notification + async createNotification( + message: string, + link: string, + userIds: string[], + roles: string[], + ): Promise { + const notification = new this.notificationModel({ + message, + link, + userIds, + roles, + }); + return await notification.save(); + } + + async getNotifications( + userIds?: string[], + roles?: string[], + ): Promise { + const query: any = { + $or: [], + }; + + // If userIds are provided, add them to the query + if (userIds && userIds.length > 0) { + query.$or.push({ userIds: { $in: userIds } }); + } + + // If roles are provided, add them to the query + if (roles && roles.length > 0) { + query.$or.push({ roles: { $in: roles } }); + } + + // If no filters are provided, return an empty array or all notifications as needed + if (query.$or.length === 0) { + return []; // or return await this.notificationModel.find().lean().exec(); to fetch all + } + + return await this.notificationModel.find(query).lean().exec(); + } + + async markAsRead(notificationId: string): Promise { + return await this.notificationModel + .findByIdAndUpdate(notificationId, { isRead: true }, { new: true }) + .lean() + .exec(); } }