Skip to content

Commit fa05e55

Browse files
committed
Add categories
1 parent b1e7628 commit fa05e55

26 files changed

+1094
-469
lines changed

.eslintrc

+7
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@
5656
"env": {
5757
"mocha": true
5858
}
59+
},
60+
{
61+
"files": ["public/js/**"],
62+
"env": {
63+
"browser": true,
64+
"jquery": true
65+
}
5966
}
6067
]
6168
}

app.js

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const limiter = rateLimit({
6565
app.use(require('./routes/users'))
6666
app.use(require('./routes/api'))
6767
app.use(require('./routes/collections'))
68+
app.use(require('./routes/categories'))
6869
app.use(function (err, req, res, next) {
6970
if (err.code !== 'EBADCSRFTOKEN') {
7071
console.log('here', err)

config/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ try {
1616
module.exports.session.secret = process.env.NR_SESSION_SECRET || module.exports.session.secret
1717
if (process.env.NR_ADMINS) {
1818
module.exports.admins = process.env.NR_ADMINS.split(',').map(t => t.trim())
19+
} else {
20+
module.exports.admins = []
21+
}
22+
if (process.env.NR_MODERATORS) {
23+
module.exports.moderators = process.env.NR_MODS.split(',').map(t => t.trim())
24+
} else {
25+
module.exports.moderators = []
1926
}
2027

2128
module.exports.mastodon.url = process.env.NR_MASTODON_URL || module.exports.mastodon.url

lib/categories.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const db = require('./db')
2+
const { generateSummary } = require('./utils')
3+
4+
// Given they are largely static and there are not many, we can cache the category list
5+
// to save hitting the DB for every page view
6+
let categoryCache
7+
8+
async function refreshCategoryCache () {
9+
categoryCache = await db.categories.find().toArray()
10+
categoryCache.sort((a, b) => a.name.localeCompare(b.name))
11+
}
12+
13+
function normaliseName (name) {
14+
return name.toLowerCase().replace(/[ /]+/g, '-').replace(/&/g, 'and')
15+
}
16+
17+
async function createCategory (category) {
18+
category._id = normaliseName(category.name)
19+
category.updated_at = (new Date()).toISOString()
20+
category.summary = generateSummary(category.description)
21+
await db.categories.insertOne(category, { upsert: true })
22+
await refreshCategoryCache()
23+
return category._id
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 getCategories () {
43+
if (!categoryCache) {
44+
await refreshCategoryCache()
45+
}
46+
return categoryCache
47+
}
48+
async function getCategory (id) {
49+
const data = await db.categories.find({ _id: id }).toArray()
50+
if (!data || data.length === 0) {
51+
throw new Error(`Category ${id} not found`)
52+
}
53+
return data[0]
54+
}
55+
56+
async function updateCategory (category) {
57+
category.updated_at = (new Date()).toISOString()
58+
if (Object.prototype.hasOwnProperty.call(category, 'description')) {
59+
category.summary = generateSummary(category.description)
60+
}
61+
try {
62+
await db.categories.updateOne(
63+
{ _id: category._id },
64+
{ $set: category }
65+
)
66+
} catch (err) {
67+
console.log('Update category', category._id, 'ERR', err.toString())
68+
throw err
69+
}
70+
await refreshCategoryCache()
71+
return category._id
72+
}
73+
74+
module.exports = {
75+
getAll: getCategories,
76+
create: createCategory,
77+
get: getCategory,
78+
update: updateCategory
79+
}

lib/collections.js

+1-14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const crypto = require('crypto')
22

33
const db = require('./db')
44
const users = require('./users')
5+
const { generateSummary } = require('./utils')
56
const view = require('./view')
67

78
async function createCollection (collection) {
@@ -46,20 +47,6 @@ async function getCollection (id) {
4647
return data[0]
4748
}
4849

49-
function generateSummary (desc) {
50-
let summary = (desc || '').split('\n')[0]
51-
const re = /\[(.*?)\]\(.*?\)/g
52-
let m
53-
while ((m = re.exec(summary)) !== null) {
54-
summary = summary.substring(0, m.index) + m[1] + summary.substring(m.index + m[0].length)
55-
}
56-
57-
if (summary.length > 150) {
58-
summary = summary.substring(0, 150).split('\n')[0] + '...'
59-
}
60-
return summary
61-
}
62-
6350
async function updateCollection (collection) {
6451
delete collection.type
6552
collection.updated_at = (new Date()).toISOString()

lib/db.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const api = {
77
}
88

99
async function init () {
10-
const collections = ['flows', 'nodes', 'users', 'tags', 'events', 'ratings']
10+
const collections = ['flows', 'nodes', 'users', 'tags', 'events', 'ratings', 'categories']
1111
const client = new MongoClient(settings.mongo.url)
1212
await client.connect()
1313
const db = client.db()

lib/utils.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,51 @@ async function renderMarkdown (src, opt) {
100100
return DOMPurify.sanitize(content)
101101
}
102102

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+
103143
module.exports = {
144+
generateSummary,
104145
renderMarkdown,
105146
formatDate,
106147
formatShortDate,
107-
csrfProtection: () => csrfProtection
148+
csrfProtection: () => csrfProtection,
149+
requireRole
108150
}

lib/view.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,11 @@ module.exports = {
107107
}
108108
}
109109
// const typeNode = !findQuery.type || findQuery.type === 'node' || (Array.isArray(query.type) && query.type.indexOf('node') > -1)
110-
111-
if (query.username || query.npm_username) {
110+
if (query.category) {
111+
const categories = Array.isArray(query.category) ? query.category : [query.category]
112+
findQuery.categories = { $in: categories }
113+
countQuery.categories = { $in: categories }
114+
} else if (query.username || query.npm_username) {
112115
findQuery.$or = [{ gitOwners: query.username }, { npmOwners: query.npm_username || query.username }]
113116
countQuery.$or = findQuery.$or
114117
} else {

0 commit comments

Comments
 (0)