diff --git a/prisma/migrations/20240226163703_is_read_is_read_at/migration.sql b/prisma/migrations/20240226163703_is_read_is_read_at/migration.sql new file mode 100644 index 0000000..ad70cad --- /dev/null +++ b/prisma/migrations/20240226163703_is_read_is_read_at/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `readed` on the `Notification` table. All the data in the column will be lost. + - You are about to drop the column `readedAt` on the `Notification` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "readed", +DROP COLUMN "readedAt", +ADD COLUMN "isRead" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isReaddAt" TIMESTAMP(3); diff --git a/prisma/migrations/20240226165313_fix_is_read_at/migration.sql b/prisma/migrations/20240226165313_fix_is_read_at/migration.sql new file mode 100644 index 0000000..be1c0d2 --- /dev/null +++ b/prisma/migrations/20240226165313_fix_is_read_at/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `isReaddAt` on the `Notification` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "isReaddAt", +ADD COLUMN "isReadAt" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1000a37..e86b0f9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,9 +38,9 @@ model Notification { id String @id @default(uuid()) userId String message String - readed Boolean @default(false) + isRead Boolean @default(false) createdAt DateTime @default(now()) - readedAt DateTime? + isReadAt DateTime? queue String User User @relation(fields: [userId], references: [id], onDelete: Cascade) } @@ -78,7 +78,6 @@ model Admin { model ContactInfo { id String @id phoneNumber String - address String cityId Int city City @relation(fields: [cityId], references: [id]) user User @relation(fields: [id], references: [id], onDelete: Cascade) diff --git a/src/presentation/animals/routes.ts b/src/presentation/animals/routes.ts index d1aff51..940585c 100644 --- a/src/presentation/animals/routes.ts +++ b/src/presentation/animals/routes.ts @@ -16,7 +16,7 @@ import { PaginationDto, UpdateAnimalDto, } from '../../domain'; -import { ProducerService, S3Service } from '../common/services'; +import { QueueService, S3Service } from '../common/services'; export class AnimalRoutes { static get routes() { @@ -30,11 +30,8 @@ export class AnimalRoutes { envs.AWS_SECRET_ACCESS_KEY, envs.AWS_BUCKET ); - const emailService = new ProducerService( - envs.RABBITMQ_URL, - 'email-request' - ); - const notificationService = new ProducerService( + const emailService = new QueueService(envs.RABBITMQ_URL, 'email-request'); + const notificationService = new QueueService( envs.RABBITMQ_URL, 'notification-request' ); diff --git a/src/presentation/auth/routes.ts b/src/presentation/auth/routes.ts index 2b50446..8a8e04a 100644 --- a/src/presentation/auth/routes.ts +++ b/src/presentation/auth/routes.ts @@ -2,7 +2,7 @@ import { Router } from 'express'; import { AuthController } from './controller'; import { AuthMiddleware, ValidationMiddleware } from '../middlewares'; -import { ProducerService } from '../common/services'; +import { QueueService } from '../common/services'; import { AuthService } from './service'; import { JWTAdapter, envs } from '../../config'; import { @@ -18,10 +18,7 @@ export class AuthRoutes { const jwt = new JWTAdapter(envs.JWT_SEED); - const emailService = new ProducerService( - envs.RABBITMQ_URL, - 'email-request' - ); + const emailService = new QueueService(envs.RABBITMQ_URL, 'email-request'); const authService = new AuthService(jwt, emailService); const authController = new AuthController(authService); diff --git a/src/presentation/users/controller.ts b/src/presentation/users/controller.ts index 91a0bb0..7583bcd 100644 --- a/src/presentation/users/controller.ts +++ b/src/presentation/users/controller.ts @@ -160,10 +160,23 @@ export class UserController { * Retrieves user's notifications. */ getUserNotifications = async (req: Request, res: Response) => { + const { limit = 5, page = 1 } = req.query; const user = req.user; - const notifications = await this.userService.getNotifications(user.id!); + const notifications = await this.userService.getNotifications(user.id!, { + limit: +limit, + page: +page, + }); res.status(HttpCodes.OK).json(notifications); }; + + readNotification = async (req: Request, res: Response) => { + const { id } = req.params; + const user = req.user; + + await this.userService.readNotification(user, id); + + res.status(HttpCodes.OK).json({ message: 'Notification marked as read' }); + }; } diff --git a/src/presentation/users/routes.ts b/src/presentation/users/routes.ts index 1a39328..1618f06 100644 --- a/src/presentation/users/routes.ts +++ b/src/presentation/users/routes.ts @@ -13,7 +13,7 @@ import { UpdatePasswordDto, UpdateSocialMediaDto, } from '../../domain'; -import { S3Service, ProducerService } from '../common/services'; +import { S3Service, QueueService } from '../common/services'; import { UserService } from './service'; export class UserRoutes { @@ -28,7 +28,7 @@ export class UserRoutes { envs.AWS_SECRET_ACCESS_KEY, envs.AWS_BUCKET ); - const notificationService = new ProducerService( + const notificationService = new QueueService( envs.RABBITMQ_URL, 'notification-request' ); @@ -38,22 +38,28 @@ export class UserRoutes { router.use(authMiddleware.authenticateUser); - router.get('/me', userController.getCurrentUser); - + // Favorites router.get( '/me/favorites', - // authMiddleware.authorizePermissions('adopter'), + authMiddleware.authorizePermissions('adopter'), userController.getUserFavorites ); + // Notifications router.get('/me/notifications', userController.getUserNotifications); + router.put('/me/notifications/read/:id', userController.readNotification); + + // Animals router.get( '/me/animals/', - // authMiddleware.authorizePermissions('shelter'), + authMiddleware.authorizePermissions('shelter'), userController.getUserAnimals ); + // Current User CRUD + router.get('/me', userController.getCurrentUser); + router.put( '/me', ValidationMiddleware.validate(UpdateUserDto), @@ -78,13 +84,16 @@ export class UserRoutes { router.delete('/me', userController.deleteUser); router.get('/:id', userController.getSingleUser); + // End Current User CRUD + // All users router.get( '/', - // authMiddleware.authorizePermissions('admin'), + authMiddleware.authorizePermissions('admin'), userController.getAllUsers ); + // Images router.post( '/upload-images', [ diff --git a/src/presentation/users/service.ts b/src/presentation/users/service.ts index 49d01e1..5c2afc9 100644 --- a/src/presentation/users/service.ts +++ b/src/presentation/users/service.ts @@ -599,15 +599,81 @@ export class UserService { /** * Fetches notifications for a user. * @param id - ID of the user. - * @returns Array of notification objects. + * @param paginationDto - DTO containing pagination parameters. + * @returns Object containing paginated list of user's notifications. */ - public async getNotifications(id: string) { - const notifications = await prisma.notification.findMany({ + public async getNotifications(id: string, paginationDto: PaginationDto) { + const { limit = 5, page = 1 } = paginationDto; + + const [total, notifications] = await prisma.$transaction([ + prisma.notification.count({ where: { userId: id } }), + prisma.notification.findMany({ + skip: (page - 1) * limit, + take: limit, + where: { + userId: id, + }, + }), + ]); + + const maxPages = Math.ceil(total / limit); + + return { + currentPage: page, + maxPages, + limit, + total, + next: + page + 1 <= maxPages + ? `/api/users/me/notifications?page=${page + 1}&limit=${limit}` + : null, + prev: + page - 1 > 0 + ? `/api/users/me/notifications?page=${page - 1}&limit=${limit}` + : null, + notifications, + }; + } + + /** + * Marks a notification as read for a user. + * @param user - PayloadUser object representing the user. + * @param id - ID of the notification to mark as read or 'all' to mark all notifications as read + */ + public async readNotification(user: PayloadUser, id: string) { + if (id === 'all') + await prisma.notification.updateMany({ + where: { + userId: user.id, + }, + data: { + isRead: true, + isReadAt: new Date(), + }, + }); + + const notification = await prisma.notification.findUnique({ where: { - userId: id, + id, + }, + }); + + if (!notification) throw new NotFoundError('Notification not found'); + if (notification.isRead) + throw new BadRequestError('Notification is already read'); + + CheckPermissions.check(user, notification.userId); + + await prisma.notification.update({ + where: { + id: id, + }, + data: { + isRead: true, + isReadAt: new Date(), }, }); - return notifications; + return true; } } diff --git a/src/test/presentation/animals/routes.test.ts b/src/test/presentation/animals/routes.test.ts index d2ce681..21cb9c3 100644 --- a/src/test/presentation/animals/routes.test.ts +++ b/src/test/presentation/animals/routes.test.ts @@ -4,7 +4,6 @@ import { TestUser, cleanDB } from '../auth/routes.test'; import request from 'supertest'; import { CreateAnimalDto } from '../../../domain'; import { gender } from '../../../domain/interfaces'; -import { S3Service } from '../../../presentation/services'; jest.mock('../../../presentation/services/s3.service.ts');