Initial commit

This commit is contained in:
2025-10-02 08:54:03 +02:00
commit ea54638227
1642 changed files with 53677 additions and 0 deletions

View File

@@ -0,0 +1,919 @@
const { bigcommerceApiOAuth, bigcommerceStoreHash, channelId } = require("../config.js")
/**
*
* @param {number} status
*/
function statusIsValid(status) {
return !!status && status < 400
}
/**
*
* @param {number} orderId
* @returns {V2OrderProductsResponseBase[]}
*/
function getOrdersProducts(orderId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders/${orderId}/products`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {number} orderId∂
* @returns {V2OrderResponseBase}
*/
function getOrderById(orderId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders/${orderId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
/**
* @type {V2OrderResponseBase}
*/
const order = response.body.json()
order.productObjs = getOrdersProducts(orderId)
order.shipping_addressObjs = getOrderShippingAddressesById(String(orderId))
return order
}
/**
*
* @param {number} orderId
* @param {V2OrderResponseBase} order
* @returns
*/
function updateOrderById(orderId, order) {
delete order.id
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders/${orderId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify(order),
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {number} customerId
*/
function getOrdersForCustomer(customerId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders?customer_id=${customerId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
/**
* @type {V2OrderResponseBase[]}
*/
const orders = response.body.json()
orders.forEach((order) => {
order.productObjs = getOrdersProducts(order.id)
order.shipping_addressObjs = getOrderShippingAddressesById(String(order.id))
})
// every order has date_created field, so we can sort by it (latest first)
orders.sort((a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime())
return orders
}
/**
* @returns {V2OrderProductsResponseBase[]}
*/
function getAllProducts() {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/catalog/products?limit=250`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
* @param {string} [orderId]
* @returns {V2OrderShippingAddressesResponseBase[]}
*/
function getOrderShippingAddressesById(orderId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders/${orderId}/shipping_addresses`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {string} [productId]
* @returns {V2OrderProductsResponseBase}
*/
function getProductById(productId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/catalog/products/${productId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
* @param {string} orderId
* @returns {V2OrderProductsResponseBase[]}
*/
function getOrderProductsById(orderId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/orders/${orderId}/products`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {string} [productId]
* @returns {ProductImage[]}
*/
function getProductImages(productId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/catalog/products/${productId}/images`,
{
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
* @param {string} [productId]
* @param {string} [ratingId]
*/
function deleteRating(productId, ratingId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/catalog/products/${productId}/reviews/${ratingId}`,
{
method: "DELETE",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
}
/**
* @param {string} [cartId]
* @returns {RestApiCart}
*/
function getCart(cartId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/carts/${cartId}`,
{
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText + " cart",
}
}
return response.body.json().data
}
/**
* @param {string} [cartId]
* @returns {any}
*/
function getRedirectUrl(cartId) {
const response = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/carts/${cartId}/redirect_urls`,
{
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText + "redirect_url",
}
}
const data = response.body.json().data
return { embeddedCheckoutURL: data?.embedded_checkout_url, checkoutURL: data?.checkout_url }
}
/**
*
* @param {string} cartId
* @param { { merchandiseId: string; quantity: number; productId?: string }[]} items
*/
function addCartItem(cartId, items) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/carts/${cartId}/items`,
{
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify({ line_items: items }),
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data
}
/**
*
* @param {string} cartId
* @param {{ variant_id: string;product_id: string, quantity: number;} } item
* @param {string} entityId
* @returns {any}
*/
function updateCartItem(cartId, item, entityId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/carts/${cartId}/items/${entityId}`,
{
method: "PUT",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify({ line_item: item }),
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data
}
/**
*
* @param {string} cartId
* @param {string} entityId
* @returns {boolean}
*/
function deleteCartItem(cartId, entityId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/carts/${cartId}/items/${entityId}`,
{
method: "DELETE",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return true
}
/**
*
* @param {number} customerId
* @param {string} email
* @returns {BigCommerceCustomer}
*/
function getCustomerById(customerId, email = "", noException = false) {
let res
if (customerId) {
res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers?id:in=${customerId}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
} else {
let encodedEmail = encodeURIComponent(email)
res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers?email:in=${encodedEmail}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
}
if (!statusIsValid(res.status)) {
if (noException) return null
throw {
status: res.status,
error: res.statusText,
}
}
if (res.body.json().data.length === 0) return null
/** @type {BigCommerceCustomer} */
const customer = res.body.json().data[0]
const formFields = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/form-field-values?customer_id=${customer.id}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(formFields.status)) {
if (noException) return null
throw {
status: formFields.status,
error: formFields.statusText,
}
}
try {
if (formFields.body && formFields.body.json()) customer.form_fields = formFields.body.json()?.data || []
} catch (e) {}
return customer
}
/**
*
* @param {number} customerId
* @param {number} addressId
* @returns {BigCommerceAddress}
*/
function getCustomerAddressById(customerId, addressId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/addresses/?customer_id:in=${customerId}&id:in=${addressId}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data[0]
}
/**
*
* @param {number} customerId
* @param {number} addressId
*
*/
function deleteCustomerAddressById(customerId, addressId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v2/customers/${customerId}/addresses/${addressId}`,
{
method: "DELETE",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
console.log(res.status, "status")
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return true
}
/**
* @param {BigCommerceAddress} address
*/
function updateCustomerAddressById(address) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/addresses`,
{
method: "PUT",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify([address]),
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data[0]
}
/**
*
* @param {number} customerId
* @returns {BigCommerceAddress[]}
*/
function getCustomerAddresses(customerId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/addresses?customer_id:in=${customerId}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data
}
/**
* @param {BigCommerceAddress} address
* @returns
*/
function addCustomerAddress(address) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/addresses`,
{
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify([address]),
}
)
if (!statusIsValid(res.status))
throw {
status: res.status,
error: res.statusText,
}
return res.body.json().data[0]
}
/**
*
* @param {string} email
* @param {string} password
* @returns {{is_valid: boolean}}
*/
function validateCredentials(email, password) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers/validate-credentials`,
{
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify({
email,
password,
channelId,
}),
}
)
if (res.status == 429) {
throw {
status: 429,
error: "Too many requests",
log: false,
}
}
if (res.status == 422) {
throw {
status: 422,
error: "Invalid email or password",
log: false,
}
}
if (!statusIsValid(res.status)) {
throw {
status: res.status,
error: res.statusText,
}
}
return res.body.json()
}
/**
*
* @param {Partial<BigCommerceCustomer>} customer
* @returns {BigCommerceCustomer}
*/
function updateCustomer(customer) {
const customerRes = context.http.fetch(`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/customers`, {
method: "PUT",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify([customer]),
})
if (!statusIsValid(customerRes.status))
throw {
status: customerRes.status,
error: customerRes.statusText,
}
return customerRes.body.json().data[0]
}
function generateJTI() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
}
/**
*
* @param {number} customerId
* @param {string} storeHash
* @param {string} storeUrl
* @param {string} clientId
* @param {string} clientSecret
* @param {string} [redirect_to]
* @returns
*/
function getLoginUrl(customerId, storeHash, storeUrl, clientId, clientSecret, redirect_to = "") {
const dateCreated = Math.round(new Date().getTime() / 1000)
const payload = {
iss: clientId,
iat: dateCreated,
jti: generateJTI(),
operation: "customer_login",
store_hash: storeHash,
customer_id: customerId,
redirect_to,
}
const token = context.jwt.create(payload, { secret: clientSecret, validityDuration: 600 })
return `${storeUrl}/login/token/${token}`
}
/**
* @param {number} customerId
* @returns {RestWishlist | false}
*/
function getWishlistEntries(customerId) {
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/wishlists?customer_id=${customerId}`,
{
method: "GET",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (res.status == 404) {
return false
}
if (!statusIsValid(res.status)) {
throw {
status: res.status,
error: res.statusText,
}
}
if (res.body.json().data?.length === 0) return false
return res.body.json().data[0]
}
/**
* @param {number} customerId
* @param {number} productId
* @param {number} variantId
* @returns {RestWishlist}
*/
function createWishlistEntry(productId, variantId, customerId) {
const currentWishlist = getWishlistEntries(customerId)
let res
if (currentWishlist) {
res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/wishlists/${currentWishlist?.id}/items`,
{
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify({
items: [
{
product_id: Number(productId),
variant_id: Number(variantId),
},
],
}),
}
)
} else {
res = context.http.fetch(`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/wishlists`, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
body: JSON.stringify({
customer_id: Number(customerId),
is_public: false,
name: "Customer Wishlist",
items: [
{
product_id: Number(productId),
variant_id: Number(variantId),
},
],
}),
})
}
if (!statusIsValid(res.status)) {
throw {
status: res.status,
error: res.statusText,
}
}
return res.body.json().data
}
/**
* @param {number} customerId
* @param {number} productId
* @param {number} variantId
* @returns {boolean}
*/
function removeWishlistEntry(customerId, productId, variantId) {
const currentWishlist = getWishlistEntries(customerId)
if (!currentWishlist) {
console.log("no wishlist at all")
return true
}
const wishListEntry = currentWishlist.items.find(
(item) => item.product_id == productId && item.variant_id == variantId
)
if (!wishListEntry) {
console.log("no wishlist entry")
return true
}
const res = context.http.fetch(
`https://api.bigcommerce.com/stores/${bigcommerceStoreHash}/v3/wishlists/${currentWishlist.id}/items/${wishListEntry.id}`,
{
method: "DELETE",
headers: {
accept: "application/json",
"content-type": "application/json",
"X-Auth-Token": bigcommerceApiOAuth,
},
}
)
if (!statusIsValid(res.status)) {
throw {
status: res.status,
error: res.statusText,
}
}
return true
}
/**
*
* @param {number} orderId
* @returns {Partial<Order>}
*/
function createInternalOrderObject(orderId) {
const orderProducts = getOrderProductsById(String(orderId))
const bigCommerceId = orderId
const order = getOrderById(bigCommerceId)
let [internalCustomer] = context.db.find("bigCommerceCustomer", {
filter: {
bigCommerceId: order?.customer_id,
},
})
/** @type {Partial<Order>} */
const internalOrderReference = {
bigCommerceId,
customerBigCommerceId: order?.customer_id,
cost: order?.total_inc_tax,
customerTibiId: internalCustomer?.id,
status: "draft",
products: orderProducts.map((product) => {
const internalProduct = context.db.find("bigCommerceProduct", {
filter: {
bigCommerceId: product.product_id,
},
})[0]
return {
bigCommerceId: product.product_id,
tibiId: internalProduct?.id,
quantity: product.quantity,
}
}),
}
return internalOrderReference
}
module.exports = {
updateCustomer,
getOrderById,
getOrderProductsById,
getProductImages,
getProductById,
getOrderShippingAddressesById,
deleteRating,
getCart,
getRedirectUrl,
addCartItem,
deleteCartItem,
updateCartItem,
getCustomerById,
validateCredentials,
updateCustomerAddressById,
deleteCustomerAddressById,
getCustomerAddressById,
getCustomerAddresses,
addCustomerAddress,
getLoginUrl,
getOrdersForCustomer,
removeWishlistEntry,
createWishlistEntry,
getWishlistEntries,
updateOrderById,
getAllProducts,
createInternalOrderObject,
}

View File

@@ -0,0 +1,40 @@
const { DeepLkey } = require("../config.js")
const { statusIsValid } = require("./utils.js")
/**
*
* @param {string} text
* @returns {string}
*/
function translateText(text) {
const response = context.http.fetch(`https://api.deepl.com/v2/translate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `DeepL-Auth-Key ${DeepLkey}`,
"Content-Length": String(text.length),
},
body: JSON.stringify({
text: [text],
target_lang: "DE",
formality: "less",
context:
"Es handelt sich um eine Produktbeschreibung - insbesondere um die Größenbeschreibungen. Dinge wie A Länge, B Breite, C Ärmellänge etc. sollten präzise übersetzt werden, da die buchstaben hier auf ein Bild verweisen, welches du nicht kennst!",
}),
})
if (!statusIsValid(response.status)) {
console.log(JSON.stringify(response), JSON.stringify(response.body.json()))
throw {
status: response.status,
error: response.statusText,
}
}
const res = response.body.json()
const textMerged = res.translations.map((t) => t.text).join(" ")
return textMerged
}
module.exports = {
translateText,
}

View File

@@ -0,0 +1,108 @@
const { fb_accessToken } = require("../config")
function postAddToCart() {
const request = context.request()
const headers = request.header("User-Agent")
const ipAddress = request.clientIp()
const eventPayload = {
data: [
{
event_name: "AddToCart",
event_time: Math.floor(Date.now() / 1000),
event_source_url: "https://www.binkrassdufass.de",
action_source: "website",
user_data: {
client_user_agent: headers,
client_ip_address: ipAddress,
},
},
],
}
const res = context.http.fetch(
"https://graph.facebook.com/v16.0/1117933239951751/events?access_token=" + fb_accessToken,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventPayload),
}
)
console.log(JSON.stringify(res), "CARTADD")
const response = res.body.json()
console.log(JSON.stringify(response))
}
function postUserRegister() {
const request = context.request()
const headers = request.header("User-Agent")
const ipAddress = request.clientIp()
const eventPayload = {
data: [
{
event_name: "CompleteRegistration",
event_time: Math.floor(Date.now() / 1000),
event_source_url: "https://www.binkrassdufass.de",
action_source: "website",
user_data: {
client_user_agent: headers,
client_ip_address: ipAddress,
},
},
],
}
const res = context.http.fetch(
"https://graph.facebook.com/v16.0/1117933239951751/events?access_token=" + fb_accessToken,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventPayload),
}
)
}
/**
*
* @param {string} total
*/
function postPurchase(total) {
const eventPayload = {
data: [
{
event_name: "Purchase",
event_time: Math.floor(Date.now() / 1000),
event_source_url: "https://www.binkrassdufass.de",
action_source: "website",
user_data: {
// dummy data bc inside webhook we don't have access to user data
client_user_agent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
client_ip_address: "192.168.1.1", // random IP address
},
custom_data: {
currency: "EUR",
value: total,
},
},
],
}
const res = context.http.fetch(
"https://graph.facebook.com/v12.0/1117933239951751/events?access_token=" + fb_accessToken,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventPayload),
}
)
}
module.exports = {
postAddToCart,
postUserRegister,
postPurchase,
}

View File

@@ -0,0 +1,61 @@
const { omnisendApiKey } = require("../config.js")
const { statusIsValid } = require("./utils.js")
/**
*
* @param {string} email
*/
function createNewsletterSubscriber(email) {
const res = context.http.fetch("https://api.omnisend.com/v5/contacts", {
method: "POST",
headers: {
"X-API-KEY": omnisendApiKey,
accept: "application/json",
"content-type": "application/json",
},
body: JSON.stringify({
createdAt: new Date().toISOString(),
identifiers: [
{
channels: {
email: {
status: "subscribed",
statusDate: new Date().toISOString(),
},
},
consent: {
createdAt: new Date().toISOString(),
source: "headless-frontend",
},
id: email,
type: "email",
},
],
}),
})
if (!statusIsValid(res.status)) {
throw {
status: res.status,
error: res.statusText,
}
}
}
/**
* @param {string} email
* @returns {boolean}
*/
function checkIfNewsletterSubscriber(email) {
const res = context.http.fetch("https://api.omnisend.com/v5/contacts", {
headers: {
"X-API-KEY": omnisendApiKey,
accept: "application/json",
},
})
const resJson = res.body.json()
return resJson.contacts.some((contact) => contact.email === email)
}
module.exports = {
createNewsletterSubscriber,
checkIfNewsletterSubscriber,
}

View File

@@ -0,0 +1,238 @@
const { printfulAPIToken, serverBaseURL } = require("../config")
const { translateText } = require("./deepLRestApi")
const { statusIsValid } = require("./utils")
/**
*
* @param {number} bigCommerceOrderId
*/
function getPrintfulOrder(bigCommerceOrderId) {
const response = context.http.fetch("https://api.printful.com/v2/orders/@" + bigCommerceOrderId, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
})
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
*
* @param {number} bigCommerceOrderId
*/
function getPrintfulOrderShipments(bigCommerceOrderId) {
const response = context.http.fetch(`https://api.printful.com/v2/orders/@${bigCommerceOrderId}/shipments`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
})
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
*
* @param {number} bigCommerceOrderId
* @param {any} data
* @returns
*/
function patchPrintfulOrder(bigCommerceOrderId, data) {
const response = context.http.fetch("https://api.printful.com/v2/orders/@" + bigCommerceOrderId, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
body: JSON.stringify(data),
})
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
*
* @param {string} type
*/
function createPrintfulWebhook(type) {
const response = context.http.fetch("https://api.printful.com/v2/webhooks/" + type, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
body: JSON.stringify({
type,
url: `${serverBaseURL}webhook`,
}),
})
console.log("response:", response.status)
if (!statusIsValid(response.status)) {
console.log("wtf?!")
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {string} type
*/
function deletePrintfulWebhook(type) {
const response = context.http.fetch("https://api.printful.com/v2/webhooks/" + type, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
})
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return
}
/**
* @param {number} bigCommerceOrderId
*/
function cancelPrintfulOrder(bigCommerceOrderId) {
const response = context.http.fetch("https://api.printful.com/orders/@" + bigCommerceOrderId, {
method: "delete",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
})
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json()
}
/**
* @param {string} printfulProductId
*/
function getPrintfulProductSizingChart(printfulProductId) {
const response = context.http.fetch(
`https://api.printful.com/v2/catalog-products/${printfulProductId}/sizes?unit=cm`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${printfulAPIToken}`,
},
}
)
if (!statusIsValid(response.status)) {
throw {
status: response.status,
error: response.statusText,
}
}
return response.body.json().data
}
/**
*
* @param {string} printfulProductId
* @param {string} productId
* @returns
*/
function extractSizingChart(printfulProductId, productId) {
if (!printfulProductId) return null
const productSizingChart = getPrintfulProductSizingChart(printfulProductId)
const sizeTable = productSizingChart.size_tables.find((t) => t.type === "measure_yourself")
if (!sizeTable) {
throw {
status: 404,
message: "No sizing chart found for this product",
}
}
const productSizingImageDescriptionTranslation = translateText(sizeTable.image_description)
const generalProductSizingDescription = translateText(sizeTable.description)
return {
imageURL: sizeTable.image_url,
imageDescription: productSizingImageDescriptionTranslation,
availableSizes: productSizingChart.available_sizes,
generalDescription: generalProductSizingDescription,
columns: sizeTable.measurements.map((m) => {
let label = context.db.find("module", {
filter: {
label: m.type_label,
type: "sizeLabel",
},
})
if (label.length === 0) {
const germanLabelTranslation = translateText(m.type_label)
label[0] = context.db.create("module", {
label: m.type_label,
type: "sizeLabel",
germanLabelTranslation: germanLabelTranslation,
})
} else {
label = [...label]
}
return {
label: label[0].label,
sizes: m.values.map((v) => {
if (v.min_value && v.max_value) {
if (sizeTable.unit === "inches") {
return `${Math.round(Number(v.min_value) * 2.54)} - ${Math.round(
Number(v.max_value) * 2.54
)}`
}
return `${v.min_value} - ${v.max_value}`
}
if (sizeTable.unit === "inches") {
return String(Math.round(Number(v.value) * 2.54))
}
return v.value
}),
}
}),
}
}
module.exports = {
getPrintfulOrder,
patchPrintfulOrder,
createPrintfulWebhook,
deletePrintfulWebhook,
cancelPrintfulOrder,
getPrintfulProductSizingChart,
getPrintfulOrderShipments,
extractSizingChart,
}

View File

@@ -0,0 +1,67 @@
const { apiSsrBaseURL } = require("../config")
/**
* api request via server, cache result in context.ssrCache
* should be elimated in client code via tree shaking
*
* @param {string} cacheKey
* @param {string} endpoint
* @param {string} query
* @param {ApiOptions} options
* @returns {ApiResult<any>}
*/
function ssrRequest(cacheKey, endpoint, query, options) {
let url = endpoint + (query ? "?" + query : "")
// console.log("############ FETCHING ", apiSsrBaseURL + url)
const response = context.http.fetch(apiSsrBaseURL + url, {
method: options.method,
headers: options.headers,
})
// console.log(JSON.stringify(response.headers, null, 2))
const json = response.body.json()
const count = parseInt(response.headers["X-Results-Count"] || "0")
// json is go data structure and incompatible with js, so we need to convert it
const r = { data: JSON.parse(JSON.stringify(json)), count: count }
// @ts-ignore
context.ssrCache[cacheKey] = r
return r
}
/**
* api request via server, cache result in context.ssrCache
* for BigCommerce requests, using context.http.fetch
*
* @param {string} cacheKey
* @param {string} endpoint
* @param {string} query
* @param {ApiOptions} options
* @returns {{status: number, body: any}}
*/
function ssrRequestBigCommerce(cacheKey, endpoint, query, options) {
const response = context.http.fetch(endpoint, {
method: options.method,
headers: options.headers,
body: options.body ? JSON.stringify(options.body) : undefined,
})
if (response.status >= 300) {
console.log("SSR ERROR?!?!?!", response.status, JSON.stringify(response))
return { status: response.status, body: { data: {}, count: 0 } }
}
const count = parseInt(response.headers["X-Results-Count"] || "0")
const json = response.body.json()
const result = { status: response.status, body: JSON.parse(JSON.stringify(json)), count: count }
// @ts-ignore
context.ssrCache[cacheKey] = result
return result
}
module.exports = {
ssrRequest,
ssrRequestBigCommerce,
}

163
api/hooks/lib/ssr.js Normal file
View File

@@ -0,0 +1,163 @@
const { login, newNotification } = require("../../../frontend/src/lib/store")
const { apiClientBaseURL } = require("../config-client")
/**
* convert object to string
* @param {any} obj object
* @returns {string} string
*/
function obj2str(obj) {
if (Array.isArray(obj)) {
return JSON.stringify(
obj.map(function (idx) {
return obj2str(idx)
})
)
} else if (typeof obj === "object" && obj !== null) {
var elements = Object.keys(obj)
.sort()
.map(function (key) {
var val = obj2str(obj[key])
if (val) {
return key + ":" + val
}
})
var elementsCleaned = []
for (var i = 0; i < elements.length; i++) {
if (elements[i]) elementsCleaned.push(elements[i])
}
return "{" + elementsCleaned.join("|") + "}"
}
if (obj) return obj
}
/**
* api request via client or server
* server function ssrRequest is called via context.ssrRequest, binded in ssr hook
*
* @param {string} endpoint
* @param {ApiOptions} options
* @param {any} body
* @param {import("../../../frontend/src/sentry")} sentry
* @param {typeof fetch} _fetch
* @returns {Promise<ApiResult<any>>}
*/
function apiRequest(endpoint, options, body, sentry, _fetch) {
// first check cache if on client
const cacheKey = obj2str({ endpoint: endpoint, options: options })
let method = options?.method || "GET"
// @ts-ignore
if (
typeof window !== "undefined" &&
window.__SSR_CACHE__ &&
method === "GET" &&
!location.pathname.includes("profile")
) {
// @ts-ignore
const cache = window.__SSR_CACHE__[cacheKey]
if (cache) {
return Promise.resolve(cache)
}
}
let query = "&count=1"
if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter))
if (options?.sort) query += "&sort=" + options.sort + "&sort=_id"
if (options?.limit) query += "&limit=" + options.limit
if (options?.offset) query += "&offset=" + options.offset
if (options?.projection) query += "&projection=" + options.projection
if (options?.lookup) query += "&lookup=" + options.lookup
if (options?.params) {
Object.keys(options.params).forEach((p) => {
query += "&" + p + "=" + encodeURIComponent(options.params[p])
})
}
/** @type {{[key: string]: string}} */
let headers = {
"Content-Type": "application/json",
}
if (options?.headers) headers = { ...headers, ...options.headers }
if (typeof window === "undefined" && method === "GET") {
// server
// reference via context from get hook to tree shake in client
// @ts-ignore
const d = context.ssrRequest(cacheKey, endpoint, query, Object.assign({}, options, { method, headers }))
return Promise.resolve(d)
} else {
// client
let url = (endpoint.startsWith("/") ? "" : apiClientBaseURL) + endpoint + (query ? "?" + query : "")
const span = sentry?.currentTransaction()?.startChild({
op: "fetch",
description: method + " " + url,
data: Object.assign({}, options, { url }),
})
const trace_id = span?.toTraceparent()
if (trace_id) {
headers["sentry-trace"] = trace_id
}
/** @type {{[key: string]: any}} */
const requestOptions = {
method,
mode: "cors",
headers,
}
if (method === "POST" || method === "PUT") {
requestOptions.body = JSON.stringify(body)
}
const response = _fetch(url, requestOptions)
.then((response) => {
return response?.json().then((json) => {
if (response?.status < 200 || response?.status >= 400) {
if (response?.status === 401) {
login.set(null)
newNotification({
html: "Nicht autorisiert. Bitte melde dich sich erneut an.",
class: "error",
})
}
return Promise.reject({ response, data: json })
}
return Promise.resolve({ data: json || null, count: response.headers?.get("x-results-count") || 0 })
})
})
.catch((error) => {
if (options.noError) {
return Promise.resolve({
data: {
...error.data,
},
count: 0,
})
}
if (error.status === 401) {
login.set(null)
newNotification({
html: "Nicht autorisiert. Bitte melde dich sich erneut an.",
class: "error",
})
} else throw error
})
// @ts-ignore
return response
}
}
module.exports = {
obj2str,
apiRequest,
}

494
api/hooks/lib/utils.js Normal file
View File

@@ -0,0 +1,494 @@
/**
*
* @param {any} str
*/
function log(str) {
console.log(JSON.stringify(str, undefined, 4))
}
/**
* convert object to string
* @param {any} obj object
* @returns {Object | undefined}
*/
function obj2str(obj) {
if (Array.isArray(obj)) {
return JSON.stringify(
obj.map(function (idx) {
return obj2str(idx)
})
)
} else if (typeof obj === "object" && obj !== null) {
var elements = Object.keys(obj)
.sort()
.map(function (key) {
var val = obj2str(obj[key])
if (val) {
return key + ":" + val
}
})
var elementsCleaned = []
for (var i = 0; i < elements.length; i++) {
if (elements[i]) elementsCleaned.push(elements[i])
}
return "{" + elementsCleaned.join("|") + "}"
}
if (obj) return obj
}
var { LIGHTHOUSE_TOKEN } = require("../config")
/**
* clear SSR cache
*/
function clearSSRCache() {
var info = context.db.deleteMany("ssr", {})
context.response.header("X-SSR-Cleared", info.removed)
}
/**
* @param {{ [x: string]: any; }[]} dbObjs
*/
function calculateAverageDynamically(dbObjs) {
const sumObj = {}
let count = 0
dbObjs.forEach((/** @type {{ [x: string]: any; }} */ obj) => {
accumulate(obj, sumObj)
count++
})
/**
* @param {{ [x: string]: any; }} sourceObj
* @param {{ [x: string]: any; }} targetObj
*/
function accumulate(sourceObj, targetObj) {
for (const key in sourceObj) {
if (typeof sourceObj[key] === "number") {
targetObj[key] = (targetObj[key] || 0) + sourceObj[key]
} else if (typeof sourceObj[key] === "object" && sourceObj[key] !== null) {
targetObj[key] = targetObj[key] || {}
accumulate(sourceObj[key], targetObj[key])
}
}
}
/**
* @param {{ [x: string]: any; }} targetObj
*/
function average(targetObj) {
for (const key in targetObj) {
if (typeof targetObj[key] === "number") {
targetObj[key] = targetObj[key] / count
} else if (typeof targetObj[key] === "object") {
average(targetObj[key])
}
}
}
average(sumObj)
return sumObj
}
/**
* @param {string} url
*/
function run(url) {
const response = context.http
.fetch(url, {
timeout: 300,
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.body.json()
// needs enough traffic to be collected
const cruxMetrics = {
"First Contentful Paint": response?.loadingExperience?.metrics?.FIRST_CONTENTFUL_PAINT_MS?.category,
"First Input Delay": response?.loadingExperience?.metrics?.FIRST_INPUT_DELAY_MS?.category,
}
const lighthouse = response.lighthouseResult
const lighthouseMetrics = {
FCPS: lighthouse.audits["first-contentful-paint"].score * 100,
FCPV: lighthouse.audits["first-contentful-paint"].numericValue / 1000,
FMPS: lighthouse.audits["first-meaningful-paint"].score * 100,
FMPV: lighthouse.audits["first-meaningful-paint"].numericValue / 1000,
SIS: lighthouse.audits["speed-index"].score * 100,
SIV: lighthouse.audits["speed-index"].numericValue / 1000,
TTIS: lighthouse.audits["interactive"].score * 100,
TTIV: lighthouse.audits["interactive"].numericValue / 1000,
FPIDS: lighthouse.audits["max-potential-fid"].score * 100,
FPIDV: lighthouse.audits["max-potential-fid"].numericValue / 1000,
}
let dbObject = {
cruxMetrics,
lighthouseMetrics,
performance: Math.round(lighthouse.categories.performance.score * 100),
accessibility: Math.round(lighthouse.categories.accessibility.score * 100),
bestPractices: Math.round(lighthouse.categories["best-practices"].score * 100),
seo: Math.round(lighthouse.categories.seo.score * 100),
}
return dbObject
}
function setUpQuery(subPath = "/") {
const api = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
let params = `category=performance&category=accessibility&category=best-practices&category=seo`
const parameters = {
url: encodeURIComponent(`https://allkids-erfurt.de/${subPath}`),
key: LIGHTHOUSE_TOKEN,
}
let query = `${api}?`
for (let key in parameters) {
// @ts-ignore
query += `${key}=${parameters[key]}&`
}
query += params // Append other parameters without URL encoding
return query
}
var config = require("../config")
/**
*
* @param {HookContext} c
* @param {string} filename
* @param {string} locale
* @returns {string}
*/
function tpl(c, filename, locale) {
return c.tpl.execute(c.fs.readFile(filename), {
context: c,
config: config,
})
}
const { operatorEmail, operatorName } = require("../config")
const { cryptchaSiteId } = require("../config-client")
const { postUserRegister } = require("./facebookRestAPI")
function sendOperatorRatingMail() {
if (!context.data) context.data = {}
let locale = context.request().query("locale")
locale = locale ? locale : "de-DE"
context.data.tibiLink = `project/${context.project().id}/collection/rating/edit/${context.data.id}` //projekt ID
context.smtp.sendMail({
to: operatorEmail,
from: operatorEmail,
fromName: operatorName,
subject: tpl(context, "templates/operator_rating_subject.de-DE.txt", locale),
html: tpl(context, "templates/operator_rating_body.de-DE.html", locale),
})
}
/**@param {ProductRating} currentRating */
function validateAndModifyRating(currentRating) {
// Fetch the rating object from the database
/** @type {ProductRating} */ // @ts-ignore
let oldRating = context.db.find("rating", {
filter: { orderId: currentRating.bigcommerceOrderId, productId: currentRating.bigCommerceProductId },
})[0]
if (oldRating && (oldRating.status != "pending" || currentRating.status != "pending")) {
//@ts-ignore
oldRating.review_date = convertDateToUTCISO(oldRating.review_date)
if (oldRating.status == "pending") {
oldRating.status = currentRating.status
if (currentRating.status == "approved") return currentRating
}
// if currentrating is approved but oldrating is not pending, disallow the change by returning the old rating
return context?.user?.auth()?.id &&
(currentRating?.status == "pending" || currentRating?.status == "rejected") &&
(oldRating?.status == "approved" || oldRating?.status == "rejected")
? currentRating
: oldRating
}
// If the status of the current rating is 'pending', or not provided
if (currentRating.status == "pending" || !currentRating.status) {
currentRating.status = "pending"
if (!currentRating.review_date) {
//@ts-ignore
currentRating.review_date = new Date().toISOString()
}
}
return currentRating
}
/** @param {ProductRating} [orderRating] */ // @ts-ignore
function productInsideOrder(orderRating) {
/**@type {Order} */
//@ts-ignore
withAccount((login) => {
let order = context.db.find("bigCommerceOrder", {
filter: {
bigCommerceId: orderRating?.bigcommerceOrderId,
customerBigCommerceId: login.bigCommerceId,
},
})[0]
if (!order) {
throw {
status: 404,
data: {
message: "Order not found",
},
}
}
})
let order = context.db.find("bigCommerceOrder", {
filter: { bigCommerceId: orderRating?.bigcommerceOrderId },
})[0]
if (!order) throw { error: "No Order object with given ID.", status: 400 }
const productIds = order.products?.map((product) => product.bigCommerceId)
if (productIds.length == 0) throw { error: "No products inside the Order.", status: 400 }
let productInRating = orderRating?.bigCommerceProductId
let productInsideOrder = productIds.includes(productInRating)
if (!productInsideOrder) throw { error: "Rated products are not inside the Order.", status: 400 }
}
/**
*
* @param {HookContext | string} c
*/
function getJwt(c, required = true) {
let tokenString
if (typeof c == "string") {
tokenString = c
} else {
const authHeader = c.request().header("Authorization")
if (!authHeader) {
if (required) {
throw {
status: 403,
error: "missing authorization header",
log: false,
}
} else {
return null
}
}
tokenString = authHeader.replace("Bearer ", "")
}
if (!tokenString) {
if (required) {
throw {
status: 403,
error: "token: is empty",
log: false,
}
} else {
return null
}
}
const token = context.jwt.parse(tokenString, {
secret: config.jwtSecret,
})
if (required && !token.valid) {
throw {
status: 403,
error: "token: " + token.error,
log: false,
}
}
return token
}
/**
* call callback with login clains
*
* @param {(claims: JWTLoginClaims) => void} callback
* @param {boolean} required
* @param {string} apiTokenName
* @returns {boolean} void if backendAuth
*/
function withAccount(callback, required = true, apiTokenName = null, allowViaRefreshToken = false) {
if (apiTokenName) {
const r = context.request()
if (r.header(apiTokenName)) {
// checked via api collection config, so ensure it is given
return
}
}
const backendAuth = context.user.auth()
if (!backendAuth) {
// require authorization header with jwt
let token
/** @type {JWTLoginClaims} */ // @ts-ignore
let loginClaims
try {
token = getJwt(context, required)
// @ts-ignore
loginClaims = token && token.claims
} catch (e) {
if (allowViaRefreshToken) {
const rT = getRefreshToken()
if (rT?.valid) {
/** @type {JWTRefreshClaims} */ // @ts-ignore
const refreshClaims = rT.claims
const accountId = refreshClaims && refreshClaims.tibiId
/** @type {Customer} */ // @ts-ignore
const customer = context.db.find("bigCommerceCustomer", {
filter: { _id: accountId },
})[0]
loginClaims = {
bigCommerceId: customer.bigCommerceId,
email: customer.email,
tibiId: customer.id,
}
} else {
throw {
status: 403,
error: "token: invalid refresh token",
log: false,
}
}
} else {
throw e
}
}
// log(loginClaims)
if (!loginClaims || !loginClaims.tibiId || !loginClaims.bigCommerceId || !loginClaims.email) {
if (required) {
throw {
status: 403,
error: "token: invalid claims",
log: false,
}
}
return false
}
callback(loginClaims)
return true
}
}
/**
*/
function getRefreshToken() {
const refreshToken = context.cookie.get("bkdfRefreshToken")
if (refreshToken) {
console.log("REFRESHFOUND")
return getJwt(refreshToken, false)
}
return null
}
/**
* @param {BigCommerceCustomer} customer
*/
function createTibiCustomer(customer) {
const username = customer.form_fields.find((field) => field.name === "username")?.value
const internalCustomer = context.db.create("bigCommerceCustomer", {
bigCommerceId: customer.id,
email: customer.email.toLowerCase(),
username: username.toLowerCase(),
})
const orders = context.db.find("bigCommerceOrder", {
filter: {
customerBigCommerceId: customer.id,
},
})
if (orders && orders.length) {
orders.forEach((order) => {
context.db.update("bigCommerceOrder", order.id, {
customerTibiId: internalCustomer.id,
})
})
}
/*
not necessary for now, as bigcommerce itself takes care of it
try {
postUserRegister()
} catch (e) {
console.error(e)
}*/
}
/**
* @param {BigCommerceCustomer} customer
* @param {Customer} internalCustomer
*/
function updateTibiCustomer(customer, internalCustomer) {
const username = customer.form_fields.find((field) => field.name === "username")?.value
context.db.update("bigCommerceCustomer", internalCustomer.id, {
email: customer.email.toLowerCase(),
username: username,
})
}
/**
*
* @param {number} status
*/
function statusIsValid(status) {
return !!status && status < 400
}
function cryptchaCheck() {
const solutionId = context.data._sId
const solution = context.data._s
const body = JSON.stringify({ solution: solution })
const resp = context.http.fetch(
"https://cryptcha.webmakers.de/api/command?siteId=" +
cryptchaSiteId +
"&cmd=check&clear=1&solutionId=" +
solutionId,
{
method: "POST",
body,
headers: { "Content-Type": "application/json" },
}
)
const j = resp.body.json()
const corrent = j.status == "ok" && resp.status == 200
if (!corrent) {
throw {
status: 400,
log: false,
error: "incorrect data",
}
}
return true
}
module.exports = {
log,
clearSSRCache,
obj2str,
run,
setUpQuery,
calculateAverageDynamically,
sendOperatorRatingMail,
validateAndModifyRating,
productInsideOrder,
getJwt,
withAccount,
getRefreshToken,
statusIsValid,
createTibiCustomer,
updateTibiCustomer,
cryptchaCheck,
}