Initial commit
This commit is contained in:
494
api/hooks/lib/utils.js
Normal file
494
api/hooks/lib/utils.js
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user