Skip to content
This repository was archived by the owner on Apr 20, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions components/GitHubAuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,10 @@ const GitHubAuthButton = ({
if (!chosenRepo || deployed) return;

const webhookSecret = `${uuid()}`;
saveGitHubContext(chosenRepo, webhookSecret, gitHubToken).catch(err =>
alert(`Error saving repo to DB: ${err}`)
);

setGitHubWebook(gitHubToken, chosenRepo, webhookSecret)

// First save to database, then create webhook on GitHub
saveGitHubContext(chosenRepo, webhookSecret, gitHubToken)
.then(() => setGitHubWebook(gitHubToken, chosenRepo, webhookSecret))
.then(res => {
if (res.errors) {
alert(res.errors[0].message);
Expand Down
44 changes: 44 additions & 0 deletions pages/api/github/refresh-webhook-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { refreshWebhookSecret } from "../../../utils/webhook/refreshSecret";

// POST /api/github/refresh-webhook-secret
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).send({
message: "Only POST requests are accepted."
});
}

const { repoId } = req.body;

if (!repoId) {
return res.status(400).send({
error: "Missing required parameter: repoId"
});
}

try {
const result = await refreshWebhookSecret(BigInt(repoId));

if (result.success) {
return res.status(200).json({
success: true,
message: "Webhook secret refreshed successfully"
});
} else {
return res.status(400).json({
success: false,
error: result.error || "Failed to refresh webhook secret"
});
}
} catch (error) {
console.error("Error in refresh-webhook-secret endpoint:", error);
return res.status(500).json({
success: false,
error: "Internal server error"
});
}
}
15 changes: 13 additions & 2 deletions pages/api/github/save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ export default async function handle(
const { repoId, repoName, webhookSecret } = JSON.parse(req.body);

try {
// Calculate expiration date (30 days from now)
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 30);

const result = await prisma.gitHubRepo.upsert({
where: { repoId: repoId },
update: { repoName, webhookSecret },
update: {
repoName,
webhookSecret,
webhookSecretCreatedAt: new Date(),
webhookSecretExpiresAt: expiresAt
},
create: {
repoId,
repoName,
webhookSecret
webhookSecret,
webhookSecretCreatedAt: new Date(),
webhookSecretExpiresAt: expiresAt
}
});

Expand Down
125 changes: 125 additions & 0 deletions pages/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next";
import { linearWebhookHandler } from "../../utils/webhook/linear.handler";
import { githubWebhookHandler } from "../../utils/webhook/github.handler";
import prisma from "../../prisma";

export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
Expand All @@ -25,8 +26,53 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
originIp = originIp.split(",")[0].trim();
}

// Check for Linear-Delivery header for deduplication
const linearDeliveryId = req.headers["linear-delivery"] as string;
if (linearDeliveryId) {
const existingWebhook = await prisma.webhookEvent.findUnique({
where: {
platform_eventId: {
platform: "linear",
eventId: linearDeliveryId
}
}
});

if (existingWebhook) {
console.log(`Duplicate Linear webhook detected: ${linearDeliveryId}`);
return res.status(200).send({
success: true,
message: "Webhook already processed"
});
}

// Create webhook event record
await prisma.webhookEvent.create({
data: {
platform: "linear",
eventId: linearDeliveryId,
status: "processing"
}
});
}

const result = await linearWebhookHandler(req.body, originIp);

// Update webhook status
if (linearDeliveryId) {
await prisma.webhookEvent.update({
where: {
platform_eventId: {
platform: "linear",
eventId: linearDeliveryId
}
},
data: {
status: "completed"
}
});
}

if (result) {
console.log(result);
return res.status(200).send({
Expand All @@ -38,12 +84,57 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
/**
* GitHub webhook consumer
*/
// Check for X-GitHub-Delivery header for deduplication
const githubDeliveryId = req.headers["x-github-delivery"] as string;
if (githubDeliveryId) {
const existingWebhook = await prisma.webhookEvent.findUnique({
where: {
platform_eventId: {
platform: "github",
eventId: githubDeliveryId
}
}
});

if (existingWebhook) {
console.log(`Duplicate GitHub webhook detected: ${githubDeliveryId}`);
return res.status(200).send({
success: true,
message: "Webhook already processed"
});
}

// Create webhook event record
await prisma.webhookEvent.create({
data: {
platform: "github",
eventId: githubDeliveryId,
status: "processing"
}
});
}

const result = await githubWebhookHandler(
req.body,
req.headers["x-hub-signature-256"] as string,
req.headers["x-github-event"] as string
);

// Update webhook status
if (githubDeliveryId) {
await prisma.webhookEvent.update({
where: {
platform_eventId: {
platform: "github",
eventId: githubDeliveryId
}
},
data: {
status: "completed"
}
});
}

if (result) {
console.log(result);
return res.status(200).send({
Expand All @@ -53,6 +144,26 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
}
}
} catch (e) {
// Update webhook status to failed if we have a delivery ID
const linearDeliveryId = req.headers["linear-delivery"] as string;
const githubDeliveryId = req.headers["x-github-delivery"] as string;
const deliveryId = linearDeliveryId || githubDeliveryId;
const platform = linearDeliveryId ? "linear" : "github";

if (deliveryId) {
await prisma.webhookEvent.update({
where: {
platform_eventId: {
platform,
eventId: deliveryId
}
},
data: {
status: "failed"
}
}).catch(err => console.error("Failed to update webhook status:", err));
}

return res.status(e.statusCode || 500).send({
success: false,
message: e.message
Expand All @@ -65,3 +176,17 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
message: "Webhook received."
});
};

// Clean up old webhook events (older than 30 days)
export const cleanupOldWebhookEvents = async () => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

await prisma.webhookEvent.deleteMany({
where: {
createdAt: {
lt: thirtyDaysAgo
}
}
});
};
Loading