From 3e80092ba728c6add428e0b2077f8598452cfe5a Mon Sep 17 00:00:00 2001 From: Arafat Date: Sat, 25 May 2024 20:32:59 +0200 Subject: [PATCH] Add opt-out option for email notifications in user settings (#163) --- .../src/api/routes/settings.controller.ts | 50 ++- .../src/api/routes/users.controller.ts | 9 +- .../settings/settings.component.tsx | 51 ++- .../notifications/notification.service.ts | 12 +- .../organizations/organization.repository.ts | 1 + .../src/database/prisma/schema.prisma | 359 +++++++++--------- .../database/prisma/users/users.repository.ts | 22 ++ .../database/prisma/users/users.service.ts | 11 + 8 files changed, 315 insertions(+), 200 deletions(-) diff --git a/apps/backend/src/api/routes/settings.controller.ts b/apps/backend/src/api/routes/settings.controller.ts index e195b242..3e56dbf0 100644 --- a/apps/backend/src/api/routes/settings.controller.ts +++ b/apps/backend/src/api/routes/settings.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; -import { Organization } from '@prisma/client'; +import { Organization, User } from '@prisma/client'; import { StarsService } from '@gitroom/nestjs-libraries/database/prisma/stars/stars.service'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; import { @@ -8,15 +8,18 @@ import { Sections, } from '@gitroom/backend/services/auth/permissions/permissions.service'; import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; -import {AddTeamMemberDto} from "@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto"; -import {ApiTags} from "@nestjs/swagger"; +import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto'; +import { ApiTags } from '@nestjs/swagger'; +import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; +import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; @ApiTags('Settings') @Controller('/settings') export class SettingsController { constructor( private _starsService: StarsService, - private _organizationService: OrganizationService + private _organizationService: OrganizationService, + private _usersService: UsersService ) {} @Get('/github') @@ -105,26 +108,51 @@ export class SettingsController { } @Get('/team') - @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + @CheckPolicies( + [AuthorizationActions.Create, Sections.TEAM_MEMBERS], + [AuthorizationActions.Create, Sections.ADMIN] + ) async getTeam(@GetOrgFromRequest() org: Organization) { return this._organizationService.getTeam(org.id); } @Post('/team') - @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + @CheckPolicies( + [AuthorizationActions.Create, Sections.TEAM_MEMBERS], + [AuthorizationActions.Create, Sections.ADMIN] + ) async inviteTeamMember( - @GetOrgFromRequest() org: Organization, - @Body() body: AddTeamMemberDto, + @GetOrgFromRequest() org: Organization, + @Body() body: AddTeamMemberDto ) { return this._organizationService.inviteTeamMember(org.id, body); } @Delete('/team/:id') - @CheckPolicies([AuthorizationActions.Create, Sections.TEAM_MEMBERS], [AuthorizationActions.Create, Sections.ADMIN]) + @CheckPolicies( + [AuthorizationActions.Create, Sections.TEAM_MEMBERS], + [AuthorizationActions.Create, Sections.ADMIN] + ) deleteTeamMember( - @GetOrgFromRequest() org: Organization, - @Param('id') id: string + @GetOrgFromRequest() org: Organization, + @Param('id') id: string ) { return this._organizationService.deleteTeamMember(org, id); } + + @Get('/email-notifications/:userId') + async getEmailNotifications(@Param('userId') userId: string) { + return this._usersService.getEmailNotifications(userId); + } + + @Post('/email-notifications/:userId') + async updateEmailNotifications( + @Param('userId') userId: string, + @Body('emailNotifications') emailNotifications: boolean + ) { + return await this._usersService.updateEmailNotifications( + userId, + emailNotifications + ); + } } diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index a048b748..64f6eb6c 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -47,11 +47,18 @@ export class UsersController { throw new HttpException('Organization not found', 401); } + const isEmailNotification = await this._userService.getEmailNotifications( + user.id + ); + return { ...user, + emailNotifications: isEmailNotification.emailNotifications, orgId: organization.id, // @ts-ignore - totalChannels: organization?.subscription?.totalChannels || pricing.FREE.channel, + totalChannels: + // @ts-ignore + organization?.subscription?.totalChannels || pricing.FREE.channel, // @ts-ignore tier: organization?.subscription?.subscriptionTier || 'FREE', // @ts-ignore diff --git a/apps/frontend/src/components/settings/settings.component.tsx b/apps/frontend/src/components/settings/settings.component.tsx index 6b1c542b..6e5e2893 100644 --- a/apps/frontend/src/components/settings/settings.component.tsx +++ b/apps/frontend/src/components/settings/settings.component.tsx @@ -1,20 +1,24 @@ 'use client'; import { GithubComponent } from '@gitroom/frontend/components/settings/github.component'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { TeamsComponent } from '@gitroom/frontend/components/settings/teams.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import useSWR from 'swr'; +import { useSWRConfig } from 'swr'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import { useRouter } from 'next/navigation'; import { isGeneral } from '@gitroom/react/helpers/is.general'; +import { Checkbox } from '@gitroom/react/form/checkbox'; const general = isGeneral(); export const SettingsComponent = () => { const user = useUser(); const router = useRouter(); + const { mutate } = useSWRConfig(); + const [isChecked, setIsChecked] = useState(false); const fetch = useFetch(); @@ -32,6 +36,34 @@ export const SettingsComponent = () => { return { github, organizations }; }, []); + const toggleEmailNotification = useCallback(async () => { + if (!user || Object.keys(user).length === 0) return; + + const newSetting = !isChecked; + setIsChecked(newSetting); + + await fetch(`/settings/email-notifications/${user.id}`, { + method: 'POST', + body: JSON.stringify({ emailNotifications: newSetting }), + }); + + mutate( + '/user/self', + { + ...user, + emailNotifications: newSetting, + }, + { + revalidate: false, + } + ); + }, [user, fetch, mutate, isChecked, setIsChecked]); + + useEffect(() => { + if (!user || Object.keys(user).length === 0) return; + setIsChecked(user.emailNotifications); + }, [user, setIsChecked]); + const { isLoading: isLoadingSettings, data: loadAll } = useSWR( 'load-all', load @@ -63,12 +95,17 @@ export const SettingsComponent = () => { github={loadAll.github} organizations={loadAll.organizations} /> - {/*
*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
Show news with everybody in Gitroom
*/} - {/*
*/} +
+
+ +
+
Show news with everybody in Gitroom
+
)} {!!user?.tier?.team_members && } diff --git a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts index bbe8d5b7..e699e5ce 100644 --- a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts @@ -25,7 +25,12 @@ export class NotificationService { ); } - async inAppNotification(orgId: string, subject: string, message: string, sendEmail = false) { + async inAppNotification( + orgId: string, + subject: string, + message: string, + sendEmail = false + ) { await this._notificationRepository.createNotification(orgId, message); if (!sendEmail) { return; @@ -33,7 +38,10 @@ export class NotificationService { const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId); for (const user of userOrg?.users || []) { - await this.sendEmail(user.user.email, subject, message); + if (user.user.emailNotifications) { + // Check if user wants email notifications + await this.sendEmail(user.user.email, subject, message); + } } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index a3d208a2..eae4b991 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -242,6 +242,7 @@ export class OrganizationRepository { select: { email: true, id: true, + emailNotifications: true, }, }, }, diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 18ca29d2..d5fdfa8f 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -11,54 +11,55 @@ datasource db { } model Organization { - id String @id @default(uuid()) - name String - description String? - users UserOrganization[] - media Media[] - paymentId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - github GitHub[] - subscription Subscription? - Integration Integration[] - post Post[] @relation("organization") - submittedPost Post[] @relation("submittedForOrg") - Comments Comments[] - notifications Notifications[] + id String @id @default(uuid()) + name String + description String? + users UserOrganization[] + media Media[] + paymentId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + github GitHub[] + subscription Subscription? + Integration Integration[] + post Post[] @relation("organization") + submittedPost Post[] @relation("submittedForOrg") + Comments Comments[] + notifications Notifications[] buyerOrganization MessagesGroup[] } model User { - id String @id @default(uuid()) - email String - password String? - providerName Provider - name String? - lastName String? - isSuperAdmin Boolean @default(false) - bio String? - audience Int @default(0) - pictureId String? - picture Media? @relation(fields: [pictureId], references: [id]) - providerId String? - organizations UserOrganization[] - timezone Int - comments Comments[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastReadNotifications DateTime @default(now()) - inviteId String? - items ItemUser[] - marketplace Boolean @default(true) - account String? - connectedAccount Boolean @default(false) - groupBuyer MessagesGroup[] @relation("groupBuyer") - groupSeller MessagesGroup[] @relation("groupSeller") - orderBuyer Orders[] @relation("orderBuyer") - orderSeller Orders[] @relation("orderSeller") - payoutProblems PayoutProblems[] - lastOnline DateTime @default(now()) + id String @id @default(uuid()) + email String + password String? + providerName Provider + name String? + lastName String? + isSuperAdmin Boolean @default(false) + bio String? + audience Int @default(0) + pictureId String? + picture Media? @relation(fields: [pictureId], references: [id]) + providerId String? + organizations UserOrganization[] + timezone Int + comments Comments[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastReadNotifications DateTime @default(now()) + inviteId String? + items ItemUser[] + marketplace Boolean @default(true) + account String? + connectedAccount Boolean @default(false) + groupBuyer MessagesGroup[] @relation("groupBuyer") + groupSeller MessagesGroup[] @relation("groupSeller") + orderBuyer Orders[] @relation("orderBuyer") + orderSeller Orders[] @relation("orderSeller") + payoutProblems PayoutProblems[] + lastOnline DateTime @default(now()) + emailNotifications Boolean @default(false) @@unique([email, providerName]) @@index([lastReadNotifications]) @@ -121,20 +122,20 @@ model Trending { } model TrendingLog { - id String @id @default(uuid()) - language String? - date DateTime + id String @id @default(uuid()) + language String? + date DateTime } model ItemUser { - id String @id @default(uuid()) - user User @relation(fields: [userId], references: [id]) - userId String - key String + id String @id @default(uuid()) + user User @relation(fields: [userId], references: [id]) + userId String + key String + @@unique([userId, key]) @@index([userId]) @@index([key]) - @@unique([userId, key]) } model Star { @@ -165,62 +166,62 @@ model Media { } model Subscription { - id String @id @default(cuid()) - organizationId String @unique - organization Organization @relation(fields: [organizationId], references: [id]) - subscriptionTier SubscriptionTier - identifier String? - cancelAt DateTime? - period Period - totalChannels Int - isLifetime Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(cuid()) + organizationId String @unique + organization Organization @relation(fields: [organizationId], references: [id]) + subscriptionTier SubscriptionTier + identifier String? + cancelAt DateTime? + period Period + totalChannels Int + isLifetime Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([organizationId]) @@index([deletedAt]) } model Integration { - id String @id @default(cuid()) - internalId String - organizationId String - name String - organization Organization @relation(fields: [organizationId], references: [id]) - picture String? + id String @id @default(cuid()) + internalId String + organizationId String + name String + organization Organization @relation(fields: [organizationId], references: [id]) + picture String? providerIdentifier String - type String - token String - disabled Boolean @default(false) - tokenExpiration DateTime? - refreshToken String? - posts Post[] - profile String? - deletedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime? @updatedAt - orderItems OrderItems[] + type String + token String + disabled Boolean @default(false) + tokenExpiration DateTime? + refreshToken String? + posts Post[] + profile String? + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime? @updatedAt + orderItems OrderItems[] + @@unique([organizationId, internalId]) @@index([updatedAt]) @@index([deletedAt]) - @@unique([organizationId, internalId]) } model Comments { - id String @id @default(uuid()) - content String - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - userId String - user User @relation(fields: [userId], references: [id]) - date DateTime - parentCommentId String? - parentComment Comments? @relation("parentCommentId", fields: [parentCommentId], references: [id]) - childrenComment Comments[] @relation("parentCommentId") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(uuid()) + content String + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + date DateTime + parentCommentId String? + parentComment Comments? @relation("parentCommentId", fields: [parentCommentId], references: [id]) + childrenComment Comments[] @relation("parentCommentId") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([createdAt]) @@index([organizationId]) @@ -230,35 +231,35 @@ model Comments { } model Post { - id String @id @default(cuid()) - state State @default(QUEUE) - publishDate DateTime - organizationId String - integrationId String - content String - group String - organization Organization @relation("organization", fields: [organizationId], references: [id]) - integration Integration @relation(fields: [integrationId], references: [id]) - title String? - description String? - parentPostId String? - releaseId String? - releaseURL String? - settings String? - parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id]) - childrenPost Post[] @relation("parentPostId") - image String? - submittedForOrderId String? - submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id]) + id String @id @default(cuid()) + state State @default(QUEUE) + publishDate DateTime + organizationId String + integrationId String + content String + group String + organization Organization @relation("organization", fields: [organizationId], references: [id]) + integration Integration @relation(fields: [integrationId], references: [id]) + title String? + description String? + parentPostId String? + releaseId String? + releaseURL String? + settings String? + parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id]) + childrenPost Post[] @relation("parentPostId") + image String? + submittedForOrderId String? + submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id]) submittedForOrganizationId String? - submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id]) - approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO) - lastMessageId String? - lastMessage Messages? @relation(fields: [lastMessageId], references: [id]) - payoutProblems PayoutProblems[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id]) + approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO) + lastMessageId String? + lastMessage Messages? @relation(fields: [lastMessageId], references: [id]) + payoutProblems PayoutProblems[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([group]) @@index([deletedAt]) @@ -276,14 +277,14 @@ model Post { } model Notifications { - id String @id @default(uuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - content String - link String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(uuid()) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + content String + link String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([createdAt]) @@index([organizationId]) @@ -291,17 +292,17 @@ model Notifications { } model MessagesGroup { - id String @id @default(uuid()) + id String @id @default(uuid()) buyerOrganizationId String buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id]) buyerId String - buyer User @relation("groupBuyer", fields: [buyerId], references: [id]) + buyer User @relation("groupBuyer", fields: [buyerId], references: [id]) sellerId String - seller User @relation("groupSeller", fields: [sellerId], references: [id]) + seller User @relation("groupSeller", fields: [sellerId], references: [id]) messages Messages[] orders Orders[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([buyerId, sellerId]) @@index([createdAt]) @@ -310,34 +311,34 @@ model MessagesGroup { } model PayoutProblems { - id String @id @default(uuid()) - status String - orderId String - order Orders @relation(fields: [orderId], references: [id]) - userId String - user User @relation(fields: [userId], references: [id]) - postId String? - post Post? @relation(fields: [postId], references: [id]) - amount Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + status String + orderId String + order Orders @relation(fields: [orderId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + postId String? + post Post? @relation(fields: [postId], references: [id]) + amount Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Orders { - id String @id @default(uuid()) - buyerId String - sellerId String - posts Post[] - buyer User @relation("orderBuyer", fields: [buyerId], references: [id]) - seller User @relation("orderSeller", fields: [sellerId], references: [id]) - status OrderStatus - ordersItems OrderItems[] - messageGroupId String - messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id]) - captureId String? - payoutProblems PayoutProblems[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + buyerId String + sellerId String + posts Post[] + buyer User @relation("orderBuyer", fields: [buyerId], references: [id]) + seller User @relation("orderSeller", fields: [sellerId], references: [id]) + status OrderStatus + ordersItems OrderItems[] + messageGroupId String + messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id]) + captureId String? + payoutProblems PayoutProblems[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([buyerId]) @@index([sellerId]) @@ -347,29 +348,29 @@ model Orders { } model OrderItems { - id String @id @default(uuid()) - orderId String - order Orders @relation(fields: [orderId], references: [id]) - integrationId String - integration Integration @relation(fields: [integrationId], references: [id]) - quantity Int - price Int + id String @id @default(uuid()) + orderId String + order Orders @relation(fields: [orderId], references: [id]) + integrationId String + integration Integration @relation(fields: [integrationId], references: [id]) + quantity Int + price Int @@index([orderId]) @@index([integrationId]) } model Messages { - id String @id @default(uuid()) - from From - content String? - groupId String - group MessagesGroup @relation(fields: [groupId], references: [id]) - special String? - posts Post[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(uuid()) + from From + content String? + groupId String + group MessagesGroup @relation(fields: [groupId], references: [id]) + special String? + posts Post[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @@index([groupId]) @@index([createdAt]) @@ -420,4 +421,4 @@ enum APPROVED_SUBMIT_FOR_ORDER { NO WAITING_CONFIRMATION YES -} \ No newline at end of file +} diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts index 0c2d297c..4a391a19 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts @@ -227,4 +227,26 @@ export class UsersRepository { count, }; } + + async getEmailNotifications(userId: string) { + return await this._user.model.user.findFirst({ + where: { + id: userId, + }, + select: { + emailNotifications: true, + }, + }); + } + + updateEmailNotifications(userId: string, emailNotifications: boolean) { + return this._user.model.user.update({ + where: { + id: userId, + }, + data: { + emailNotifications, + }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts index 097582c2..337080b7 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts @@ -51,4 +51,15 @@ export class UsersService { changePersonal(userId: string, body: UserDetailDto) { return this._usersRepository.changePersonal(userId, body); } + + getEmailNotifications(userId: string) { + return this._usersRepository.getEmailNotifications(userId); + } + + updateEmailNotifications(userId: string, emailNotifications: boolean) { + return this._usersRepository.updateEmailNotifications( + userId, + emailNotifications + ); + } }