feat: initial commit — storefront, convex backend, and shared packages
Completes the first milestone of The Pet Loft ecommerce platform: - apps/storefront: full customer-facing Next.js app with HeroUI (cart, checkout, orders, wishlist, product detail, shop, search, auth) - convex/: serverless backend with schema, queries, mutations, actions, HTTP routes, Stripe/Shippo integrations, and co-located tests - packages/types, packages/utils, packages/convex: shared workspace packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
convex/http.ts
Normal file
80
convex/http.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { httpRouter } from "convex/server";
|
||||
import { httpAction } from "./_generated/server";
|
||||
import { internal } from "./_generated/api";
|
||||
import type { WebhookEvent } from "@clerk/backend";
|
||||
import { Webhook } from "svix";
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
http.route({
|
||||
path: "/clerk-users-webhook",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
const event = await validateRequest(request);
|
||||
if (!event) return new Response("Error", { status: 400 });
|
||||
|
||||
switch (event.type) {
|
||||
case "user.created":
|
||||
case "user.updated":
|
||||
await ctx.runMutation(internal.users.upsertFromClerk, {
|
||||
externalId: event.data.id,
|
||||
name:
|
||||
`${event.data.first_name ?? ""} ${event.data.last_name ?? ""}`.trim(),
|
||||
email: event.data.email_addresses[0]?.email_address ?? "",
|
||||
avatarUrl: event.data.image_url ?? undefined,
|
||||
});
|
||||
break;
|
||||
case "user.deleted":
|
||||
if (event.data.id) {
|
||||
await ctx.runMutation(internal.users.deleteFromClerk, {
|
||||
externalId: event.data.id,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Ignored webhook event:", event.type);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 200 });
|
||||
}),
|
||||
});
|
||||
|
||||
async function validateRequest(
|
||||
req: Request,
|
||||
): Promise<WebhookEvent | null> {
|
||||
const payload = await req.text();
|
||||
const headers = {
|
||||
"svix-id": req.headers.get("svix-id")!,
|
||||
"svix-timestamp": req.headers.get("svix-timestamp")!,
|
||||
"svix-signature": req.headers.get("svix-signature")!,
|
||||
};
|
||||
try {
|
||||
return new Webhook(process.env.CLERK_WEBHOOK_SECRET!).verify(
|
||||
payload,
|
||||
headers,
|
||||
) as WebhookEvent;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
http.route({
|
||||
path: "/stripe/webhook",
|
||||
method: "POST",
|
||||
handler: httpAction(async (ctx, request) => {
|
||||
const body = await request.text();
|
||||
const signature = request.headers.get("stripe-signature");
|
||||
if (!signature) {
|
||||
return new Response("Missing stripe-signature header", { status: 400 });
|
||||
}
|
||||
|
||||
const result = await ctx.runAction(internal.stripeActions.handleWebhook, {
|
||||
payload: body,
|
||||
signature,
|
||||
});
|
||||
|
||||
return new Response(null, { status: result.success ? 200 : 400 });
|
||||
}),
|
||||
});
|
||||
|
||||
export default http;
|
||||
Reference in New Issue
Block a user