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