Skip to content

Commit 78e6663

Browse files
authored
Merge pull request #1835 from hackmdio/release/2.5.0
Release 2.5.0
2 parents 5d84066 + afe49f4 commit 78e6663

34 files changed

+4013
-2415
lines changed

.buildpacks

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
https://github.com/alex88/heroku-buildpack-vips
1+
https://github.com/Scalingo/apt-buildpack
22
https://github.com/Scalingo/nodejs-buildpack

.devcontainer/Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# [Choice] Node.js version: 16, 14, 12
2-
ARG VARIANT=12-buster
1+
# [Choice] Node.js version: 16, 14
2+
ARG VARIANT=14-buster
33
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
44

55
# [Optional] Uncomment this section to install additional OS packages.

.devcontainer/docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
context: ..
77
dockerfile: .devcontainer/Dockerfile
88
args:
9-
VARIANT: 12-buster
9+
VARIANT: 14-buster
1010
environment:
1111
- CMD_DB_URL=postgres://codimd:codimd@localhost/codimd
1212
- CMD_USECDN=false

.github/workflows/build.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
node-version: [10.x, 12.x]
13+
node-version: [14.x, 16.x]
1414

1515
steps:
1616
- uses: actions/checkout@v2
@@ -39,9 +39,9 @@ jobs:
3939
steps:
4040
- uses: actions/checkout@v2
4141
- uses: actions/setup-node@v2
42-
name: Use Node.js 12
42+
name: Use Node.js 14
4343
with:
44-
node-version: 12
44+
node-version: 14
4545
check-latest: true
4646
- name: Install doctoc-check
4747
run: |

.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v10.20.1
1+
v16.20.2

Aptfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
libvips-dev

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ CodiMD is a service that runs on Node.js, while users use the service through br
8181
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" /> Chrome >= 47, Chrome for Android >= 47
8282
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" /> Safari >= 9, iOS Safari >= 8.4
8383
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" /> Firefox >= 44
84-
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" /> IE >= 9, Edge >= 12
84+
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" /> Edge >= 12
8585
- <img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" /> Opera >= 34, Opera Mini not supported
8686
- Android Browser >= 4.4
8787

app.json

+8
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@
8080
"description": "GitHub API client secret",
8181
"required": false
8282
},
83+
"CMD_GITHUB_ORGANIZATIONS": {
84+
"description": "GitHub whitelist of orgs",
85+
"required": false
86+
},
87+
"CMD_GITHUB_SCOPES": {
88+
"description": "GitHub OAuth API scopes",
89+
"required": false
90+
},
8391
"CMD_BITBUCKET_CLIENTID": {
8492
"description": "Bitbucket API client id",
8593
"required": false

config.json.example

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
},
5454
"github": {
5555
"clientID": "change this",
56-
"clientSecret": "change this"
56+
"clientSecret": "change this",
57+
"organizations": ["names of github organizations allowed, optional"],
58+
"scopes": ["defaults to 'read:user' scope for auth user"]
5759
},
5860
"gitlab": {
5961
"baseURL": "change this",

lib/auth/github/index.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
'use strict'
22

33
const Router = require('express').Router
4+
const request = require('request')
45
const passport = require('passport')
56
const GithubStrategy = require('passport-github').Strategy
7+
const { InternalOAuthError } = require('passport-oauth2')
68
const config = require('../../config')
79
const response = require('../../response')
810
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
911
const { URL } = require('url')
12+
const { promisify } = require('util')
13+
14+
const rp = promisify(request)
1015

1116
const githubAuth = module.exports = Router()
1217

@@ -15,20 +20,48 @@ function githubUrl (path) {
1520
}
1621

1722
passport.use(new GithubStrategy({
23+
scope: (config.github.organizations ? config.github.scopes.concat(['read:org']) : config.github.scope),
1824
clientID: config.github.clientID,
1925
clientSecret: config.github.clientSecret,
2026
callbackURL: config.serverURL + '/auth/github/callback',
2127
authorizationURL: githubUrl('login/oauth/authorize'),
2228
tokenURL: githubUrl('login/oauth/access_token'),
2329
userProfileURL: githubUrl('api/v3/user')
24-
}, passportGeneralCallback))
30+
}, async (accessToken, refreshToken, profile, done) => {
31+
if (!config.github.organizations) {
32+
return passportGeneralCallback(accessToken, refreshToken, profile, done)
33+
}
34+
const { statusCode, body: data } = await rp({
35+
url: `https://api.github.com/user/orgs`,
36+
method: 'GET',
37+
json: true,
38+
timeout: 2000,
39+
headers: {
40+
Authorization: `token ${accessToken}`,
41+
'User-Agent': 'nodejs-http'
42+
}
43+
})
44+
if (statusCode !== 200) {
45+
return done(InternalOAuthError(
46+
`Failed to query organizations for user: ${profile.username}`
47+
))
48+
}
49+
const orgs = data.map(({ login }) => login)
50+
for (const org of orgs) {
51+
if (config.github.organizations.includes(org)) {
52+
return passportGeneralCallback(accessToken, refreshToken, profile, done)
53+
}
54+
}
55+
return done(InternalOAuthError(
56+
`User orgs not whitelisted: ${profile.username} (${orgs.join(',')})`
57+
))
58+
}))
2559

2660
githubAuth.get('/auth/github', function (req, res, next) {
2761
setReturnToFromReferer(req)
2862
passport.authenticate('github')(req, res, next)
2963
})
3064

31-
// github auth callback
3265
githubAuth.get('/auth/github/callback',
3366
passport.authenticate('github', {
3467
successReturnToOrRedirect: config.serverURL + '/',

lib/auth/index.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ if (config.isEmailEnable) authRouter.use(require('./email'))
4949
if (config.isOpenIDEnable) authRouter.use(require('./openid'))
5050

5151
// logout
52-
authRouter.get('/logout', function (req, res) {
52+
authRouter.get('/logout', function (req, res, next) {
5353
if (config.debug && req.isAuthenticated()) {
5454
logger.debug('user logout: ' + req.user.id)
5555
}
56-
req.logout()
57-
res.redirect(config.serverURL + '/')
56+
57+
req.logout((err) => {
58+
if (err) { return next(err) }
59+
60+
res.redirect(config.serverURL + '/')
61+
})
5862
})

lib/auth/oauth2/strategy.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { Strategy, InternalOAuthError } = require('passport-oauth2')
44
const config = require('../../config')
55

66
function parseProfile (data) {
7+
const id = extractProfileAttribute(data, config.oauth2.userProfileIdAttr)
78
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
89
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
910
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
@@ -14,7 +15,7 @@ function parseProfile (data) {
1415
}
1516

1617
return {
17-
id: username,
18+
id: id || username,
1819
username: username,
1920
displayName: displayName,
2021
email: email,
@@ -41,6 +42,16 @@ function extractProfileAttribute (data, path) {
4142
return data
4243
}
4344

45+
function checkAuthorization (data, done) {
46+
const roles = extractProfileAttribute(data, config.oauth2.rolesClaim)
47+
48+
if (config.oauth2.accessRole && roles) {
49+
if (!roles.includes(config.oauth2.accessRole)) {
50+
return done('Permission denied', null)
51+
}
52+
}
53+
}
54+
4455
class OAuth2CustomStrategy extends Strategy {
4556
constructor (options, verify) {
4657
options.customHeaders = options.customHeaders || {}
@@ -59,6 +70,7 @@ class OAuth2CustomStrategy extends Strategy {
5970
let profile, json
6071
try {
6172
json = JSON.parse(body)
73+
checkAuthorization(json, done)
6274
profile = parseProfile(json)
6375
} catch (ex) {
6476
return done(new InternalOAuthError('Failed to parse user profile' + ex.toString()))

lib/config/default.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ module.exports = {
115115
github: {
116116
enterpriseURL: undefined, // if you use github.com, not need to specify
117117
clientID: undefined,
118-
clientSecret: undefined
118+
clientSecret: undefined,
119+
organizations: [],
120+
scopes: ['read:user']
119121
},
120122
gitlab: {
121123
baseURL: undefined,

lib/config/environment.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ module.exports = {
6969
github: {
7070
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
7171
clientID: process.env.CMD_GITHUB_CLIENTID,
72-
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
72+
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET,
73+
organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS),
74+
scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES)
7375
},
7476
bitbucket: {
7577
clientID: process.env.CMD_BITBUCKET_CLIENTID,
@@ -96,6 +98,9 @@ module.exports = {
9698
userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL,
9799
scope: process.env.CMD_OAUTH2_SCOPE,
98100
state: process.env.CMD_OAUTH2_STATE,
101+
rolesClaim: process.env.CMD_OAUTH2_ROLES_CLAIM,
102+
accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE,
103+
userProfileIdAttr: process.env.CMD_OAUTH2_USER_PROFILE_ID_ATTR,
99104
userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
100105
userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
101106
userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,

lib/imageRouter/index.js

+18-13
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,42 @@ const response = require('../response')
1616
const imageRouter = module.exports = Router()
1717

1818
function checkImageValid (filepath) {
19-
const buffer = readChunk.sync(filepath, 0, 12)
20-
/** @type {{ ext: string, mime: string } | null} */
21-
const mimetypeFromBuf = imageType(buffer)
22-
const mimeTypeFromExt = mime.lookup(path.extname(filepath))
19+
try {
20+
const buffer = readChunk.sync(filepath, 0, 12)
21+
/** @type {{ ext: string, mime: string } | null} */
22+
const mimetypeFromBuf = imageType(buffer)
23+
const mimeTypeFromExt = mime.lookup(path.extname(filepath))
2324

24-
return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
25-
mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
25+
return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
26+
mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
27+
} catch (err) {
28+
logger.error(err)
29+
return false
30+
}
2631
}
2732

2833
// upload image
2934
imageRouter.post('/uploadimage', function (req, res) {
30-
var form = new formidable.IncomingForm()
31-
32-
form.keepExtensions = true
35+
var form = new formidable.IncomingForm({
36+
keepExtensions: true
37+
})
3338

3439
form.parse(req, function (err, fields, files) {
35-
if (err || !files.image || !files.image.path) {
40+
if (err || !files.image || !files.image.filepath) {
3641
response.errorForbidden(req, res)
3742
} else {
3843
if (config.debug) {
3944
logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image))
4045
}
4146

42-
if (!checkImageValid(files.image.path)) {
47+
if (!checkImageValid(files.image.filepath)) {
4348
return response.errorForbidden(req, res)
4449
}
4550

4651
const uploadProvider = require('./' + config.imageUploadType)
47-
uploadProvider.uploadImage(files.image.path, function (err, url) {
52+
uploadProvider.uploadImage(files.image.filepath, function (err, url) {
4853
// remove temporary upload file, and ignore any error
49-
fs.unlink(files.image.path, () => {})
54+
fs.unlink(files.image.filepath, () => {})
5055
if (err !== null) {
5156
logger.error(err)
5257
return res.status(500).end('upload image error')

lib/imageRouter/s3.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ exports.uploadImage = function (imagePath, callback) {
4848
const command = new PutObjectCommand(params)
4949

5050
s3.send(command).then(data => {
51-
let s3Endpoint = 's3.amazonaws.com'
51+
// default scheme settings to https
52+
let s3Endpoint = 'https://s3.amazonaws.com'
53+
if (config.s3.region && config.s3.region !== 'us-east-1') {
54+
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
55+
}
56+
// rewrite endpoint from config
5257
if (config.s3.endpoint) {
5358
s3Endpoint = config.s3.endpoint
54-
} else if (config.s3.region && config.s3.region !== 'us-east-1') {
55-
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
5659
}
57-
callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`)
60+
callback(null, `${s3Endpoint}/${config.s3bucket}/${params.Key}`)
5861
}).catch(err => {
5962
if (err) {
6063
callback(new Error(err), null)

lib/note/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ const updateNote = async (req, res) => {
309309
}
310310

311311
if (req.isAuthenticated()) {
312-
updateHistory(req.user.id, noteId, content)
312+
updateHistory(req.user.id, Note.encodeNoteId(noteId), content)
313313
}
314314

315315
Revision.saveNoteRevision(note, (err, revision) => {

lib/note/noteActions.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,17 @@ async function actionPandoc (req, res, note) {
132132
var path = config.tmpPath + '/' + Date.now()
133133
content = content.replace(/\]\(\//g, '](' + url + '/')
134134

135-
// TODO: check export type
136135
const { exportType } = req.query
136+
const contentType = outputFormats[exportType]
137137

138138
try {
139139
// TODO: timeout rejection
140+
if (!contentType) {
141+
return res.sendStatus(400)
142+
}
140143

141144
await pandoc.convertToFile(content, 'markdown', exportType, path, [
142-
'--metadata', `title=${title}`
145+
'--metadata', `title=${title}`, '--sandbox'
143146
])
144147

145148
var stream = fs.createReadStream(path)
@@ -149,7 +152,7 @@ async function actionPandoc (req, res, note) {
149152
// Ideally this should strip them
150153
res.setHeader('Content-disposition', `attachment; filename="${filename}.${exportType}"`)
151154
res.setHeader('Cache-Control', 'private')
152-
res.setHeader('Content-Type', `${outputFormats[exportType]}; charset=UTF-8`)
155+
res.setHeader('Content-Type', `${contentType}; charset=UTF-8`)
153156
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
154157
stream.pipe(res)
155158
} catch (err) {

locales/fr.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"No history": "Pas d'historique",
2929
"Import from browser": "Importer depuis le navigateur",
3030
"Releases": "Versions",
31-
"Are you sure?": "Ëtes-vous sûr ?",
31+
"Are you sure?": "Êtes-vous sûr ?",
3232
"Do you really want to delete this note?": "Voulez-vous vraiment supprimer cette note ?",
3333
"All users will lose their connection.": "Tous les utilisateurs perdront leur connexion.",
3434
"Cancel": "Annuler",
@@ -73,7 +73,7 @@
7373
"Syntax": "Syntaxe",
7474
"Header": "En-tête",
7575
"Unordered List": "Liste à puce",
76-
"Ordered List": "List numérotée",
76+
"Ordered List": "Liste numérotée",
7777
"Todo List": "Liste de tâches",
7878
"Blockquote": "Citation",
7979
"Bold font": "Gras",
@@ -84,7 +84,7 @@
8484
"Link": "Lien",
8585
"Image": "Image",
8686
"Code": "Code",
87-
"Externals": "Externes",
87+
"Externals": "Contenus externes",
8888
"This is a alert area.": "Ceci est un texte d'alerte.",
8989
"Revert": "Revenir en arrière",
9090
"Import from clipboard": "Importer depuis le presse-papier",
@@ -116,4 +116,4 @@
116116
"Source Code": "Code source",
117117
"Register": "S'enregistrer",
118118
"Powered by %s": "Propulsé par %s"
119-
}
119+
}

0 commit comments

Comments
 (0)