Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,33 @@ import { initTracking, countVisit } from './tracking.js'
//
// Auxiliary functions
//
function checkPath(path) {
function checkPath (path) {
return !!path.match(/^[a-zA-Z0-9][a-zA-Z0-9-]+$/)
}
function genericTemplateHandler(req, res, filename, baseURL, varianted = true, mode = 'text') {
function genericTemplateHandler (req, res, filename, baseURL, varianted = true, mode = 'text') {
const template = readFileSync(fileURLToPath(new URL(filename, baseURL)), 'utf-8')
const clientConfig = JSON.parse(readFileSync(fileURLToPath(new URL('../resources/clients.json', import.meta.url)), 'utf-8'))
const opts = {
role: req.auth?.client ?? 'guest',
roles: req.auth?.clients ?? [],
}
if(varianted) {
if (varianted) {
opts.variant = req.params.variant
if(Array.isArray(clientConfig.variantLabels)) {
opts.variants = Object.fromEntries((req.params.variant?.split('-') || []).map((it, idx) => [ clientConfig.variantLabels[idx] ?? `variant-${idx + 1}`, it ]))
if (Array.isArray(clientConfig.variantLabels)) {
opts.variants = Object.fromEntries((req.params.variant?.split('-') || []).map((it, idx) => [clientConfig.variantLabels[idx] ?? `variant-${idx + 1}`, it]))
// Check that there are no more keys than variant labels
if(Object.keys(opts.variants).length > clientConfig.variantLabels.length) {
if (Object.keys(opts.variants).length > clientConfig.variantLabels.length) {
res.sendStatus(404)
return
}
// Check if root-level key for each variant label exists
if(!clientConfig.variantLabels.every(label => clientConfig.via?.[label]?.[opts.variants[label]])) {
if (!clientConfig.variantLabels.every(label => clientConfig.via?.[label]?.[opts.variants[label]])) {
res.sendStatus(404)
return
}
} else {
// Check if root-level variant key exists
if(!clientConfig.via?.variant?.[opts.variant]) {
if (!clientConfig.via?.variant?.[opts.variant]) {
res.sendStatus(404)
return
}
Expand All @@ -54,7 +54,7 @@ function genericTemplateHandler(req, res, filename, baseURL, varianted = true, m
//
const jwksUrl = await fetch(`${process.env.KEYCLOAK_ADDRESS}/.well-known/openid-configuration`)
.then(stream => stream.json())
.then(({ jwks_uri }) => jwks_uri)
.then(({ jwks_uri: jwksUri }) => jwksUri)

//
// Express basic configuration
Expand All @@ -67,7 +67,7 @@ app.use((req, res, next) => {
next()
})
app.use(async (req, res, next) => {
if(!req.cookies.token) {
if (!req.cookies.token) {
next()
return
}
Expand All @@ -79,22 +79,24 @@ app.use(async (req, res, next) => {
const publicKey = jwkToPem(jwk)
req.auth = jwt.verify(req.cookies.token, publicKey)
next()
} catch(e) {
console.log(e)
} catch (e) {
console.warn(e)
delete req.cookies.token
next()
}
})
app.use(express.static('dist'))

if(process.env.VARIANT_ONLY == true) {
// We want to check for truthy, not true exactly
// eslint-disable-next-line eqeqeq
if (process.env.VARIANT_ONLY == true) {
// Rewrite to portal (variant-only)
app.use((req, res, next) => {
if(req.url.startsWith('/resources/')) {
if (req.url.startsWith('/resources/')) {
next()
return
}
if(req.url.startsWith('/portal/')) {
if (req.url.startsWith('/portal/')) {
res.redirect(308, req.url.slice(7))
return
}
Expand Down Expand Up @@ -132,22 +134,22 @@ app.use('/resources', express.static('resources'))
// Portal folder
//
app.get('/:portal', (req, res) => {
if(!checkPath(req.params.portal)) {
if (!checkPath(req.params.portal)) {
res.sendStatus(404)
return
}
res.redirect(308, `/${req.params.portal}/`)
})
app.use('/:portal/', (req, res, next) => {
if(!checkPath(req.params.portal)) {
if (!checkPath(req.params.portal)) {
res.sendStatus(404)
return
}
req.portalURL = new URL(`${req.params.portal}/`, new URL('../portal/', import.meta.url))
try {
const stats = statSync(fileURLToPath(req.portalURL))
if(!stats.isDirectory()) throw new Error()
} catch(e) {
if (!stats.isDirectory()) throw new Error()
} catch (e) {
res.sendStatus(404)
return
}
Expand All @@ -163,19 +165,19 @@ app.get('/:portal/favicon.ico', (req, res) => {
})

// Set tracking via monitoring software if configured
if(process.env.TRACKING_API_KEY_NAME && process.env.TRACKING_API_URL && process.env.TRACKING_API_KEY) {
if (process.env.TRACKING_API_KEY_NAME && process.env.TRACKING_API_URL && process.env.TRACKING_API_KEY) {
initTracking()
}

// Portal file handler
function indexHandler(req, res) {
function indexHandler (req, res) {
countVisit(req.params.variant)
genericTemplateHandler(req, res, 'index.html', req.portalURL, true, 'text')
}
function configJsHandler(req, res) {
function configJsHandler (req, res) {
genericTemplateHandler(req, res, 'config.js', req.portalURL, true, 'text')
}
function configJsonHandler(req, res) {
function configJsonHandler (req, res) {
genericTemplateHandler(req, res, 'config.json', req.portalURL, true, 'json')
}

Expand All @@ -189,14 +191,14 @@ app.use('/:portal', express.static('portal'))

// Variant-based portal
app.get('/:portal/:variant', (req, res) => {
if(!checkPath(req.params.variant)) {
if (!checkPath(req.params.variant)) {
res.sendStatus(404)
return
}
res.redirect(308, `/${req.params.portal}/${req.params.variant}/`)
})
app.use('/:portal/:variant/', (req, res, next) => {
if(!checkPath(req.params.variant)) {
if (!checkPath(req.params.variant)) {
res.sendStatus(404)
return
}
Expand All @@ -206,7 +208,7 @@ app.get('/:portal/:variant/', indexHandler)
app.get('/:portal/:variant/config.js', configJsHandler)
app.get('/:portal/:variant/config.json', configJsonHandler)

if(process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV === 'development') {
app.listen(9000, () => {
console.info('App is running at PORT: 9000')
})
Expand Down
76 changes: 39 additions & 37 deletions app/templater.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
function getByPath(obj, path) {
function getByPath (obj, path) {
return path.reduce((acc, it) => acc?.[it], obj)
}

function translatePath(s) {
function translatePath (s) {
const parts = s.split('.')
return parts.flatMap(part => {
const [ first, ...indices ] = part.split('[')
return [ first, ...indices.map(i => i.slice(0, -1)) ]
const [first, ...indices] = part.split('[')
return [first, ...indices.map(i => i.slice(0, -1))]
})
}

function splitCommandParameter(s) {
function splitCommandParameter (s) {
let bc = 0
let start
const res = []
for(let i = 0; i < s.length; i++) {
switch(s[i]) {
for (let i = 0; i < s.length; i++) {
switch (s[i]) {
case '(':
if(bc === 0) {
if (bc === 0) {
start = i + 1
}
bc++
break
case ')':
bc--
if(bc === 0) {
if (bc === 0) {
res.push(s.slice(start, i))
}
break
Expand All @@ -33,88 +33,90 @@ function splitCommandParameter(s) {
return res
}

function checkCommand(op, s, config, opts) {
function checkCommand (op, s, config, opts) {
const opPrefix = `${op} `
if(!s.startsWith(opPrefix)) return false
if (!s.startsWith(opPrefix)) return false
return splitCommandParameter(s.slice(opPrefix.length))
.map(par => getTemplate(par, config, opts))
}

function getTemplate(s, config, opts) {
switch(s) {
// This function is long, but not complex...
// eslint-disable-next-line complexity
function getTemplate (s, config, opts) {
switch (s) {
case 'otherwise': return true
case 'true': return true
case 'false': return false
case 'role': return opts.role
case 'roles': return opts.roles
case 'variant': return opts.variant
}
if(s.startsWith("'") && s.endsWith("'")) {
if (s.startsWith("'") && s.endsWith("'")) {
return s.slice(1, -1)
}
let res
if(!isNaN(res = +s)) {
if (!isNaN(res = +s)) {
return res
}
if(res = s.match(/^opt\((?<via>.+)\)$/)) {
if (res = s.match(/^opt\((?<via>.+)\)$/)) {
return opts[res.groups.via] || opts.variants?.[res.groups.via]
}
if(res = s.match(/^env\((?<env>.+)\)$/)) {
if (res = s.match(/^env\((?<env>.+)\)$/)) {
return process.env[res.groups.env]
}
if(res = s.match(/^via\((?<via>.+)\) (?<template>.+)$/)) {
if (res = s.match(/^via\((?<via>.+)\) (?<template>.+)$/)) {
const viaMap = config.via?.[res.groups.via]
const viaValue = opts[res.groups.via] || opts.variants?.[res.groups.via]
return getTemplate(res.groups.template, viaMap?.[viaValue] ?? viaMap?.fallback, opts)
}
if(res = checkCommand('not', s, config, opts)) return !res[0]
if(res = checkCommand('eq', s, config, opts)) return res[0] === res[1]
if(res = checkCommand('neq', s, config, opts)) return res[0] !== res[1]
if(res = checkCommand('and', s, config, opts)) return res[0] && res[1]
if(res = checkCommand('or', s, config, opts)) return res[0] || res[1]
if(res = checkCommand('elem', s, config, opts)) return Array.isArray(res[1]) && res[1].includes(res[0])
if (res = checkCommand('not', s, config, opts)) return !res[0]
if (res = checkCommand('eq', s, config, opts)) return res[0] === res[1]
if (res = checkCommand('neq', s, config, opts)) return res[0] !== res[1]
if (res = checkCommand('and', s, config, opts)) return res[0] && res[1]
if (res = checkCommand('or', s, config, opts)) return res[0] || res[1]
if (res = checkCommand('elem', s, config, opts)) return Array.isArray(res[1]) && res[1].includes(res[0])
const path = translatePath(s)
return getByPath(config, path)
}

export function fillTemplateText(s, config, opts) {
export function fillTemplateText (s, config, opts) {
const exactMatch = s.match(/^{{ ([^}]+?) }}$/)
if(exactMatch) {
if (exactMatch) {
return getTemplate(exactMatch[1], config, opts)
}

const textMatches = s.matchAll(/{{ (.+?) }}/g)
for(const [ from, target ] of textMatches) {
for (const [from, target] of textMatches) {
s = s.replace(from, getTemplate(target, config, opts))
}
return s
}

function checkTemplate(s, config, opts) {
function checkTemplate (s, config, opts) {
const res = s.match(/^{{ (?<target>.+) }}$/)
return res ? getTemplate(res.groups.target, config, opts) : null
}

export function fillTemplate(template, config, opts) {
if(Array.isArray(template)) {
export function fillTemplate (template, config, opts) {
if (Array.isArray(template)) {
return template
.map(it => fillTemplate(it, config, opts))
.filter(it => !(typeof it === 'object' && Object.keys(it).length === 0))
}
if(typeof template === 'object' && template !== null) {
for(const [ k, v ] of Object.entries(template)) {
if (typeof template === 'object' && template !== null) {
for (const [k, v] of Object.entries(template)) {
const res = checkTemplate(k, config, opts)
if(res) {
if (res) {
return fillTemplate(v, config, opts)
} else if(res !== null) {
} else if (res !== null) {
delete template[k]
}
}
return Object.fromEntries(Object.entries(template)
.map(([ k, v ]) => ([ k, fillTemplate(v, config, opts) ]))
.filter(([ k, v ]) => !(typeof v === 'object' && Object.keys(v).length === 0)))
.map(([k, v]) => ([k, fillTemplate(v, config, opts)]))
.filter(([k, v]) => !(typeof v === 'object' && Object.keys(v).length === 0)))
}
if(typeof template === 'string') {
if (typeof template === 'string') {
return fillTemplateText(template, config, opts)
}
return template
Expand Down
19 changes: 9 additions & 10 deletions app/tracking.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@

import { readFileSync } from 'node:fs'
import { URL, fileURLToPath } from 'node:url'

const hitRatePerPortal = {}

export function countVisit(reqParamsVariant) {
if(Object.prototype.hasOwnProperty.call(hitRatePerPortal, reqParamsVariant)) {
export function countVisit (reqParamsVariant) {
if (Object.prototype.hasOwnProperty.call(hitRatePerPortal, reqParamsVariant)) {
hitRatePerPortal[reqParamsVariant] += 1
}
}

function setHitRatePerPortal(labels, via, prefix = '') {
if(labels.length === 0) {
function setHitRatePerPortal (labels, via, prefix = '') {
if (labels.length === 0) {
hitRatePerPortal[prefix.slice(1)] = 0
return
}
const [ firstLabel, ...restLabels ] = labels
const [firstLabel, ...restLabels] = labels
const variants = Object.keys(via[firstLabel])

variants.forEach(variant => {
setHitRatePerPortal(restLabels, via, `${prefix}-${variant}`)
})
}

function sendRequestCountsToApi() {
function sendRequestCountsToApi () {
const data = JSON.stringify({
series: Object.keys(hitRatePerPortal).map(portal => ({
metric: 'portal.hit.rates',
Expand All @@ -33,7 +32,7 @@ function sendRequestCountsToApi() {
value: hitRatePerPortal[portal],
},
],
tags: [ `portal:${portal}`, `environment:${process.env.TRACKING_ENVIRONMENT}` ],
tags: [`portal:${portal}`, `environment:${process.env.TRACKING_ENVIRONMENT}`],
})),
})

Expand All @@ -48,7 +47,7 @@ function sendRequestCountsToApi() {
})
.then(response => response.text())
.then(responseData => {
console.log('Hit rates sent to Monitoring API', responseData)
console.info('Hit rates sent to Monitoring API', responseData)
Object.keys(hitRatePerPortal).forEach(portal => {
hitRatePerPortal[portal] = 0
})
Expand All @@ -58,7 +57,7 @@ function sendRequestCountsToApi() {
})
}

export function initTracking() {
export function initTracking () {
const clientConfig = JSON.parse(readFileSync(fileURLToPath(new URL('../resources/clients.json', import.meta.url)), 'utf-8'))
setHitRatePerPortal(clientConfig.variantLabels, clientConfig.via)

Expand Down
Loading