feat: notifcations settings

This commit is contained in:
Nevo David 2025-12-05 14:32:20 +07:00
parent d89eb44b8f
commit c982e30e96
12 changed files with 586 additions and 146 deletions

View File

@ -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) {

View File

@ -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<EmailNotifications>('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<EmailNotifications>({
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 (
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px]">
<div className="animate-pulse">
{t('loading', 'Loading...')}
</div>
</div>
);
}
return (
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[24px]">
<div className="mt-[4px]">
{t('email_notifications', 'Email Notifications')}
</div>
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="text-[14px]">
{t('success_emails', 'Success Emails')}
</div>
<div className="text-[12px] text-customColor18">
{t(
'success_emails_description',
'Receive email notifications when posts are published successfully'
)}
</div>
</div>
<Slider
value={localSettings.sendSuccessEmails ? 'on' : 'off'}
onChange={handleSuccessEmailsChange}
fill={true}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="text-[14px]">
{t('failure_emails', 'Failure Emails')}
</div>
<div className="text-[12px] text-customColor18">
{t(
'failure_emails_description',
'Receive email notifications when posts fail to publish'
)}
</div>
</div>
<Slider
value={localSettings.sendFailureEmails ? 'on' : 'off'}
onChange={handleFailureEmailsChange}
fill={true}
/>
</div>
</div>
);
};
export default EmailNotificationsComponent;

View File

@ -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 (
<div className="flex flex-col">
<h3 className="text-[20px]">{t('global_settings', 'Global Settings')}</h3>
<MetricComponent />
<EmailNotificationsComponent />
</div>
);
};

View File

@ -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'
);
}

View File

@ -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<NotificationType[]> {
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);
}
}

View File

@ -293,6 +293,8 @@ export class OrganizationRepository {
select: {
email: true,
id: true,
sendSuccessEmails: true,
sendFailureEmails: true,
},
},
},

View File

@ -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('<br />');
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
);
}
}

View File

@ -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

View File

@ -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: {

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
import { IsBoolean } from 'class-validator';
export class EmailNotificationsDto {
@IsBoolean()
sendSuccessEmails: boolean;
@IsBoolean()
sendFailureEmails: boolean;
}

View File

@ -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",