From 2a56067362b947eb36d50ea44ae12bb7ee9de92b Mon Sep 17 00:00:00 2001 From: Matthieu Bessat Date: Fri, 2 Sep 2022 19:21:24 +0200 Subject: [PATCH] feat: return json format of organization page with .json extension and http content negotiation chore(deps): add negotiator and @types/negotiator refactor: put obfuscating logic in a twig template filter --- package.json | 1 + src/Utils.ts | 34 +++++++- src/app.ts | 3 + src/controllers/PublicController.ts | 124 +++++++++++++--------------- views/organization.twig | 8 +- yarn.lock | 5 ++ 6 files changed, 104 insertions(+), 71 deletions(-) diff --git a/package.json b/package.json index 5113f89..6e5a3ac 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@types/multer": "^1.4.3", "@types/multer-s3": "^2.7.7", "@types/mustache": "^4.0.1", + "@types/negotiator": "^0.6.1", "@types/nodemailer": "^6.4.0", "@types/papaparse": "^5.0.6", "@types/sanitize-html": "^1.23.3", diff --git a/src/Utils.ts b/src/Utils.ts index 3e2afd2..dd101bc 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -23,4 +23,36 @@ export default class Utils { } return bool } -} \ No newline at end of file + + public static obfuscate(original: string) { + if (original.length === 0) { return original } + let cipher: string[] = [] + let data: string = Buffer.from(original, 'utf-8').toString('base64') + data = data.replace(/A/gm, '$') + data = data.replace(/c/gm, '£') + data = data.replace(/b/gm, 'ù') + data = data.replace(/d/gm, '#') + data = data.replace(/e/gm, 'µ') + data = data.replace(/z/gm, '§') + data = data.replace(/i/gm, 'à') + data = data.replace(/f/gm, '|') + let remain = data + + let take: number = 0 + + while (remain.length != 0) { + take = remain.length + if (take > 5) { take = 5 } + take = Math.floor(Math.random() * (take + 1)) + if (take === 0) { take = 2 } + if (take > remain.length) { + take = remain.length + } + cipher.push('%_' + remain.substr(0, take) + '%_') + remain = remain.substr(take) + } + + return JSON.stringify(cipher).replace(/"/gm, "'") + } + +} diff --git a/src/app.ts b/src/app.ts index 0097887..87c9c0f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -89,6 +89,9 @@ let main = async () => { console.log('> App: Connected to mongodb') }) + // add custom twig extensions (filters and functions) + twig.extendFilter('obfuscate', Utils.obfuscate) + app.set('twig options', { allow_async: true, strict_variables: false diff --git a/src/controllers/PublicController.ts b/src/controllers/PublicController.ts index 38c070f..abcddef 100644 --- a/src/controllers/PublicController.ts +++ b/src/controllers/PublicController.ts @@ -12,6 +12,7 @@ import ErrorController from './ErrorController' import Utils from '../Utils' import mongoose from 'mongoose' import MediaService from '../MediaService' +import Negotiator from 'negotiator' export default class PublicController { @@ -72,7 +73,25 @@ export default class PublicController { } static async organization(req: express.Request, res: express.Response) { - Organization.find({ slugs: { '$in': req.params.slug } }).then(data => { + const rawSlug = req.params.slug + let format = 'html' + let slug = rawSlug + // allow json format by using 'file' extension + if (rawSlug.endsWith('.json')) { + slug = rawSlug.slice(0, -('.json'.length)) + format = 'json' + } + + // allow json format by Accept header + if (req.headers.accept) { + let neg = new Negotiator(req) + let negotiationResults = neg.mediaTypes(['text/html', 'application/json']) + if (negotiationResults.length > 0 && negotiationResults[0] === 'application/json') { + format = 'json' + } + } + + Organization.find({ slugs: { '$in': slug } }).then(data => { if (data.length === 0) { return ErrorController.notFoundHandler()(req, res) } else { @@ -80,6 +99,7 @@ export default class PublicController { let version = org.get('publishedVersion') let lastPublished = org.get('publishedAt') let isProposed = false + if (req.query.version === 'proposed') { isProposed = true lastPublished = org.get('updatedAt') @@ -87,62 +107,23 @@ export default class PublicController { } else if (lastPublished === undefined || lastPublished === null) { return ErrorController.notFoundHandler()(req, res) } + if (!Utils.isStrUsable(version, 'cover.location')) { version.cover = { location: "https://fva-condorcet.s3.fr-par.scw.cloud/default-cover-plz-get-you-a-real-image.jpg" } } - let offuscate = (original: string) => { - if (original.length === 0) { return original } - let cipher: string[] = [] - let data: string = Buffer.from(original, 'utf-8').toString('base64') - data = data.replace(/A/gm, '$') - data = data.replace(/c/gm, '£') - data = data.replace(/b/gm, 'ù') - data = data.replace(/d/gm, '#') - data = data.replace(/e/gm, 'µ') - data = data.replace(/z/gm, '§') - data = data.replace(/i/gm, 'à') - data = data.replace(/f/gm, '|') - let remain = data - - let take: number = 0 - - while (remain.length != 0) { - take = remain.length - if (take > 5) { take = 5 } - take = Math.floor(Math.random() * (take + 1)) - if (take === 0) { take = 2 } - if (take > remain.length) { - take = remain.length - } - cipher.push('%_' + remain.substr(0, take) + '%_') - remain = remain.substr(take) - } - - return JSON.stringify(cipher).replace(/"/gm, "'") - } - /** * Parse contact */ if (Utils.isUsable(version.contacts)) { - if (Utils.isStrUsable(version.contacts, 'email')) { - version.contacts.email = offuscate(version.contacts.email) - } if (Utils.isStrUsable(version.contacts, 'address')) { version.contacts.address = version.contacts.address.replace('%postalsep%', ' ').split('\n') } - if (Utils.isStrUsable(version.contacts, 'phone')) { - //let formated: any = formatPhone(version.contacts.phone) - version.contacts.phone = offuscate(version.contacts.phone) - } if (Utils.isUsable(version.contacts, 'peoples') && Array.isArray(version.contacts.peoples)) { version.contacts.peoples = version.contacts.peoples.map((p: any) => { // let formated: any = formatPhone(p.phone) // p.phoneInt = formated[0] // p.phoneSplit = formated[1] - p.email = offuscate(p.email) - p.phone = offuscate(p.phone) return p }) } @@ -202,8 +183,6 @@ export default class PublicController { media.isVideo = media.contentType.indexOf('video/') !== -1 return media }) - version.firstGallery = version.gallery.slice(0, 5) - version.secondGallery = version.gallery.slice(5) } /** @@ -217,7 +196,6 @@ export default class PublicController { when: days.map((day: any) => { return { day, - wow: 'wow', hours: s.when .filter((w: any) => w.day === day) .map((hour: any) => { return { from: hour.from, to: hour.to } }) @@ -229,18 +207,6 @@ export default class PublicController { }) } - if (version.schedule.length > 8) { - let isFirst = true; - version.scheduleParts = [[], []] - version.schedule.forEach((schedule: any) => { - version.scheduleParts[isFirst ? 0 : 1].push(schedule) - isFirst = !isFirst - }) - } else { - version.scheduleParts = [version.schedule] - } - - version.cutDescription = Utils.isStrUsable(version, 'descriptionLong') && version.descriptionLong.length > 800 // if (version.cutDescription) { // version.descriptionFirstHalf = version.descriptionLong.substr(0, 200) // not gonna lie @@ -251,18 +217,44 @@ export default class PublicController { // hour = '0' + hour // } - /** - * Obfuscate emails and phone - */ + if (format == 'html') { + // Do some adjustements that are specifics to the html format + + // TODO: Clarify this piece of code + // TODO: don't split up in two use directly the display grid or any other better way of doing this on the view side + if (version.schedule.length > 8) { + let isFirst = true; + version.scheduleParts = [[], []] + version.schedule.forEach((schedule: any) => { + version.scheduleParts[isFirst ? 0 : 1].push(schedule) + isFirst = !isFirst + }) + } else { + version.scheduleParts = [version.schedule] + } + version.cutDescription = Utils.isStrUsable(version, 'descriptionLong') && version.descriptionLong.length > 800 + + // Split gallery in two parts to only show at max 5 media on the page + if (Array.isArray(version.gallery)) { + version.firstGallery = version.gallery.slice(0, 5) + version.secondGallery = version.gallery.slice(5) + } + + res.render('organization.twig', { + layout: 'standalone', + data: version, + //lastPublished: lastPublished.toLocaleDateString('fr-FR') + ' ' + hour.substr(0, 5), + isProposed, + id: org.get('_id') + }) + } + if (format == 'json') { + res.json({ + ...version + }) + } - res.render('organization.twig', { - layout: 'standalone', - data: version, - //lastPublished: lastPublished.toLocaleDateString('fr-FR') + ' ' + hour.substr(0, 5), - isProposed, - id: org.get('_id') - }) } }).catch(err => () => { console.error(err) diff --git a/views/organization.twig b/views/organization.twig index 749766e..79ebba2 100644 --- a/views/organization.twig +++ b/views/organization.twig @@ -256,7 +256,7 @@ href="mailto:data" class="people-email-content offuscated" data-source-type="email" - data-source="{{ data.contacts.email }}"> + data-source="{{ data.contacts.email | obfuscate }}"> {% if data.contacts.phone is not empty %} @@ -268,7 +268,7 @@ href="telto:data" class="people-email-content offuscated" data-source-type="phone" - data-source="{{ data.contacts.phone }}"> + data-source="{{ data.contacts.phone | obfuscate }}"> {% endif %} @@ -293,7 +293,7 @@ href="mailto:data" class="people-email-content offuscated" data-source-type="email" - data-source="{{ people.email }}"> + data-source="{{ people.email | obfuscate }}"> {% if people.phone is not empty %} @@ -305,7 +305,7 @@ href="telto:data" class="people-email-content offuscated" data-source-type="phone" - data-source="{{ people.phone }}"> + data-source="{{ people.phone | obfuscate }}"> {% endif %} diff --git a/yarn.lock b/yarn.lock index d0aaf23..2b0bff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -759,6 +759,11 @@ resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-4.0.1.tgz#e4d421ed2d06d463b120621774185a5cd1b92d77" integrity sha512-wH6Tu9mbiOt0n5EvdoWy0VGQaJMHfLIxY/6wS0xLC7CV1taM6gESEzcYy0ZlWvxxiiljYvfDIvz4hHbUUDRlhw== +"@types/negotiator@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.1.tgz#4c75543f6ef87f427f4705e731a933595b7397f5" + integrity sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA== + "@types/node@*": version "14.6.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"