This commit is contained in:
MuslemRahimi 2025-02-12 23:26:51 +01:00
parent 93a9ae46ab
commit 9d2d59172b
2 changed files with 113 additions and 79 deletions

View File

@ -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" },
}
);
}
};

View File

@ -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"
>
<div
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-gray-500"
/>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
@ -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
<input
@ -620,11 +620,11 @@
</button>
{#if isClicked === true}
<label
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-red-600 text-center text-white text-[1rem] font-normal"
class="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"
>
<div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span>
<span class="text-white ml-2">Proceeding</span>
<span class="text-black ml-2">Proceeding</span>
</div>
</label>
{/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"
>
<div
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-gray-500"
/>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Reactivate Subscription</h3>
@ -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
<input
@ -677,11 +677,11 @@
</button>
{#if isClicked === true}
<label
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#417143] text-center text-white text-[1rem] font-normal"
class="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"
>
<div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span>
<span class="text-white ml-2">Proceeding</span>
<span class="text-black ml-2">Proceeding</span>
</div>
</label>
{/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"
>
<div
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-gray-500"
/>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
@ -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
<input
@ -730,7 +730,7 @@
</button>
{#if isClicked === true}
<label
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#fff] text-center text-black text-[1rem] font-normal"
class="cursor-pointer px-7 py-2 mb-5 rounded bg-[#fff] text-center text-black text-[1rem] font-normal"
>
<div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span>
@ -752,9 +752,9 @@
></label>
<!-- Desktop modal content -->
<div class="modal-box w-full bg-default flex flex-col items-center">
<div class="modal-box w-full bg-secondary flex flex-col items-center">
<div
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-gray-500"
/>
<div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Paypal not supported</h3>