ui fixes
This commit is contained in:
parent
93a9ae46ab
commit
9d2d59172b
@ -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" },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user