diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 3d9dd968..88e80263 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -22,6 +22,7 @@ import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions import { ApiTags } from '@nestjs/swagger'; import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; +import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto'; import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter'; import { RealIP } from 'nestjs-real-ip'; import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent'; @@ -125,6 +126,19 @@ export class UsersController { return this._userService.changePersonal(user.id, body); } + @Get('/email-notifications') + async getEmailNotifications(@GetUserFromRequest() user: User) { + return this._userService.getEmailNotifications(user.id); + } + + @Post('/email-notifications') + async updateEmailNotifications( + @GetUserFromRequest() user: User, + @Body() body: EmailNotificationsDto + ) { + return this._userService.updateEmailNotifications(user.id, body); + } + @Get('/subscription') @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) async getSubscription(@GetOrgFromRequest() organization: Organization) { diff --git a/apps/frontend/src/components/settings/email-notifications.component.tsx b/apps/frontend/src/components/settings/email-notifications.component.tsx new file mode 100644 index 00000000..e780ae4a --- /dev/null +++ b/apps/frontend/src/components/settings/email-notifications.component.tsx @@ -0,0 +1,146 @@ +'use client'; + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { Slider } from '@gitroom/react/form/slider'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; + +interface EmailNotifications { + sendSuccessEmails: boolean; + sendFailureEmails: boolean; +} + +export const useEmailNotifications = () => { + const fetch = useFetch(); + + const load = useCallback(async () => { + return (await fetch('/user/email-notifications')).json(); + }, []); + + return useSWR('email-notifications', load, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: true, + refreshWhenHidden: false, + refreshWhenOffline: false, + }); +}; + +const EmailNotificationsComponent = () => { + const t = useT(); + const fetch = useFetch(); + const toaster = useToaster(); + const { data, isLoading } = useEmailNotifications(); + + const [localSettings, setLocalSettings] = useState({ + sendSuccessEmails: true, + sendFailureEmails: true, + }); + + // Keep a ref to always have the latest state + const settingsRef = useRef(localSettings); + settingsRef.current = localSettings; + + // Sync local state with fetched data + useEffect(() => { + if (data) { + setLocalSettings(data); + } + }, [data]); + + const updateSetting = useCallback( + async (key: keyof EmailNotifications, value: boolean) => { + // Use ref to get the latest state + const currentSettings = settingsRef.current; + const newData = { + ...currentSettings, + [key]: value, + }; + + // Update local state immediately + setLocalSettings(newData); + + await fetch('/user/email-notifications', { + method: 'POST', + body: JSON.stringify(newData), + }); + + toaster.show(t('settings_updated', 'Settings updated'), 'success'); + }, + [] + ); + + const handleSuccessEmailsChange = useCallback( + (value: 'on' | 'off') => { + updateSetting('sendSuccessEmails', value === 'on'); + }, + [updateSetting] + ); + + const handleFailureEmailsChange = useCallback( + (value: 'on' | 'off') => { + updateSetting('sendFailureEmails', value === 'on'); + }, + [updateSetting] + ); + + if (isLoading) { + return ( +
+
+ {t('loading', 'Loading...')} +
+
+ ); + } + + return ( +
+
+ {t('email_notifications', 'Email Notifications')} +
+
+
+
+ {t('success_emails', 'Success Emails')} +
+
+ {t( + 'success_emails_description', + 'Receive email notifications when posts are published successfully' + )} +
+
+ +
+
+
+
+ {t('failure_emails', 'Failure Emails')} +
+
+ {t( + 'failure_emails_description', + 'Receive email notifications when posts fail to publish' + )} +
+
+ +
+
+ ); +}; + +export default EmailNotificationsComponent; + diff --git a/apps/frontend/src/components/settings/global.settings.tsx b/apps/frontend/src/components/settings/global.settings.tsx index 449e38f8..970a960a 100644 --- a/apps/frontend/src/components/settings/global.settings.tsx +++ b/apps/frontend/src/components/settings/global.settings.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import dynamic from 'next/dynamic'; +import EmailNotificationsComponent from '@gitroom/frontend/components/settings/email-notifications.component'; const MetricComponent = dynamic( () => import('@gitroom/frontend/components/settings/metric.component'), @@ -10,12 +11,14 @@ const MetricComponent = dynamic( ssr: false, } ); + export const GlobalSettings = () => { const t = useT(); return (

{t('global_settings', 'Global Settings')}

+
); }; diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 0ce96727..5ab3c3c4 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -187,7 +187,9 @@ export class IntegrationService { orgId, `Could not refresh your ${integration.providerIdentifier} channel ${err}`, `Could not refresh your ${integration.providerIdentifier} channel ${err}. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`, - true + true, + false, + 'info' ); } 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 c100e832..70dce81b 100644 --- a/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/notifications/notification.service.ts @@ -6,6 +6,8 @@ import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/cl import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; import dayjs from 'dayjs'; +export type NotificationType = 'success' | 'fail' | 'info'; + @Injectable() export class NotificationService { constructor( @@ -41,7 +43,8 @@ export class NotificationService { subject: string, message: string, sendEmail = false, - digest = false + digest = false, + type: NotificationType = 'success' ) { const date = new Date().toISOString(); await this._notificationRepository.createNotification(orgId, message); @@ -52,6 +55,12 @@ export class NotificationService { if (digest) { await ioRedis.watch('digest_' + orgId); const value = await ioRedis.get('digest_' + orgId); + + // Track notification types in the digest + const typesKey = 'digest_types_' + orgId; + await ioRedis.sadd(typesKey, type); + await ioRedis.expire(typesKey, 120); // Slightly longer than digest window + if (value) { return; } @@ -77,12 +86,66 @@ export class NotificationService { return; } - await this.sendEmailsToOrg(orgId, subject, message); + await this.sendEmailsToOrg(orgId, subject, message, type); } - async sendEmailsToOrg(orgId: string, subject: string, message: string) { + async sendEmailsToOrg( + orgId: string, + subject: string, + message: string, + type?: NotificationType + ) { const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId); for (const user of userOrg?.users || []) { + // 'info' type is always sent regardless of preferences + if (type !== 'info') { + // Filter users based on their email preferences + if (type === 'success' && !user.user.sendSuccessEmails) { + continue; + } + if (type === 'fail' && !user.user.sendFailureEmails) { + continue; + } + } + await this.sendEmail(user.user.email, subject, message); + } + } + + async getDigestTypes(orgId: string): Promise { + const typesKey = 'digest_types_' + orgId; + const types = await ioRedis.smembers(typesKey); + // Clean up the types key after reading + await ioRedis.del(typesKey); + return types as NotificationType[]; + } + + async sendDigestEmailsToOrg( + orgId: string, + subject: string, + message: string, + types: NotificationType[] + ) { + const userOrg = await this._organizationRepository.getAllUsersOrgs(orgId); + const hasInfo = types.includes('info'); + const hasSuccess = types.includes('success'); + const hasFail = types.includes('fail'); + + for (const user of userOrg?.users || []) { + // 'info' type is always sent regardless of preferences + if (hasInfo) { + await this.sendEmail(user.user.email, subject, message); + continue; + } + + // For digest, check if user wants any of the notification types in the digest + const wantsSuccess = hasSuccess && user.user.sendSuccessEmails; + const wantsFail = hasFail && user.user.sendFailureEmails; + + // Only send if user wants at least one type of notification in the digest + if (!wantsSuccess && !wantsFail) { + continue; + } + 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 919c7b7a..6a574c78 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -293,6 +293,8 @@ export class OrganizationRepository { select: { email: true, id: true, + sendSuccessEmails: true, + sendFailureEmails: true, }, }, }, diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index d20b7faa..ee82d18a 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -304,7 +304,9 @@ export class PostsService { firstPost.organizationId, `We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`, `We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name} because you need to reconnect it. Please enable it and try again.`, - true + true, + false, + 'info' ); return; } @@ -314,7 +316,9 @@ export class PostsService { firstPost.organizationId, `We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`, `We couldn't post to ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name} because it's disabled. Please enable it and try again.`, - true + true, + false, + 'info' ); return; } @@ -343,7 +347,9 @@ export class PostsService { firstPost.organizationId, `Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`, `An error occurred while posting on ${firstPost.integration?.providerIdentifier}`, - true + true, + false, + 'fail' ); return; @@ -362,7 +368,9 @@ export class PostsService { `An error occurred while posting on ${ firstPost.integration?.providerIdentifier }${err?.message ? `: ${err?.message}` : ``}`, - true + true, + false, + 'fail' ); console.error( @@ -959,15 +967,20 @@ export class PostsService { return; } + // Get the types of notifications in this digest + const types = await this._notificationService.getDigestTypes(orgId); + const message = getNotificationsForOrgSince .map((p) => p.content) .join('
'); - await this._notificationService.sendEmailsToOrg( + + await this._notificationService.sendDigestEmailsToOrg( orgId, getNotificationsForOrgSince.length === 1 ? subject : '[Postiz] Your latest notifications', - message + message, + types.length > 0 ? types : ['success'] // Default to success if no types tracked ); } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 333e6e7d..e0a2c836 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -1,6 +1,3 @@ -// 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" runtime = "nodejs" @@ -16,32 +13,32 @@ model Organization { name String description String? apiKey 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") allowTrial Boolean @default(false) isTrailing Boolean @default(false) - Comments Comments[] - notifications Notifications[] - buyerOrganization MessagesGroup[] - usedCodes UsedCodes[] - credits Credits[] - plugs Plugs[] - customers Customer[] - webhooks Webhooks[] - tags Tags[] - signatures Signatures[] autoPost AutoPost[] - sets Sets[] - thirdParty ThirdParty[] + Comments Comments[] + credits Credits[] + customers Customer[] errors Errors[] + github GitHub[] + Integration Integration[] + media Media[] + buyerOrganization MessagesGroup[] + notifications Notifications[] + plugs Plugs[] + post Post[] @relation("organization") + submittedPost Post[] @relation("submittedForOrg") + sets Sets[] + signatures Signatures[] + subscription Subscription? + tags Tags[] + thirdParty ThirdParty[] + usedCodes UsedCodes[] + users UserOrganization[] + webhooks Webhooks[] } model Tags { @@ -49,11 +46,11 @@ model Tags { name String color String orgId String - organization Organization @relation(fields: [orgId], references: [id]) - posts TagsPosts[] deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [orgId], references: [id]) + posts TagsPosts[] @@index([orgId]) @@index([deletedAt]) @@ -61,50 +58,52 @@ model Tags { model TagsPosts { postId String - post Post @relation(fields: [postId], references: [id]) tagId String - tag Tags @relation(fields: [tagId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + post Post @relation(fields: [postId], references: [id]) + tag Tags @relation(fields: [tagId], references: [id]) @@id([postId, tagId]) @@unique([postId, tagId]) } model User { - id String @id @default(uuid()) + id String @id @default(uuid()) email String password String? providerName Provider name String? lastName String? - isSuperAdmin Boolean @default(false) + isSuperAdmin Boolean @default(false) bio String? - audience Int @default(0) + 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()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastReadNotifications DateTime @default(now()) inviteId String? - activated Boolean @default(true) - items ItemUser[] - marketplace Boolean @default(true) + activated Boolean @default(true) + 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()) - agencies SocialMediaAgency[] + connectedAccount Boolean @default(false) + lastOnline DateTime @default(now()) ip String? agent String? + comments Comments[] + items ItemUser[] + groupBuyer MessagesGroup[] @relation("groupBuyer") + groupSeller MessagesGroup[] @relation("groupSeller") + orderBuyer Orders[] @relation("orderBuyer") + orderSeller Orders[] @relation("orderSeller") + payoutProblems PayoutProblems[] + agencies SocialMediaAgency? + picture Media? @relation(fields: [pictureId], references: [id]) + organizations UserOrganization[] + sendSuccessEmails Boolean @default(true) + sendFailureEmails Boolean @default(true) @@unique([email, providerName]) @@index([lastReadNotifications]) @@ -118,23 +117,23 @@ model UsedCodes { id String @id @default(uuid()) code String orgId String - organization Organization @relation(fields: [orgId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [orgId], references: [id]) @@index([code]) } model UserOrganization { id String @id @default(uuid()) - user User @relation(fields: [userId], references: [id]) userId String - organization Organization @relation(fields: [organizationId], references: [id]) organizationId String disabled Boolean @default(false) role Role @default(USER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) + user User @relation(fields: [userId], references: [id]) @@unique([userId, organizationId]) @@index([disabled]) @@ -146,10 +145,10 @@ model GitHub { name String? token String jobId String? - organization Organization @relation(fields: [organizationId], references: [id]) organizationId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) @@index([login]) @@index([organizationId]) @@ -158,13 +157,12 @@ model GitHub { model Trending { id String @id @default(uuid()) trendingList String - language String? + language String? @unique hash String date DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@unique([language]) @@index([hash]) } @@ -176,9 +174,9 @@ model TrendingLog { model ItemUser { id String @id @default(uuid()) - user User @relation(fields: [userId], references: [id]) userId String key String + user User @relation(fields: [userId], references: [id]) @@unique([userId, key]) @@index([userId]) @@ -203,18 +201,18 @@ model Media { id String @id @default(uuid()) name String path String - organization Organization @relation(fields: [organizationId], references: [id]) organizationId String - thumbnail String? - thumbnailTimestamp Int? - alt String? - fileSize Int @default(0) - type String @default("image") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - userPicture User[] - agencies SocialMediaAgency[] deletedAt DateTime? + fileSize Int @default(0) + type String @default("image") + thumbnail String? + alt String? + thumbnailTimestamp Int? + organization Organization @relation(fields: [organizationId], references: [id]) + agencies SocialMediaAgency[] + userPicture User[] @@index([name]) @@index([organizationId]) @@ -222,15 +220,12 @@ model Media { } model SocialMediaAgency { - id String @id @default(uuid()) - user User @relation(fields: [userId], references: [id]) - userId String @unique() - name String - logoId String? - logo Media? @relation(fields: [logoId], references: [id]) - website String? - slug String? - + id String @id @default(uuid()) + userId String @unique + name String + logoId String? + website String? + slug String? facebook String? instagram String? twitter String? @@ -238,15 +233,15 @@ model SocialMediaAgency { youtube String? tiktok String? otherSocialMedia String? - shortDescription String description String - niches SocialMediaAgencyNiche[] approved Boolean @default(false) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + logo Media? @relation(fields: [logoId], references: [id]) + user User @relation(fields: [userId], references: [id]) + niches SocialMediaAgencyNiche[] @@index([userId]) @@index([deletedAt]) @@ -255,20 +250,20 @@ model SocialMediaAgency { model SocialMediaAgencyNiche { agencyId String - agency SocialMediaAgency @relation(fields: [agencyId], references: [id]) niche String + agency SocialMediaAgency @relation(fields: [agencyId], references: [id]) @@id([agencyId, niche]) } model Credits { id String @id @default(uuid()) - organization Organization @relation(fields: [organizationId], references: [id]) organizationId String credits Int - type String @default("ai_images") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + type String @default("ai_images") + organization Organization @relation(fields: [organizationId], references: [id]) @@index([organizationId]) @@index([createdAt]) @@ -277,7 +272,6 @@ model Credits { model Subscription { id String @id @default(cuid()) organizationId String @unique - organization Organization @relation(fields: [organizationId], references: [id]) subscriptionTier SubscriptionTier identifier String? cancelAt DateTime? @@ -287,6 +281,7 @@ model Subscription { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + organization Organization @relation(fields: [organizationId], references: [id]) @@index([organizationId]) @@index([deletedAt]) @@ -296,11 +291,11 @@ model Customer { id String @id @default(uuid()) name String orgId String - organization Organization @relation(fields: [orgId], references: [id]) - integrations Integration[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + organization Organization @relation(fields: [orgId], references: [id]) + integrations Integration[] @@unique([orgId, name, deletedAt]) } @@ -310,7 +305,6 @@ model Integration { internalId String organizationId String name String - organization Organization @relation(fields: [organizationId], references: [id]) picture String? providerIdentifier String type String @@ -318,23 +312,24 @@ model Integration { disabled Boolean @default(false) tokenExpiration DateTime? refreshToken String? - posts Post[] profile String? deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime? @updatedAt - orderItems OrderItems[] inBetweenSteps Boolean @default(false) refreshNeeded Boolean @default(false) postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]") customInstanceDetails String? customerId String? - customer Customer? @relation(fields: [customerId], references: [id]) - plugs Plugs[] - exisingPlugData ExisingPlugData[] rootInternalId String? additionalSettings String? @default("[]") + exisingPlugData ExisingPlugData[] + customer Customer? @relation(fields: [customerId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id]) webhooks IntegrationsWebhooks[] + orderItems OrderItems[] + plugs Plugs[] + posts Post[] @@unique([organizationId, internalId]) @@index([rootInternalId]) @@ -352,12 +347,12 @@ model Integration { model Signatures { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) content String autoAdd Boolean createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + organization Organization @relation(fields: [organizationId], references: [id]) @@index([createdAt]) @@index([organizationId]) @@ -368,14 +363,14 @@ model Comments { id String @id @default(uuid()) content String organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) postId String - post Post @relation(fields: [postId], references: [id]) userId String - user User @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + organization Organization @relation(fields: [organizationId], references: [id]) + post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) @@index([createdAt]) @@index([organizationId]) @@ -392,33 +387,33 @@ model Post { 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]) intervalInDays Int? - payoutProblems PayoutProblems[] - comments Comments[] - tags TagsPosts[] - errors Errors[] error String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + comments Comments[] + errors Errors[] + payoutProblems PayoutProblems[] + integration Integration @relation(fields: [integrationId], references: [id]) + lastMessage Messages? @relation(fields: [lastMessageId], references: [id]) + organization Organization @relation("organization", fields: [organizationId], references: [id]) + parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id]) + childrenPost Post[] @relation("parentPostId") + submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id]) + submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id]) + tags TagsPosts[] @@index([group]) @@index([deletedAt]) @@ -439,12 +434,12 @@ 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? + organization Organization @relation(fields: [organizationId], references: [id]) @@index([createdAt]) @@index([organizationId]) @@ -454,15 +449,15 @@ model Notifications { model MessagesGroup { id String @id @default(uuid()) buyerOrganizationId String - buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id]) buyerId String - buyer User @relation("groupBuyer", fields: [buyerId], references: [id]) sellerId String - seller User @relation("groupSeller", fields: [sellerId], references: [id]) - messages Messages[] - orders Orders[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + messages Messages[] + buyer User @relation("groupBuyer", fields: [buyerId], references: [id]) + buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id]) + seller User @relation("groupSeller", fields: [sellerId], references: [id]) + orders Orders[] @@unique([buyerId, sellerId]) @@index([createdAt]) @@ -474,31 +469,31 @@ 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 + order Orders @relation(fields: [orderId], references: [id]) + post Post? @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) } 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 + ordersItems OrderItems[] + buyer User @relation("orderBuyer", fields: [buyerId], references: [id]) + messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id]) + seller User @relation("orderSeller", fields: [sellerId], references: [id]) + payoutProblems PayoutProblems[] + posts Post[] @@index([buyerId]) @@index([sellerId]) @@ -510,11 +505,11 @@ 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 + integration Integration @relation(fields: [integrationId], references: [id]) + order Orders @relation(fields: [orderId], references: [id]) @@index([orderId]) @@index([integrationId]) @@ -525,12 +520,12 @@ model Messages { 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? + group MessagesGroup @relation(fields: [groupId], references: [id]) + posts Post[] @@index([groupId]) @@index([createdAt]) @@ -540,12 +535,12 @@ model Messages { model Plugs { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) plugFunction String data String integrationId String - integration Integration @relation(fields: [integrationId], references: [id]) activated Boolean @default(true) + integration Integration @relation(fields: [integrationId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id]) @@unique([plugFunction, integrationId]) @@index([organizationId]) @@ -554,9 +549,9 @@ model Plugs { model ExisingPlugData { id String @id @default(uuid()) integrationId String - integration Integration @relation(fields: [integrationId], references: [id]) methodName String value String + integration Integration @relation(fields: [integrationId], references: [id]) @@unique([integrationId, methodName, value]) } @@ -573,8 +568,8 @@ model PopularPosts { model IntegrationsWebhooks { integrationId String - integration Integration @relation(fields: [integrationId], references: [id]) webhookId String + integration Integration @relation(fields: [integrationId], references: [id]) webhook Webhooks @relation(fields: [webhookId], references: [id]) @@id([integrationId, webhookId]) @@ -587,12 +582,12 @@ model Webhooks { id String @id @default(uuid()) name String organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - integrations IntegrationsWebhooks[] url String deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + integrations IntegrationsWebhooks[] + organization Organization @relation(fields: [organizationId], references: [id]) @@index([organizationId]) @@index([deletedAt]) @@ -601,7 +596,6 @@ model Webhooks { model AutoPost { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) title String content String? onSlot Boolean @@ -615,6 +609,7 @@ model AutoPost { deletedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) @@index([deletedAt]) } @@ -622,11 +617,11 @@ model AutoPost { model Sets { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) name String content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id]) @@index([organizationId]) } @@ -634,7 +629,6 @@ model Sets { model ThirdParty { id String @id @default(uuid()) organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) identifier String name String internalId String @@ -642,6 +636,7 @@ model ThirdParty { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + organization Organization @relation(fields: [organizationId], references: [id]) @@unique([organizationId, internalId]) @@index([organizationId]) @@ -651,14 +646,14 @@ model ThirdParty { model Errors { id String @id @default(uuid()) message String - body String @default("{}") platform String organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - postId String - post Post @relation(fields: [postId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + postId String + body String @default("{}") + organization Organization @relation(fields: [organizationId], references: [id]) + post Post @relation(fields: [postId], references: [id]) @@index([organizationId]) @@index([createdAt]) @@ -668,14 +663,171 @@ model Mentions { name String username String platform String - image String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + image String @@id([name, username, platform, image]) @@index([createdAt]) } +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model mastra_ai_spans { + traceId String + spanId String + parentSpanId String? + name String + scope Json? + spanType String + attributes Json? + metadata Json? + links Json? + input Json? + output Json? + error Json? + startedAt DateTime @db.Timestamp(6) + endedAt DateTime? @db.Timestamp(6) + createdAt DateTime @db.Timestamp(6) + updatedAt DateTime? @db.Timestamp(6) + isEvent Boolean + startedAtZ DateTime? @default(now()) @db.Timestamptz(6) + endedAtZ DateTime? @default(now()) @db.Timestamptz(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + updatedAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@index([name], map: "public_mastra_ai_spans_name_idx") + @@index([parentSpanId, startedAt(sort: Desc)], map: "public_mastra_ai_spans_parentspanid_startedat_idx") + @@index([spanType, startedAt(sort: Desc)], map: "public_mastra_ai_spans_spantype_startedat_idx") + @@index([traceId, startedAt(sort: Desc)], map: "public_mastra_ai_spans_traceid_startedat_idx") + @@ignore +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model mastra_evals { + input String + output String + result Json + agent_name String + metric_name String + instructions String + test_info Json? + global_run_id String + run_id String + created_at DateTime @db.Timestamp(6) + createdAt DateTime? @db.Timestamp(6) + created_atZ DateTime? @default(now()) @db.Timestamptz(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@index([agent_name, created_at(sort: Desc)], map: "public_mastra_evals_agent_name_created_at_idx") + @@ignore +} + +model mastra_messages { + id String @id + thread_id String + content String + role String + type String + createdAt DateTime @db.Timestamp(6) + resourceId String? + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@index([thread_id, createdAt(sort: Desc)], map: "public_mastra_messages_thread_id_createdat_idx") +} + +model mastra_resources { + id String @id + workingMemory String? + metadata Json? + createdAt DateTime @db.Timestamp(6) + updatedAt DateTime @db.Timestamp(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + updatedAtZ DateTime? @default(now()) @db.Timestamptz(6) +} + +model mastra_scorers { + id String @id + scorerId String + traceId String? + runId String + scorer Json + preprocessStepResult Json? + extractStepResult Json? + analyzeStepResult Json? + score Float + reason String? + metadata Json? + preprocessPrompt String? + extractPrompt String? + generateScorePrompt String? + generateReasonPrompt String? + analyzePrompt String? + reasonPrompt String? + input Json + output Json + additionalContext Json? + runtimeContext Json? + entityType String? + entity Json? + entityId String? + source String + resourceId String? + threadId String? + createdAt DateTime @db.Timestamp(6) + updatedAt DateTime @db.Timestamp(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + updatedAtZ DateTime? @default(now()) @db.Timestamptz(6) + spanId String? + + @@index([traceId, spanId, createdAt(sort: Desc)], map: "public_mastra_scores_trace_id_span_id_created_at_idx") +} + +model mastra_threads { + id String @id + resourceId String + title String + metadata String? + createdAt DateTime @db.Timestamp(6) + updatedAt DateTime @db.Timestamp(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + updatedAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@index([resourceId, createdAt(sort: Desc)], map: "public_mastra_threads_resourceid_createdat_idx") +} + +model mastra_traces { + id String @id + parentSpanId String? + name String + traceId String + scope String + kind Int + attributes Json? + status Json? + events Json? + links Json? + other String? + startTime BigInt + endTime BigInt + createdAt DateTime @db.Timestamp(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@index([name, startTime(sort: Desc)], map: "public_mastra_traces_name_starttime_idx") +} + +model mastra_workflow_snapshot { + workflow_name String + run_id String + resourceId String? + snapshot String + createdAt DateTime @db.Timestamp(6) + updatedAt DateTime @db.Timestamp(6) + createdAtZ DateTime? @default(now()) @db.Timestamptz(6) + updatedAtZ DateTime? @default(now()) @db.Timestamptz(6) + + @@unique([workflow_name, run_id], map: "public_mastra_workflow_snapshot_workflow_name_run_id_key") +} + enum OrderStatus { PENDING ACCEPTED 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 2df55338..1c4e6c50 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts @@ -5,6 +5,7 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; +import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto'; @Injectable() export class UsersRepository { @@ -161,6 +162,30 @@ export class UsersRepository { }); } + async getEmailNotifications(userId: string) { + return this._user.model.user.findUnique({ + where: { + id: userId, + }, + select: { + sendSuccessEmails: true, + sendFailureEmails: true, + }, + }); + } + + async updateEmailNotifications(userId: string, body: EmailNotificationsDto) { + await this._user.model.user.update({ + where: { + id: userId, + }, + data: { + sendSuccessEmails: body.sendSuccessEmails, + sendFailureEmails: body.sendFailureEmails, + }, + }); + } + async getMarketplacePeople(orgId: string, userId: string, items: ItemsDto) { const info = { id: { 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 e7580632..9bc6d9b1 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts @@ -3,6 +3,7 @@ import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users import { Provider } from '@prisma/client'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; +import { EmailNotificationsDto } from '@gitroom/nestjs-libraries/dtos/users/email-notifications.dto'; import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; @Injectable() @@ -55,4 +56,12 @@ 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, body: EmailNotificationsDto) { + return this._usersRepository.updateEmailNotifications(userId, body); + } } diff --git a/libraries/nestjs-libraries/src/dtos/users/email-notifications.dto.ts b/libraries/nestjs-libraries/src/dtos/users/email-notifications.dto.ts new file mode 100644 index 00000000..d9d62d08 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/users/email-notifications.dto.ts @@ -0,0 +1,10 @@ +import { IsBoolean } from 'class-validator'; + +export class EmailNotificationsDto { + @IsBoolean() + sendSuccessEmails: boolean; + + @IsBoolean() + sendFailureEmails: boolean; +} + diff --git a/package.json b/package.json index d6446ba6..b71b6a96 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "cron": "rm -rf dist/cron && pnpm --filter ./apps/cron run dev", "prisma-generate": "pnpm dlx prisma@6.5.0 generate --schema ./libraries/nestjs-libraries/src/database/prisma/schema.prisma", "prisma-db-push": "pnpm dlx prisma@6.5.0 db push --schema ./libraries/nestjs-libraries/src/database/prisma/schema.prisma", + "prisma-db-pull": "pnpm dlx prisma@6.5.0 db pull --schema ./libraries/nestjs-libraries/src/database/prisma/schema.prisma", "prisma-reset": "cd ./libraries/nestjs-libraries/src/database/prisma && pnpm dlx prisma@6.5.0 db push --force-reset && pnpx prisma@6.5.0 db push", "docker-build": "./var/docker/docker-build.sh", "docker-create": "./var/docker/docker-create.sh",