From 9d2d59172bf70a77964c8761223982a67eb2c1f8 Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Wed, 12 Feb 2025 23:26:51 +0100 Subject: [PATCH] ui fixes --- src/routes/payment/+server.ts | 158 +++++++++++++++++++------------- src/routes/profile/+page.svelte | 34 +++---- 2 files changed, 113 insertions(+), 79 deletions(-) diff --git a/src/routes/payment/+server.ts b/src/routes/payment/+server.ts index 2198ee07..ad1c1646 100644 --- a/src/routes/payment/+server.ts +++ b/src/routes/payment/+server.ts @@ -1,89 +1,123 @@ import crypto from "node:crypto"; -export const config = { - runtime: "nodejs20.x", -}; - // Your secret key provided by Lemon Squeezy const SECRET_KEY = import.meta.env.VITE_LEMON_SQUEEZY_SECRET_KEY; -// Request handler for the payment route +if (!SECRET_KEY) { + throw new Error("Missing Lemon Squeezy secret key."); +} + +/** + * Verifies that the provided signature matches the HMAC digest for the given payload. + * + * @param {string} payload - The raw request body. + * @param {string} signatureHeader - The signature from the request header. + * @returns {boolean} - True if the signature is valid; otherwise, false. + */ +function isValidSignature(payload, signatureHeader) { + const hmac = crypto.createHmac("sha256", SECRET_KEY); + const computedDigestHex = hmac.update(payload).digest("hex"); + + // Convert both values to buffers for timing-safe comparison + const computedBuffer = Buffer.from(computedDigestHex, "utf8"); + const signatureBuffer = Buffer.from(signatureHeader, "utf8"); + + // Ensure the buffers are the same length; if not, they can't be equal. + if (computedBuffer.length !== signatureBuffer.length) { + return false; + } + + return crypto.timingSafeEqual(computedBuffer, signatureBuffer); +} + +/** + * Determines the user's tier based on the payment status and refund flag. + * + * @param {string} status - The payment status. + * @param {boolean} refunded - Whether the payment was refunded. + * @returns {string} - "Pro" if conditions match, otherwise "Free". + */ +function determineTier(status, refunded) { + // List of statuses that qualify for the "Pro" tier if not refunded + const proStatuses = new Set(["paid", "active", "cancelled", "on_trial"]); + return !refunded && proStatuses.has(status) ? "Pro" : "Free"; +} + export const POST = async ({ request, locals }) => { try { - // Retrieve the X-Signature header from the request - const body = await request.text(); + const bodyText = await request.text(); - const hmac = crypto.createHmac("sha256", SECRET_KEY); - const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8"); - const signature = Buffer.from( - request?.headers?.get("x-Signature") || "", - "utf8", - ); - - if (!crypto.timingSafeEqual(digest, signature)) { - console.log("error"); - return new Response(JSON.stringify({ error: "Invalid signature" }), { - status: 403, - headers: { - "Content-Type": "application/json", - }, - }); + // Retrieve the signature header; return early if missing. + const signatureHeader = request.headers.get("x-Signature"); + if (!signatureHeader) { + console.error("Missing x-Signature header."); + return new Response( + JSON.stringify({ error: "Missing signature header" }), + { + status: 403, + headers: { "Content-Type": "application/json" }, + } + ); } - // Print out the data (replace this with your actual handling logic) - const output = JSON.parse(body); - //console.log('Received payment data:', output); - const userId = output?.meta?.custom_data?.userId; - const status = output?.data?.attributes?.status; - const refunded = output?.data?.attributes?.refunded; - let tier; - if (status === "paid" && refunded !== true) { - tier = "Pro"; - } else if (status === "active" && refunded !== true) { - tier = "Pro"; - } else if (status === "cancelled" && refunded !== true) { - tier = "Pro"; - } else if (status === "on_trial" && refunded !== true) { - tier = "Pro"; - } else { - tier = "Free"; + if (!isValidSignature(bodyText, signatureHeader)) { + console.error("Signature verification failed."); + return new Response( + JSON.stringify({ error: "Invalid signature" }), + { + status: 403, + headers: { "Content-Type": "application/json" }, + } + ); } - //console.log(status, refunded, tier) + + // Parse the JSON payload + const payload = JSON.parse(bodyText); + const userId = payload?.meta?.custom_data?.userId; + const { status, refunded } = payload?.data?.attributes || {}; + + if (!userId || status === undefined) { + console.error("Missing userId or status in payload:", payload); + return new Response( + JSON.stringify({ error: "Invalid payload structure" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ); + } + + const tier = determineTier(status, refunded); + + // Update the user and log the payment try { await locals.pb.collection("users").update(userId, { - tier: tier, + tier, freeTrial: false, }); - /* - if(status !== 'paid') { - const data = {'user': userId, 'data': output} - await locals.pb.collection('payments').create(data); - } - */ - const data = { user: userId, data: output }; - await locals.pb.collection("payments").create(data); - } catch (e) { - console.log(e); + const paymentData = { user: userId, data: payload }; + await locals.pb.collection("payments").create(paymentData); + } catch (dbError) { + console.error("Database error:", dbError); + // Depending on your requirements, you might want to propagate this error. } - // Return a response indicating successful receipt of data return new Response( JSON.stringify({ message: "Payment data received successfully" }), { status: 200, - headers: { - "Content-Type": "application/json", - }, - }, + headers: { "Content-Type": "application/json" }, + } ); } catch (error) { console.error("Error processing request:", error); - return new Response(JSON.stringify({ error: "Internal server error" }), { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }); + return new Response( + JSON.stringify({ error: "Internal server error" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + } + ); } }; diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte index af3b2a13..ecbfd6a0 100644 --- a/src/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -509,7 +509,7 @@ ? 'cursor-pointer' : 'cursor-not-allowed'} {subscriptionData?.card_brand !== null && subscriptionData?.card_brand?.length !== 0 - ? 'bg-white sm:hover:bg-white/80 text-black font-semibold' + ? 'bg-white sm:hover:bg-white/80 text-black' : 'bg-gray-600 opacity-[0.8] text-white'} text-sm sm:text-[1rem] px-4 py-2 rounded mt-5" > Change to Annual Plan @@ -592,10 +592,10 @@ method="POST" action="?/cancelSubscription" use:enhance={submitCancellation} - class="modal-box w-full bg-[#272727A] flex flex-col items-center" + class="modal-box w-full bg-secondary flex flex-col items-center" >

Are you sure?

@@ -609,7 +609,7 @@ on:click={() => (isClicked = !isClicked)} class="{!isClicked ? '' - : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded bg-red-600 text-center text-white text-[1rem] font-normal" + : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded bg-white sm:hover:bg-white/80 ease-out duration-50 text-center text-black text-[1rem] font-normal" > Cancel Subscription {#if isClicked === true} {/if} @@ -649,10 +649,10 @@ method="POST" action="?/reactivateSubscription" use:enhance={submitReactivate} - class="modal-box w-full bg-[#272727A] flex flex-col items-center" + class="modal-box w-full bg-secondary flex flex-col items-center" >

Reactivate Subscription

@@ -666,7 +666,7 @@ on:click={() => (isClicked = !isClicked)} class="{!isClicked ? '' - : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded bg-[#fff] sm:hover:bg-gray-300 text-center text-black text-[1rem] font-medium" + : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded bg-white sm:hover:bg-white/80 ease-out duration-50 text-center text-black text-[1rem] font-normal" > Proceed {#if isClicked === true} {/if} @@ -703,10 +703,10 @@ method="POST" action="?/changeSubscription" use:enhance={submitChangePlan} - class="modal-box w-full bg-[#272727A] flex flex-col items-center" + class="modal-box w-full bg-secondary flex flex-col items-center" >

Are you sure?

@@ -719,7 +719,7 @@ on:click={() => (isClicked = !isClicked)} class="{!isClicked ? '' - : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded-full text-center bg-[#fff] text-black font-semibold text-[1rem] font-semibold" + : 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded text-center bg-[#fff] text-black text-[1rem]" > Proceed {#if isClicked === true}