5 |
6 | {{/hideOptions}}
7 | {{#hideOptions}}
8 |
45 |
46 |
47 | Sort by:
48 |
49 | Recently Updated
50 | Downloads
51 | Rating
52 |
53 |
54 | {{/hideOptions}}
55 |
56 |
57 | {{>_gistitems}}
58 |
59 | {{^hideNav}}
60 |
65 | {{/hideNav}}
66 |
69 |
70 |
71 |
72 | {{^hideOptions}}
73 |
74 |
75 | {{/hideOptions}}
76 |
--------------------------------------------------------------------------------
/lib/ratings.js:
--------------------------------------------------------------------------------
1 | const db = require('./db')
2 | const events = require('./events')
3 | const npmNodes = require('./nodes')
4 |
5 | async function saveRating (thingId, user, rating) {
6 | await db.ratings.updateOne(
7 | {
8 | module: thingId,
9 | user
10 | },
11 | {
12 | $set: {
13 | module: thingId,
14 | user,
15 | rating,
16 | time: new Date()
17 | }
18 | },
19 | { upsert: true }
20 | )
21 | }
22 |
23 | async function removeRating (thingId, user) {
24 | await db.ratings.deleteOne({
25 | module: thingId,
26 | user
27 | })
28 | }
29 |
30 | async function getModuleRating (npmModule) {
31 | const results = await db.ratings.aggregate(
32 | [
33 | { $match: { module: npmModule } },
34 | {
35 | $group: { _id: '$module', total: { $sum: '$rating' }, count: { $sum: 1 } }
36 | }
37 | ]
38 | ).toArray()
39 | console.log(results)
40 | if (results.length > 0) {
41 | return {
42 | module: npmModule,
43 | total: results[0].total,
44 | count: results[0].count
45 | }
46 | }
47 | }
48 |
49 | async function getForUser (npmModule, user) {
50 | return await db.ratings.findOne({
51 | user,
52 | module: npmModule
53 | })
54 | }
55 |
56 | async function removeForModule (npmModule) {
57 | return db.ratings.deleteOne({ module: npmModule })
58 | }
59 |
60 | async function getRatedModules () {
61 | return db.ratings.distinct('module', {})
62 | }
63 |
64 | async function rateThing (thingId, userId, rating) {
65 | try {
66 | rating = Number(rating)
67 | if (isNaN(rating) || rating === 0) {
68 | await removeRating(thingId, userId)
69 | await events.add({
70 | action: 'module_rating',
71 | module: thingId,
72 | message: 'removed',
73 | user: userId
74 | })
75 | } else {
76 | await saveRating(thingId, userId, rating)
77 | await events.add({
78 | action: 'module_rating',
79 | module: thingId,
80 | message: rating,
81 | user: userId
82 | })
83 | }
84 | const currentRating = await module.exports.get(thingId)
85 | let nodeRating = {}
86 | if (currentRating && currentRating.count > 0) {
87 | nodeRating = {
88 | score: currentRating.total / currentRating.count,
89 | count: currentRating.count
90 | }
91 | }
92 | return npmNodes.update(thingId, { rating: nodeRating })
93 | } catch (err) {
94 | console.log('error rating node module: ' + thingId, err)
95 | }
96 | }
97 |
98 | module.exports = {
99 | rateThing,
100 | get: async function (thingId, user) {
101 | console.log('rate get', thingId, user)
102 | let rating = null
103 | const totalRatings = await getModuleRating(thingId)
104 | if (!totalRatings) {
105 | return null
106 | }
107 | rating = totalRatings
108 | const userRating = await getForUser(thingId, user)
109 | if (userRating) {
110 | rating.userRating = userRating
111 | }
112 | return rating
113 | },
114 | getUserRating: getForUser,
115 | getRatedModules,
116 | removeForModule
117 | }
118 |
--------------------------------------------------------------------------------
/routes/categories.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const mustache = require('mustache')
3 |
4 | const categories = require('../lib/categories')
5 | const templates = require('../lib/templates')
6 | const appUtils = require('../lib/utils')
7 |
8 | const app = express()
9 |
10 | /**
11 | * Page: Browse Categories
12 | */
13 | app.get('/categories', async function (req, res) {
14 | const context = {}
15 | context.categories = await categories.getAll()
16 | context.sessionuser = req.session.user
17 | context.isAdmin = req.session.user?.isAdmin
18 | context.isModerator = req.session.user?.isModerator
19 | res.send(mustache.render(templates.categories, context, templates.partials))
20 | })
21 |
22 | /**
23 | * Page: Add Category
24 | */
25 | app.get('/add/category', appUtils.requireRole('admin'), function (req, res) {
26 | if (!req.session.user) {
27 | return res.redirect('/add')
28 | }
29 | const context = {}
30 | context.sessionuser = req.session.user
31 | res.send(mustache.render(templates.addCategory, context, templates.partials))
32 | })
33 |
34 | /**
35 | * API: Add Category
36 | */
37 | app.post('/categories', appUtils.requireRole('admin'), async function (req, res) {
38 | const collection = {
39 | name: req.body.title,
40 | description: req.body.description
41 | }
42 | try {
43 | const id = await categories.create(collection)
44 | res.send('/categories/' + id)
45 | } catch (err) {
46 | console.log('Error creating category:', err)
47 | res.send(err)
48 | }
49 | })
50 |
51 | /**
52 | * Page: Category view
53 | */
54 | app.get('/categories/:category', async function (req, res) {
55 | const context = {}
56 | context.sessionuser = req.session.user
57 | context.isAdmin = req.session.user?.isAdmin
58 | context.isModerator = req.session.user?.isModerator
59 | context.query = {
60 | category: req.params.category,
61 | type: 'node',
62 | hideOptions: true,
63 | ignoreQueryParams: true
64 | }
65 | try {
66 | context.category = await categories.get(req.params.category)
67 | context.category.summary = await appUtils.renderMarkdown(context.category.summary)
68 | context.category.description = await appUtils.renderMarkdown(context.category.description)
69 |
70 | res.send(mustache.render(templates.category, context, templates.partials))
71 | } catch (err) {
72 | if (err) {
73 | console.log('error loading nodes:', err)
74 | }
75 | res.status(404).send(mustache.render(templates['404'], context, templates.partials))
76 | }
77 | })
78 |
79 | /**
80 | * Page: Edit Category
81 | */
82 | app.get('/categories/:category/edit', appUtils.csrfProtection(), appUtils.requireRole('admin'), async function (req, res) {
83 | const context = {}
84 | context.csrfToken = req.csrfToken()
85 | context.sessionuser = req.session.user
86 | try {
87 | context.category = await categories.get(req.params.category)
88 | res.send(mustache.render(templates.addCategory, context, templates.partials))
89 | res.end()
90 | } catch (err) {
91 | console.log('err', err)
92 | res.sendStatus(400)
93 | }
94 | })
95 |
96 | /**
97 | * API: Edit Category
98 | */
99 | app.put('/categories/:category', appUtils.csrfProtection(), appUtils.requireRole('admin'), async function (req, res) {
100 | const category = {
101 | _id: req.params.category
102 | }
103 | if (req.body.title) {
104 | category.name = req.body.title.trim()
105 | }
106 | if (Object.prototype.hasOwnProperty.call(req.body, 'description')) {
107 | category.description = req.body.description
108 | }
109 | try {
110 | await categories.update(category)
111 | res.send('/categories/' + req.params.category)
112 | } catch (err) {
113 | console.log('Error updating category:', err)
114 | res.status(400).json(err)
115 | }
116 | })
117 |
118 | module.exports = app
119 |
--------------------------------------------------------------------------------
/public/js/tags.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | const tagger = function (options) {
3 | let tags = []
4 | options = options || {}
5 | const lipre = options.lipre || ''
6 | const lipost = options.lipost || ''
7 |
8 | const tagList = $('ul#add-flow-tags')
9 | const originalTags = []
10 |
11 | function formatTag (tag) {
12 | return lipre.replace(/@@TAG@@/g, tag) + tag + lipost.replace(/@@TAG@@/g, tag)
13 | }
14 |
15 | $('li', tagList).each(function (i, e) {
16 | const li = $(e)
17 | const tag = li.attr('tag')
18 | li.html(tag + '
')
19 | $('a', li).click(function (e) {
20 | removeTag(tag)
21 | e.preventDefault()
22 | })
23 | tags.push(tag)
24 | originalTags.push(tag)
25 | })
26 |
27 | const listInput = $('
')
28 | tagList.append(listInput)
29 |
30 | const tagInput = $('#add-flow-tags-input')
31 | tagList.click(function (e) {
32 | tagInput.focus()
33 | })
34 | tagInput.on('focusin', function (e) {
35 | tagList.addClass('active')
36 | })
37 | tagInput.on('focusout', function (e) {
38 | tagList.removeClass('active')
39 | const val = tagInput.val()
40 | if (val !== '') {
41 | addTag(val)
42 | tagInput.val('')
43 | }
44 | })
45 | tagInput.on('keydown', function (e) {
46 | if (e.which === 32 || (e.which === 188 && !e.shiftKey)) {
47 | const val = tagInput.val()
48 | if (val !== '') {
49 | if (addTag(val)) {
50 | tagInput.val('')
51 | }
52 | }
53 | e.preventDefault()
54 | } else if (e.which === 8) {
55 | const val = tagInput.val()
56 | if (val === '') {
57 | const prevTag = $(this).parent().prev().attr('tag')
58 | if (prevTag) {
59 | removeTag(prevTag)
60 | }
61 | e.preventDefault()
62 | }
63 | }
64 | })
65 |
66 | function strip () {
67 | $('li', tagList).each(function (i, e) {
68 | const li = $(e)
69 | if (li.hasClass('tag-input')) {
70 | li.remove()
71 | } else {
72 | const tag = $(li).attr('tag')
73 | li.html(formatTag(tag))
74 | }
75 | })
76 | }
77 |
78 | function cancel () {
79 | $('li', tagList).remove()
80 | tags = originalTags
81 | for (const i in tags) {
82 | tagList.append($('
').html(formatTag(tags[i])).attr('tag', tags[i]))
83 | }
84 | }
85 | function addTag (tag) {
86 | tag = tag.replace(/&/g, '&').replace(//g, '>')
87 | const i = $.inArray(tag, tags)
88 | if (i === -1) {
89 | tags.push(tag)
90 |
91 | const newtag = $(' ').html(tag + ' ')
92 | $(newtag).attr('tag', tag)
93 | $('a', newtag).click(function (e) {
94 | removeTag(tag)
95 | e.preventDefault()
96 | })
97 | tagInput.parent().before(newtag)
98 | return true
99 | } else {
100 | const existingTag = $("li[tag='" + tag + "']", tagList)
101 | existingTag.css({ borderColor: '#f00', background: '#fcc' })
102 | window.setTimeout(function () {
103 | existingTag.css({ borderColor: '#ccc', background: '#f5f5f5' })
104 | }, 1000)
105 | return false
106 | }
107 | }
108 | function removeTag (tag) {
109 | const i = $.inArray(tag, tags)
110 | if (i !== -1) {
111 | tags.splice(i, 1)
112 |
113 | $('li', tagList).each(function (i, e) {
114 | if ($(e).attr('tag') === tag) {
115 | e.remove()
116 | }
117 | })
118 | }
119 | }
120 | return {
121 | add: addTag,
122 | remove: removeTag,
123 | get: function () { return tags },
124 | strip,
125 | cancel
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | const csrf = require('csurf')
2 | const createDOMPurify = require('dompurify')
3 | const { JSDOM } = require('jsdom')
4 | const window = new JSDOM('').window
5 | const DOMPurify = createDOMPurify(window)
6 | const { marked } = require('marked')
7 |
8 | function formatDate (dateString) {
9 | if (!dateString) {
10 | return ''
11 | }
12 | const now = Date.now()
13 | const d = new Date(dateString)
14 | let delta = now - d.getTime()
15 |
16 | delta /= 1000
17 |
18 | if (delta < 60) {
19 | return 'seconds ago'
20 | }
21 |
22 | delta = Math.floor(delta / 60)
23 |
24 | if (delta < 10) {
25 | return 'minutes ago'
26 | }
27 | if (delta < 60) {
28 | return delta + ' minutes ago'
29 | }
30 |
31 | delta = Math.floor(delta / 60)
32 |
33 | if (delta < 24) {
34 | return delta + ' hour' + (delta > 1 ? 's' : '') + ' ago'
35 | }
36 |
37 | delta = Math.floor(delta / 24)
38 |
39 | if (delta < 7) {
40 | return delta + ' day' + (delta > 1 ? 's' : '') + ' ago'
41 | }
42 | let weeks = Math.floor(delta / 7)
43 | const days = delta % 7
44 |
45 | if (weeks < 4) {
46 | if (days === 0) {
47 | return weeks + ' week' + (weeks > 1 ? 's' : '') + ' ago'
48 | } else {
49 | return weeks + ' week' + (weeks > 1 ? 's' : '') + ', ' + days + ' day' + (days > 1 ? 's' : '') + ' ago'
50 | }
51 | }
52 |
53 | let months = Math.floor(weeks / 4)
54 | weeks = weeks % 4
55 |
56 | if (months < 12) {
57 | if (weeks === 0) {
58 | return months + ' month' + (months > 1 ? 's' : '') + ' ago'
59 | } else {
60 | return months + ' month' + (months > 1 ? 's' : '') + ', ' + weeks + ' week' + (weeks > 1 ? 's' : '') + ' ago'
61 | }
62 | }
63 |
64 | const years = Math.floor(months / 12)
65 | months = months % 12
66 |
67 | if (months === 0) {
68 | return years + ' year' + (years > 1 ? 's' : '') + ' ago'
69 | } else {
70 | return years + ' year' + (years > 1 ? 's' : '') + ', ' + months + ' month' + (months > 1 ? 's' : '') + ' ago'
71 | }
72 | }
73 |
74 | function formatShortDate (d) {
75 | let delta = Date.now() - (new Date(d)).getTime()
76 | delta /= 1000
77 | const days = Math.floor(delta / (60 * 60 * 24))
78 | const weeks = Math.floor(days / 7)
79 | let months = Math.floor(weeks / 4)
80 | const years = Math.floor(months / 12)
81 | if (days < 7) {
82 | return days + 'd'
83 | } else if (weeks < 4) {
84 | return weeks + 'w'
85 | } else if (months < 12) {
86 | return months + 'm'
87 | } else {
88 | months = months % 12
89 | if (months > 0) {
90 | return years + 'y ' + months + 'm'
91 | }
92 | return years + 'y'
93 | }
94 | }
95 |
96 | const csrfProtection = csrf({ cookie: true })
97 |
98 | async function renderMarkdown (src, opt) {
99 | const content = await marked.parse(src, { async: true, ...opt })
100 | return DOMPurify.sanitize(content)
101 | }
102 |
103 | /**
104 | * Middleware that validates the user has a given role
105 | * @param {String} role one of user/mod/admin
106 | */
107 | function requireRole (role) {
108 | return (req, res, next) => {
109 | if (req.session.user) {
110 | if (!role || role === 'user') {
111 | // Logged in user
112 | next()
113 | return
114 | }
115 | if (role === 'admin' && req.session.user.isAdmin) {
116 | next()
117 | return
118 | }
119 | if (role === 'mod' && (req.session.user.isAdmin || req.session.user.isModerator)) {
120 | next()
121 | return
122 | }
123 | }
124 | console.log('rejecting request', role, req.session.user)
125 | res.status(404).send()
126 | }
127 | }
128 |
129 | function generateSummary (desc) {
130 | let summary = (desc || '').split('\n')[0]
131 | const re = /\[(.*?)\]\(.*?\)/g
132 | let m
133 | while ((m = re.exec(summary)) !== null) {
134 | summary = summary.substring(0, m.index) + m[1] + summary.substring(m.index + m[0].length)
135 | }
136 |
137 | if (summary.length > 150) {
138 | summary = summary.substring(0, 150).split('\n')[0] + '...'
139 | }
140 | return summary
141 | }
142 |
143 | module.exports = {
144 | generateSummary,
145 | renderMarkdown,
146 | formatDate,
147 | formatShortDate,
148 | csrfProtection: () => csrfProtection,
149 | requireRole
150 | }
151 |
--------------------------------------------------------------------------------
/test/lib/ratings_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable n/no-unpublished-require */
2 | // eslint-disable-next-line no-unused-vars
3 | const should = require('should')
4 | const sinon = require('sinon')
5 |
6 | const db = require('../../lib/db')
7 | const ratings = require('../../lib/ratings')
8 | const sandbox = sinon.createSandbox()
9 |
10 | // With the move to the async mongodb client, how we mock the db module needs to change
11 | // I haven't figured it all out yet, so keeping this spec in place for the time being
12 |
13 | describe.skip('ratings', function () {
14 | before(async function () {
15 | return db.init()
16 | })
17 | afterEach(function () {
18 | sandbox.restore()
19 | })
20 |
21 | it('#save', function (done) {
22 | const dbUpdate = sandbox.stub(db.ratings, 'update').yields(null)
23 |
24 | const testRating = {
25 | user: 'testuser',
26 | module: 'node-red-dashboard',
27 | time: new Date(),
28 | rating: 4
29 | }
30 |
31 | ratings.save(testRating).then(function () {
32 | sinon.assert.calledWith(dbUpdate,
33 | { module: testRating.module, user: testRating.user }, testRating, { upsert: true })
34 | done()
35 | }).catch(function (err) {
36 | done(err)
37 | })
38 | })
39 |
40 | it('#remove', function (done) {
41 | const dbRemove = sandbox.stub(db.ratings, 'remove').yields(null)
42 | const testRating = {
43 | user: 'testuser',
44 | module: 'node-red-dashboard'
45 | }
46 | ratings.remove(testRating).then(function () {
47 | sinon.assert.calledWith(dbRemove, testRating)
48 | done()
49 | }).catch(function (err) {
50 | done(err)
51 | })
52 | })
53 |
54 | it('#get', function (done) {
55 | const totalRating = [{ _id: 'node-red-dashboard', total: 19, count: 2 }]
56 | const userRating = {
57 | user: 'test',
58 | module: 'node-red-dashboard',
59 | rating: 4,
60 | version: '2.6.1',
61 | time: new Date('2018-01-15T00:34:27.998Z')
62 | }
63 |
64 | sandbox.stub(db.ratings, 'aggregate').yields(null,
65 | totalRating
66 | )
67 |
68 | sandbox.stub(db.ratings, 'findOne').yields(null, userRating)
69 |
70 | ratings.get('node-red-dashboard', 'test').then(function (found) {
71 | found.should.eql({
72 | module: 'node-red-dashboard',
73 | total: 19,
74 | count: 2,
75 | userRating: {
76 | user: 'test',
77 | module: 'node-red-dashboard',
78 | rating: 4,
79 | version: '2.6.1',
80 | time: new Date('2018-01-15T00:34:27.998Z')
81 | }
82 | })
83 | done()
84 | }).catch(function (err) {
85 | done(err)
86 | })
87 | })
88 |
89 | it('#get no user rating', function (done) {
90 | sandbox.stub(db.ratings, 'aggregate').yields(null,
91 | [{ _id: 'node-red-dashboard', total: 19, count: 2 }]
92 | )
93 | const foundRating = {
94 | user: 'test',
95 | module: 'node-red-dashboard',
96 | rating: 4,
97 | version: '2.6.1',
98 | time: new Date('2018-01-15T00:34:27.998Z')
99 | }
100 |
101 | const dbFindOne = sandbox.stub(db.ratings, 'findOne').yields(null,
102 | foundRating
103 | )
104 |
105 | ratings.get('node-red-dashboard').then(function (found) {
106 | found.should.eql({
107 | module: 'node-red-dashboard',
108 | total: 19,
109 | count: 2
110 | })
111 | sinon.assert.notCalled(dbFindOne)
112 | done()
113 | }).catch(function (err) {
114 | done(err)
115 | })
116 | })
117 |
118 | it('#getRatedModules', function (done) {
119 | const list = ['node-red-dashboard', 'node-red-contrib-influxdb', 'node-red-contrib-noble']
120 | sandbox.stub(db.ratings, 'distinct').yields(null, list)
121 |
122 | ratings.getRatedModules().then(function (modList) {
123 | modList.should.eql(list)
124 | done()
125 | })
126 | })
127 |
128 | it('#removeForModule', function (done) {
129 | const dbRemove = sandbox.stub(db.ratings, 'remove').yields(null)
130 |
131 | ratings.removeForModule('node-red-dashboard').then(function () {
132 | sinon.assert.calledWith(dbRemove, { module: 'node-red-dashboard' })
133 | done()
134 | })
135 | })
136 | })
137 |
--------------------------------------------------------------------------------
/lib/github.js:
--------------------------------------------------------------------------------
1 | const https = require('https')
2 |
3 | const settings = require('../config')
4 | const defaultAccessToken = settings.github.accessToken
5 |
6 | function send (opts) {
7 | return new Promise((resolve, reject) => {
8 | const accessToken = opts.accessToken || defaultAccessToken
9 | const method = (opts.method || 'GET').toUpperCase()
10 | const path = opts.path
11 | const headers = opts.headers || {}
12 | const body = opts.body
13 |
14 | const _headers = {
15 | 'user-agent': 'node-red',
16 | accept: 'application/vnd.github.v3',
17 | authorization: 'token ' + accessToken
18 | }
19 | if (body) {
20 | _headers['content-type'] = 'application/json'
21 | }
22 | for (const h in headers) {
23 | _headers[h] = headers[h]
24 | }
25 | const options = {
26 | host: 'api.github.com',
27 | port: 443,
28 | path,
29 | method,
30 | headers: _headers
31 | }
32 | // console.log("---------------");
33 | // console.log(options);
34 | // console.log("---------------");
35 | const req = https.request(options, function (res) {
36 | res.setEncoding('utf8')
37 | let data = ''
38 | res.on('data', function (chunk) {
39 | data += chunk
40 | })
41 | res.on('end', function () {
42 | if (/^application\/json/.test(res.headers['content-type'])) {
43 | data = JSON.parse(data)
44 | data.etag = res.headers.etag
45 | data.rateLimit = {
46 | limit: res.headers['x-ratelimit-limit'],
47 | remaining: res.headers['x-ratelimit-remaining'],
48 | reset: res.headers['x-ratelimit-reset']
49 | }
50 | }
51 | resolve({ statusCode: res.statusCode, headers: res.headers, data })
52 | })
53 | })
54 | req.on('error', function (e) {
55 | console.log('problem with request: ' + e.message)
56 | reject(e)
57 | })
58 |
59 | if (body) {
60 | req.write(JSON.stringify(body) + '\n')
61 | }
62 | req.end()
63 | })
64 | }
65 |
66 | function getSimple (path, lastEtag) {
67 | return new Promise((resolve, reject) => {
68 | const headers = {}
69 | if (lastEtag) {
70 | headers['If-None-Match'] = lastEtag
71 | }
72 | console.log('github.getSimple', path)
73 | send({ path, headers }).then(function (result) {
74 | if (lastEtag && result.statusCode === 304) {
75 | resolve(null)
76 | return null
77 | } else if (result.statusCode === 404) {
78 | reject(result)
79 | return null
80 | } else {
81 | resolve(result.data)
82 | return null
83 | }
84 | }).catch(function (er) { reject(er) })
85 | })
86 | }
87 |
88 | function getGistFile (fileUrl) {
89 | return new Promise((resolve, reject) => {
90 | const req = https.get(fileUrl, function (res) {
91 | res.setEncoding('utf8')
92 | let data = ''
93 | res.on('data', function (chunk) {
94 | data += chunk
95 | })
96 | res.on('end', function () {
97 | resolve(data)
98 | })
99 | })
100 | req.on('error', function (e) {
101 | console.log('problem with request: ' + e.message)
102 | reject(e)
103 | })
104 | req.end()
105 | })
106 | }
107 |
108 | module.exports = {
109 | getGistFile,
110 | getAuthedUser: function (accessToken) {
111 | return new Promise((resolve, reject) => {
112 | send({ path: '/user', accessToken }).then(function (result) {
113 | resolve(result.data)
114 | return null
115 | }).catch(function (er) { reject(er) })
116 | })
117 | },
118 | getUser: function (user, lastEtag) {
119 | return getSimple('/users/' + user, lastEtag)
120 | },
121 |
122 | getGist: function (id, lastEtag) {
123 | return getSimple('/gists/' + id, lastEtag)
124 | },
125 |
126 | createGist: function (gistData, accessToken) {
127 | return new Promise((resolve, reject) => {
128 | send({ path: '/gists', method: 'POST', body: gistData, accessToken }).then(function (result) {
129 | resolve(result.data)
130 | return null
131 | }).catch(function (er) { reject(er) })
132 | })
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const bodyParser = require('body-parser')
4 | const MongoStore = require('connect-mongo')
5 | const cookieParser = require('cookie-parser')
6 | const express = require('express')
7 | const { rateLimit } = require('express-rate-limit')
8 | const session = require('express-session')
9 | const mustache = require('mustache')
10 | const serveStatic = require('serve-static')
11 |
12 | const settings = require('./config')
13 | const db = require('./lib/db')
14 | const templates = require('./lib/templates')
15 |
16 | const limiter = rateLimit({
17 | windowMs: 5 * 60 * 1000, // 5 minutes
18 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
19 | standardHeaders: false, // Return rate limit info in the `RateLimit-*` headers
20 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
21 | handler: (req, res, next, options) => {
22 | console.log(`Rate Limit: ${req.method} ${req.url} ${req.ip} `)
23 | res.status(options.statusCode).send(options.message)
24 | }
25 | })
26 |
27 | ;(async function () {
28 | await db.init()
29 | const app = express()
30 |
31 | app.use(cookieParser())
32 | if (!settings.maintenance) {
33 | app.use(session({
34 | store: MongoStore.create({
35 | mongoUrl: settings.mongo.url,
36 | touchAfter: 24 * 3600,
37 | collectionName: settings.session.collection || 'sessions_new'
38 | }),
39 | key: settings.session.key,
40 | secret: settings.session.secret,
41 | saveUninitialized: false,
42 | resave: false
43 | }))
44 | app.use(bodyParser.json())
45 | app.use(bodyParser.urlencoded({ extended: true }))
46 | }
47 |
48 | app.use('/', serveStatic(path.join(__dirname, 'public')))
49 | if (process.env.FLOW_ENV !== 'PRODUCTION') {
50 | app.use('*', function (req, res, next) {
51 | console.log('>', req.url)
52 | next()
53 | })
54 | }
55 |
56 | app.use(limiter)
57 |
58 | if (!settings.maintenance) {
59 | app.set('trust proxy', 1)
60 | app.use(require('./routes/index'))
61 | app.use(require('./routes/auth'))
62 | app.use(require('./routes/flows'))
63 | app.use(require('./routes/nodes'))
64 | app.use(require('./routes/admin'))
65 | app.use(require('./routes/users'))
66 | app.use(require('./routes/api'))
67 | app.use(require('./routes/collections'))
68 | app.use(require('./routes/categories'))
69 | app.use(function (err, req, res, next) {
70 | if (err.code !== 'EBADCSRFTOKEN') {
71 | console.log('here', err)
72 | return next(err)
73 | }
74 | // handle CSRF token errors here
75 | res.status(403)
76 | res.send('Invalid request')
77 | let stringBody = ''
78 | if (req.method === 'POST') {
79 | stringBody = req.body
80 | if (typeof req.body === 'object') {
81 | try {
82 | stringBody = JSON.stringify(req.body)
83 | } catch (err) {
84 | }
85 | }
86 | if (typeof stringBody !== 'string') {
87 | stringBody = '' + stringBody
88 | }
89 | if (stringBody.length > 30) {
90 | const l = stringBody.length
91 | stringBody = stringBody.substring(0, 30) + `...[length:${l}]`
92 | }
93 | }
94 | console.log(`CSRF Error: ${req.method} ${req.url} ${req.ip} ${stringBody} `)
95 | })
96 | app.use(function (req, res) {
97 | // We see lots of requests to these paths that we don't want to flood
98 | // the logs with so we missing more interesting things
99 | if (!/^\/(js|flow|node|css|font|jquery|images|font-awesome)\/?$/i.test(req.url)) {
100 | console.log(`404: ${req.method} ${req.url} ${req.ip}`)
101 | }
102 | res.status(404).send(mustache.render(templates['404'], { sessionuser: req.session.user }, templates.partials))
103 | })
104 | } else {
105 | app.use(function (req, res) {
106 | res.send(mustache.render(templates.maintenance, {}, templates.partials))
107 | })
108 | }
109 | app.listen(settings.port || 20982)
110 | console.log(`Listening on http://localhost:${settings.port || 20982}`)
111 | if (process.env.FLOW_ENV === 'PRODUCTION') {
112 | require('./lib/events').add({
113 | action: 'started',
114 | message: 'Flow Library app started'
115 | })
116 | }
117 | })()
118 |
--------------------------------------------------------------------------------
/lib/nodes.js:
--------------------------------------------------------------------------------
1 | const db = require('./db')
2 |
3 | const CORE_NODES = ['inject', 'debug', 'complete', 'catch', 'status', 'link in', 'link out', 'link call', 'comment', 'unknown', 'function', 'switch', 'change', 'range', 'template', 'delay', 'trigger', 'exec', 'rbe', 'tls-config', 'http proxy', 'mqtt in', 'mqtt out', 'mqtt-broker', 'http in', 'http response', 'http request', 'websocket in', 'websocket out', 'websocket-listener', 'websocket-client', 'tcp in', 'tcp out', 'tcp request', 'udp in', 'udp out', 'csv', 'html', 'json', 'xml', 'yaml', 'split', 'join', 'sort', 'batch', 'file', 'file in', 'watch'].reduce(function (o, v, i) {
4 | o[v] = 1
5 | return o
6 | }, {})
7 |
8 | async function saveToDb (info) {
9 | try {
10 | if (info) {
11 | info.type = 'node'
12 | info.updated_at = info.time.modified
13 | info.npmOwners = info.maintainers.map(function (m) { return m.name })
14 | console.log('saveToDb update', info._id)
15 | await db.flows.updateOne(
16 | { _id: info._id },
17 | { $set: info },
18 | { upsert: true }
19 | )
20 | return info._id + ' (' + info['dist-tags'].latest + ')'
21 | } else {
22 | // If the module was already downloaded, then this will get passed
23 | // null. Had it rejected, we would delete the module.
24 | }
25 | } catch (err) {
26 | console.log('!!!! saveToDb err', err)
27 | throw err
28 | }
29 | }
30 |
31 | async function update (id, info) {
32 | return db.flows.updateOne({ _id: id }, { $set: info }, {})
33 | }
34 |
35 | function removeFromDb (id) {
36 | return db.flows.deleteOne({ _id: id })
37 | }
38 |
39 | async function get (name, projection) {
40 | let query = {}
41 | let proj = {}
42 | // var proj = {
43 | // name:1,
44 | // description:1,
45 | // "dist-tags":1,
46 | // time:1,
47 | // author:1,
48 | // keywords:1
49 | // };
50 | if (typeof name === 'object') {
51 | proj = name
52 | } else if (typeof name === 'string') {
53 | query = { _id: name }
54 | if (typeof projection === 'object') {
55 | proj = projection
56 | }
57 | }
58 |
59 | query.type = 'node'
60 |
61 | const docs = await db.flows.find(query, { projection: proj }).sort({ 'time.modified': 1 }).toArray()
62 | if (query._id) {
63 | if (!docs[0]) {
64 | const err = new Error('node not found:' + name)
65 | err.code = 'NODE_NOT_FOUND'
66 | throw err
67 | } else {
68 | if (docs[0].versions) {
69 | docs[0].versions.latest = JSON.parse(docs[0].versions.latest)
70 | }
71 | return docs[0]
72 | }
73 | } else {
74 | return docs
75 | }
76 | }
77 | async function findTypes (types) {
78 | if (types.length === 0) {
79 | return {}
80 | } else {
81 | const query = types.map(function (t) {
82 | return { types: t }
83 | })
84 | const result = {}
85 | const docs = await db.flows.find({ type: 'node', $or: query }, { _id: 1, types: 1 }).toArray()
86 | docs.forEach(function (d) {
87 | d.types.forEach(function (t) {
88 | try {
89 | result[t] = result[t] || []
90 | result[t].push(d._id)
91 | } catch (err) {
92 | console.log('Unexpected error lib/nodes.findTypes', err)
93 | console.log(' - known types:', Object.keys(t))
94 | console.log(' - trying to add:', t)
95 | console.log(' - from:', d._id)
96 | }
97 | })
98 | })
99 | return result
100 | }
101 | }
102 |
103 | async function getLastUpdateTime (name) {
104 | const query = { type: 'node' }
105 | if (name) {
106 | query._id = name
107 | }
108 | const docs = await db.flows.find(query, { projection: { _id: 1, 'time.modified': 1, updated_at: 1 } }).sort({ 'time.modified': -1 }).limit(1).toArray()
109 | if (docs.length === 1) {
110 | // console.log(docs[0].updated_at)
111 | return (new Date(docs[0].updated_at)).getTime()
112 | }
113 | return 0
114 | }
115 |
116 | function getPopularByDownloads () {
117 | return db.flows.find({ type: 'node' }, { projection: { _id: 1, downloads: 1 } })
118 | .sort({ 'downloads.week': -1 })
119 | .limit(30)
120 | .toArray()
121 | }
122 |
123 | function getSummary () {
124 | return db.flows.find({ type: 'node' }, { projection: { _id: 1, downloads: 1, time: 1 } }).toArray()
125 | }
126 |
127 | module.exports = {
128 | CORE_NODES,
129 | save: saveToDb,
130 | remove: removeFromDb,
131 | update,
132 | close: async function () { return db.close() },
133 | get,
134 | findTypes,
135 | getLastUpdateTime,
136 | getPopularByDownloads,
137 | getSummary
138 | }
139 |
--------------------------------------------------------------------------------
/public/font/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Copyright (C) 2019 by original authors @ fontello.com
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/routes/users.js:
--------------------------------------------------------------------------------
1 | const https = require('https')
2 |
3 | const express = require('express')
4 | const mustache = require('mustache')
5 |
6 | const db = require('../lib/db')
7 | const templates = require('../lib/templates')
8 | const users = require('../lib/users')
9 | const appUtils = require('../lib/utils')
10 |
11 | const app = express()
12 |
13 | app.get('/user/:username', async function (req, res) {
14 | const context = {}
15 | context.sessionuser = req.session.user
16 | context.username = req.params.username
17 | context.query = {
18 | id: Math.floor(Math.random() * 16777215).toString(16),
19 | username: req.params.username,
20 | sort: 'recent',
21 | type: ''
22 | }
23 |
24 | const user = await db.users.find({ _id: context.query.username }).toArray()
25 | if (user && user.length > 0) {
26 | context.user = user[0]
27 | if (user[0].npm_login && user[0].npm_login !== context.username) {
28 | context.query.npm_username = user[0].npm_login
29 | }
30 | }
31 | res.send(mustache.render(templates.user, context, templates.partials))
32 | })
33 |
34 | app.get('/settings', appUtils.csrfProtection(), async function (req, res) {
35 | if (!req.session.accessToken) {
36 | res.writeHead(302, {
37 | Location: '/'
38 | })
39 | res.end()
40 | return
41 | }
42 | const context = {}
43 | context.sessionuser = req.session.user
44 | context.csrfToken = req.csrfToken()
45 | const username = req.session.user.login
46 | try {
47 | context.user = await users.get(username)
48 | res.send(mustache.render(templates.userSettings, context, templates.partials))
49 | } catch (err) {
50 | context.err = err
51 | res.send(mustache.render(templates.userSettings, context, templates.partials))
52 | }
53 | })
54 |
55 | app.post('/settings/github-refresh', appUtils.csrfProtection(), async function (req, res) {
56 | if (!req.session.accessToken) {
57 | res.status(401).end()
58 | return
59 | }
60 | const username = req.session.user.login
61 | try {
62 | await users.refreshUserGitHub(username)
63 | res.writeHead(303, {
64 | Location: '/settings'
65 | })
66 | res.end()
67 | } catch (err) {
68 | console.log('Refresh github failed. ERR:', err)
69 | res.writeHead(303, {
70 | Location: '/settings'
71 | })
72 | res.end()
73 | }
74 | })
75 | app.post('/settings/npm-remove', appUtils.csrfProtection(), async function (req, res) {
76 | if (!req.session.accessToken) {
77 | res.status(401).end()
78 | return
79 | }
80 | const username = req.session.user.login
81 | try {
82 | const user = await users.get(username)
83 | user.npm_verified = false
84 | user.npm_login = ''
85 | await users.update(user)
86 | res.writeHead(303, {
87 | Location: '/settings'
88 | })
89 | res.end()
90 | } catch (err) {
91 | console.log('Error updating user: ' + err)
92 | res.status(400).end()
93 | }
94 | })
95 |
96 | app.post('/settings/npm-verify', appUtils.csrfProtection(), function (req, res) {
97 | if (!req.session.accessToken) {
98 | res.status(401).end()
99 | return
100 | }
101 | const username = req.session.user.login
102 | const token = req.body.token || ''
103 | const options = {
104 | host: 'registry.npmjs.org',
105 | port: 443,
106 | path: '/-/npm/v1/user',
107 | method: 'get',
108 | headers: {
109 | Authorization: 'Bearer ' + token
110 | }
111 | }
112 | const request = https.request(options, function (response) {
113 | response.setEncoding('utf8')
114 | let data = ''
115 | response.on('data', function (chunk) {
116 | data += chunk
117 | })
118 | response.on('end', function () {
119 | if (/^application\/json/.test(response.headers['content-type'])) {
120 | data = JSON.parse(data)
121 | }
122 | if (response.statusCode !== 200) {
123 | res.writeHead(303, {
124 | Location: '/settings#npm-verify=fail'
125 | })
126 | res.end()
127 | return
128 | }
129 | users.get(username).then(function (user) {
130 | user.npm_verified = true
131 | user.npm_login = data.name
132 | return users.update(user)
133 | }).then(user => {
134 | res.writeHead(303, {
135 | Location: '/settings#npm-verify=success'
136 | })
137 | res.end()
138 | return null
139 | }).catch(err => {
140 | console.log('Error updating user: ' + err)
141 | res.status(400).end()
142 | })
143 | })
144 | })
145 | request.on('error', function (e) {
146 | console.log('problem with request: ' + e.message)
147 | res.status(400).end()
148 | })
149 | request.end()
150 | })
151 |
152 | module.exports = app
153 |
--------------------------------------------------------------------------------
/public/images/user-backdrop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/collections.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto')
2 |
3 | const db = require('./db')
4 | const users = require('./users')
5 | const { generateSummary } = require('./utils')
6 | const view = require('./view')
7 |
8 | async function createCollection (collection) {
9 | const collectionID = crypto.randomBytes(9).toString('base64').replace(/\//g, '-').replace(/\+/g, '_')
10 | const tags = collection.tags || []
11 | for (let i = 0; i < tags.length; i++) {
12 | await db.tags.updateOne({ _id: tags[i] }, { $inc: { count: 1 } }, { upsert: true })
13 | }
14 | collection.type = 'collection'
15 | collection._id = collectionID
16 | collection.updated_at = (new Date()).toISOString()
17 | collection.summary = generateSummary(collection.description)
18 | try {
19 | await db.flows.replaceOne({ _id: collectionID }, collection, { upsert: true })
20 | } finally {
21 | view.resetTypeCountCache()
22 | }
23 | return collectionID
24 | }
25 |
26 | async function removeCollection (id) {
27 | const collection = await getCollection(id)
28 | const tags = collection.tags || []
29 | const promises = []
30 | for (let i = 0; i < tags.length; i++) {
31 | promises.push(db.tags.updateOne({ _id: tags[i] }, { $inc: { count: -1 } }))
32 | }
33 | promises.push(db.tags.deleteMany({ count: { $lte: 0 } }))
34 | await Promise.all(promises)
35 | try {
36 | await db.flows.deleteOne({ _id: id })
37 | } finally {
38 | view.resetTypeCountCache()
39 | }
40 | }
41 |
42 | async function getCollection (id) {
43 | const data = await db.flows.find({ _id: id }).toArray()
44 | if (!data || data.length === 0) {
45 | throw new Error(`Collection ${id} not found`)
46 | }
47 | return data[0]
48 | }
49 |
50 | async function updateCollection (collection) {
51 | delete collection.type
52 | collection.updated_at = (new Date()).toISOString()
53 | const errors = {}
54 | if (Object.prototype.hasOwnProperty.call(collection, 'name')) {
55 | if (collection.name.trim().length < 10) {
56 | errors.name = 'Must be at least 10 characters'
57 | }
58 | }
59 | if (Object.prototype.hasOwnProperty.call(collection, 'description')) {
60 | if (collection.description.trim().length < 30) {
61 | errors.description = 'Must be at least 30 characters'
62 | }
63 | collection.summary = generateSummary(collection.description)
64 | }
65 | if (Object.prototype.hasOwnProperty.call(collection, 'gitOwners')) {
66 | const unmatched = await users.checkAllExist(collection.gitOwners)
67 | if (unmatched && unmatched.length > 0) {
68 | errors.owners = unmatched
69 | }
70 | }
71 | if (Object.keys(errors).length > 0) {
72 | throw errors
73 | }
74 | try {
75 | await db.flows.updateOne(
76 | { _id: collection._id },
77 | { $set: collection }
78 | )
79 | } catch (err) {
80 | console.log('Update collection', collection._id, 'ERR', err.toString())
81 | throw err
82 | }
83 | return collection._id
84 | }
85 |
86 | async function addItem (collectionId, itemId) {
87 | try {
88 | await db.flows.updateOne(
89 | { _id: collectionId },
90 | { $addToSet: { items: itemId } }
91 | )
92 | } catch (err) {
93 | console.log('Adding collection item', collectionId, itemId, 'ERR', err.toString())
94 | throw err
95 | }
96 | return collectionId
97 | }
98 |
99 | async function removeItem (collectionId, itemId) {
100 | try {
101 | await db.flows.updateOne(
102 | { _id: collectionId },
103 | { $pull: { items: itemId } }
104 | )
105 | } catch (err) {
106 | console.log('Remove collection item', collectionId, itemId, 'ERR', err.toString())
107 | throw err
108 | }
109 | return collectionId
110 | }
111 |
112 | async function getSiblings (collectionId, itemId) {
113 | const docs = db.flows.aggregate([
114 | { $match: { _id: collectionId } },
115 | {
116 | $project: {
117 | name: 1,
118 | items: 1,
119 | index: { $indexOfArray: ['$items', itemId] }
120 | }
121 | },
122 | {
123 | $project: {
124 | name: 1,
125 | items: 1,
126 | prevIndex: { $subtract: ['$index', 1] },
127 | nextIndex: { $add: ['$index', 1] }
128 | }
129 | },
130 | {
131 | $project: {
132 | name: 1,
133 | prev: { $cond: { if: { $gte: ['$prevIndex', 0] }, then: { $arrayElemAt: ['$items', '$prevIndex'] }, else: '' } },
134 | next: { $arrayElemAt: ['$items', '$nextIndex'] }
135 | }
136 | }
137 | ]).toArray()
138 |
139 | if (docs && docs.length > 0) {
140 | docs[0].prevType = await view.getThingType(docs[0].prev)
141 | docs[0].nextType = await view.getThingType(docs[0].next)
142 | } else {
143 | return docs
144 | }
145 | }
146 |
147 | module.exports = {
148 | get: getCollection,
149 | update: updateCollection,
150 | remove: removeCollection,
151 | create: createCollection,
152 | addItem,
153 | removeItem,
154 | getSiblings
155 | }
156 |
--------------------------------------------------------------------------------
/template/addNode.html:
--------------------------------------------------------------------------------
1 | {{>_header}}
2 |
3 |
4 |
5 |
Adding a node
6 |
Node-RED nodes are packaged as modules and published to
7 | the public npm repository .
8 |
Once published to npm, they can be added to the Flow Library using the form below.
9 |
To add a node to the library, follow these steps:
10 |
11 |
1
12 |
13 |
Create your node and package it as an npm module.
14 |
Your module must have
15 |
16 | a name that follows the project's naming guidelines ,
17 | a README.md file that describes what your node does and how to use it,
18 | a package.json file with:
19 |
20 | a node-red section listing the node files,
21 | and "node-red" in its list of keywords.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
2
31 |
32 |
Publish your module to the public npm repository.
33 |
You can use the npm publish command to do this.
34 |
35 |
36 |
37 |
38 |
39 |
3
40 |
41 |
Add your node to the Flow Library
42 |
Use this form to tell the Flow Library about your node.
43 |
49 |
50 |
51 |
52 |
53 |
54 |
81 |
82 |
130 |
131 | {{>_footer}}
132 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const querystring = require('querystring')
2 |
3 | const express = require('express')
4 | const mustache = require('mustache')
5 |
6 | const settings = require('../config')
7 | const categories = require('../lib/categories')
8 | const templates = require('../lib/templates')
9 | const viewster = require('../lib/view')
10 |
11 | const app = express()
12 |
13 | function queryFromRequest (req) {
14 | const query = Object.assign({}, req.query)
15 | query.page = Number(query.page) || 1
16 | query.num_pages = Number(query.num_pages) || 1
17 | query.page_size = Number(query.page_size) || viewster.DEFAULT_PER_PAGE
18 | return query
19 | }
20 | function getNextPageQueryString (count, query) {
21 | const currentPage = parseInt(query.page) || 1
22 | if (viewster.DEFAULT_PER_PAGE * currentPage < count) {
23 | return querystring.stringify(Object.assign({}, query, { page: currentPage + 1 }))
24 | }
25 | return null
26 | }
27 | function getPrevPageQueryString (count, query) {
28 | const currentPage = parseInt(query.page) || 1
29 | if (currentPage > 1) {
30 | return querystring.stringify(Object.assign({}, query, { page: currentPage - 1 }))
31 | }
32 | return null
33 | }
34 | app.use(function (req, res, next) {
35 | if (req.session.user) {
36 | req.session.user.isAdmin = settings.admins.indexOf(req.session.user.login) !== -1
37 | req.session.user.isModerator = req.session.user.isAdmin || settings.moderators.indexOf(req.session.user.login) !== -1
38 | }
39 | next()
40 | })
41 | app.get('/', async function (req, res) {
42 | const context = {}
43 |
44 | context.categories = await categories.getAll()
45 | context.sessionuser = req.session.user
46 | context.isAdmin = req.session.user?.isAdmin
47 | context.isModerator = req.session.user?.isModerator
48 | context.nodes = {
49 | type: 'node',
50 | per_page: context.sessionuser ? 6 : 3,
51 | hideOptions: true,
52 | hideNav: true,
53 | ignoreQueryParams: true
54 | }
55 | context.flows = {
56 | type: 'flow',
57 | per_page: context.sessionuser ? 6 : 3,
58 | hideOptions: true,
59 | hideNav: true,
60 | ignoreQueryParams: true
61 | }
62 | context.collections = {
63 | type: 'collection',
64 | per_page: context.sessionuser ? 6 : 3,
65 | hideOptions: true,
66 | hideNav: true,
67 | ignoreQueryParams: true
68 | }
69 | const counts = await viewster.getTypeCounts()
70 | context.nodes.count = counts.node
71 | context.flows.count = counts.flow
72 | context.collections.count = counts.collection
73 |
74 | res.send(mustache.render(templates.index, context, templates.partials))
75 | })
76 |
77 | app.get('/things', async function (req, res) {
78 | const response = {
79 | links: {
80 | self: '/things?' + querystring.stringify(req.query),
81 | prev: null,
82 | next: null
83 | },
84 | meta: {
85 | pages: {
86 | current: parseInt(req.query.page) || 1
87 | },
88 | results: {
89 |
90 | }
91 | }
92 | }
93 | const query = queryFromRequest(req)
94 |
95 | try {
96 | const result = await viewster.getForQuery(query)
97 | result.things = result.things || []
98 | result.things.forEach(function (thing) {
99 | thing.isNode = thing.type === 'node'
100 | thing.isFlow = thing.type === 'flow'
101 | thing.isCollection = thing.type === 'collection'
102 | })
103 | response.meta.results.count = result.count
104 | response.meta.results.total = result.total
105 | response.meta.pages.total = Math.ceil(result.count / viewster.DEFAULT_PER_PAGE)
106 | const nextQS = getNextPageQueryString(result.count, req.query)
107 | const prevQS = getPrevPageQueryString(result.count, req.query)
108 |
109 | if (nextQS) {
110 | response.links.next = '/things?' + nextQS
111 | }
112 | if (prevQS) {
113 | response.links.prev = '/things?' + prevQS
114 | }
115 | const context = {
116 | things: result.things,
117 | toFixed: function () {
118 | return function (num, render) {
119 | return parseFloat(render(num)).toFixed(1)
120 | }
121 | }
122 | }
123 | if (req.session.user) {
124 | context.showTools = {}
125 | if (result.collectionOwners) {
126 | for (let i = 0; i < result.collectionOwners.length; i++) {
127 | if (result.collectionOwners[i] === req.session.user.login) {
128 | context.showTools.ownedCollection = true
129 | break
130 | }
131 | }
132 | }
133 | }
134 | if (query.collection) {
135 | context.collection = query.collection
136 | }
137 | if (query.format !== 'json') {
138 | response.html = mustache.render(templates.partials._gistitems, context, templates.partials)
139 | } else {
140 | response.data = result.things
141 | }
142 | setTimeout(function () {
143 | res.json(response)
144 | }, 0)// 2000);
145 | } catch (err) {
146 | response.err = err
147 | res.json(response)
148 | }
149 | })
150 |
151 | app.get('/search', async function (req, res) {
152 | const context = {}
153 | context.sessionuser = req.session.user
154 | context.isAdmin = req.session.user?.isAdmin
155 | context.isModerator = req.session.user?.isModerator
156 | context.fullsearch = true
157 | context.categories = await categories.getAll()
158 | const query = queryFromRequest(req)
159 | context.query = query
160 | res.send(mustache.render(templates.search, context, templates.partials))
161 | })
162 |
163 | app.get('/add', function (req, res) {
164 | const context = {}
165 | context.sessionuser = req.session.user
166 | res.send(mustache.render(templates.add, context, templates.partials))
167 | })
168 |
169 | app.get('/inspect', function (req, res) {
170 | const context = {}
171 | res.send(mustache.render(templates.flowInspector, context, templates.partials))
172 | })
173 | module.exports = app
174 |
--------------------------------------------------------------------------------
/template/_toolbar.html:
--------------------------------------------------------------------------------
1 |
100 |
--------------------------------------------------------------------------------
/template/addCollection.html:
--------------------------------------------------------------------------------
1 | {{>_header}}
2 |
27 |
28 |
29 |
154 |
155 | {{>_footer}}
156 |
--------------------------------------------------------------------------------
/template/addCategory.html:
--------------------------------------------------------------------------------
1 | {{>_header}}
2 |
28 |
29 |
30 |
160 |
161 | {{>_footer}}
162 |
--------------------------------------------------------------------------------
/template/gist.html:
--------------------------------------------------------------------------------
1 | {{>_header}}
2 |
3 |
4 |
5 |
{{ description }}
6 | {{{ readme }}}
7 | {{>_flowViewer}}
8 |
Copy {{ flow }}
9 |
10 |
11 | {{>_collectionNavBox}}
12 |
19 |
23 |
24 |
43 |
44 |
45 |
67 |
79 |
80 | Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option
81 |
82 |
83 |
84 | {{>_rateTools}}
85 | {{>_tagTools}}
86 |
154 | {{>_footer}}
155 |
--------------------------------------------------------------------------------
/lib/gists.js:
--------------------------------------------------------------------------------
1 | const db = require('./db')
2 | const github = require('./github')
3 | const users = require('./users')
4 | const view = require('./view')
5 |
6 | async function getGist (id, projection) {
7 | projection = projection || {}
8 | return db.flows.findOne({ _id: id }, projection)
9 | }
10 |
11 | async function refreshGist (id) {
12 | console.log(`Request to refresh gist ${id}`)
13 | const gist = await getGist(id, { etag: 1, tags: 1, added_at: 1 })
14 | if (!gist) {
15 | const err = new Error('not_found')
16 | err.code = 404
17 | throw err
18 | }
19 | const etag = process.env.FORCE_UPDATE ? null : gist.etag
20 | console.log(` - using etag ${etag}`)
21 | try {
22 | const data = await github.getGist(id, etag)
23 | if (data == null) {
24 | console.log(' - github returned null')
25 | // no update needed
26 | await db.flows.updateOne({ _id: id }, { $set: { refreshed_at: Date.now() } })
27 | return null
28 | } else {
29 | data.added_at = gist.added_at
30 | data.tags = gist.tags
31 | data.type = 'flow'
32 | return addGist(data)
33 | }
34 | } catch (err) {
35 | console.log(` - error during refresh - removing gist: ${err.toString()}`)
36 | await removeGist(id)
37 | throw err
38 | }
39 | }
40 |
41 | async function createGist (accessToken, gist, tags) {
42 | try {
43 | const data = await github.createGist(gist, accessToken)
44 | for (let i = 0; i < tags.length; i++) {
45 | db.tags.updateOne({ _id: tags[i] }, { $inc: { count: 1 } }, { upsert: true })
46 | }
47 | data.added_at = Date.now()
48 | data.tags = tags
49 | data.type = 'flow'
50 | return addGist(data)
51 | } catch (err) {
52 | console.log('ERROR createGist', err)
53 | throw err
54 | }
55 | }
56 |
57 | function generateSummary (desc) {
58 | let summary = (desc || '').split('\n')[0]
59 | const re = /!?\[(.*?)\]\(.*?\)/g
60 | let m
61 | while ((m = re.exec(summary)) !== null) {
62 | summary = summary.substring(0, m.index) + m[1] + summary.substring(m.index + m[0].length)
63 | }
64 |
65 | if (summary.length > 150) {
66 | summary = summary.substring(0, 150).split('\n')[0] + '...'
67 | }
68 | return summary
69 | }
70 |
71 | async function addGist (data) {
72 | const originalFiles = data.files
73 | if (!originalFiles['flow.json']) {
74 | throw new Error('Missing file flow.json')
75 | }
76 | if (originalFiles['flow.json'].truncated) {
77 | if (originalFiles['flow.json'].size < 300000) {
78 | originalFiles['flow.json'].content = await github.getGistFile(originalFiles['flow.json'].raw_url)
79 | } else {
80 | throw new Error('Flow file too big')
81 | }
82 | }
83 | if (!originalFiles['README.md']) {
84 | throw new Error('Missing file README.md')
85 | }
86 | if (originalFiles['README.md'].truncated) {
87 | if (originalFiles['README.md'].size < 300000) {
88 | originalFiles['README.md'].content = await github.getGistFile(originalFiles['README.md'].raw_url)
89 | } else {
90 | throw new Error('README file too big')
91 | }
92 | }
93 | data.flow = originalFiles['flow.json'].content
94 | data.readme = originalFiles['README.md'].content
95 | data.summary = generateSummary(data.readme)
96 | delete data.files
97 | delete data.history
98 | data.gitOwners = [
99 | data.owner.login
100 | ]
101 |
102 | delete data.rateLimit
103 |
104 | data.type = 'flow'
105 | data.refreshed_at = Date.now()
106 | data._id = data.id
107 |
108 | await db.flows.replaceOne({ _id: data._id }, data, { upsert: true })
109 |
110 | await users.ensureExists(data.owner.login)
111 |
112 | view.resetTypeCountCache()
113 | return data.id
114 | }
115 |
116 | async function addGistById (id) {
117 | console.log('Add gist [', id, ']')
118 | const data = await github.getGist(id)
119 | data.added_at = Date.now()
120 | data.tags = []
121 | view.resetTypeCountCache()
122 | return addGist(data)
123 | }
124 |
125 | async function removeGist (id) {
126 | const gist = await getGist(id)
127 | if (gist) {
128 | const promises = []
129 | for (let i = 0; i < gist.tags.length; i++) {
130 | promises.push(db.tags.updateOne({ _id: gist.tags[i] }, { $inc: { count: -1 } }))
131 | }
132 | promises.push(db.tags.deleteMany({ count: { $lte: 0 } }))
133 | await Promise.all(promises)
134 | await db.flows.deleteOne({ _id: id })
135 | view.resetTypeCountCache()
136 | }
137 | }
138 |
139 | async function getGists (query) {
140 | query.type = 'flow'
141 | return db.flows.find(query, { sort: { refreshed_at: -1 }, projection: { id: 1, description: 1, tags: 1, refreshed_at: 1, 'owner.login': true } }).toArray()
142 | }
143 |
144 | async function getGistsForUser (userId) {
145 | return getGists({ 'owner.login': userId })
146 | }
147 | async function getGistsForTag (tag) {
148 | return getGists({ tags: tag })
149 | }
150 | async function getAllGists () {
151 | return getGists({})
152 | }
153 |
154 | async function getUser (id) {
155 | return db.users.findOne({ _id: id })
156 | }
157 |
158 | async function updateTags (id, tags) {
159 | tags = tags || []
160 | const gist = await getGist(id, { tags: 1, description: 1, 'files.README-md': 1, 'owner.login': 1 })
161 | if (!gist) {
162 | const err = new Error('not_found')
163 | err.code = 404
164 | throw err
165 | }
166 |
167 | const oldTags = gist.tags
168 |
169 | if (oldTags.length === tags.length) {
170 | let matches = true
171 | for (let i = 0; i < oldTags.length; i++) {
172 | if (tags.indexOf(oldTags[i]) === -1) {
173 | matches = false
174 | break
175 | }
176 | }
177 | if (matches) {
178 | return
179 | }
180 | }
181 | const promises = []
182 |
183 | for (let i = 0; i < oldTags.length; i++) {
184 | if (tags.indexOf(oldTags[i]) === -1) {
185 | promises.push(db.tags.updateOne({ _id: oldTags[i] }, { $inc: { count: -1 } }))
186 | }
187 | }
188 | for (let i = 0; i < tags.length; i++) {
189 | if (oldTags.indexOf(tags[i]) === -1) {
190 | promises.push(db.tags.updateOne({ _id: tags[i] }, { $inc: { count: 1 } }, { upsert: true }))
191 | }
192 | }
193 | promises.push(db.tags.deleteMany({ count: { $lte: 0 } }))
194 | promises.push(db.flows.updateOne({ _id: id }, { $set: { tags } }))
195 | return Promise.all(promises)
196 | }
197 |
198 | async function getTags (query) {
199 | return db.tags.find(query, { sort: { count: -1, _id: 1 } }).toArray()
200 | }
201 |
202 | function getAllTags () {
203 | return getTags({})
204 | }
205 |
206 | module.exports = {
207 | add: addGistById,
208 | refresh: refreshGist,
209 | remove: removeGist,
210 | updateTags,
211 | get: getGist,
212 | getAll: getAllGists,
213 | getGists,
214 | getForUser: getGistsForUser,
215 | getUser,
216 | create: createGist,
217 | getAllTags,
218 | getForTag: getGistsForTag
219 | }
220 |
221 | // var repo = "https://gist.github.com/6c3b201624588e243f82.git";
222 | // var sys = require('sys');
223 | // var exec = require('child_process').exec;
224 | // function puts(error, stdout, stderr) { sys.puts(stdout); sys.puts(stderr); }
225 | // exec("git clone "+repo, puts);
226 | //
227 |
--------------------------------------------------------------------------------