From 16ed641e3b959136e1766aacf707067a9e5f0b0a Mon Sep 17 00:00:00 2001 From: Julian Toledo Date: Fri, 16 Mar 2018 15:03:09 +0100 Subject: [PATCH] Fixes for API --- .eslintrc.json | 3 + controllers/api/pwa.js | 154 +++++++++++++++++++++----------- test/app/controllers/api/pwa.js | 37 -------- views/pwas/view-rss.hbs | 14 +++ 4 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 views/pwas/view-rss.hbs diff --git a/.eslintrc.json b/.eslintrc.json index bde83c71..c6f454ff 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,5 +31,8 @@ // http://eslint.org/docs/user-guide/configuring#specifying-environments "env": { "node": true + }, + "parserOptions": { + "ecmaVersion": 2017 } } diff --git a/controllers/api/pwa.js b/controllers/api/pwa.js index c135d192..4669d815 100644 --- a/controllers/api/pwa.js +++ b/controllers/api/pwa.js @@ -18,28 +18,11 @@ const express = require('express'); require('express-csv'); const pwaLib = require('../../lib/pwa'); +const libMetadata = require('../../lib/metadata'); const router = express.Router(); // eslint-disable-line new-cap const CACHE_CONTROL_EXPIRES = 60 * 60 * 1; // 1 hour const RSS = require('rss'); -const config = require('../../config/config'); -const apiKeyArray = config.get('API_TOKENS'); - -/** - * Checks for the presence of an API key from API_TOKENS in config.json - * - * Skip API key check of RSS feed - */ -function checkApiKey(req, res, next) { - if (req.query.key && - (apiKeyArray === req.query.key || - apiKeyArray.indexOf(req.query.key) !== -1) || - req.query.format === 'rss') { - return next(); - } - return res.sendStatus(403); -} - function getDate(date) { return new Date(date).toISOString().split('T')[0]; } @@ -88,34 +71,83 @@ class JsonWriter { } } +function render(res, view, options) { + return new Promise((resolve, reject) => { + res.render(view, options, (err, html) => { + if (err) { + console.log(err); + reject(err); + } + resolve(html); + }); + }); +} + +function renderOnePwaRss(pwa, req, res) { + const url = req.originalUrl; + const contentOnly = false || req.query.contentOnly; + let arg = Object.assign(libMetadata.fromRequest(req, url), { + pwa: pwa, + title: 'PWA Directory: ' + pwa.name, + description: 'PWA Directory: ' + pwa.name + ' - ' + pwa.description, + backlink: true, + contentOnly: contentOnly + }); + return render(res, 'pwas/view-rss.hbs', arg); +} + +async function asyncForEach(array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} + class RssWriter { - write(result, pwas) { + write(req, res, pwas) { const feed = new RSS({ /* eslint-disable camelcase */ title: 'PWA Directory', description: 'A Directory of Progressive Web Apps', - feed_url: 'https://pwa-directory.appspot.com/api/pwa?format=rss', + feed_url: 'https://pwa-directory.appspot.com/api/pwa/?format=rss', site_url: 'https://pwa-directory.appspot.com/', image_url: 'https://pwa-directory.appspot.com/favicons/android-chrome-144x144.png', pubDate: new Date(), custom_namespaces: { + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + l: 'http://purl.org/rss/1.0/modules/link/', + media: 'http://search.yahoo.com/mrss/', content: 'http://purl.org/rss/1.0/modules/content/' } }); - pwas.forEach(pwa => { - feed.item({ - title: pwa.displayName, - description: pwa.description, - url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id, - guid: pwa.id, - date: pwa.created, - custom_elements: [{'content:encoded': JSON.stringify(pwa)}] + const start = async _ => { + await asyncForEach(pwas, async pwa => { + let html = await renderOnePwaRss(pwa, req, res); + + const customElements = []; + customElements.push({'content:encoded': html}); + customElements.push({'l:link': {_attr: {'l:rel': 'http://purl.org/rss/1.0/modules/link/#alternate', + 'l:type': 'application/json', + 'rdf:resource': 'https://pwa-directory.appspot.com/api/pwa/' + pwa.id}}}); + if (pwa.iconUrl128) { + customElements.push({'media:thumbnail': {_attr: {url: pwa.iconUrl128, + height: '128', width: '128'}}}); + } + + feed.item({ + title: pwa.displayName, + url: 'https://pwa-directory.appspot.com/pwas/' + pwa.id, + description: html, + guid: pwa.id, + date: pwa.created, + custom_elements: customElements + }); }); - }); + res.setHeader('Content-Type', 'application/rss+xml'); + res.status(200).send(feed.xml()); + }; + start(); /* eslint-enable camelcase */ - result.setHeader('Content-Type', 'application/rss+xml'); - result.status(200).send(feed.xml()); } } @@ -128,34 +160,48 @@ const rssWriter = new RssWriter(); * * Returns all PWAs as JSON or ?format=csv for CSV. */ -router.get('/', checkApiKey, (req, res) => { +router.get('/:id*?', (req, res) => { let format = req.query.format || 'json'; let sort = req.query.sort || 'newest'; let skip = parseInt(req.query.skip, 10); - let limit = parseInt(req.query.limit, 10); - + let limit = parseInt(req.query.limit, 10) || 100; res.setHeader('Cache-Control', 'public, max-age=' + CACHE_CONTROL_EXPIRES); - pwaLib.list(skip, limit, sort) - .then(result => { - switch (format) { - case 'csv': { - csvWriter.write(res, result.pwas); - break; - } - case 'rss': { - rssWriter.write(res, result.pwas); - break; - } - default: { - jsonWriter.write(res, result.pwas); - } + + return new Promise((resolve, reject) => { + if (req.params.id) { // Single PWA + pwaLib.find(req.params.id) + .then(onePwa => { + resolve({pwas: [onePwa]}); + }) + .catch(err => { + console.log(err); + res.status(404); + res.json(err); + }); + } else { + resolve(pwaLib.list(skip, limit, sort)); + } + }) + .then(result => { + switch (format) { + case 'csv': { + csvWriter.write(res, result.pwas); + break; } - }) - .catch(err => { - console.log(err); - res.status(500); - res.json(err); - }); + case 'rss': { + rssWriter.write(req, res, result.pwas); + break; + } + default: { + jsonWriter.write(res, result.pwas); + } + } + }) + .catch(err => { + console.log(err); + res.status(500); + res.json(err); + }); }); module.exports = router; diff --git a/test/app/controllers/api/pwa.js b/test/app/controllers/api/pwa.js index d959d41f..8edbc2d5 100644 --- a/test/app/controllers/api/pwa.js +++ b/test/app/controllers/api/pwa.js @@ -56,30 +56,6 @@ describe('controllers.api.pwa', () => { // simpleMock.restore(); }); - it('respond with 400 without API key', done => { - simpleMock.mock(libPwa, 'list'); - // /api/ is part of the router, we need to start from /pwa/ - request(app) - .get('/pwa/') - .expect(400) - .expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => { - assert.equal(libPwa.list.callCount, 0); - done(); - }); - }); - - it('respond with 400 with wrong API key', done => { - simpleMock.mock(libPwa, 'list'); - // /api/ is part of the router, we need to start from /pwa/ - request(app) - .get('/pwa?key=xxxxxxx') - .expect(400) - .expect('Content-Type', 'text/plain; charset=utf-8').should.be.rejected.then(_ => { - assert.equal(libPwa.list.callCount, 0); - done(); - }); - }); - it('respond with 200 and json', done => { simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result)); // /api/ is part of the router, we need to start from /pwa/ @@ -103,18 +79,5 @@ describe('controllers.api.pwa', () => { done(); }); }); - - it('respond with 200 and rss, without key', done => { - simpleMock.mock(libPwa, 'list').resolveWith(Promise.resolve(result)); - // /api/ is part of the router, we need to start from /pwa/ - request(app) - .get('/pwa?format=rss') - .expect(200) - .expect('Content-Type', 'application/rss+xml; charset=utf-8') - .should.be.fulfilled.then(_ => { - assert.equal(libPwa.list.callCount, 1); - done(); - }); - }); }); }); diff --git a/views/pwas/view-rss.hbs b/views/pwas/view-rss.hbs new file mode 100644 index 00000000..c430ea1d --- /dev/null +++ b/views/pwas/view-rss.hbs @@ -0,0 +1,14 @@ +
+ + {{#if pwa.iconUrl128}} + + {{/if}} + +

{{pwa.displayName}}

+
{{pwa.description}}
+
+ + + + Open in PWA Directory +