WhatsApp/Signal/Formstack/admin updates

This commit is contained in:
Darren Clarke 2025-11-21 14:55:28 +01:00
parent bcecf61a46
commit d0cc5a21de
451 changed files with 16139 additions and 39623 deletions

View file

@ -1 +1,4 @@
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
export { receiveMessage as POST } from "@link-stack/bridge-ui";

View file

@ -0,0 +1,4 @@
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
export { relinkBot as POST } from "@link-stack/bridge-ui";

View file

@ -1 +1,4 @@
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
export { getBot as GET } from "@link-stack/bridge-ui";

View file

@ -1 +1,4 @@
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
export { sendMessage as POST } from "@link-stack/bridge-ui";

View file

@ -1,3 +1,6 @@
import { handleWebhook } from "@link-stack/bridge-ui";
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
export { handleWebhook as GET, handleWebhook as POST };

View file

@ -1,6 +1,9 @@
import NextAuth from "next-auth";
import { authOptions } from "@/app/_lib/authentication";
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View file

@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from "next/server";
import { createLogger } from "@link-stack/logger";
import { getWorkerUtils } from "@link-stack/bridge-common";
import { timingSafeEqual } from "crypto";
// Force this route to be dynamic (not statically generated at build time)
export const dynamic = 'force-dynamic';
const logger = createLogger('formstack-webhook');
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
const clientIp = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || 'unknown';
// Get the shared secret from environment variable
const expectedSecret = process.env.FORMSTACK_SHARED_SECRET;
if (!expectedSecret) {
logger.error('FORMSTACK_SHARED_SECRET environment variable is not configured');
return NextResponse.json(
{ error: "Server configuration error" },
{ status: 500 }
);
}
// Get the shared secret from the request body
const body = await req.json();
const receivedSecret = body.HandshakeKey;
// Validate that secret is provided
if (!receivedSecret || typeof receivedSecret !== 'string') {
logger.warn({ clientIp }, 'Missing or invalid HandshakeKey');
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Use timing-safe comparison to prevent timing attacks
const expectedBuffer = Buffer.from(expectedSecret);
const receivedBuffer = Buffer.from(receivedSecret);
let secretsMatch = false;
if (expectedBuffer.length === receivedBuffer.length) {
try {
secretsMatch = timingSafeEqual(expectedBuffer, receivedBuffer);
} catch (e) {
secretsMatch = false;
}
}
if (!secretsMatch) {
logger.warn({
secretMatch: false,
timestamp: new Date().toISOString(),
userAgent: req.headers.get('user-agent'),
clientIp
}, 'Invalid shared secret received');
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Log webhook receipt with non-PII metadata only
logger.info({
formId: body.FormID,
uniqueId: body.UniqueID,
timestamp: new Date().toISOString(),
fieldCount: Object.keys(body).length
}, 'Received Formstack webhook');
// Enqueue a bridge-worker task to process this form submission
const worker = await getWorkerUtils();
await worker.addJob('formstack/create-ticket-from-form', {
formData: body,
receivedAt: new Date().toISOString(),
});
logger.info('Formstack webhook task enqueued successfully');
return NextResponse.json({
status: "success",
message: "Webhook received and queued for processing"
});
} catch (error) {
logger.error({
error: error instanceof Error ? error.message : error,
stack: error instanceof Error ? error.stack : undefined,
}, 'Error processing Formstack webhook');
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const allCookies = request.cookies.getAll();
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
const signOutURL = `${zammadURL}/api/v1/signout`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
Cookie: allCookies
.map((cookie) => `${cookie.name}=${cookie.value}`)
.join("; "),
};
// Add timeout to prevent hanging requests
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5 second timeout
try {
await fetch(signOutURL, {
headers,
signal: controller.signal
});
} catch (error) {
// Log but don't fail logout if Zammad signout fails
console.error('Zammad signout failed:', error);
} finally {
clearTimeout(timeout);
}
const cookiePrefixesToRemove = ["_zammad"];
const response = NextResponse.json({ message: "ok" });
for (const cookie of allCookies) {
if (
cookiePrefixesToRemove.some((prefix) => cookie.name.startsWith(prefix))
) {
response.cookies.set(cookie.name, "", { path: "/", maxAge: 0 });
}
}
return response;
}