Files
kontextwerk/api/hooks/lib/utils.js
2025-10-02 08:54:03 +02:00

495 lines
14 KiB
JavaScript

/**
*
* @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,
}