Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,306 +1,306 @@
import { z } from 'zod'
import { router, superAdminProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import {
generateWebhookSecret,
deliverWebhook,
} from '@/server/services/webhook-dispatcher'
export const WEBHOOK_EVENTS = [
'evaluation.submitted',
'evaluation.updated',
'project.created',
'project.statusChanged',
'round.activated',
'round.closed',
'stage.activated',
'stage.closed',
'assignment.created',
'assignment.completed',
'user.invited',
'user.activated',
] as const
export const webhookRouter = router({
/**
* List all webhooks with delivery stats.
*/
list: superAdminProcedure.query(async ({ ctx }) => {
const webhooks = await ctx.prisma.webhook.findMany({
include: {
_count: {
select: { deliveries: true },
},
createdBy: {
select: { id: true, name: true, email: true },
},
},
orderBy: { createdAt: 'desc' },
})
// Compute recent delivery stats for each webhook
const now = new Date()
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const stats = await Promise.all(
webhooks.map(async (wh) => {
const [delivered, failed] = await Promise.all([
ctx.prisma.webhookDelivery.count({
where: {
webhookId: wh.id,
status: 'DELIVERED',
createdAt: { gte: twentyFourHoursAgo },
},
}),
ctx.prisma.webhookDelivery.count({
where: {
webhookId: wh.id,
status: 'FAILED',
createdAt: { gte: twentyFourHoursAgo },
},
}),
])
return {
...wh,
recentDelivered: delivered,
recentFailed: failed,
}
})
)
return stats
}),
/**
* Create a new webhook.
*/
create: superAdminProcedure
.input(
z.object({
name: z.string().min(1).max(200),
url: z.string().url(),
events: z.array(z.string()).min(1),
headers: z.any().optional(),
maxRetries: z.number().int().min(0).max(10).default(3),
})
)
.mutation(async ({ ctx, input }) => {
const secret = generateWebhookSecret()
const webhook = await ctx.prisma.webhook.create({
data: {
name: input.name,
url: input.url,
secret,
events: input.events,
headers: input.headers ?? undefined,
maxRetries: input.maxRetries,
createdById: ctx.user.id,
},
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'CREATE_WEBHOOK',
entityType: 'Webhook',
entityId: webhook.id,
detailsJson: { name: input.name, url: input.url, events: input.events },
})
} catch {}
return webhook
}),
/**
* Update a webhook.
*/
update: superAdminProcedure
.input(
z.object({
id: z.string(),
name: z.string().min(1).max(200).optional(),
url: z.string().url().optional(),
events: z.array(z.string()).min(1).optional(),
headers: z.any().optional(),
isActive: z.boolean().optional(),
maxRetries: z.number().int().min(0).max(10).optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input
const webhook = await ctx.prisma.webhook.update({
where: { id },
data: {
...(data.name !== undefined ? { name: data.name } : {}),
...(data.url !== undefined ? { url: data.url } : {}),
...(data.events !== undefined ? { events: data.events } : {}),
...(data.headers !== undefined ? { headers: data.headers } : {}),
...(data.isActive !== undefined ? { isActive: data.isActive } : {}),
...(data.maxRetries !== undefined ? { maxRetries: data.maxRetries } : {}),
},
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'UPDATE_WEBHOOK',
entityType: 'Webhook',
entityId: id,
detailsJson: { updatedFields: Object.keys(data) },
})
} catch {}
return webhook
}),
/**
* Delete a webhook and its delivery history.
*/
delete: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Cascade delete is defined in schema, so just delete the webhook
await ctx.prisma.webhook.delete({
where: { id: input.id },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'DELETE_WEBHOOK',
entityType: 'Webhook',
entityId: input.id,
})
} catch {}
return { success: true }
}),
/**
* Send a test payload to a webhook.
*/
test: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const webhook = await ctx.prisma.webhook.findUnique({
where: { id: input.id },
})
if (!webhook) {
throw new Error('Webhook not found')
}
const testPayload = {
event: 'test',
timestamp: new Date().toISOString(),
data: {
message: 'This is a test webhook delivery from MOPC Platform.',
webhookId: webhook.id,
webhookName: webhook.name,
},
}
const delivery = await ctx.prisma.webhookDelivery.create({
data: {
webhookId: webhook.id,
event: 'test',
payload: testPayload,
status: 'PENDING',
attempts: 0,
},
})
await deliverWebhook(delivery.id)
// Fetch updated delivery to get the result
const result = await ctx.prisma.webhookDelivery.findUnique({
where: { id: delivery.id },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'TEST_WEBHOOK',
entityType: 'Webhook',
entityId: input.id,
detailsJson: { deliveryStatus: result?.status },
})
} catch {}
return result
}),
/**
* Get paginated delivery log for a webhook.
*/
getDeliveryLog: superAdminProcedure
.input(
z.object({
webhookId: z.string(),
page: z.number().int().min(1).default(1),
pageSize: z.number().int().min(1).max(100).default(20),
})
)
.query(async ({ ctx, input }) => {
const skip = (input.page - 1) * input.pageSize
const [items, total] = await Promise.all([
ctx.prisma.webhookDelivery.findMany({
where: { webhookId: input.webhookId },
orderBy: { createdAt: 'desc' },
skip,
take: input.pageSize,
}),
ctx.prisma.webhookDelivery.count({
where: { webhookId: input.webhookId },
}),
])
return {
items,
total,
page: input.page,
pageSize: input.pageSize,
totalPages: Math.ceil(total / input.pageSize),
}
}),
/**
* Regenerate the HMAC secret for a webhook.
*/
regenerateSecret: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const newSecret = generateWebhookSecret()
const webhook = await ctx.prisma.webhook.update({
where: { id: input.id },
data: { secret: newSecret },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'REGENERATE_WEBHOOK_SECRET',
entityType: 'Webhook',
entityId: input.id,
})
} catch {}
return webhook
}),
/**
* Get available webhook events.
*/
getAvailableEvents: superAdminProcedure.query(() => {
return WEBHOOK_EVENTS
}),
})
import { z } from 'zod'
import { router, superAdminProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import {
generateWebhookSecret,
deliverWebhook,
} from '@/server/services/webhook-dispatcher'
export const WEBHOOK_EVENTS = [
'evaluation.submitted',
'evaluation.updated',
'project.created',
'project.statusChanged',
'round.activated',
'round.closed',
'stage.activated',
'stage.closed',
'assignment.created',
'assignment.completed',
'user.invited',
'user.activated',
] as const
export const webhookRouter = router({
/**
* List all webhooks with delivery stats.
*/
list: superAdminProcedure.query(async ({ ctx }) => {
const webhooks = await ctx.prisma.webhook.findMany({
include: {
_count: {
select: { deliveries: true },
},
createdBy: {
select: { id: true, name: true, email: true },
},
},
orderBy: { createdAt: 'desc' },
})
// Compute recent delivery stats for each webhook
const now = new Date()
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const stats = await Promise.all(
webhooks.map(async (wh) => {
const [delivered, failed] = await Promise.all([
ctx.prisma.webhookDelivery.count({
where: {
webhookId: wh.id,
status: 'DELIVERED',
createdAt: { gte: twentyFourHoursAgo },
},
}),
ctx.prisma.webhookDelivery.count({
where: {
webhookId: wh.id,
status: 'FAILED',
createdAt: { gte: twentyFourHoursAgo },
},
}),
])
return {
...wh,
recentDelivered: delivered,
recentFailed: failed,
}
})
)
return stats
}),
/**
* Create a new webhook.
*/
create: superAdminProcedure
.input(
z.object({
name: z.string().min(1).max(200),
url: z.string().url(),
events: z.array(z.string()).min(1),
headers: z.any().optional(),
maxRetries: z.number().int().min(0).max(10).default(3),
})
)
.mutation(async ({ ctx, input }) => {
const secret = generateWebhookSecret()
const webhook = await ctx.prisma.webhook.create({
data: {
name: input.name,
url: input.url,
secret,
events: input.events,
headers: input.headers ?? undefined,
maxRetries: input.maxRetries,
createdById: ctx.user.id,
},
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'CREATE_WEBHOOK',
entityType: 'Webhook',
entityId: webhook.id,
detailsJson: { name: input.name, url: input.url, events: input.events },
})
} catch {}
return webhook
}),
/**
* Update a webhook.
*/
update: superAdminProcedure
.input(
z.object({
id: z.string(),
name: z.string().min(1).max(200).optional(),
url: z.string().url().optional(),
events: z.array(z.string()).min(1).optional(),
headers: z.any().optional(),
isActive: z.boolean().optional(),
maxRetries: z.number().int().min(0).max(10).optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input
const webhook = await ctx.prisma.webhook.update({
where: { id },
data: {
...(data.name !== undefined ? { name: data.name } : {}),
...(data.url !== undefined ? { url: data.url } : {}),
...(data.events !== undefined ? { events: data.events } : {}),
...(data.headers !== undefined ? { headers: data.headers } : {}),
...(data.isActive !== undefined ? { isActive: data.isActive } : {}),
...(data.maxRetries !== undefined ? { maxRetries: data.maxRetries } : {}),
},
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'UPDATE_WEBHOOK',
entityType: 'Webhook',
entityId: id,
detailsJson: { updatedFields: Object.keys(data) },
})
} catch {}
return webhook
}),
/**
* Delete a webhook and its delivery history.
*/
delete: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Cascade delete is defined in schema, so just delete the webhook
await ctx.prisma.webhook.delete({
where: { id: input.id },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'DELETE_WEBHOOK',
entityType: 'Webhook',
entityId: input.id,
})
} catch {}
return { success: true }
}),
/**
* Send a test payload to a webhook.
*/
test: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const webhook = await ctx.prisma.webhook.findUnique({
where: { id: input.id },
})
if (!webhook) {
throw new Error('Webhook not found')
}
const testPayload = {
event: 'test',
timestamp: new Date().toISOString(),
data: {
message: 'This is a test webhook delivery from MOPC Platform.',
webhookId: webhook.id,
webhookName: webhook.name,
},
}
const delivery = await ctx.prisma.webhookDelivery.create({
data: {
webhookId: webhook.id,
event: 'test',
payload: testPayload,
status: 'PENDING',
attempts: 0,
},
})
await deliverWebhook(delivery.id)
// Fetch updated delivery to get the result
const result = await ctx.prisma.webhookDelivery.findUnique({
where: { id: delivery.id },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'TEST_WEBHOOK',
entityType: 'Webhook',
entityId: input.id,
detailsJson: { deliveryStatus: result?.status },
})
} catch {}
return result
}),
/**
* Get paginated delivery log for a webhook.
*/
getDeliveryLog: superAdminProcedure
.input(
z.object({
webhookId: z.string(),
page: z.number().int().min(1).default(1),
pageSize: z.number().int().min(1).max(100).default(20),
})
)
.query(async ({ ctx, input }) => {
const skip = (input.page - 1) * input.pageSize
const [items, total] = await Promise.all([
ctx.prisma.webhookDelivery.findMany({
where: { webhookId: input.webhookId },
orderBy: { createdAt: 'desc' },
skip,
take: input.pageSize,
}),
ctx.prisma.webhookDelivery.count({
where: { webhookId: input.webhookId },
}),
])
return {
items,
total,
page: input.page,
pageSize: input.pageSize,
totalPages: Math.ceil(total / input.pageSize),
}
}),
/**
* Regenerate the HMAC secret for a webhook.
*/
regenerateSecret: superAdminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const newSecret = generateWebhookSecret()
const webhook = await ctx.prisma.webhook.update({
where: { id: input.id },
data: { secret: newSecret },
})
try {
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'REGENERATE_WEBHOOK_SECRET',
entityType: 'Webhook',
entityId: input.id,
})
} catch {}
return webhook
}),
/**
* Get available webhook events.
*/
getAvailableEvents: superAdminProcedure.query(() => {
return WEBHOOK_EVENTS
}),
})