Skip to content

Commit

Permalink
feat(4732): send Webhook request on create events
Browse files Browse the repository at this point in the history
  • Loading branch information
junminahn committed Jan 23, 2025
1 parent 2f3d398 commit 71b1dd9
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 105 deletions.
24 changes: 22 additions & 2 deletions app/app/api/private-cloud/products/_operations/create.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DecisionStatus, ProjectStatus, RequestType, EventType, TaskType } from '@prisma/client';
import { Session } from 'next-auth';
import { defaultResourceRequests } from '@/constants';
import prisma from '@/core/prisma';
import { OkResponse, UnauthorizedResponse, UnprocessableEntityResponse } from '@/core/responses';
import generateLicencePlate from '@/helpers/licence-plate';
import { sendRequestNatsMessage } from '@/helpers/nats-message';
Expand Down Expand Up @@ -30,8 +31,18 @@ export default async function createOp({ session, body }: { session: Session; bo

await upsertUsers([body.projectOwner.email, body.primaryTechnicalLead.email, body.secondaryTechnicalLead?.email]);

const { requestComment, quotaContactName, quotaContactEmail, quotaJustification, isAgMinistryChecked, ...rest } =
body;
const {
requestComment,
quotaContactName,
quotaContactEmail,
quotaJustification,
isAgMinistryChecked,
url,
secret,
username,
password,
...rest
} = body;

const productData = {
...rest,
Expand Down Expand Up @@ -75,6 +86,15 @@ export default async function createOp({ session, body }: { session: Session; bo

const proms: (Promise<any> | undefined)[] = [
createEvent(EventType.CREATE_PRIVATE_CLOUD_PRODUCT, session.user.id, { requestId: newRequest.id }),
prisma.privateCloudProductWebhook.create({
data: {
licencePlate,
url,
secret,
username,
password,
},
}),
];

if (decisionStatus === DecisionStatus.AUTO_APPROVED) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@mantine/core';
import { ResourceRequestsEnv } from '@prisma/client';
import { IconInfoCircle, IconWebhook, IconUsersGroup, IconSettings, IconComponents } from '@tabler/icons-react';
import { IconInfoCircle, IconUsersGroup, IconSettings, IconComponents } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { z } from 'zod';
Expand Down
8 changes: 8 additions & 0 deletions app/app/private-cloud/products/(product)/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TeamContacts from '@/components/form/TeamContacts';
import PageAccordion from '@/components/generic/accordion/PageAccordion';
import FormErrorNotification from '@/components/generic/FormErrorNotification';
import { openPrivateCloudProductCreateSubmitModal } from '@/components/modal/privateCloudProductCreateSubmit';
import Webhooks from '@/components/private-cloud/sections/Webhooks';
import { GlobalRole } from '@/constants';
import createClientPage from '@/core/client-page';
import { privateCloudCreateRequestBodySchema } from '@/validation-schemas/private-cloud';
Expand Down Expand Up @@ -57,6 +58,13 @@ export default privateCloudProductNew(({ session }) => {
Component: CommonComponents,
componentArgs: {},
},
{
LeftIcon: IconWebhook,
label: 'Webhooks',
description: '',
Component: Webhooks,
componentArgs: {},
},
];

return (
Expand Down
4 changes: 2 additions & 2 deletions app/components/private-cloud/SiloAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { IconWebhook } from '@tabler/icons-react';
import PageAccordion from '@/components/generic/accordion/PageAccordion';
import Webhooks from '@/components/private-cloud/sections/Webhooks';
import FormWebhooks from '@/components/private-cloud/sections/FormWebhooks';
import { cn } from '@/utils/js';

export default function SiloAccordion({
Expand All @@ -19,7 +19,7 @@ export default function SiloAccordion({
LeftIcon: IconWebhook,
label: 'Webhooks',
description: '',
Component: Webhooks,
Component: FormWebhooks,
componentArgs: {
disabled,
licencePlate,
Expand Down
66 changes: 66 additions & 0 deletions app/components/private-cloud/sections/FormWebhooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Box, Button, LoadingOverlay } from '@mantine/core';
import _get from 'lodash-es/get';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import FormErrorNotification from '@/components/generic/FormErrorNotification';
import { success, failure } from '@/components/notification';
import {
getPrivateCloudProductWebhook,
upsertPrivateCloudProductWebhook,
} from '@/services/backend/private-cloud/webhooks';
import { cn } from '@/utils/js';
import { privateCloudProductWebhookBodySchema } from '@/validation-schemas';
import Webhooks from './Webhooks';

export default function FormWebhooks({
disabled,
licencePlate,
className,
}: {
disabled: boolean;
licencePlate: string;
className?: string;
}) {
const [loading, setLoading] = useState(true);
const methods = useForm({
resolver: zodResolver(privateCloudProductWebhookBodySchema),
defaultValues: async () => {
const result = await getPrivateCloudProductWebhook(licencePlate);
setLoading(false);
return result;
},
});

return (
<div className={cn(className)}>
<Box pos={'relative'}>
<LoadingOverlay
visible={loading}
zIndex={1000}
overlayProps={{ radius: 'sm', blur: 2 }}
loaderProps={{ color: 'pink', type: 'bars' }}
/>
<FormProvider {...methods}>
<FormErrorNotification />
<form
onSubmit={methods.handleSubmit(async (formData) => {
const result = await upsertPrivateCloudProductWebhook(licencePlate, formData);
if (result) {
success({ title: 'Webhook', message: 'Updated!' });
} else {
failure({ title: 'Webhook', message: 'Failed to update!' });
}
})}
autoComplete="off"
>
<Webhooks disabled={disabled} />
<Button variant="success" type="submit" className="mt-1">
Update
</Button>
</form>
</FormProvider>
</Box>
</div>
);
}
161 changes: 61 additions & 100 deletions app/components/private-cloud/sections/Webhooks.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,77 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Code, Button } from '@mantine/core';
import { Code } from '@mantine/core';
import { RequestType } from '@prisma/client';
import _get from 'lodash-es/get';
import { FormProvider, useForm } from 'react-hook-form';
import ExternalLink from '@/components/generic/button/ExternalLink';
import FormErrorNotification from '@/components/generic/FormErrorNotification';
import HookFormTextInput from '@/components/generic/input/HookFormTextInput';
import { success, failure } from '@/components/notification';
import {
getPrivateCloudProductWebhook,
upsertPrivateCloudProductWebhook,
} from '@/services/backend/private-cloud/webhooks';
import { cn } from '@/utils/js';
import { privateCloudProductWebhookBodySchema } from '@/validation-schemas';

export default function Webhooks({
disabled,
licencePlate,
className,
}: {
disabled: boolean;
licencePlate: string;
className?: string;
}) {
const methods = useForm({
resolver: zodResolver(privateCloudProductWebhookBodySchema),
defaultValues: async () => getPrivateCloudProductWebhook(licencePlate),
});

export default function Webhooks({ disabled, className }: { disabled: boolean; className?: string }) {
return (
<div className={cn(className)}>
<FormProvider {...methods}>
<FormErrorNotification />
<form
onSubmit={methods.handleSubmit(async (formData) => {
const result = await upsertPrivateCloudProductWebhook(licencePlate, formData);
if (result) {
success({ title: 'Webhook', message: 'Updated!' });
} else {
failure({ title: 'Webhook', message: 'Failed to update!' });
}
})}
autoComplete="off"
>
<p>
We will send a <span className="font-semibold">POST</span> request to the URL below with the following data
in <span className="font-semibold">JSON</span> format when the requests are provisioned and completed.
</p>
<Code block>{`{
"action": "<create | update | delete>",
<p>
We will send a <span className="font-semibold">POST</span> request to the URL below with the following data in{' '}
<span className="font-semibold">JSON</span> format when the requests are provisioned and completed.
</p>
<Code block>{`{
"action": "<${RequestType.CREATE} | ${RequestType.EDIT} | ${RequestType.DELETE}>",
"product": {
"id": "<this product's ID>",
"licencePlate": "<this product's licencePlate>"
}
}`}</Code>
<h3 className="font-semibold mt-2">Validation Mechanism</h3>
<ul className="list-disc pl-8">
<li>
<span className="font-semibold">x-hub-signature:</span> An HTTP header commonly used in webhook
implementations to ensure the authenticity and integrity of incoming requests. It is widely utilized in
webhooks provided by platforms like GitHub, GitLab, and others. Refer to{' '}
<ExternalLink href="https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries">
GitHub&apos;s documentation on validating webhook deliveries
</ExternalLink>{' '}
for details on validation logic.
<br />- Provide the <span className="font-semibold">Secret</span> below to use this mechanism.
</li>
<li>
<span className="font-semibold">Basic Authentication:</span> A simple and widely used method for a client
to authenticate itself to a server. It involves sending a username and password encoded in Base64 in the
<code>Authorization</code> header of an HTTP request. Ensure the use of HTTPS to securely transmit
credentials.
<br />- Provide the <span className="font-semibold">Username</span> and{' '}
<span className="font-semibold">Password</span> below to use this mechanism.
</li>
</ul>
<h3 className="font-semibold mt-2">Validation Mechanism</h3>
<ul className="list-disc pl-8">
<li>
<span className="font-semibold">x-hub-signature:</span> An HTTP header commonly used in webhook
implementations to ensure the authenticity and integrity of incoming requests. It is widely utilized in
webhooks provided by platforms like GitHub, GitLab, and others. Refer to{' '}
<ExternalLink href="https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries">
GitHub&apos;s documentation on validating webhook deliveries
</ExternalLink>{' '}
for details on validation logic.
<br />- Provide the <span className="font-semibold">Secret</span> below to use this mechanism.
</li>
<li>
<span className="font-semibold">Basic Authentication:</span> A simple and widely used method for a client to
authenticate itself to a server. It involves sending a username and password encoded in Base64 in the
<code>Authorization</code> header of an HTTP request. Ensure the use of HTTPS to securely transmit
credentials.
<br />- Provide the <span className="font-semibold">Username</span> and{' '}
<span className="font-semibold">Password</span> below to use this mechanism.
</li>
</ul>

<HookFormTextInput
label="URL"
name="url"
placeholder="Enter Webhook URL"
disabled={disabled}
error="Please provide a valid HTTPS URL"
classNames={{ wrapper: 'col-span-full mt-2' }}
/>
<HookFormTextInput
label="Secret"
name="secret"
placeholder="Enter Webhook Secret"
disabled={disabled}
classNames={{ wrapper: 'col-span-full mt-2' }}
/>
<div className="flex justify-between gap-2 mt-2">
<HookFormTextInput
label="Username"
name="username"
placeholder="Enter Webhook Username"
disabled={disabled}
classNames={{ wrapper: 'w-1/2' }}
/>
<HookFormTextInput
label="Password"
name="password"
placeholder="Enter Webhook Password"
disabled={disabled}
classNames={{ wrapper: 'w-1/2' }}
/>
</div>
<Button variant="success" type="submit" className="mt-1">
Update
</Button>
</form>
</FormProvider>
<HookFormTextInput
label="URL"
name="url"
placeholder="Enter Webhook URL"
disabled={disabled}
error="Please provide a valid HTTPS URL"
classNames={{ wrapper: 'col-span-full mt-2' }}
/>
<HookFormTextInput
label="Secret"
name="secret"
placeholder="Enter Webhook Secret"
disabled={disabled}
classNames={{ wrapper: 'col-span-full mt-2' }}
/>
<div className="flex justify-between gap-2 mt-2">
<HookFormTextInput
label="Username"
name="username"
placeholder="Enter Webhook Username"
disabled={disabled}
classNames={{ wrapper: 'w-1/2' }}
/>
<HookFormTextInput
label="Password"
name="password"
placeholder="Enter Webhook Password"
disabled={disabled}
classNames={{ wrapper: 'w-1/2' }}
/>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions app/validation-schemas/private-cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export const privateCloudCreateRequestBodySchema = _privateCloudCreateRequestBod
isAgMinistryChecked: z.boolean().optional(),
}),
)
.merge(privateCloudProductWebhookBodySchema)
.refine(
(formData) => {
return AGMinistries.includes(formData.ministry) ? formData.isAgMinistryChecked : true;
Expand Down

0 comments on commit 71b1dd9

Please sign in to comment.