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"; import crypto from "node:crypto";
export const config = {
runtime: "nodejs20.x",
};
// Your secret key provided by Lemon Squeezy // Your secret key provided by Lemon Squeezy
const SECRET_KEY = import.meta.env.VITE_LEMON_SQUEEZY_SECRET_KEY; 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 }) => { export const POST = async ({ request, locals }) => {
try { try {
// Retrieve the X-Signature header from the request const bodyText = await request.text();
const body = await request.text();
const hmac = crypto.createHmac("sha256", SECRET_KEY); // Retrieve the signature header; return early if missing.
const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8"); const signatureHeader = request.headers.get("x-Signature");
const signature = Buffer.from( if (!signatureHeader) {
request?.headers?.get("x-Signature") || "", console.error("Missing x-Signature header.");
"utf8", return new Response(
); JSON.stringify({ error: "Missing signature header" }),
{
if (!crypto.timingSafeEqual(digest, signature)) { status: 403,
console.log("error"); headers: { "Content-Type": "application/json" },
return new Response(JSON.stringify({ error: "Invalid signature" }), { }
status: 403, );
headers: {
"Content-Type": "application/json",
},
});
} }
// Print out the data (replace this with your actual handling logic) if (!isValidSignature(bodyText, signatureHeader)) {
const output = JSON.parse(body); console.error("Signature verification failed.");
//console.log('Received payment data:', output); return new Response(
const userId = output?.meta?.custom_data?.userId; JSON.stringify({ error: "Invalid signature" }),
const status = output?.data?.attributes?.status; {
const refunded = output?.data?.attributes?.refunded; status: 403,
let tier; headers: { "Content-Type": "application/json" },
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";
} }
//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 { try {
await locals.pb.collection("users").update(userId, { await locals.pb.collection("users").update(userId, {
tier: tier, tier,
freeTrial: false, freeTrial: false,
}); });
/* const paymentData = { user: userId, data: payload };
if(status !== 'paid') { await locals.pb.collection("payments").create(paymentData);
const data = {'user': userId, 'data': output} } catch (dbError) {
await locals.pb.collection('payments').create(data); console.error("Database error:", dbError);
} // Depending on your requirements, you might want to propagate this error.
*/
const data = { user: userId, data: output };
await locals.pb.collection("payments").create(data);
} catch (e) {
console.log(e);
} }
// Return a response indicating successful receipt of data
return new Response( return new Response(
JSON.stringify({ message: "Payment data received successfully" }), JSON.stringify({ message: "Payment data received successfully" }),
{ {
status: 200, status: 200,
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json", }
},
},
); );
} catch (error) { } catch (error) {
console.error("Error processing request:", error); console.error("Error processing request:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), { return new Response(
status: 500, JSON.stringify({ error: "Internal server error" }),
headers: { {
"Content-Type": "application/json", status: 500,
}, headers: { "Content-Type": "application/json" },
}); }
);
} }
}; };

View File

@ -509,7 +509,7 @@
? 'cursor-pointer' ? 'cursor-pointer'
: 'cursor-not-allowed'} {subscriptionData?.card_brand !== : 'cursor-not-allowed'} {subscriptionData?.card_brand !==
null && subscriptionData?.card_brand?.length !== 0 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" : 'bg-gray-600 opacity-[0.8] text-white'} text-sm sm:text-[1rem] px-4 py-2 rounded mt-5"
> >
Change to Annual Plan Change to Annual Plan
@ -592,10 +592,10 @@
method="POST" method="POST"
action="?/cancelSubscription" action="?/cancelSubscription"
use:enhance={submitCancellation} 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 <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"> <div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3> <h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
@ -609,7 +609,7 @@
on:click={() => (isClicked = !isClicked)} on:click={() => (isClicked = !isClicked)}
class="{!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 Cancel Subscription
<input <input
@ -620,11 +620,11 @@
</button> </button>
{#if isClicked === true} {#if isClicked === true}
<label <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"> <div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span> <span class="loading loading-infinity"></span>
<span class="text-white ml-2">Proceeding</span> <span class="text-black ml-2">Proceeding</span>
</div> </div>
</label> </label>
{/if} {/if}
@ -649,10 +649,10 @@
method="POST" method="POST"
action="?/reactivateSubscription" action="?/reactivateSubscription"
use:enhance={submitReactivate} 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 <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"> <div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Reactivate Subscription</h3> <h3 class="font-bold text-2xl mb-5">Reactivate Subscription</h3>
@ -666,7 +666,7 @@
on:click={() => (isClicked = !isClicked)} on:click={() => (isClicked = !isClicked)}
class="{!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 Proceed
<input <input
@ -677,11 +677,11 @@
</button> </button>
{#if isClicked === true} {#if isClicked === true}
<label <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"> <div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span> <span class="loading loading-infinity"></span>
<span class="text-white ml-2">Proceeding</span> <span class="text-black ml-2">Proceeding</span>
</div> </div>
</label> </label>
{/if} {/if}
@ -703,10 +703,10 @@
method="POST" method="POST"
action="?/changeSubscription" action="?/changeSubscription"
use:enhance={submitChangePlan} 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 <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"> <div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3> <h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
@ -719,7 +719,7 @@
on:click={() => (isClicked = !isClicked)} on:click={() => (isClicked = !isClicked)}
class="{!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 Proceed
<input <input
@ -730,7 +730,7 @@
</button> </button>
{#if isClicked === true} {#if isClicked === true}
<label <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"> <div class="flex flex-row m-auto">
<span class="loading loading-infinity"></span> <span class="loading loading-infinity"></span>
@ -752,9 +752,9 @@
></label> ></label>
<!-- Desktop modal content --> <!-- 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 <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"> <div class="text-white mb-5 text-center">
<h3 class="font-bold text-2xl mb-5">Paypal not supported</h3> <h3 class="font-bold text-2xl mb-5">Paypal not supported</h3>