feature(dashboard): add integrations and notifications

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-10-02 22:12:05 +02:00
parent d920f6951c
commit f65a633403
94 changed files with 3692 additions and 127 deletions

View File

@@ -1,9 +1,15 @@
import type { WebhookEvent } from '@clerk/fastify';
import { AccessLevel, db } from '@openpanel/db';
import {
sendSlackNotification,
slackInstaller,
} from '@openpanel/integrations/src/slack';
import { getRedisPub } from '@openpanel/redis';
import { zSlackAuthResponse } from '@openpanel/validation';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { pathOr } from 'ramda';
import { Webhook } from 'svix';
import { AccessLevel, db } from '@openpanel/db';
import { z } from 'zod';
if (!process.env.CLERK_SIGNING_SECRET) {
throw new Error('CLERK_SIGNING_SECRET is required');
@@ -152,3 +158,98 @@ export async function clerkWebhook(
reply.send({ success: true });
}
const paramsSchema = z.object({
code: z.string(),
state: z.string(),
});
const metadataSchema = z.object({
organizationId: z.string(),
integrationId: z.string(),
});
export async function slackWebhook(
request: FastifyRequest<{
Querystring: WebhookEvent;
}>,
reply: FastifyReply,
) {
const parsedParams = paramsSchema.safeParse(request.query);
if (!parsedParams.success) {
request.log.error('Invalid params', parsedParams);
return reply.status(400).send({ error: 'Invalid params' });
}
const veryfiedState = await slackInstaller.stateStore?.verifyStateParam(
new Date(),
parsedParams.data.state,
);
const parsedMetadata = metadataSchema.safeParse(
JSON.parse(veryfiedState?.metadata ?? '{}'),
);
if (!parsedMetadata.success) {
request.log.error('Invalid metadata', parsedMetadata.error.errors);
return reply.status(400).send({ error: 'Invalid metadata' });
}
const slackOauthAccessUrl = [
'https://slack.com/api/oauth.v2.access',
`?client_id=${process.env.SLACK_CLIENT_ID}`,
`&client_secret=${process.env.SLACK_CLIENT_SECRET}`,
`&code=${parsedParams.data.code}`,
`&redirect_uri=${process.env.SLACK_OAUTH_REDIRECT_URL}`,
].join('');
try {
const response = await fetch(slackOauthAccessUrl);
const json = await response.json();
const parsedJson = zSlackAuthResponse.safeParse(json);
if (!parsedJson.success) {
request.log.error(
{
zod: parsedJson,
json,
},
'Failed to parse slack auth response',
);
return reply
.status(400)
.header('Content-Type', 'text/html')
.send('<h1>Failed to exchange code for token</h1>');
}
// Send a notification first to confirm the connection
await sendSlackNotification({
webhookUrl: parsedJson.data.incoming_webhook.url,
message:
'👋 Hello. You have successfully connected OpenPanel.dev to your Slack workspace.',
});
await db.integration.update({
where: {
id: parsedMetadata.data.integrationId,
organizationId: parsedMetadata.data.organizationId,
},
data: {
config: {
type: 'slack',
...parsedJson.data,
},
},
});
getRedisPub().publish('integrations:slack', 'ok');
reply.send({ success: true });
} catch (err) {
request.log.error(err);
return reply
.status(500)
.header('Content-Type', 'text/html')
.send('<h1>Failed to exchange code for token</h1>');
}
}