diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts
index e7f9386e..4fb94fbf 100644
--- a/apps/backend/src/api/api.module.ts
+++ b/apps/backend/src/api/api.module.ts
@@ -28,6 +28,7 @@ import { RootController } from '@gitroom/backend/api/routes/root.controller';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
+import { WebhookController } from '@gitroom/backend/api/routes/webhooks.controller';
const authenticatedController = [
UsersController,
@@ -42,6 +43,7 @@ const authenticatedController = [
MessagesController,
CopilotController,
AgenciesController,
+ WebhookController,
];
@Module({
imports: [
diff --git a/apps/backend/src/api/routes/webhooks.controller.ts b/apps/backend/src/api/routes/webhooks.controller.ts
new file mode 100644
index 00000000..19a78730
--- /dev/null
+++ b/apps/backend/src/api/routes/webhooks.controller.ts
@@ -0,0 +1,50 @@
+import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
+import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
+import { Organization } from '@prisma/client';
+import { ApiTags } from '@nestjs/swagger';
+import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
+import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
+import {
+ AuthorizationActions,
+ Sections,
+} from '@gitroom/backend/services/auth/permissions/permissions.service';
+import {
+ UpdateDto,
+ WebhooksDto,
+} from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
+
+@ApiTags('Webhooks')
+@Controller('/webhooks')
+export class WebhookController {
+ constructor(private _webhooksService: WebhooksService) {}
+
+ @Get('/')
+ async getStatistics(@GetOrgFromRequest() org: Organization) {
+ return this._webhooksService.getWebhooks(org.id);
+ }
+
+ @Post('/')
+ @CheckPolicies([AuthorizationActions.Create, Sections.WEBHOOKS])
+ async createAWebhook(
+ @GetOrgFromRequest() org: Organization,
+ @Body() body: WebhooksDto
+ ) {
+ return this._webhooksService.createWebhook(org.id, body);
+ }
+
+ @Put('/')
+ async updateWebhook(
+ @GetOrgFromRequest() org: Organization,
+ @Body() body: UpdateDto
+ ) {
+ return this._webhooksService.createWebhook(org.id, body);
+ }
+
+ @Delete('/:id')
+ async deleteWebhook(
+ @GetOrgFromRequest() org: Organization,
+ @Param('id') id: string
+ ) {
+ return this._webhooksService.deleteWebhook(org.id, id);
+ }
+}
diff --git a/apps/backend/src/services/auth/permissions/permissions.service.ts b/apps/backend/src/services/auth/permissions/permissions.service.ts
index 7538dc9e..8c4f87f3 100644
--- a/apps/backend/src/services/auth/permissions/permissions.service.ts
+++ b/apps/backend/src/services/auth/permissions/permissions.service.ts
@@ -5,6 +5,7 @@ import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/s
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import dayjs from 'dayjs';
+import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
export enum Sections {
CHANNEL = 'channel',
@@ -15,6 +16,7 @@ export enum Sections {
AI = 'ai',
IMPORT_FROM_CHANNELS = 'import_from_channels',
ADMIN = 'admin',
+ WEBHOOKS = 'webhooks',
}
export enum AuthorizationActions {
@@ -31,7 +33,8 @@ export class PermissionsService {
constructor(
private _subscriptionService: SubscriptionService,
private _postsService: PostsService,
- private _integrationService: IntegrationService
+ private _integrationService: IntegrationService,
+ private _webhooksService: WebhooksService,
) {}
async getPackageOptions(orgId: string) {
const subscription =
@@ -93,6 +96,14 @@ export class PermissionsService {
}
}
+ if (section === Sections.WEBHOOKS) {
+ const totalWebhooks = await this._webhooksService.getTotal(orgId);
+ if (totalWebhooks < options.webhooks) {
+ can(AuthorizationActions.Create, section);
+ continue;
+ }
+ }
+
// check for posts per month
if (section === Sections.POSTS_PER_MONTH) {
const createdAt =
diff --git a/apps/backend/src/services/auth/permissions/subscription.exception.ts b/apps/backend/src/services/auth/permissions/subscription.exception.ts
index 09b6cc06..3088674b 100644
--- a/apps/backend/src/services/auth/permissions/subscription.exception.ts
+++ b/apps/backend/src/services/auth/permissions/subscription.exception.ts
@@ -1,11 +1,17 @@
-import {ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus} from "@nestjs/common";
-import {AuthorizationActions, Sections} from "@gitroom/backend/services/auth/permissions/permissions.service";
+import {
+ ArgumentsHost,
+ Catch,
+ ExceptionFilter,
+ HttpException,
+ HttpStatus,
+} from '@nestjs/common';
+import {
+ AuthorizationActions,
+ Sections,
+} from '@gitroom/backend/services/auth/permissions/permissions.service';
export class SubscriptionException extends HttpException {
- constructor(message: {
- section: Sections,
- action: AuthorizationActions
- }) {
+ constructor(message: { section: Sections; action: AuthorizationActions }) {
super(message, HttpStatus.PAYMENT_REQUIRED);
}
}
@@ -16,19 +22,23 @@ export class SubscriptionExceptionFilter implements ExceptionFilter {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
- const error: {section: Sections, action: AuthorizationActions} = exception.getResponse() as any;
+ const error: { section: Sections; action: AuthorizationActions } =
+ exception.getResponse() as any;
const message = getErrorMessage(error);
response.status(status).json({
- statusCode: status,
- message,
- url: process.env.FRONTEND_URL + '/billing',
+ statusCode: status,
+ message,
+ url: process.env.FRONTEND_URL + '/billing',
});
}
}
-const getErrorMessage = (error: {section: Sections, action: AuthorizationActions}) => {
+const getErrorMessage = (error: {
+ section: Sections;
+ action: AuthorizationActions;
+}) => {
switch (error.section) {
case Sections.POSTS_PER_MONTH:
switch (error.action) {
@@ -40,5 +50,10 @@ const getErrorMessage = (error: {section: Sections, action: AuthorizationActions
default:
return 'You have reached the maximum number of channels for your subscription. Please upgrade your subscription to add more channels.';
}
+ case Sections.WEBHOOKS:
+ switch (error.action) {
+ default:
+ return 'You have reached the maximum number of webhooks for your subscription. Please upgrade your subscription to add more webhooks.';
+ }
}
-}
+};
diff --git a/apps/frontend/src/app/(site)/settings/page.tsx b/apps/frontend/src/app/(site)/settings/page.tsx
index ba9acb8e..84b16a2c 100644
--- a/apps/frontend/src/app/(site)/settings/page.tsx
+++ b/apps/frontend/src/app/(site)/settings/page.tsx
@@ -1,9 +1,7 @@
+import { SettingsPopup } from '@gitroom/frontend/components/layout/settings.component';
+
export const dynamic = 'force-dynamic';
-import { SettingsComponent } from '@gitroom/frontend/components/settings/settings.component';
-import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
-import { redirect } from 'next/navigation';
-import { RedirectType } from 'next/dist/client/components/redirect';
import { Metadata } from 'next';
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
@@ -16,14 +14,5 @@ export default async function Index({
}: {
searchParams: { code: string };
}) {
- if (searchParams.code) {
- await internalFetch('/settings/github', {
- method: 'POST',
- body: JSON.stringify({ code: searchParams.code }),
- });
-
- return redirect('/settings', RedirectType.replace);
- }
-
- return ;
+ return ;
}
diff --git a/apps/frontend/src/components/launches/menu/menu.tsx b/apps/frontend/src/components/launches/menu/menu.tsx
index 2b287e5c..6398e10d 100644
--- a/apps/frontend/src/components/launches/menu/menu.tsx
+++ b/apps/frontend/src/components/launches/menu/menu.tsx
@@ -17,7 +17,6 @@ import { BotPicture } from '@gitroom/frontend/components/launches/bot.picture';
import { CustomerModal } from '@gitroom/frontend/components/launches/customer.modal';
import { Integration } from '@prisma/client';
import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal';
-import { string } from 'yup';
import { CustomVariables } from '@gitroom/frontend/components/launches/add.provider.component';
import { useRouter } from 'next/navigation';
diff --git a/apps/frontend/src/components/layout/impersonate.tsx b/apps/frontend/src/components/layout/impersonate.tsx
index d6ae692b..2e94f20c 100644
--- a/apps/frontend/src/components/layout/impersonate.tsx
+++ b/apps/frontend/src/components/layout/impersonate.tsx
@@ -114,7 +114,7 @@ export const Impersonate = () => {
}, [data]);
return (
-
+
diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx
index 6eeddc5d..8a6edf98 100644
--- a/apps/frontend/src/components/layout/settings.component.tsx
+++ b/apps/frontend/src/components/layout/settings.component.tsx
@@ -19,9 +19,11 @@ import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.comp
import { useSearchParams } from 'next/navigation';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { PublicComponent } from '@gitroom/frontend/components/public-api/public.component';
+import Link from 'next/link';
+import { Webhooks } from '@gitroom/frontend/components/webhooks/webhooks';
export const SettingsPopup: FC<{ getRef?: Ref
}> = (props) => {
- const {isGeneral} = useVariables();
+ const { isGeneral } = useVariables();
const { getRef } = props;
const fetch = useFetch();
const toast = useToaster();
@@ -39,7 +41,7 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => {
}, []);
const url = useSearchParams();
- const showLogout = !url.get('onboarding') || user?.tier?.current === "FREE";
+ const showLogout = !url.get('onboarding') || user?.tier?.current === 'FREE';
const loadProfile = useCallback(async () => {
const personal = await (await fetch('/user/personal')).json();
@@ -85,8 +87,8 @@ export const SettingsPopup: FC<{ getRef?: Ref }> = (props) => {
)}
{/*{!getRef && (*/}
@@ -196,7 +198,10 @@ export const SettingsPopup: FC<{ getRef?: Ref
}> = (props) => {
{/* */}
{/*)}*/}
{!!user?.tier?.team_members && isGeneral && }
- {!!user?.tier?.public_api && isGeneral && showLogout && }
+ {!!user?.tier?.webhooks && }
+ {!!user?.tier?.public_api && isGeneral && showLogout && (
+
+ )}
{showLogout && }
@@ -205,32 +210,21 @@ export const SettingsPopup: FC<{ getRef?: Ref
}> = (props) => {
};
export const SettingsComponent = () => {
- const settings = useModals();
- const openModal = useCallback(() => {
- settings.openModal({
- children: ,
- classNames: {
- modal: 'bg-transparent text-textColor',
- },
- withCloseButton: false,
- size: '100%',
- });
- }, []);
-
return (
-
+
+
+
);
};
diff --git a/apps/frontend/src/components/layout/top.menu.tsx b/apps/frontend/src/components/layout/top.menu.tsx
index c84ba34e..f4bc2f37 100644
--- a/apps/frontend/src/components/layout/top.menu.tsx
+++ b/apps/frontend/src/components/layout/top.menu.tsx
@@ -55,6 +55,13 @@ export const useMenuItems = () => {
role: ['ADMIN', 'SUPERADMIN'],
requireBilling: true,
},
+ {
+ name: 'Settings',
+ icon: 'settings',
+ path: '/settings',
+ role: ['ADMIN', 'SUPERADMIN'],
+ hide: true,
+ },
{
name: 'Affiliate',
icon: 'affiliate',
@@ -76,6 +83,9 @@ export const TopMenu: FC = () => {
{menuItems
.filter((f) => {
+ if (f.hide) {
+ return false;
+ }
if (f.requireBilling && !billingEnabled) {
return false;
}
@@ -97,6 +107,9 @@ export const TopMenu: FC = () => {
'flex gap-2 items-center box px-[6px] md:px-[24px] py-[8px]',
menuItems
.filter((f) => {
+ if (f.hide) {
+ return false;
+ }
if (f.role) {
return f.role.includes(user?.role!);
}
diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx
index 1f211c3a..93c7038c 100644
--- a/apps/frontend/src/components/public-api/public.component.tsx
+++ b/apps/frontend/src/components/public-api/public.component.tsx
@@ -22,7 +22,7 @@ export const PublicComponent = () => {
return (
-
Public API
+
Public API
Use Postiz API to integrate with your tools.
diff --git a/apps/frontend/src/components/settings/teams.component.tsx b/apps/frontend/src/components/settings/teams.component.tsx
index 5ff00158..b834a431 100644
--- a/apps/frontend/src/components/settings/teams.component.tsx
+++ b/apps/frontend/src/components/settings/teams.component.tsx
@@ -186,8 +186,7 @@ export const TeamsComponent = () => {
return (
-
Team Members
-
Account Managers
+
Team Members
Invite your assistant or team member to manage your account
@@ -238,7 +237,7 @@ export const TeamsComponent = () => {
))}
-
diff --git a/apps/frontend/src/components/webhooks/webhooks.tsx b/apps/frontend/src/components/webhooks/webhooks.tsx
new file mode 100644
index 00000000..6d964d99
--- /dev/null
+++ b/apps/frontend/src/components/webhooks/webhooks.tsx
@@ -0,0 +1,245 @@
+import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
+import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
+import useSWR from 'swr';
+import { useUser } from '@gitroom/frontend/components/layout/user.context';
+import { Button } from '@gitroom/react/form/button';
+import { useModals } from '@mantine/modals';
+import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
+import { Input } from '@gitroom/react/form/input';
+import { FormProvider, useForm } from 'react-hook-form';
+import { array, object, string } from 'yup';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { Select } from '@gitroom/react/form/select';
+import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
+import { useToaster } from '@gitroom/react/toaster/toaster';
+import clsx from 'clsx';
+import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
+
+export const Webhooks: FC = () => {
+ const fetch = useFetch();
+ const user = useUser();
+ const modal = useModals();
+ const toaster = useToaster();
+
+ const list = useCallback(async () => {
+ return (await fetch('/webhooks')).json();
+ }, []);
+
+ const { data, mutate } = useSWR('webhooks', list);
+
+ const addWebhook = useCallback(
+ (data?: any) => () => {
+ modal.openModal({
+ title: '',
+ withCloseButton: false,
+ classNames: {
+ modal: 'bg-transparent text-textColor',
+ },
+ children:
,
+ });
+ },
+ []
+ );
+
+ const deleteHook = useCallback(
+ (data: any) => async () => {
+ if (await deleteDialog(`Are you sure you want to delete ${data.name}?`)) {
+ await fetch(`/webhooks/${data.id}`, { method: 'DELETE' });
+ mutate();
+ toaster.show('Webhook deleted successfully', 'success');
+ }
+ },
+ []
+ );
+
+ return (
+
+
+ Webhooks ({data?.length || 0}/{user?.tier?.webhooks})
+
+
+ Webhooks are a way to get notified when something happens in Postiz via
+ an HTTP request.
+
+
+
+ {!!data?.length && (
+
+
Name
+
URL
+
Edit
+
Delete
+ {data?.map((p: any) => (
+
+ {p.name}
+ {p.url}
+
+
+
+ ))}
+
+ )}
+
+ 0 && 'my-[16px]')}
+ >
+ Add a webhook
+
+
+
+
+
+ );
+};
+
+const details = object().shape({
+ name: string().required(),
+ url: string().url().required(),
+ integrations: array(),
+});
+
+const options = [
+ { label: 'All integrations', value: 'all' },
+ { label: 'Specific integrations', value: 'specific' },
+];
+
+export const AddOrEditWebhook: FC<{ data?: any; reload: () => void }> = (
+ props
+) => {
+ const { data, reload } = props;
+ const fetch = useFetch();
+ const [allIntegrations, setAllIntegrations] = useState(
+ (data?.integrations?.length || 0) > 0 ? options[1] : options[0]
+ );
+ const modal = useModals();
+ const toast = useToaster();
+ const form = useForm({
+ resolver: yupResolver(details),
+ values: {
+ name: data?.name || '',
+ url: data?.url || '',
+ integrations: data?.integrations?.map((p: any) => p.integration) || [],
+ },
+ });
+
+ const integrations = form.watch('integrations');
+
+ const integration = useCallback(async () => {
+ return (await fetch('/integrations/list')).json();
+ }, []);
+
+ const changeIntegration = useCallback(
+ (e: React.ChangeEvent
) => {
+ const findValue = options.find(
+ (option) => option.value === e.target.value
+ )!;
+ setAllIntegrations(findValue);
+ if (findValue.value === 'all') {
+ form.setValue('integrations', []);
+ }
+ },
+ []
+ );
+
+ const { data: dataList, isLoading } = useSWR('integrations', integration);
+
+ const callBack = useCallback(
+ async (values: any) => {
+ await fetch('/webhooks', {
+ method: data?.id ? 'PUT' : 'POST',
+ body: JSON.stringify({
+ ...(data?.id ? { id: data.id } : {}),
+ ...values,
+ }),
+ });
+
+ toast.show(
+ data?.id
+ ? 'Webhook updated successfully'
+ : 'Webhook added successfully',
+ 'success'
+ );
+
+ modal.closeAll();
+ reload();
+ },
+ [data, integrations]
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/workers/src/app/posts.controller.ts b/apps/workers/src/app/posts.controller.ts
index f644dc32..90ef07b0 100644
--- a/apps/workers/src/app/posts.controller.ts
+++ b/apps/workers/src/app/posts.controller.ts
@@ -1,10 +1,15 @@
import { Controller } from '@nestjs/common';
import { EventPattern, Transport } from '@nestjs/microservices';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
+import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
@Controller()
export class PostsController {
- constructor(private _postsService: PostsService) {}
+ constructor(
+ private _postsService: PostsService,
+ private _webhooksService: WebhooksService
+ ) {}
+
@EventPattern('post', Transport.REDIS)
async post(data: { id: string }) {
console.log('processing', data);
@@ -17,7 +22,19 @@ export class PostsController {
}
@EventPattern('sendDigestEmail', Transport.REDIS)
- async sendDigestEmail(data: { subject: string, org: string; since: string }) {
- return this._postsService.sendDigestEmail(data.subject, data.org, data.since);
+ async sendDigestEmail(data: { subject: string; org: string; since: string }) {
+ return this._postsService.sendDigestEmail(
+ data.subject,
+ data.org,
+ data.since
+ );
+ }
+
+ @EventPattern('webhooks', Transport.REDIS)
+ async webhooks(data: { org: string; since: string }) {
+ return this._webhooksService.fireWebhooks(
+ data.org,
+ data.since
+ );
}
}
diff --git a/libraries/nestjs-libraries/src/database/prisma/database.module.ts b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
index 9345983e..610eb7db 100644
--- a/libraries/nestjs-libraries/src/database/prisma/database.module.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/database.module.ts
@@ -29,6 +29,8 @@ import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agenc
import { AgenciesRepository } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.repository';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
+import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.repository';
+import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
@Global()
@Module({
@@ -47,6 +49,8 @@ import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.
SubscriptionRepository,
NotificationService,
NotificationsRepository,
+ WebhooksRepository,
+ WebhooksService,
IntegrationService,
IntegrationRepository,
PostsService,
diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
index 3b0d25ed..825deb8e 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts
@@ -515,4 +515,33 @@ export class PostsRepository {
},
});
}
+
+ async getPostsSince(orgId: string, since: string) {
+ return this._post.model.post.findMany({
+ where: {
+ organizationId: orgId,
+ publishDate: {
+ gte: new Date(since),
+ },
+ deletedAt: null,
+ parentPostId: null,
+ },
+ select: {
+ id: true,
+ content: true,
+ publishDate: true,
+ releaseURL: true,
+ state: true,
+ integration: {
+ select: {
+ id: true,
+ name: true,
+ providerIdentifier: true,
+ picture: true,
+ type: 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 89d7390f..a3041482 100644
--- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
@@ -22,6 +22,7 @@ import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/
import utc from 'dayjs/plugin/utc';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
+import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
dayjs.extend(utc);
type PostWithConditionals = Post & {
@@ -40,7 +41,8 @@ export class PostsService {
private _stripeService: StripeService,
private _integrationService: IntegrationService,
private _mediaService: MediaService,
- private _shortLinkService: ShortLinkService
+ private _shortLinkService: ShortLinkService,
+ private _webhookService: WebhooksService
) {}
async getStatistics(orgId: string, id: string) {
@@ -372,6 +374,11 @@ export class PostsService {
true
);
+ await this._webhookService.digestWebhooks(
+ integration.organizationId,
+ dayjs(newPosts[0].publishDate).format('YYYY-MM-DDTHH:mm:00')
+ );
+
await this.checkPlugs(
integration.organizationId,
getIntegration.identifier,
diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
index ef4dbf0b..4cbd162d 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -33,6 +33,7 @@ model Organization {
credits Credits[]
plugs Plugs[]
customers Customer[]
+ webhooks Webhooks[]
}
model User {
@@ -290,6 +291,7 @@ model Integration {
exisingPlugData ExisingPlugData[]
rootInternalId String?
additionalSettings String? @default("[]")
+ webhooks IntegrationsWebhooks[]
@@index([rootInternalId])
@@index([updatedAt])
@@ -501,6 +503,33 @@ model PopularPosts {
updatedAt DateTime @updatedAt
}
+model IntegrationsWebhooks {
+ integrationId String
+ integration Integration @relation(fields: [integrationId], references: [id])
+ webhookId String
+ webhook Webhooks @relation(fields: [webhookId], references: [id])
+
+ @@unique([integrationId, webhookId])
+ @@id([integrationId, webhookId])
+ @@index([integrationId])
+ @@index([webhookId])
+}
+
+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
+
+ @@index([organizationId])
+ @@index([deletedAt])
+}
+
enum OrderStatus {
PENDING
ACCEPTED
diff --git a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts
index 7fb692f5..e1752c88 100644
--- a/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts
+++ b/libraries/nestjs-libraries/src/database/prisma/subscriptions/pricing.ts
@@ -12,6 +12,7 @@ export interface PricingInnerInterface {
image_generator?: boolean;
image_generation_count: number;
public_api: boolean;
+ webhooks: number;
}
export interface PricingInterface {
[key: string]: PricingInnerInterface;
@@ -31,6 +32,7 @@ export const pricing: PricingInterface = {
import_from_channels: false,
image_generator: false,
public_api: false,
+ webhooks: 0,
},
STANDARD: {
current: 'STANDARD',
@@ -46,6 +48,7 @@ export const pricing: PricingInterface = {
import_from_channels: true,
image_generator: false,
public_api: true,
+ webhooks: 2,
},
TEAM: {
current: 'TEAM',
@@ -61,6 +64,7 @@ export const pricing: PricingInterface = {
import_from_channels: true,
image_generator: true,
public_api: true,
+ webhooks: 10,
},
PRO: {
current: 'PRO',
@@ -76,6 +80,7 @@ export const pricing: PricingInterface = {
import_from_channels: true,
image_generator: true,
public_api: true,
+ webhooks: 30,
},
ULTIMATE: {
current: 'ULTIMATE',
@@ -91,5 +96,6 @@ export const pricing: PricingInterface = {
import_from_channels: true,
image_generator: true,
public_api: true,
+ webhooks: 10000,
},
};
diff --git a/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.repository.ts b/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.repository.ts
new file mode 100644
index 00000000..cc1b025c
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.repository.ts
@@ -0,0 +1,87 @@
+import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
+import { Injectable } from '@nestjs/common';
+import { WebhooksDto } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
+import { v4 as uuidv4 } from 'uuid';
+
+@Injectable()
+export class WebhooksRepository {
+ constructor(private _webhooks: PrismaRepository<'webhooks'>) {}
+
+ getTotal(orgId: string) {
+ return this._webhooks.model.webhooks.count({
+ where: {
+ organizationId: orgId,
+ deletedAt: null,
+ },
+ });
+ }
+
+ getWebhooks(orgId: string) {
+ return this._webhooks.model.webhooks.findMany({
+ where: {
+ organizationId: orgId,
+ deletedAt: null,
+ },
+ include: {
+ integrations: {
+ select: {
+ integration: {
+ select: {
+ id: true,
+ picture: true,
+ name: true,
+ },
+ },
+ },
+ },
+ },
+ });
+ }
+
+ deleteWebhook(orgId: string, id: string) {
+ return this._webhooks.model.webhooks.update({
+ where: {
+ id,
+ organizationId: orgId,
+ },
+ data: {
+ deletedAt: new Date(),
+ },
+ });
+ }
+
+ async createWebhook(orgId: string, body: WebhooksDto) {
+ const { id } = await this._webhooks.model.webhooks.upsert({
+ where: {
+ id: body.id || uuidv4(),
+ organizationId: orgId,
+ },
+ create: {
+ organizationId: orgId,
+ url: body.url,
+ name: body.name,
+ },
+ update: {
+ url: body.url,
+ name: body.name,
+ },
+ });
+
+ await this._webhooks.model.webhooks.update({
+ where: {
+ id,
+ organizationId: orgId,
+ },
+ data: {
+ integrations: {
+ deleteMany: {},
+ create: body.integrations.map((integration) => ({
+ integrationId: integration.id,
+ })),
+ },
+ },
+ });
+
+ return { id };
+ }
+}
diff --git a/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.service.ts b/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.service.ts
new file mode 100644
index 00000000..d23bed29
--- /dev/null
+++ b/libraries/nestjs-libraries/src/database/prisma/webhooks/webhooks.service.ts
@@ -0,0 +1,98 @@
+import { Injectable } from '@nestjs/common';
+import { WebhooksRepository } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.repository';
+import { WebhooksDto } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto';
+import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service';
+import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
+import { PostsRepository } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.repository';
+
+@Injectable()
+export class WebhooksService {
+ constructor(
+ private _webhooksRepository: WebhooksRepository,
+ private _postsRepository: PostsRepository,
+ private _workerServiceProducer: BullMqClient
+ ) {}
+
+ getTotal(orgId: string) {
+ return this._webhooksRepository.getTotal(orgId);
+ }
+
+ getWebhooks(orgId: string) {
+ return this._webhooksRepository.getWebhooks(orgId);
+ }
+
+ createWebhook(orgId: string, body: WebhooksDto) {
+ return this._webhooksRepository.createWebhook(orgId, body);
+ }
+
+ deleteWebhook(orgId: string, id: string) {
+ return this._webhooksRepository.deleteWebhook(orgId, id);
+ }
+
+ async digestWebhooks(orgId: string, since: string) {
+ const date = new Date().toISOString();
+ await ioRedis.watch('webhook_' + orgId);
+ const value = await ioRedis.get('webhook_' + orgId);
+ if (value) {
+ return;
+ }
+
+ await ioRedis
+ .multi()
+ .set('webhook_' + orgId, date)
+ .expire('webhook_' + orgId, 60)
+ .exec();
+
+ this._workerServiceProducer.emit('webhooks', {
+ id: 'digest_' + orgId,
+ options: {
+ delay: 60000,
+ },
+ payload: {
+ org: orgId,
+ since,
+ },
+ });
+ }
+
+ async fireWebhooks(orgId: string, since: string) {
+ const list = await this._postsRepository.getPostsSince(orgId, since);
+ const webhooks = await this._webhooksRepository.getWebhooks(orgId);
+ const sendList = [];
+ for (const webhook of webhooks) {
+ const toSend = [];
+ if (webhook.integrations.length === 0) {
+ toSend.push(...list);
+ } else {
+ toSend.push(
+ ...list.filter((post) =>
+ webhook.integrations.some(
+ (i) => i.integration.id === post.integration.id
+ )
+ )
+ );
+ }
+
+ sendList.push({
+ url: webhook.url,
+ data: toSend,
+ });
+ }
+
+ return Promise.all(
+ sendList.map(async (s) => {
+ try {
+ await fetch(s.url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(s.data),
+ });
+ } catch (e) {
+ /**empty**/
+ }
+ })
+ );
+ }
+}
diff --git a/libraries/nestjs-libraries/src/dtos/webhooks/webhooks.dto.ts b/libraries/nestjs-libraries/src/dtos/webhooks/webhooks.dto.ts
new file mode 100644
index 00000000..d05db43b
--- /dev/null
+++ b/libraries/nestjs-libraries/src/dtos/webhooks/webhooks.dto.ts
@@ -0,0 +1,44 @@
+import { IsDefined, IsOptional, IsString, IsUrl } from 'class-validator';
+import { Type } from 'class-transformer';
+
+export class WebhooksIntegrationDto {
+ @IsString()
+ @IsDefined()
+ id: string;
+}
+
+export class WebhooksDto {
+ id: string;
+
+ @IsString()
+ @IsDefined()
+ name: string;
+
+ @IsString()
+ @IsUrl()
+ @IsDefined()
+ url: string;
+
+ @Type(() => WebhooksIntegrationDto)
+ @IsDefined()
+ integrations: WebhooksIntegrationDto[];
+}
+
+export class UpdateDto {
+ @IsString()
+ @IsDefined()
+ id: string;
+
+ @IsString()
+ @IsDefined()
+ name: string;
+
+ @IsString()
+ @IsUrl()
+ @IsDefined()
+ url: string;
+
+ @Type(() => WebhooksIntegrationDto)
+ @IsDefined()
+ integrations: WebhooksIntegrationDto[];
+}