This commit is contained in:
Dominic Ferrando
2025-10-17 01:50:12 -04:00
parent eb8a2ab933
commit 0e57e60f3f
9 changed files with 635 additions and 81 deletions
+3
View File
@@ -1,2 +1,5 @@
deploy-secrets: deploy-secrets:
fly secrets import < .env fly secrets import < .env
deploy:
bun vite build && fly deploy
+3 -3
View File
@@ -4741,7 +4741,7 @@
"metric": "weight", "metric": "weight",
"spread": 3, "spread": 3,
"step": 12, "step": 12,
"ratio": "inverse" "ratio": "normal"
} }
], ],
"unit": "ms", "unit": "ms",
@@ -5285,13 +5285,13 @@
"metric": "weight", "metric": "weight",
"spread": 3, "spread": 3,
"step": 12, "step": 12,
"ratio": "inverse" "ratio": "normal"
}, },
{ {
"metric": "age", "metric": "age",
"spread": 4, "spread": 4,
"step": 10, "step": 10,
"ratio": "inverse" "ratio": "normal"
} }
], ],
"unit": "ms", "unit": "ms",
+53 -4
View File
@@ -3,11 +3,11 @@ import { cors } from "@elysiajs/cors";
import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main"; import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main";
import type { ActivityPerformance, Player } from "$lib/services/calculator/util"; import type { ActivityPerformance, Player } from "$lib/services/calculator/util";
import rawStandards from "$lib/data/standards.json" assert { type: "json" } import rawStandards from "$lib/data/standards.json" assert { type: "json" }
import { Printful } from "../services/commerce/util/types"; import { Printful, Webflow } from "../services/commerce/util/types";
import WebflowService from "../services/commerce/webflow"; import WebflowService from "../services/commerce/webflow";
import SyncService from "../services/commerce/sync"; import SyncService from "../services/commerce/sync";
import PrintfulService from "../services/commerce/printful"; import PrintfulService from "../services/commerce/printful";
import { FetchError } from "../services/commerce/util/misc"; import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
export const app = new Elysia({ prefix: "/api" }) export const app = new Elysia({ prefix: "/api" })
.use(cors({ origin: '*' })) .use(cors({ origin: '*' }))
@@ -16,10 +16,10 @@ export const app = new Elysia({ prefix: "/api" })
FetchError FetchError
}) })
.onError(({ code, error }) => { .onError(async ({ code, error }) => {
switch (code) { switch (code) {
case "FetchError": case "FetchError":
console.error(error.message, error.payload); console.error(error.message, await error.parse());
return error; return error;
} }
}) })
@@ -75,6 +75,55 @@ export const app = new Elysia({ prefix: "/api" })
} }
case Printful.Webhook.Event.ProductDeleted: { case Printful.Webhook.Event.ProductDeleted: {
await WebflowService.Products.remove(payload.data.sync_product.external_id); await WebflowService.Products.remove(payload.data.sync_product.external_id);
break;
}
}
})
.post("/webhook/webflow", async ({ request, body, set }) => {
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
set.status = 400;
return "Invalid signature";
}
const payload = body as Webflow.Webhook.EventPayload;
console.log(payload.triggerType);
switch (payload.triggerType) {
case Webflow.Webhook.Event.OrderCreated: {
const webflowOrder = payload.payload;
console.log("Creating printful order...");
await PrintfulService.Orders.create({
external_id: webflowOrder.orderId,
// TODO: derive from webflow
shipping: "STANDARD",
recipient: {
name: webflowOrder.shippingAddress.addressee,
address1: webflowOrder.shippingAddress.line1,
address2: webflowOrder.shippingAddress.line2,
city: webflowOrder.shippingAddress.city,
state_code: getStateFromZip(webflowOrder.shippingAddress.postalCode),
country_code: webflowOrder.shippingAddress.country,
zip: webflowOrder.shippingAddress.postalCode
},
items: webflowOrder.purchasedItems.map(webflowOrderSku => ({
external_variant_id: webflowOrderSku.variantId,
quantity: webflowOrderSku.count
}))
});
// set webflow order to pending
console.log("Complete");
break;
}
case Webflow.Webhook.Event.OrderUpdated: {
// read printful order status
// set webflow order to fulfulled (if indeed printful order got confirmed)
break; break;
} }
} }
+25 -10
View File
@@ -178,11 +178,15 @@ export class LevelCalculator {
} }
export type StandardsConfig = { export type StandardsConfig = {
global: {
maxLevel: number, maxLevel: number,
},
activity: Record<Activity, {
weightModifier: number, weightModifier: number,
weightSkew: number, weightSkew: number,
ageModifier: number, ageModifier: number,
disableGeneration: boolean disableGeneration: boolean
}>
} }
export class Standards { export class Standards {
@@ -190,12 +194,23 @@ export class Standards {
private activityStandards: ActivityStandards; private activityStandards: ActivityStandards;
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) { constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) {
const defaultActivitiesConfig = Object.fromEntries(
Object.values(Activity).map(activity => [
activity,
{
disableGeneration: false,
weightModifier: 0.1,
weightSkew: 0,
ageModifier: 0.1,
},
])
) as Record<Activity, StandardsConfig["activity"][Activity]>;
this.cfg = { this.cfg = {
maxLevel: cfg?.maxLevel ?? 100, global: {
weightModifier: cfg?.weightModifier ?? .1, maxLevel: cfg?.global?.maxLevel ?? 100,
weightSkew: cfg?.weightSkew ?? .1, },
ageModifier: cfg?.ageModifier ?? .1, activity: Object.assign(defaultActivitiesConfig, cfg?.activity)
disableGeneration: cfg?.disableGeneration ?? false
}; };
// prepare data // prepare data
@@ -204,12 +219,12 @@ export class Standards {
// expand/compress // expand/compress
for (const standard of this.activityStandards[activity].standards) { for (const standard of this.activityStandards[activity].standards) {
const expandedLevels = this.expandLevels(standard.levels, 5); const expandedLevels = this.expandLevels(standard.levels, 5);
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.maxLevel); const compressedLevels = this.compressLevels(expandedLevels, this.cfg.global.maxLevel);
standard.levels = compressedLevels; standard.levels = compressedLevels;
} }
// generate data // generate data
const allGenerators = this.cfg.disableGeneration ? const allGenerators = this.cfg.activity[activity].disableGeneration ?
[] : [] :
this.activityStandards[activity].metadata.generators; this.activityStandards[activity].metadata.generators;
@@ -241,7 +256,7 @@ export class Standards {
for (const lvl in referenceStandard.levels) { for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue; if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.ageModifier; const scalingExponent = this.cfg.activity[activity].ageModifier;
const coefficient = ageGenerator.ratio === "inverse" ? const coefficient = ageGenerator.ratio === "inverse" ?
referenceAge / currAge : referenceAge / currAge :
currAge / referenceAge; currAge / referenceAge;
@@ -271,7 +286,7 @@ export class Standards {
for (const age of this.agesFor(activity, gender)) { for (const age of this.agesFor(activity, gender)) {
const referenceWeight = getAvgWeight(gender, age); const referenceWeight = getAvgWeight(gender, age);
const skewRatio = this.cfg.weightSkew; // 0 = normal, 1 = max skew const skewRatio = this.cfg.activity[activity].weightSkew; // 0 = normal, 1 = max skew
const minWeight = referenceWeight - (weightGenerator.step * weightGenerator.spread * (1 - skewRatio)); const minWeight = referenceWeight - (weightGenerator.step * weightGenerator.spread * (1 - skewRatio));
const maxWeight = referenceWeight + (weightGenerator.step * weightGenerator.spread * (1 + skewRatio)); const maxWeight = referenceWeight + (weightGenerator.step * weightGenerator.spread * (1 + skewRatio));
@@ -295,7 +310,7 @@ export class Standards {
for (const lvl in referenceStandard.levels) { for (const lvl in referenceStandard.levels) {
if (!referenceStandard.levels[lvl]) continue; if (!referenceStandard.levels[lvl]) continue;
const scalingExponent = this.cfg.weightModifier; const scalingExponent = this.cfg.activity[activity].weightModifier;
const coefficient = weightGenerator.ratio === "inverse" ? const coefficient = weightGenerator.ratio === "inverse" ?
referenceWeight / currWeight : referenceWeight / currWeight :
currWeight / referenceWeight; currWeight / referenceWeight;
+21 -4
View File
@@ -15,7 +15,7 @@ export default class PrintfulService {
} }
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to get Printful variant", res); throw new FetchError("Failed to get Printful variant", res);
} }
const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> = await res.json(); const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> = await res.json();
@@ -34,7 +34,7 @@ export default class PrintfulService {
}) })
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to get all Printful products", res); throw new FetchError("Failed to get all Printful products", res);
} }
const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> = await res.json(); const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> = await res.json();
@@ -63,7 +63,7 @@ export default class PrintfulService {
} }
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to get Printful product", res); throw new FetchError("Failed to get Printful product", res);
} }
const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> = await res.json(); const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> = await res.json();
@@ -81,7 +81,24 @@ export default class PrintfulService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to update Printful product", res); throw new FetchError("Failed to update Printful product", res);
}
}
}
static Orders = class {
static async create(printfulOrder: Printful.Orders.Order) {
const res = await fetch(`${env().API_URL}/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADERS
},
body: JSON.stringify(printfulOrder)
});
if (!res.ok) {
throw new FetchError("Failed to create printful order", res);
} }
} }
} }
+189 -5
View File
@@ -20,16 +20,200 @@ export class FetchError extends Error {
this.name = "FetchError"; this.name = "FetchError";
} }
static async createAndParse(message: string, response: Response): Promise<FetchError> { async parse() {
const fetchError = new FetchError(message, response);
try { try {
fetchError.payload = await fetchError.response.clone().json(); this.payload = await this.response.clone().json();
} catch { } catch {
fetchError.payload = await fetchError.response.text().catch(() => undefined); this.payload = await this.response.text().catch(() => undefined);
} }
finally { finally {
return fetchError; return this.payload;
} }
} }
} }
export function getStateFromZip(zipString: string): string {
/* Ensure param is a string to prevent unpredictable parsing results */
if (typeof zipString !== 'string') {
console.error('Must pass the zipcode as a string.');
return "none";
}
/* Ensure we have exactly 5 characters to parse */
if (zipString.length !== 5) {
console.error('Must pass a 5-digit zipcode.');
return "none";
}
/* Ensure we don't parse strings starting with 0 as octal values */
const zipcode = parseInt(zipString, 10);
let st;
let state;
/* Code cases alphabetized by state */
if (zipcode >= 35000 && zipcode <= 36999) {
st = 'AL';
state = 'Alabama';
} else if (zipcode >= 99500 && zipcode <= 99999) {
st = 'AK';
state = 'Alaska';
} else if (zipcode >= 85000 && zipcode <= 86999) {
st = 'AZ';
state = 'Arizona';
} else if (zipcode >= 71600 && zipcode <= 72999) {
st = 'AR';
state = 'Arkansas';
} else if (zipcode >= 90000 && zipcode <= 96699) {
st = 'CA';
state = 'California';
} else if (zipcode >= 80000 && zipcode <= 81999) {
st = 'CO';
state = 'Colorado';
} else if ((zipcode >= 6000 && zipcode <= 6389) || (zipcode >= 6391 && zipcode <= 6999)) {
st = 'CT';
state = 'Connecticut';
} else if (zipcode >= 19700 && zipcode <= 19999) {
st = 'DE';
state = 'Delaware';
} else if (zipcode >= 32000 && zipcode <= 34999) {
st = 'FL';
state = 'Florida';
} else if ((zipcode >= 30000 && zipcode <= 31999) || (zipcode >= 39800 && zipcode <= 39999)) {
st = 'GA';
state = 'Georgia';
} else if (zipcode >= 96700 && zipcode <= 96999) {
st = 'HI';
state = 'Hawaii';
} else if (zipcode >= 83200 && zipcode <= 83999 && zipcode != 83414) {
st = 'ID';
state = 'Idaho';
} else if (zipcode >= 60000 && zipcode <= 62999) {
st = 'IL';
state = 'Illinois';
} else if (zipcode >= 46000 && zipcode <= 47999) {
st = 'IN';
state = 'Indiana';
} else if (zipcode >= 50000 && zipcode <= 52999) {
st = 'IA';
state = 'Iowa';
} else if (zipcode >= 66000 && zipcode <= 67999) {
st = 'KS';
state = 'Kansas';
} else if (zipcode >= 40000 && zipcode <= 42999) {
st = 'KY';
state = 'Kentucky';
} else if (zipcode >= 70000 && zipcode <= 71599) {
st = 'LA';
state = 'Louisiana';
} else if (zipcode >= 3900 && zipcode <= 4999) {
st = 'ME';
state = 'Maine';
} else if (zipcode >= 20600 && zipcode <= 21999) {
st = 'MD';
state = 'Maryland';
} else if ((zipcode >= 1000 && zipcode <= 2799) || (zipcode == 5501) || (zipcode == 5544)) {
st = 'MA';
state = 'Massachusetts';
} else if (zipcode >= 48000 && zipcode <= 49999) {
st = 'MI';
state = 'Michigan';
} else if (zipcode >= 55000 && zipcode <= 56899) {
st = 'MN';
state = 'Minnesota';
} else if (zipcode >= 38600 && zipcode <= 39999) {
st = 'MS';
state = 'Mississippi';
} else if (zipcode >= 63000 && zipcode <= 65999) {
st = 'MO';
state = 'Missouri';
} else if (zipcode >= 59000 && zipcode <= 59999) {
st = 'MT';
state = 'Montana';
} else if (zipcode >= 27000 && zipcode <= 28999) {
st = 'NC';
state = 'North Carolina';
} else if (zipcode >= 58000 && zipcode <= 58999) {
st = 'ND';
state = 'North Dakota';
} else if (zipcode >= 68000 && zipcode <= 69999) {
st = 'NE';
state = 'Nebraska';
} else if (zipcode >= 88900 && zipcode <= 89999) {
st = 'NV';
state = 'Nevada';
} else if (zipcode >= 3000 && zipcode <= 3899) {
st = 'NH';
state = 'New Hampshire';
} else if (zipcode >= 7000 && zipcode <= 8999) {
st = 'NJ';
state = 'New Jersey';
} else if (zipcode >= 87000 && zipcode <= 88499) {
st = 'NM';
state = 'New Mexico';
} else if ((zipcode >= 10000 && zipcode <= 14999) || (zipcode == 6390) || (zipcode == 501) || (zipcode == 544)) {
st = 'NY';
state = 'New York';
} else if (zipcode >= 43000 && zipcode <= 45999) {
st = 'OH';
state = 'Ohio';
} else if ((zipcode >= 73000 && zipcode <= 73199) || (zipcode >= 73400 && zipcode <= 74999)) {
st = 'OK';
state = 'Oklahoma';
} else if (zipcode >= 97000 && zipcode <= 97999) {
st = 'OR';
state = 'Oregon';
} else if (zipcode >= 15000 && zipcode <= 19699) {
st = 'PA';
state = 'Pennsylvania';
} else if (zipcode >= 300 && zipcode <= 999) {
st = 'PR';
state = 'Puerto Rico';
} else if (zipcode >= 2800 && zipcode <= 2999) {
st = 'RI';
state = 'Rhode Island';
} else if (zipcode >= 29000 && zipcode <= 29999) {
st = 'SC';
state = 'South Carolina';
} else if (zipcode >= 57000 && zipcode <= 57999) {
st = 'SD';
state = 'South Dakota';
} else if (zipcode >= 37000 && zipcode <= 38599) {
st = 'TN';
state = 'Tennessee';
} else if ((zipcode >= 75000 && zipcode <= 79999) || (zipcode >= 73301 && zipcode <= 73399) || (zipcode >= 88500 && zipcode <= 88599)) {
st = 'TX';
state = 'Texas';
} else if (zipcode >= 84000 && zipcode <= 84999) {
st = 'UT';
state = 'Utah';
} else if (zipcode >= 5000 && zipcode <= 5999) {
st = 'VT';
state = 'Vermont';
} else if ((zipcode >= 20100 && zipcode <= 20199) || (zipcode >= 22000 && zipcode <= 24699) || (zipcode == 20598)) {
st = 'VA';
state = 'Virginia';
} else if ((zipcode >= 20000 && zipcode <= 20099) || (zipcode >= 20200 && zipcode <= 20599) || (zipcode >= 56900 && zipcode <= 56999)) {
st = 'DC';
state = 'Washington DC';
} else if (zipcode >= 98000 && zipcode <= 99499) {
st = 'WA';
state = 'Washington';
} else if (zipcode >= 24700 && zipcode <= 26999) {
st = 'WV';
state = 'West Virginia';
} else if (zipcode >= 53000 && zipcode <= 54999) {
st = 'WI';
state = 'Wisconsin';
} else if ((zipcode >= 82000 && zipcode <= 83199) || zipcode == 83414) {
st = 'WY';
state = 'Wyoming';
} else {
st = 'none';
state = 'none';
}
/* Return `state` for full name or `st` for postal abbreviation */
return st;
}
+215 -11
View File
@@ -1,3 +1,5 @@
import type { DeepPartial } from "./misc";
export namespace Printful { export namespace Printful {
export namespace Products { export namespace Products {
export interface MetaDataSingle<T = any> { export interface MetaDataSingle<T = any> {
@@ -84,12 +86,37 @@ export namespace Printful {
} }
} }
export namespace Orders {
export type Order = {
external_id: string,
shipping: string
recipient: {
name: string
company?: string
address1: string
address2: string
city: string
state_code: string
state_name?: string
country_code: string
country_name?: string
zip: string
phone?: string
email?: string
tax_number?: string
}
items: Array<{ external_variant_id: string, quantity: number }>
}
}
export namespace Webhook { export namespace Webhook {
// meta // meta
interface MetaData { interface MetaData<E extends Event, D> {
type: E;
created: number; created: number;
retries: number; retries: number;
store: number; store: number;
data: D
} }
export enum Event { export enum Event {
@@ -98,9 +125,7 @@ export namespace Printful {
} }
// product updated // product updated
export interface ProductUpdated extends MetaData { export interface ProductUpdated extends MetaData<Event.ProductUpdated, {
type: Event.ProductUpdated,
data: {
sync_product: { sync_product: {
id: number; id: number;
external_id: string; external_id: string;
@@ -110,20 +135,16 @@ export namespace Printful {
thumbnail_url: string; thumbnail_url: string;
is_ignored: boolean; is_ignored: boolean;
}; };
} }> { }
}
// product deleted // product deleted
export interface ProductDeleted extends MetaData { export interface ProductDeleted extends MetaData<Event.ProductDeleted, {
type: Event.ProductDeleted,
data: {
sync_product: { sync_product: {
id: number; id: number;
external_id: string; external_id: string;
name: string; name: string;
}; };
} }> { }
}
export type EventPayload = ProductUpdated | ProductDeleted export type EventPayload = ProductUpdated | ProductDeleted
} }
@@ -185,4 +206,187 @@ export namespace Webflow {
publishStatus?: "staging" | "live"; publishStatus?: "staging" | "live";
} }
} }
export namespace Orders {
export type Order = {
orderId: string
status: string
comment: string
orderComment: string
acceptedOn: string
fulfilledOn: string | null
refundedOn: string | null
disputedOn: string | null
disputeUpdatedOn: string | null
disputeLastStatus: string | null
customerPaid: {
unit: string
value: string
string: string
}
netAmount: {
unit: string
value: string
string: string
}
applicationFee: {
unit: string
value: string
string: string
}
allAddresses: Array<{
type: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}>
shippingAddress: {
type: string
japanType: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}
billingAddress: {
type: string
addressee: string
line1: string
line2: string
city: string
state: string
country: string
postalCode: string
}
shippingProvider: string
shippingTracking: string
shippingTrackingURL: string
customerInfo: {
fullName: string
email: string
}
purchasedItems: Array<{
count: number
rowTotal: {
unit: string
value: string
string: string
}
productId: string
productName: string
productSlug: string
variantId: string
variantName: string
variantSlug: string
variantSKU: string
variantImage: {
url: string
file: {
size: number;
originalFileName: string;
createdOn: string;
contentType: string;
width: number;
height: number;
variants: {
size: number;
url: string,
originalFileName: string;
width: number;
height: number;
}[]
}
}
variantPrice: {
unit: string
value: string
string: string
}
weight: number
width: number
height: number
length: number
}>
purchasedItemsCount: number
stripeDetails: {
subscriptionId: string | null
paymentMethod: string
paymentIntentId: string
customerId: string
chargeId: string
disputeId: string | null
refundId: string
refundReason: string
}
stripeCard: {
last4: string
brand: string
ownerName: string
expires: {
year: number
month: number
}
}
paypalDetails: Object
customData: Array<Object>
metadata: {
isBuyNow: boolean
hasDownloads: boolean
paymentProcessor: string
}
isCustomerDeleted: boolean
isShippingRequired: boolean
totals: {
subtotal: {
unit: string
value: string
string: string
}
extras: Array<{
type: string
name: string
description: string
price: {
unit: string
value: string
string: string
}
}>
total: {
unit: string
value: string
string: string
}
}
downloadFiles: Array<{
id: string
name: string
url: string
}>
}
}
export namespace Webhook {
interface MetaData<E extends Event, D> {
triggerType: E;
payload: D
}
export enum Event {
OrderCreated = "ecomm_new_order",
OrderUpdated = "ecomm_order_changed"
}
export interface OrderCreated extends MetaData<Event.OrderCreated, Orders.Order> { }
export interface OrderUpdated extends MetaData<Event.OrderUpdated, Orders.Order> { }
export type EventPayload = OrderCreated | OrderUpdated;
}
} }
+55 -7
View File
@@ -1,5 +1,6 @@
import type { Webflow } from "./util/types"; import type { Webflow } from "./util/types";
import { FetchError, type DeepPartial } from "./util/misc"; import { FetchError, type DeepPartial } from "./util/misc";
import crypto from "node:crypto";
export default class WebflowService { export default class WebflowService {
static Products = class { static Products = class {
@@ -17,7 +18,7 @@ export default class WebflowService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to create Webflow product SKU", res); throw new FetchError("Failed to create Webflow product SKU", res);
} }
} }
@@ -33,7 +34,7 @@ export default class WebflowService {
}) })
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to update Webflow product SKU", res); throw new FetchError("Failed to update Webflow product SKU", res);
} }
} }
} }
@@ -49,7 +50,7 @@ export default class WebflowService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to create Webflow product", res); throw new FetchError("Failed to create Webflow product", res);
} }
const createdWebflowProduct = await res.json(); const createdWebflowProduct = await res.json();
@@ -65,7 +66,7 @@ export default class WebflowService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to get all Webflow products", res); throw new FetchError("Failed to get all Webflow products", res);
} }
const payload = await res.json(); const payload = await res.json();
@@ -85,7 +86,7 @@ export default class WebflowService {
} }
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to get Webflow product", res); throw new FetchError("Failed to get Webflow product", res);
} }
const payload = await res.json(); const payload = await res.json();
@@ -103,7 +104,7 @@ export default class WebflowService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to update Webflow product", res); throw new FetchError("Failed to update Webflow product", res);
} }
} }
@@ -118,10 +119,56 @@ export default class WebflowService {
}); });
if (!res.ok) { if (!res.ok) {
throw await FetchError.createAndParse("Failed to remove Webflow product", res); throw new FetchError("Failed to remove Webflow product", res);
} }
} }
} }
static Util = class {
static verifyWebflowSignature(request: Request, body: unknown) {
try {
const timestamp = request.headers.get("x-webflow-timestamp");
if (!timestamp) {
throw new Error("No timestamp provided");
}
const providedSignature = request.headers.get("x-webflow-signature");
if (!providedSignature) {
throw new Error("No signature provided");
}
const secret = env().WEBHOOK_SECRET;
if (!secret) {
throw new Error("No secret provided");
}
if (!body) {
throw new Error("Body is empty");
}
const requestTimestamp = parseInt(timestamp, 10);
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
const hash = crypto.createHmac('sha256', secret)
.update(data)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(providedSignature, 'hex'))) {
throw new Error('Invalid signature');
}
const currentTime = Date.now();
if (currentTime - requestTimestamp > 300000) {
throw new Error('Request is older than 5 minutes');
}
return true;
} catch (err) {
console.error(`Error verifying signature: ${err}`);
return false;
}
}
}
} }
const env = () => { const env = () => {
@@ -133,6 +180,7 @@ const env = () => {
SITE_ID: Bun.env.WEBFLOW_SITE_ID, SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID, COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH, AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET
}; };
return { return {
+49 -15
View File
@@ -20,12 +20,21 @@
weight: "", weight: "",
}, },
cfg: { cfg: {
enableGeneration: true, global: {
maxLevel: "100", maxLevel: "100",
},
activity: Object.fromEntries(
Object.values(Activity).map((activity) => [
activity,
{
enableGeneration: true,
weightModfier: ".1", weightModfier: ".1",
weightSkew: ".1", weightSkew: "0",
ageModifier: ".1", ageModifier: ".1",
}, },
]),
),
},
}); });
const selected = $derived({ const selected = $derived({
@@ -36,11 +45,22 @@
weight: lbToKg(+input.metrics.weight), weight: lbToKg(+input.metrics.weight),
} as Metrics, } as Metrics,
cfg: { cfg: {
maxLevel: +input.cfg.maxLevel, global: {
weightModifier: +input.cfg.weightModfier, maxLevel: +input.cfg.global.maxLevel,
weightSkew: +input.cfg.weightSkew, },
ageModifier: +input.cfg.ageModifier, activity: Object.fromEntries(
disableGeneration: !input.cfg.enableGeneration, Object.values(Activity).map((activity) => [
activity,
{
weightModifier:
+input.cfg.activity[activity].weightModfier,
weightSkew: +input.cfg.activity[activity].weightSkew,
ageModifier: +input.cfg.activity[activity].ageModifier,
disableGeneration:
!input.cfg.activity[activity].enableGeneration,
},
]),
),
} as StandardsConfig, } as StandardsConfig,
}); });
@@ -82,7 +102,7 @@
min="1" min="1"
max="100" max="100"
placeholder="5" placeholder="5"
bind:value={input.cfg.maxLevel} bind:value={input.cfg.global.maxLevel}
/> />
</label> </label>
</div> </div>
@@ -97,7 +117,10 @@
<label class="label"> <label class="label">
<input <input
type="checkbox" type="checkbox"
bind:checked={input.cfg.enableGeneration} bind:checked={
input.cfg.activity[selected.activity]
.enableGeneration
}
class="toggle toggle-lg m-auto" class="toggle toggle-lg m-auto"
/> />
</label> </label>
@@ -110,8 +133,12 @@
max="1" max="1"
step=".05" step=".05"
placeholder=".1" placeholder=".1"
bind:value={input.cfg.weightModfier} bind:value={
disabled={selected.cfg.disableGeneration} input.cfg.activity[selected.activity]
.weightModfier
}
disabled={selected.cfg.activity[selected.activity]
.disableGeneration}
/> />
</label> </label>
<label> <label>
@@ -123,8 +150,11 @@
max="1" max="1"
step=".05" step=".05"
placeholder=".1" placeholder=".1"
bind:value={input.cfg.weightSkew} bind:value={
disabled={selected.cfg.disableGeneration} input.cfg.activity[selected.activity].weightSkew
}
disabled={selected.cfg.activity[selected.activity]
.disableGeneration}
/> />
</label> </label>
<label> <label>
@@ -136,8 +166,12 @@
max="1" max="1"
step=".05" step=".05"
placeholder=".1" placeholder=".1"
bind:value={input.cfg.ageModifier} bind:value={
disabled={selected.cfg.disableGeneration} input.cfg.activity[selected.activity]
.ageModifier
}
disabled={selected.cfg.activity[selected.activity]
.disableGeneration}
/> />
</label> </label>
</div> </div>