Merge pull request #1384 from hackmdio/refactor/new-web-arch

Refactor/new web arch
This commit is contained in:
Max Wu 2020-01-06 14:35:33 +08:00 committed by GitHub
commit 6e74e41641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 899 additions and 845 deletions

18
app.js
View File

@ -66,7 +66,7 @@ io.engine.ws = new (require('ws').Server)({
})
// others
var realtime = require('./lib/realtime.js')
var realtime = require('./lib/realtime/realtime.js')
// assign socket io to realtime
realtime.io = io
@ -153,7 +153,7 @@ server.on('resumeSession', function (id, cb) {
})
// middleware which blocks requests when we're too busy
app.use(require('./lib/web/middleware/tooBusy'))
app.use(require('./lib/middleware/tooBusy'))
app.use(flash())
@ -162,10 +162,10 @@ app.use(passport.initialize())
app.use(passport.session())
// check uri is valid before going further
app.use(require('./lib/web/middleware/checkURIValid'))
app.use(require('./lib/middleware/checkURIValid'))
// redirect url without trailing slashes
app.use(require('./lib/web/middleware/redirectWithoutTrailingSlashes'))
app.use(require('./lib/web/middleware/codiMDVersion'))
app.use(require('./lib/middleware/redirectWithoutTrailingSlashes'))
app.use(require('./lib/middleware/codiMDVersion'))
// routes need sessions
// template files
@ -206,13 +206,7 @@ app.locals.enableDropBoxSave = config.isDropboxEnable
app.locals.enableGitHubGist = config.isGitHubEnable
app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable
app.use(require('./lib/web/baseRouter'))
app.use(require('./lib/web/statusRouter'))
app.use(require('./lib/web/auth'))
app.use(require('./lib/web/historyRouter'))
app.use(require('./lib/web/userRouter'))
app.use(require('./lib/web/imageRouter'))
app.use(require('./lib/web/noteRouter'))
app.use(require('./lib/routes').router)
// response not found if no any route matxches
app.get('*', function (req, res) {

View File

@ -3,7 +3,7 @@
const Router = require('express').Router
const passport = require('passport')
const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const bitbucketAuth = module.exports = Router()

View File

@ -3,7 +3,7 @@
const Router = require('express').Router
const passport = require('passport')
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const dropboxAuth = module.exports = Router()

View File

@ -4,12 +4,12 @@ const Router = require('express').Router
const passport = require('passport')
const validator = require('validator')
const LocalStrategy = require('passport-local').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const config = require('../../config')
const models = require('../../models')
const logger = require('../../logger')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
const response = require('../../response')
const emailAuth = module.exports = Router()

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const FacebookStrategy = require('passport-facebook').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const facebookAuth = module.exports = Router()

View File

@ -3,8 +3,8 @@
const Router = require('express').Router
const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy
const config = require('../../../config')
const response = require('../../../response')
const config = require('../../config')
const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { URL } = require('url')

View File

@ -3,8 +3,8 @@
const Router = require('express').Router
const passport = require('passport')
const GitlabStrategy = require('passport-gitlab2').Strategy
const config = require('../../../config')
const response = require('../../../response')
const config = require('../../config')
const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const HttpsProxyAgent = require('https-proxy-agent')

View File

@ -3,7 +3,7 @@
const Router = require('express').Router
const passport = require('passport')
var GoogleStrategy = require('passport-google-oauth20').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const googleAuth = module.exports = Router()

View File

@ -3,9 +3,9 @@
const Router = require('express').Router
const passport = require('passport')
const config = require('../../config')
const logger = require('../../logger')
const models = require('../../models')
const config = require('../config')
const logger = require('../logger')
const models = require('../models')
const authRouter = module.exports = Router()

View File

@ -3,12 +3,12 @@
const Router = require('express').Router
const passport = require('passport')
const LDAPStrategy = require('passport-ldapauth')
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const config = require('../../config')
const models = require('../../models')
const logger = require('../../logger')
const { setReturnToFromReferer } = require('../utils')
const { urlencodedParser } = require('../../utils')
const response = require('../../../response')
const response = require('../../response')
const ldapAuth = module.exports = Router()

View File

@ -5,7 +5,7 @@ const Router = require('express').Router
const passport = require('passport')
const MattermostClient = require('mattermost-redux/client/client4').default
const OAuthStrategy = require('passport-oauth2').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const mattermostAuth = module.exports = Router()

View File

@ -3,7 +3,7 @@
const Router = require('express').Router
const passport = require('passport')
const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const oauth2Auth = module.exports = Router()

View File

@ -3,9 +3,9 @@
const Router = require('express').Router
const passport = require('passport')
const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const config = require('../../config')
const models = require('../../models')
const logger = require('../../logger')
const { urlencodedParser } = require('../../utils')
const { setReturnToFromReferer } = require('../utils')

View File

@ -3,9 +3,9 @@
const Router = require('express').Router
const passport = require('passport')
const SamlStrategy = require('passport-saml').Strategy
const config = require('../../../config')
const models = require('../../../models')
const logger = require('../../../logger')
const config = require('../../config')
const models = require('../../models')
const logger = require('../../logger')
const { urlencodedParser } = require('../../utils')
const fs = require('fs')
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }

View File

@ -4,7 +4,7 @@ const Router = require('express').Router
const passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy
const config = require('../../../config')
const config = require('../../config')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const twitterAuth = module.exports = Router()

View File

@ -1,8 +1,8 @@
'use strict'
const models = require('../../models')
const config = require('../../config')
const logger = require('../../logger')
const models = require('../models')
const config = require('../config')
const logger = require('../logger')
exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
var referer = req.get('referer')

21
lib/errorPage/index.js Normal file
View File

@ -0,0 +1,21 @@
'use strict'
const config = require('../config')
const { responseError } = require('../response')
exports.errorForbidden = (req, res) => {
if (req.user) {
return responseError(res, '403', 'Forbidden', 'oh no.')
}
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
res.redirect(config.serverURL + '/')
}
exports.errorNotFound = (req, res) => {
responseError(res, '404', 'Not Found', 'oops.')
}
exports.errorInternalError = (req, res) => {
responseError(res, '500', 'Internal Error', 'wtf.')
}

View File

@ -4,10 +4,10 @@
var LZString = require('@hackmd/lz-string')
// core
var config = require('./config')
var logger = require('./logger')
var response = require('./response')
var models = require('./models')
var config = require('../config')
var logger = require('../logger')
var response = require('../response')
var models = require('../models')
function getHistory (userid, callback) {
models.User.findOne({

38
lib/homepage/index.js Normal file
View File

@ -0,0 +1,38 @@
'use strict'
const fs = require('fs')
const path = require('path')
const config = require('../config')
const { User } = require('../models')
const logger = require('../logger')
exports.showIndex = async (req, res) => {
const isLogin = req.isAuthenticated()
const deleteToken = ''
const data = {
signin: isLogin,
infoMessage: req.flash('info'),
errorMessage: req.flash('error'),
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
deleteToken: deleteToken
}
if (!isLogin) {
return res.render('index.ejs', data)
}
const user = await User.findOne({
where: {
id: req.user.id
}
})
if (user) {
data.deleteToken = user.deleteToken
return res.render('index.ejs', data)
}
logger.error(`error: user not found with id ${req.user.id}`)
return res.render('index.ejs', data)
}

View File

@ -1,8 +1,8 @@
'use strict'
const path = require('path')
const config = require('../../config')
const logger = require('../../logger')
const config = require('../config')
const logger = require('../logger')
const azure = require('azure-storage')

View File

@ -2,8 +2,8 @@
const URL = require('url').URL
const path = require('path')
const config = require('../../config')
const logger = require('../../logger')
const config = require('../config')
const logger = require('../logger')
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {

View File

@ -1,6 +1,6 @@
'use strict'
const config = require('../../config')
const logger = require('../../logger')
const config = require('../config')
const logger = require('../logger')
const imgur = require('@hackmd/imgur')

View File

@ -3,9 +3,9 @@
const Router = require('express').Router
const formidable = require('formidable')
const config = require('../../config')
const logger = require('../../logger')
const response = require('../../response')
const config = require('../config')
const logger = require('../logger')
const response = require('../response')
const imageRouter = module.exports = Router()

View File

@ -1,8 +1,8 @@
'use strict'
const config = require('../../config')
const logger = require('../../logger')
const config = require('../config')
const logger = require('../logger')
const lutim = require('lutim')
const lutim = require('lib/imageRouter/lutim')
exports.uploadImage = function (imagePath, callback) {
if (!imagePath || typeof imagePath !== 'string') {

View File

@ -2,11 +2,11 @@
const fs = require('fs')
const path = require('path')
const config = require('../../config')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const config = require('../config')
const { getImageMimeType } = require('../utils')
const logger = require('../logger')
const Minio = require('minio')
const Minio = require('lib/imageRouter/minio')
const minioClient = new Minio.Client({
endPoint: config.minio.endPoint,
port: config.minio.port,

View File

@ -2,9 +2,9 @@
const fs = require('fs')
const path = require('path')
const config = require('../../config')
const { getImageMimeType } = require('../../utils')
const logger = require('../../logger')
const config = require('../config')
const { getImageMimeType } = require('../utils')
const logger = require('../logger')
const AWS = require('aws-sdk')
const awsConfig = new AWS.Config(config.s3)

View File

@ -1,7 +1,7 @@
'use strict'
const logger = require('../../logger')
const response = require('../../response')
const logger = require('../logger')
const response = require('../response')
module.exports = function (req, res, next) {
try {

View File

@ -1,6 +1,6 @@
'use strict'
const config = require('../../config')
const config = require('../config')
module.exports = function (req, res, next) {
res.set({

View File

@ -1,6 +1,6 @@
'use strict'
const config = require('../../config')
const config = require('../config')
module.exports = function (req, res, next) {
if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {

View File

@ -2,8 +2,8 @@
const toobusy = require('toobusy-js')
const config = require('../../config')
const response = require('../../response')
const config = require('../config')
const response = require('../response')
toobusy.maxLag(config.responseMaxLag)

View File

@ -0,0 +1,26 @@
'use strict'
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Temp')
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
},
down: (queryInterface, Sequelize) => {
return queryInterface.createTable('Temp', {
id: {
type: Sequelize.STRING,
primaryKey: true
},
date: Sequelize.TEXT,
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE
})
}
}

View File

@ -186,6 +186,16 @@ module.exports = function (sequelize, DataTypes) {
var result = id.match(uuidRegex)
if (result && result.length === 1) { return true } else { return false }
}
Note.parseNoteIdAsync = function (noteId) {
return new Promise((resolve, reject) => {
Note.parseNoteId(noteId, (err, id) => {
if (err) {
return reject(err)
}
resolve(id)
})
})
}
Note.parseNoteId = function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {

View File

@ -1,18 +0,0 @@
'use strict'
// external modules
var shortId = require('shortid')
module.exports = function (sequelize, DataTypes) {
var Temp = sequelize.define('Temp', {
id: {
type: DataTypes.STRING,
primaryKey: true,
defaultValue: shortId.generate
},
data: {
type: DataTypes.TEXT
}
})
return Temp
}

171
lib/note/index.js Normal file
View File

@ -0,0 +1,171 @@
'use strict'
const config = require('../config')
const logger = require('../logger')
const { Note, User } = require('../models')
const { newCheckViewPermission, errorForbidden, responseCodiMD, errorNotFound } = require('../response')
const { updateHistory } = require('../history')
const { actionPublish, actionSlide, actionInfo, actionDownload, actionPDF, actionGist, actionRevision } = require('./noteActions')
async function getNoteById (noteId, { includeUser } = { includeUser: false }) {
const id = await Note.parseNoteIdAsync(noteId)
const includes = []
if (includeUser) {
includes.push({
model: User,
as: 'owner'
}, {
model: User,
as: 'lastchangeuser'
})
}
const note = await Note.findOne({
where: {
id: id
},
include: includes
})
return note
}
async function createNote (userId, noteAlias) {
if (!config.allowAnonymous && !!userId) {
throw new Error('can not create note')
}
const note = await Note.create({
ownerId: userId,
alias: noteAlias
})
if (userId) {
updateHistory(userId, note)
}
return note
}
// controller
async function showNote (req, res) {
const noteId = req.params.noteId
const userId = req.user ? req.user.id : null
let note = await getNoteById(noteId)
if (!note) {
// if allow free url enable, auto create note
if (!config.allowFreeURL || config.forbiddenNoteIDs.includes(noteId)) {
return errorNotFound(res)
}
note = await createNote(userId, noteId)
}
if (!newCheckViewPermission(note, req.isAuthenticated(), userId)) {
return errorForbidden(res)
}
// force to use note id
const id = Note.encodeNoteId(note.id)
if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) {
return res.redirect(config.serverURL + '/' + (note.alias || id))
}
return responseCodiMD(res, note)
}
async function showPublishNote (req, res) {
const shortid = req.params.shortid
const note = await getNoteById(shortid, {
includeUser: true
})
if (!note) {
return errorNotFound(res)
}
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
}
await note.increment('viewcount')
const body = note.content
const extracted = Note.extractMeta(body)
const markdown = extracted.markdown
const meta = Note.parseMeta(extracted.meta)
const createTime = note.createdAt
const updateTime = note.lastchangeAt
const title = Note.generateWebTitle(meta.title || Note.decodeTitle(note.title))
const data = {
title: title,
description: meta.description || (markdown ? Note.generateDescription(markdown) : null),
viewcount: note.viewcount,
createtime: createTime,
updatetime: updateTime,
body: body,
owner: note.owner ? note.owner.id : null,
ownerprofile: note.owner ? User.getProfile(note.owner) : null,
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
lastchangeuserprofile: note.lastchangeuser ? User.getProfile(note.lastchangeuser) : null,
robots: meta.robots || false, // default allow robots
GA: meta.GA,
disqus: meta.disqus,
cspNonce: res.locals.nonce
}
res.set({
'Cache-Control': 'private' // only cache by client
})
res.render('pretty.ejs', data)
}
async function noteActions (req, res) {
const noteId = req.params.noteId
const note = await getNoteById(noteId)
if (!note) {
return errorNotFound(res)
}
const action = req.params.action
switch (action) {
case 'publish':
case 'pretty': // pretty deprecated
return actionPublish(req, res, note)
case 'slide':
return actionSlide(req, res, note)
case 'download':
actionDownload(req, res, note)
break
case 'info':
actionInfo(req, res, note)
break
case 'pdf':
if (config.allowPDFExport) {
actionPDF(req, res, note)
} else {
logger.error('PDF export failed: Disabled by config. Set "allowPDFExport: true" to enable. Check the documentation for details')
errorForbidden(res)
}
break
case 'gist':
actionGist(req, res, note)
break
case 'revision':
actionRevision(req, res, note)
break
default:
return res.redirect(config.serverURL + '/' + noteId)
}
}
exports.showNote = showNote
exports.showPublishNote = showPublishNote
exports.noteActions = noteActions

164
lib/note/noteActions.js Normal file
View File

@ -0,0 +1,164 @@
'use strict'
const fs = require('fs')
const path = require('path')
const markdownpdf = require('markdown-pdf')
const shortId = require('shortid')
const querystring = require('querystring')
const moment = require('moment')
const config = require('../config')
const logger = require('../logger')
const { Note, Revision } = require('../models')
const { errorInternalError, errorNotFound } = require('../response')
function actionPublish (req, res, note) {
res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
}
function actionSlide (req, res, note) {
res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid))
}
function actionDownload (req, res, note) {
const body = note.content
const title = Note.decodeTitle(note.title)
const filename = encodeURIComponent(title)
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Content-Type': 'text/markdown; charset=UTF-8',
'Cache-Control': 'private',
'Content-disposition': 'attachment; filename=' + filename + '.md',
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(body)
}
function actionInfo (req, res, note) {
const body = note.content
const extracted = Note.extractMeta(body)
const markdown = extracted.markdown
const meta = Note.parseMeta(extracted.meta)
const createtime = note.createdAt
const updatetime = note.lastchangeAt
const title = Note.decodeTitle(note.title)
const data = {
title: meta.title || title,
description: meta.description || (markdown ? Note.generateDescription(markdown) : null),
viewcount: note.viewcount,
createtime: createtime,
updatetime: updatetime
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(data)
}
function actionPDF (req, res, note) {
const url = config.serverURL || 'http://' + req.get('host')
const body = note.content
const extracted = Note.extractMeta(body)
let content = extracted.markdown
const title = Note.decodeTitle(note.title)
const highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
if (!fs.existsSync(config.tmpPath)) {
fs.mkdirSync(config.tmpPath)
}
const pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
content = content.replace(/\]\(\//g, '](' + url + '/')
const markdownpdfOptions = {
highlightCssPath: highlightCssPath
}
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
if (!fs.existsSync(pdfPath)) {
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
return errorInternalError(res)
}
const stream = fs.createReadStream(pdfPath)
let filename = title
// Be careful of special characters
filename = encodeURIComponent(filename)
// Ideally this should strip them
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"')
res.setHeader('Cache-Control', 'private')
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8')
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
stream.pipe(res)
fs.unlinkSync(pdfPath)
})
}
function actionGist (req, res, note) {
const data = {
client_id: config.github.clientID,
redirect_uri: config.serverURL + '/auth/github/callback/' + Note.encodeNoteId(note.id) + '/gist',
scope: 'gist',
state: shortId.generate()
}
const query = querystring.stringify(data)
res.redirect('https://github.com/login/oauth/authorize?' + query)
}
function actionRevision (req, res, note) {
const actionId = req.params.actionId
if (actionId) {
const time = moment(parseInt(actionId))
if (!time.isValid()) {
return errorNotFound(res)
}
Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
if (err) {
logger.error(err)
return errorInternalError(res)
}
if (!content) {
return errorNotFound(res)
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(content)
})
} else {
Revision.getNoteRevisions(note, function (err, data) {
if (err) {
logger.error(err)
return errorInternalError(res)
}
const result = {
revision: data
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(result)
})
}
}
exports.actionPublish = actionPublish
exports.actionSlide = actionSlide
exports.actionDownload = actionDownload
exports.actionInfo = actionInfo
exports.actionPDF = actionPDF
exports.actionGist = actionGist
exports.actionRevision = actionRevision

View File

@ -12,13 +12,13 @@ const moment = require('moment')
const get = require('lodash/get')
// core
const config = require('./config')
const logger = require('./logger')
const history = require('./history')
const models = require('./models')
const config = require('../config')
const logger = require('../logger')
const history = require('../history')
const models = require('../models')
// ot
const ot = require('./ot')
const ot = require('../ot')
const { ProcessQueue } = require('./processQueue')
const { RealtimeClientConnection } = require('./realtimeClientConnection')
@ -247,8 +247,9 @@ async function _updateNoteAsync (note) {
}
// TODO: test it
function getStatus (callback) {
models.Note.count().then(function (notecount) {
function getStatus () {
return models.Note.count()
.then(function (notecount) {
var distinctaddresses = []
var regaddresses = []
var distinctregaddresses = []
@ -279,9 +280,10 @@ function getStatus (callback) {
}
}
})
models.User.count().then(function (regcount) {
// eslint-disable-next-line standard/no-callback-literal
return callback ? callback({
return models.User.count()
.then(function (regcount) {
return {
onlineNotes: Object.keys(notes).length,
onlineUsers: Object.keys(users).length,
distinctOnlineUsers: distinctaddresses.length,
@ -293,8 +295,9 @@ function getStatus (callback) {
connectionSocketQueueLength: connectProcessQueue.queue.length,
isDisconnectBusy: disconnectProcessQueue.lock,
disconnectSocketQueueLength: disconnectProcessQueue.queue.length
}) : null
}).catch(function (err) {
}
})
.catch(function (err) {
return logger.error('count user failed: ' + err)
})
}).catch(function (err) {

View File

@ -1,8 +1,8 @@
'use strict'
const async = require('async')
const config = require('./config')
const logger = require('./logger')
const config = require('../config')
const logger = require('../logger')
/**
* clean when user not in any rooms or user not in connected list

View File

@ -2,9 +2,9 @@
const get = require('lodash/get')
const config = require('./config')
const models = require('./models')
const logger = require('./logger')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
class RealtimeClientConnection {
constructor (socket) {

View File

@ -1,7 +1,7 @@
'use strict'
const models = require('./models')
const logger = require('./logger')
const models = require('../models')
const logger = require('../logger')
/**
* clean when user not in any rooms or user not in connected list

View File

@ -1,7 +1,7 @@
'use strict'
const config = require('./config')
const logger = require('./logger')
const config = require('../config')
const logger = require('../logger')
const moment = require('moment')
class UpdateDirtyNoteJob {

View File

@ -1,13 +1,7 @@
'use strict'
// response
// external modules
const fs = require('fs')
const path = require('path')
const markdownpdf = require('markdown-pdf')
const shortId = require('shortid')
const querystring = require('querystring')
const request = require('request')
const moment = require('moment')
// core
const config = require('./config')
@ -17,6 +11,7 @@ const utils = require('./utils')
const history = require('./history')
// public
exports.responseError = responseError
exports.errorForbidden = errorForbidden
exports.errorNotFound = errorNotFound
exports.errorBadRequest = errorBadRequest
@ -24,15 +19,14 @@ exports.errorTooLong = errorTooLong
exports.errorInternalError = errorInternalError
exports.errorServiceUnavailable = errorServiceUnavailable
exports.newNote = newNote
exports.showNote = showNote
exports.showPublishNote = showPublishNote
exports.showPublishSlide = showPublishSlide
exports.showIndex = showIndex
exports.noteActions = noteActions
exports.publishNoteActions = publishNoteActions
exports.publishSlideActions = publishSlideActions
exports.githubActions = githubActions
exports.gitlabActions = gitlabActions
exports.checkViewPermission = checkViewPermission
exports.newCheckViewPermission = newCheckViewPermission
exports.responseCodiMD = responseCodiMD
function errorForbidden (res) {
const { req } = res
@ -43,20 +37,25 @@ function errorForbidden (res) {
res.redirect(config.serverURL + '/')
}
}
function errorNotFound (res) {
responseError(res, '404', 'Not Found', 'oops.')
}
function errorBadRequest (res) {
responseError(res, '400', 'Bad Request', 'something not right.')
}
function errorTooLong (res) {
responseError(res, '413', 'Payload Too Large', 'Shorten your note!')
}
function errorInternalError (res) {
responseError(res, '500', 'Internal Error', 'wtf.')
}
function errorServiceUnavailable (res) {
res.status(503).send("I'm busy right now, try again later.")
res.status(503).send('I\'m busy right now, try again later.')
}
function responseError (res, code, detail, msg) {
@ -68,35 +67,6 @@ function responseError (res, code, detail, msg) {
})
}
function showIndex (req, res, next) {
var authStatus = req.isAuthenticated()
var deleteToken = ''
var data = {
signin: authStatus,
infoMessage: req.flash('info'),
errorMessage: req.flash('error'),
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
deleteToken: deleteToken
}
if (authStatus) {
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
if (user) {
data.deleteToken = user.deleteToken
res.render('index.ejs', data)
}
})
} else {
res.render('index.ejs', data)
}
}
function responseCodiMD (res, note) {
var body = note.content
var extracted = models.Note.extractMeta(body)
@ -148,6 +118,16 @@ function newNote (req, res, next) {
})
}
function newCheckViewPermission (note, isLogin, userId) {
if (note.permission === 'private') {
return note.ownerId === userId
}
if (note.permission === 'limited' || note.permission === 'protected') {
return isLogin
}
return true
}
function checkViewPermission (req, note) {
if (note.permission === 'private') {
if (!req.isAuthenticated() || note.ownerId !== req.user.id) { return false } else { return true }
@ -192,81 +172,6 @@ function findNote (req, res, callback, include) {
})
}
function showNote (req, res, next) {
findNote(req, res, function (note) {
// force to use note id
var noteId = req.params.noteId
var id = models.Note.encodeNoteId(note.id)
if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) { return res.redirect(config.serverURL + '/' + (note.alias || id)) }
return responseCodiMD(res, note)
})
}
function showPublishNote (req, res, next) {
var include = [{
model: models.User,
as: 'owner'
}, {
model: models.User,
as: 'lastchangeuser'
}]
findNote(req, res, function (note) {
// force to use short id
var shortid = req.params.shortid
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
}
note.increment('viewcount').then(function (note) {
if (!note) {
return errorNotFound(res)
}
var body = note.content
var extracted = models.Note.extractMeta(body)
var markdown = extracted.markdown
var meta = models.Note.parseMeta(extracted.meta)
var createtime = note.createdAt
var updatetime = note.lastchangeAt
var title = models.Note.decodeTitle(note.title)
title = models.Note.generateWebTitle(meta.title || title)
var data = {
title: title,
description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null),
viewcount: note.viewcount,
createtime: createtime,
updatetime: updatetime,
body: body,
owner: note.owner ? note.owner.id : null,
ownerprofile: note.owner ? models.User.getProfile(note.owner) : null,
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null,
robots: meta.robots || false, // default allow robots
GA: meta.GA,
disqus: meta.disqus,
cspNonce: res.locals.nonce
}
return renderPublish(data, res)
}).catch(function (err) {
logger.error(err)
return errorInternalError(res)
})
}, include)
}
function renderPublish (data, res) {
res.set({
'Cache-Control': 'private' // only cache by client
})
res.render('pretty.ejs', data)
}
function actionPublish (req, res, note) {
res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
}
function actionSlide (req, res, note) {
res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid))
}
function actionDownload (req, res, note) {
var body = note.content
var title = models.Note.decodeTitle(note.title)
@ -284,162 +189,6 @@ function actionDownload (req, res, note) {
res.send(body)
}
function actionInfo (req, res, note) {
var body = note.content
var extracted = models.Note.extractMeta(body)
var markdown = extracted.markdown
var meta = models.Note.parseMeta(extracted.meta)
var createtime = note.createdAt
var updatetime = note.lastchangeAt
var title = models.Note.decodeTitle(note.title)
var data = {
title: meta.title || title,
description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null),
viewcount: note.viewcount,
createtime: createtime,
updatetime: updatetime
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(data)
}
function actionPDF (req, res, note) {
var url = config.serverURL || 'http://' + req.get('host')
var body = note.content
var extracted = models.Note.extractMeta(body)
var content = extracted.markdown
var title = models.Note.decodeTitle(note.title)
var highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
if (!fs.existsSync(config.tmpPath)) {
fs.mkdirSync(config.tmpPath)
}
var pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
content = content.replace(/\]\(\//g, '](' + url + '/')
var markdownpdfOptions = {
highlightCssPath: highlightCssPath
}
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
if (!fs.existsSync(pdfPath)) {
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
return errorInternalError(res)
}
var stream = fs.createReadStream(pdfPath)
var filename = title
// Be careful of special characters
filename = encodeURIComponent(filename)
// Ideally this should strip them
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"')
res.setHeader('Cache-Control', 'private')
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8')
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
stream.pipe(res)
fs.unlinkSync(pdfPath)
})
}
function actionGist (req, res, note) {
var data = {
client_id: config.github.clientID,
redirect_uri: config.serverURL + '/auth/github/callback/' + models.Note.encodeNoteId(note.id) + '/gist',
scope: 'gist',
state: shortId.generate()
}
var query = querystring.stringify(data)
res.redirect('https://github.com/login/oauth/authorize?' + query)
}
function actionRevision (req, res, note) {
var actionId = req.params.actionId
if (actionId) {
var time = moment(parseInt(actionId))
if (time.isValid()) {
models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
if (err) {
logger.error(err)
return errorInternalError(res)
}
if (!content) {
return errorNotFound(res)
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(content)
})
} else {
return errorNotFound(res)
}
} else {
models.Revision.getNoteRevisions(note, function (err, data) {
if (err) {
logger.error(err)
return errorInternalError(res)
}
var out = {
revision: data
}
res.set({
'Access-Control-Allow-Origin': '*', // allow CORS as API
'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
})
res.send(out)
})
}
}
function noteActions (req, res, next) {
var noteId = req.params.noteId
findNote(req, res, function (note) {
var action = req.params.action
switch (action) {
case 'publish':
case 'pretty': // pretty deprecated
actionPublish(req, res, note)
break
case 'slide':
actionSlide(req, res, note)
break
case 'download':
actionDownload(req, res, note)
break
case 'info':
actionInfo(req, res, note)
break
case 'pdf':
if (config.allowPDFExport) {
actionPDF(req, res, note)
} else {
logger.error('PDF export failed: Disabled by config. Set "allowPDFExport: true" to enable. Check the documentation for details')
errorForbidden(res)
}
break
case 'gist':
actionGist(req, res, note)
break
case 'revision':
actionRevision(req, res, note)
break
default:
return res.redirect(config.serverURL + '/' + noteId)
}
})
}
function publishNoteActions (req, res, next) {
findNote(req, res, function (note) {
var action = req.params.action
@ -631,17 +380,13 @@ function showPublishSlide (req, res, next) {
disqus: meta.disqus,
cspNonce: res.locals.nonce
}
return renderPublishSlide(data, res)
res.set({
'Cache-Control': 'private' // only cache by client
})
res.render('slide.ejs', data)
}).catch(function (err) {
logger.error(err)
return errorInternalError(res)
})
}, include)
}
function renderPublishSlide (data, res) {
res.set({
'Cache-Control': 'private' // only cache by client
})
res.render('slide.ejs', data)
}

81
lib/routes.js Normal file
View File

@ -0,0 +1,81 @@
'use strict'
const { Router } = require('express')
const { wrap, urlencodedParser, markdownParser } = require('./utils')
// load controller
const indexController = require('./homepage')
const errorPageController = require('./errorPage')
const statusController = require('./status')
const historyController = require('./history')
const userController = require('./user')
const noteController = require('./note')
const response = require('./response')
const appRouter = Router()
// register route
// get index
appRouter.get('/', wrap(indexController.showIndex))
// ----- error page -----
// get 403 forbidden
appRouter.get('/403', errorPageController.errorForbidden)
// get 404 not found
appRouter.get('/404', errorPageController.errorNotFound)
// get 500 internal error
appRouter.get('/500', errorPageController.errorInternalError)
appRouter.get('/status', wrap(statusController.getStatus))
appRouter.get('/config', statusController.getConfig)
// register auth module
appRouter.use(require('./auth'))
// get history
appRouter.get('/history', historyController.historyGet)
// post history
appRouter.post('/history', urlencodedParser, historyController.historyPost)
// post history by note id
appRouter.post('/history/:noteId', urlencodedParser, historyController.historyPost)
// delete history
appRouter.delete('/history', historyController.historyDelete)
// delete history by note id
appRouter.delete('/history/:noteId', historyController.historyDelete)
// user
// get me info
appRouter.get('/me', wrap(userController.getMe))
// delete the currently authenticated user
appRouter.get('/me/delete/:token?', wrap(userController.deleteUser))
// export the data of the authenticated user
appRouter.get('/me/export', userController.exportMyData)
appRouter.get('/user/:username/avatar.svg', userController.getMyAvatar)
// register image upload module
appRouter.use(require('./imageRouter'))
// get new note
appRouter.get('/new', response.newNote)
// post new note with content
appRouter.post('/new', markdownParser, response.newNote)
// get publish note
appRouter.get('/s/:shortid', noteController.showPublishNote)
// publish note actions
appRouter.get('/s/:shortid/:action', response.publishNoteActions)
// get publish slide
appRouter.get('/p/:shortid', response.showPublishSlide)
// publish slide actions
appRouter.get('/p/:shortid/:action', response.publishSlideActions)
// get note by id
appRouter.get('/:noteId', wrap(noteController.showNote))
// note actions
appRouter.get('/:noteId/:action', noteController.noteActions)
// note actions with action id
appRouter.get('/:noteId/:action/:actionId', noteController.noteActions)
exports.router = appRouter

35
lib/status/index.js Normal file
View File

@ -0,0 +1,35 @@
'use strict'
const realtime = require('../realtime/realtime')
const config = require('../config')
exports.getStatus = async (req, res) => {
const data = await realtime.getStatus()
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'application/json'
})
res.send(data)
}
exports.getConfig = (req, res) => {
const data = {
domain: config.domain,
urlpath: config.urlPath,
debug: config.debug,
version: config.fullversion,
plantumlServer: config.plantuml.server,
DROPBOX_APP_KEY: config.dropbox.appKey,
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
defaultUseHardbreak: config.defaultUseHardbreak,
linkifyHeaderStyle: config.linkifyHeaderStyle
}
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'application/javascript'
})
res.render('../js/lib/common/constant.ejs', data)
}

121
lib/user/index.js Normal file
View File

@ -0,0 +1,121 @@
'use strict'
const archiver = require('archiver')
const async = require('async')
const response = require('../response')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const { generateAvatar } = require('../letter-avatars')
exports.getMe = async (req, res) => {
if (!req.isAuthenticated()) {
res.status(401).send({
status: 'forbidden'
})
}
const user = await models.User.findOne({
where: {
id: req.user.id
}
})
if (!user) {
return response.errorNotFound(res)
}
const profile = models.User.getProfile(user)
res.send({
status: 'ok',
id: req.user.id,
name: profile.name,
photo: profile.photo
})
}
exports.deleteUser = async (req, res) => {
if (!req.isAuthenticated()) {
return response.errorForbidden(res)
}
const user = await models.User.findOne({
where: {
id: req.user.id
}
})
if (!user) {
return response.errorNotFound(res)
}
if (user.deleteToken !== req.params.token) {
return response.errorForbidden(res)
}
await user.destroy()
return res.redirect(config.serverURL + '/')
}
exports.exportMyData = (req, res) => {
if (!req.isAuthenticated()) {
return response.errorForbidden(res)
}
const archive = archiver('zip', {
zlib: { level: 3 } // Sets the compression level.
})
res.setHeader('Content-Type', 'application/zip')
res.attachment('archive.zip')
archive.pipe(res)
archive.on('error', function (err) {
logger.error('export user data failed: ' + err)
return response.errorInternalError(res)
})
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
models.Note.findAll({
where: {
ownerId: user.id
}
}).then(function (notes) {
const filenames = {}
async.each(notes, function (note, callback) {
const basename = note.title.replace(/\//g, '-') // Prevent subdirectories
let filename
let suffix = 0
do {
const separator = suffix === 0 ? '' : '-'
filename = basename + separator + suffix + '.md'
suffix++
} while (filenames[filename])
filenames[filename] = true
logger.debug('Write: ' + filename)
archive.append(Buffer.from(note.content), { name: filename, date: note.lastchangeAt })
callback(null, null)
}, function (err) {
if (err) {
return response.errorInternalError(res)
}
archive.finalize()
})
})
}).catch(function (err) {
logger.error('export user data failed: ' + err)
return response.errorInternalError(res)
})
}
exports.getMyAvatar = (req, res) => {
res.setHeader('Content-Type', 'image/svg+xml')
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(generateAvatar(req.params.username))
}

View File

@ -1,6 +1,7 @@
'use strict'
const fs = require('fs')
const path = require('path')
const bodyParser = require('body-parser')
exports.isSQLite = function isSQLite (sequelize) {
return sequelize.options.dialect === 'sqlite'
@ -32,3 +33,18 @@ exports.isRevealTheme = function isRevealTheme (theme) {
}
return undefined
}
exports.wrap = innerHandler => (req, res, next) => innerHandler(req, res).catch(err => next(err))
// create application/x-www-form-urlencoded parser
exports.urlencodedParser = bodyParser.urlencoded({
extended: false,
limit: 1024 * 1024 * 10 // 10 mb
})
// create text/markdown parser
exports.markdownParser = bodyParser.text({
inflate: true,
type: ['text/plain', 'text/markdown'],
limit: 1024 * 1024 * 10 // 10 mb
})

View File

@ -1,22 +0,0 @@
'use strict'
const Router = require('express').Router
const response = require('../response')
const baseRouter = module.exports = Router()
// get index
baseRouter.get('/', response.showIndex)
// get 403 forbidden
baseRouter.get('/403', function (req, res) {
response.errorForbidden(res)
})
// get 404 not found
baseRouter.get('/404', function (req, res) {
response.errorNotFound(res)
})
// get 500 internal error
baseRouter.get('/500', function (req, res) {
response.errorInternalError(res)
})

View File

@ -1,18 +0,0 @@
'use strict'
const Router = require('express').Router
const { urlencodedParser } = require('./utils')
const history = require('../history')
const historyRouter = module.exports = Router()
// get history
historyRouter.get('/history', history.historyGet)
// post history
historyRouter.post('/history', urlencodedParser, history.historyPost)
// post history by note id
historyRouter.post('/history/:noteId', urlencodedParser, history.historyPost)
// delete history
historyRouter.delete('/history', history.historyDelete)
// delete history by note id
historyRouter.delete('/history/:noteId', history.historyDelete)

View File

@ -1,28 +0,0 @@
'use strict'
const Router = require('express').Router
const response = require('../response')
const { markdownParser } = require('./utils')
const noteRouter = module.exports = Router()
// get new note
noteRouter.get('/new', response.newNote)
// post new note with content
noteRouter.post('/new', markdownParser, response.newNote)
// get publish note
noteRouter.get('/s/:shortid', response.showPublishNote)
// publish note actions
noteRouter.get('/s/:shortid/:action', response.publishNoteActions)
// get publish slide
noteRouter.get('/p/:shortid', response.showPublishSlide)
// publish slide actions
noteRouter.get('/p/:shortid/:action', response.publishSlideActions)
// get note by id
noteRouter.get('/:noteId', response.showNote)
// note actions
noteRouter.get('/:noteId/:action', response.noteActions)
// note actions with action id
noteRouter.get('/:noteId/:action/:actionId', response.noteActions)

View File

@ -1,112 +0,0 @@
'use strict'
const Router = require('express').Router
const response = require('../response')
const realtime = require('../realtime')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const { urlencodedParser } = require('./utils')
const statusRouter = module.exports = Router()
// get status
statusRouter.get('/status', function (req, res, next) {
realtime.getStatus(function (data) {
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'application/json'
})
res.send(data)
})
})
// get status
statusRouter.get('/temp', function (req, res) {
var host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
response.errorForbidden(res)
} else {
var tempid = req.query.tempid
if (!tempid) {
response.errorForbidden(res)
} else {
models.Temp.findOne({
where: {
id: tempid
}
}).then(function (temp) {
if (!temp) {
response.errorNotFound(res)
} else {
res.header('Access-Control-Allow-Origin', '*')
res.send({
temp: temp.data
})
temp.destroy().catch(function (err) {
if (err) {
logger.error('remove temp failed: ' + err)
}
})
}
}).catch(function (err) {
logger.error(err)
return response.errorInternalError(res)
})
}
}
})
// post status
statusRouter.post('/temp', urlencodedParser, function (req, res) {
var host = req.get('host')
if (config.allowOrigin.indexOf(host) === -1) {
response.errorForbidden(res)
} else {
var data = req.body.data
if (!data) {
response.errorForbidden(res)
} else {
if (config.debug) {
logger.info('SERVER received temp from [' + host + ']: ' + req.body.data)
}
models.Temp.create({
data: data
}).then(function (temp) {
if (temp) {
res.header('Access-Control-Allow-Origin', '*')
res.send({
status: 'ok',
id: temp.id
})
} else {
response.errorInternalError(res)
}
}).catch(function (err) {
logger.error(err)
return response.errorInternalError(res)
})
}
}
})
statusRouter.get('/config', function (req, res) {
var data = {
domain: config.domain,
urlpath: config.urlPath,
debug: config.debug,
version: config.fullversion,
plantumlServer: config.plantuml.server,
DROPBOX_APP_KEY: config.dropbox.appKey,
allowedUploadMimeTypes: config.allowedUploadMimeTypes,
defaultUseHardbreak: config.defaultUseHardbreak,
linkifyHeaderStyle: config.linkifyHeaderStyle
}
res.set({
'Cache-Control': 'private', // only cache by client
'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
'Content-Type': 'application/javascript'
})
res.render('../js/lib/common/constant.ejs', data)
})

View File

@ -1,128 +0,0 @@
'use strict'
const archiver = require('archiver')
const async = require('async')
const Router = require('express').Router
const response = require('../response')
const config = require('../config')
const models = require('../models')
const logger = require('../logger')
const { generateAvatar } = require('../letter-avatars')
const UserRouter = module.exports = Router()
// get me info
UserRouter.get('/me', function (req, res) {
if (req.isAuthenticated()) {
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
if (!user) { return response.errorNotFound(res) }
var profile = models.User.getProfile(user)
res.send({
status: 'ok',
id: req.user.id,
name: profile.name,
photo: profile.photo
})
}).catch(function (err) {
logger.error('read me failed: ' + err)
return response.errorInternalError(res)
})
} else {
res.status(401).send({
status: 'forbidden'
})
}
})
// delete the currently authenticated user
UserRouter.get('/me/delete/:token?', function (req, res) {
if (req.isAuthenticated()) {
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
if (!user) {
return response.errorNotFound(res)
}
if (user.deleteToken === req.params.token) {
user.destroy().then(function () {
res.redirect(config.serverURL + '/')
})
} else {
return response.errorForbidden(res)
}
}).catch(function (err) {
logger.error('delete user failed: ' + err)
return response.errorInternalError(res)
})
} else {
return response.errorForbidden(res)
}
})
// export the data of the authenticated user
UserRouter.get('/me/export', function (req, res) {
if (req.isAuthenticated()) {
const archive = archiver('zip', {
zlib: { level: 3 } // Sets the compression level.
})
res.setHeader('Content-Type', 'application/zip')
res.attachment('archive.zip')
archive.pipe(res)
archive.on('error', function (err) {
logger.error('export user data failed: ' + err)
return response.errorInternalError(res)
})
models.User.findOne({
where: {
id: req.user.id
}
}).then(function (user) {
models.Note.findAll({
where: {
ownerId: user.id
}
}).then(function (notes) {
const filenames = {}
async.each(notes, function (note, callback) {
const basename = note.title.replace(/\//g, '-') // Prevent subdirectories
let filename
let suffix = 0
do {
const separator = suffix === 0 ? '' : '-'
filename = basename + separator + suffix + '.md'
suffix++
} while (filenames[filename])
filenames[filename] = true
logger.debug('Write: ' + filename)
archive.append(Buffer.from(note.content), { name: filename, date: note.lastchangeAt })
callback(null, null)
}, function (err) {
if (err) {
return response.errorInternalError(res)
}
archive.finalize()
})
})
}).catch(function (err) {
logger.error('export user data failed: ' + err)
return response.errorInternalError(res)
})
} else {
return response.errorForbidden(res)
}
})
UserRouter.get('/user/:username/avatar.svg', function (req, res, next) {
res.setHeader('Content-Type', 'image/svg+xml')
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(generateAvatar(req.params.username))
})

View File

@ -1,16 +0,0 @@
'use strict'
const bodyParser = require('body-parser')
// create application/x-www-form-urlencoded parser
exports.urlencodedParser = bodyParser.urlencoded({
extended: false,
limit: 1024 * 1024 * 10 // 10 mb
})
// create text/markdown parser
exports.markdownParser = bodyParser.text({
inflate: true,
type: ['text/plain', 'text/markdown'],
limit: 1024 * 1024 * 10 // 10 mb
})

View File

@ -131,8 +131,8 @@
"reveal.js": "~3.7.0",
"scrypt": "~6.0.3",
"select2": "~3.5.2-browserify",
"sequelize": "5.15.1",
"sequelize-cli": "~5.4.0",
"sequelize": "5.21.3",
"sequelize-cli": "~5.5.1",
"shortid": "~2.2.14",
"socket.io": "~2.2.0",
"socket.io-client": "~2.2.0",

View File

@ -6,8 +6,6 @@ import LZString from '@hackmd/lz-string'
import escapeHTML from 'lodash/escape'
import wurl from 'wurl'
import {
checkNoteIdValid,
encodeNoteId
@ -19,38 +17,6 @@ import { urlpath } from './lib/config'
window.migrateHistoryFromTempCallback = null
migrateHistoryFromTemp()
function migrateHistoryFromTemp () {
if (wurl('#tempid')) {
$.get(`${serverurl}/temp`, {
tempid: wurl('#tempid')
})
.done(data => {
if (data && data.temp) {
getStorageHistory(olddata => {
if (!olddata || olddata.length === 0) {
saveHistoryToStorage(JSON.parse(data.temp))
}
})
}
})
.always(() => {
let hash = location.hash.split('#')[1]
hash = hash.split('&')
for (let i = 0; i < hash.length; i++) {
if (hash[i].indexOf('tempid') === 0) {
hash.splice(i, 1)
i--
}
}
hash = hash.join('&')
location.hash = hash
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
})
}
}
export function saveHistory (notehistory) {
checkIfAuth(
() => {

View File

@ -4,7 +4,7 @@
const assert = require('assert')
const sinon = require('sinon')
const { ProcessQueue } = require('../lib/processQueue')
const { ProcessQueue } = require('../lib/realtime/processQueue')
describe('ProcessQueue', function () {
let clock

View File

@ -31,13 +31,13 @@ describe('cleanDanglingUser', function () {
afterEach(() => {
clock.restore()
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtime')
mock.stopAll()
sinon.restore()
})
it('should call queueForDisconnectSpy when user is dangling', (done) => {
const realtime = require('../../lib/realtime')
const realtime = require('../../lib/realtime/realtime')
const queueForDisconnectSpy = sinon.spy(realtime, 'queueForDisconnect')
realtime.io = {
to: sinon.stub().callsFake(function () {

View File

@ -31,7 +31,7 @@ describe('realtime#connection', function () {
mock('../../lib/realtimeCleanDanglingUserJob', realtimeJobStub)
mock('../../lib/realtimeSaveRevisionJob', realtimeJobStub)
mock('../../lib/ot', require('../testDoubles/otFake'))
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
})
afterEach(() => {

View File

@ -27,7 +27,7 @@ describe('realtime#update note is dirty timer', function () {
}
})
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.io = {
to: sinon.stub().callsFake(function () {
@ -39,8 +39,8 @@ describe('realtime#update note is dirty timer', function () {
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtimeUpdateDirtyNoteJob')
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtimeUpdateDirtyNoteJob')
removeModuleFromRequireCache('../../lib/realtime/realtime')
mock.stopAll()
clock.restore()
})

View File

@ -28,7 +28,7 @@ describe('realtime#disconnect', function () {
})
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
updateNoteStub = sinon.stub(realtime, 'updateNote').callsFake((note, callback) => {
callback(null, note)
})
@ -60,7 +60,7 @@ describe('realtime#disconnect', function () {
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtime')
mock.stopAll()
sinon.restore()
})

View File

@ -14,14 +14,14 @@ describe('realtime#extractNoteIdFromSocket', function () {
})
afterEach(() => {
delete require.cache[require.resolve('../../lib/realtime')]
delete require.cache[require.resolve('../../lib/realtime/realtime')]
mock.stopAll()
})
describe('urlPath not set', function () {
beforeEach(() => {
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
})
let realtime
@ -76,7 +76,7 @@ describe('realtime#extractNoteIdFromSocket', function () {
mock('../../lib/config', {
urlPath: urlPath
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const incomingNoteId = 'myNoteId'
const incomingSocket = makeMockSocket({
referer: `https://localhost:3000/${urlPath}/${incomingNoteId}`

View File

@ -81,7 +81,7 @@ describe('realtime#ifMayEdit', function () {
client.request.user.id = noteOwnerId
}
client.noteId = noteId
const realtime = require('../../lib/realtime')
const realtime = require('../../lib/realtime/realtime')
realtime.getNotePool()[noteId] = note
const callback = sinon.stub()
realtime.ifMayEdit(client, callback)
@ -98,7 +98,7 @@ describe('realtime#ifMayEdit', function () {
client.noteId = noteId
const callback = sinon.stub()
client.origin = 'operation'
const realtime = require('../../lib/realtime')
const realtime = require('../../lib/realtime/realtime')
realtime.getNotePool()[noteId] = note
realtime.ifMayEdit(client, callback)
assert(callback.calledOnce)
@ -116,7 +116,7 @@ describe('realtime#ifMayEdit', function () {
client.request.user.id = loggedInUserId
const callback = sinon.stub()
client.origin = 'operation'
const realtime = require('../../lib/realtime')
const realtime = require('../../lib/realtime/realtime')
realtime.getNotePool()[noteId] = note
realtime.ifMayEdit(client, callback)
assert(callback.calledOnce)

View File

@ -23,12 +23,12 @@ describe('realtime#parseNoteIdFromSocketAsync', function () {
})
afterEach(() => {
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtime')
mock.stopAll()
})
it('should return null when socket not send noteId', async function () {
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const mockSocket = makeMockSocket()
try {
const notes = await realtime.parseNoteIdFromSocketAsync(mockSocket)
@ -49,12 +49,12 @@ describe('realtime#parseNoteIdFromSocketAsync', function () {
})
})
it('should return noteId when noteId exists', async function () {
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
let parsedNoteId
try {
parsedNoteId = await realtime.parseNoteIdFromSocketAsync(mockSocket)
@ -76,12 +76,12 @@ describe('realtime#parseNoteIdFromSocketAsync', function () {
})
})
it('should return null when noteId not exists', async function () {
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const parsedNoteId = await realtime.parseNoteIdFromSocketAsync(mockSocket)
assert(parsedNoteId === null)
})
@ -99,12 +99,12 @@ describe('realtime#parseNoteIdFromSocketAsync', function () {
})
})
it('should return error when noteId parse error', async function () {
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const noteId = '123456'
const mockSocket = makeMockSocket(undefined, {
noteId: noteId
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
try {
await realtime.parseNoteIdFromSocketAsync(mockSocket)
} catch (err) {

View File

@ -50,7 +50,7 @@ describe('realtime', function () {
}
})
mock('../../lib/config', {})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
})
Object.keys(viewPermission).forEach(function (permission) {

View File

@ -34,8 +34,8 @@ describe('save revision job', function () {
afterEach(() => {
clock.restore()
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtimeSaveRevisionJob')
removeModuleFromRequireCache('../../lib/realtime/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtimeSaveRevisionJob')
mock.stopAll()
sinon.restore()
})
@ -44,7 +44,7 @@ describe('save revision job', function () {
mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
callback(null, [])
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
clock.tick(5 * 60 * 1000)
clock.restore()
setTimeout(() => {
@ -58,7 +58,7 @@ describe('save revision job', function () {
mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
callback(null, [1])
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
clock.tick(5 * 60 * 1000)
clock.restore()
setTimeout(() => {

View File

@ -76,7 +76,7 @@ describe('realtime#socket event', function () {
mock('../../lib/models', modelsMock)
mock('../../lib/config', configMock)
mock('../../lib/ot', require('../testDoubles/otFake'))
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
// get all socket event handler
clientSocket = makeMockSocket(null, {
@ -122,8 +122,8 @@ describe('realtime#socket event', function () {
})
afterEach(function () {
removeModuleFromRequireCache('../../lib/realtime')
removeModuleFromRequireCache('../../lib/realtimeClientConnection')
removeModuleFromRequireCache('../../lib/realtime/realtime')
removeModuleFromRequireCache('../../lib/realtime/realtimeClientConnection')
mock.stopAll()
sinon.restore()
clock.restore()

View File

@ -47,7 +47,7 @@ describe('realtime#updateNote', function () {
it('should save history to each edited user', function (done) {
modelsStub.Note.findOne.returns(Promise.resolve({}))
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
const updateHistoryStub = sinon.stub(realtime, 'updateHistory')
const callback = sinon.stub()
@ -86,7 +86,7 @@ describe('realtime#updateNote', function () {
name: 'User 01'
})
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
clock.restore()
@ -125,7 +125,7 @@ describe('realtime#updateNote', function () {
})
clock.tick(1000)
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
setTimeout(() => {
assert(note.lastchangeuserprofile.name === 'User 01')
@ -159,7 +159,7 @@ describe('realtime#updateNote', function () {
})
clock.tick(1000)
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
setTimeout(() => {
assert(modelsStub.User.findOne.callCount === 0)
@ -196,7 +196,7 @@ describe('realtime#updateNote', function () {
})
clock.tick(1000)
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
setTimeout(() => {
assert(modelsStub.User.findOne.callCount === 0)
@ -233,7 +233,7 @@ describe('realtime#updateNote', function () {
})
clock.tick(1000)
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
setTimeout(() => {
assert(modelsStub.User.findOne.called)
@ -272,7 +272,7 @@ describe('realtime#updateNote', function () {
})
clock.tick(1000)
realtime = require('../../lib/realtime')
realtime = require('../../lib/realtime/realtime')
realtime.updateNote(note, callback)
setTimeout(() => {
assert(note.lastchangeuserprofile.name === 'User 01')

View File

@ -11048,7 +11048,7 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-as-promised@^3.1.0:
retry-as-promised@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-3.2.0.tgz#769f63d536bec4783549db0777cb56dadd9d8543"
integrity sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==
@ -11246,7 +11246,7 @@ semver@4.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=
semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@ -11275,10 +11275,10 @@ seq-queue@^0.0.5:
resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
integrity sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=
sequelize-cli@~5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-5.4.0.tgz#6a2c2af331466414d8b2ecb6912e24d2de0d04b5"
integrity sha512-4Gvl0yH0T3hhSdiiOci3+IKIfVG9x2os0hGWsbfa8QuyGgk9mZOqgTBnSCRtuxsdAyzUix9kfcTnfNolVNtprg==
sequelize-cli@~5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/sequelize-cli/-/sequelize-cli-5.5.1.tgz#0b9c2fc04d082cc8ae0a8fe270b96bb606152bab"
integrity sha512-ZM4kUZvY3y14y+Rq3cYxGH7YDJz11jWHcN2p2x7rhAIemouu4CEXr5ebw30lzTBtyXV4j2kTO+nUjZOqzG7k+Q==
dependencies:
bluebird "^3.5.3"
cli-color "^1.4.0"
@ -11287,33 +11287,33 @@ sequelize-cli@~5.4.0:
lodash "^4.17.5"
resolve "^1.5.0"
umzug "^2.1.0"
yargs "^12.0.5"
yargs "^13.1.0"
sequelize-pool@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-2.3.0.tgz#64f1fe8744228172c474f530604b6133be64993d"
integrity sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==
sequelize@5.15.1:
version "5.15.1"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.15.1.tgz#f130ded17e74395ae7f5e265277c99577e895afb"
integrity sha512-DCzzJYvJLMKnyf8G3at2A+yM9M2fSQmTmuOYIpCWM8Gjqx3XfgNTd1NkuyPWFoi1/d1AXQsN2VDPXkPczida8A==
sequelize@5.21.3:
version "5.21.3"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.21.3.tgz#f8a6fa0245f8995d70849e4da00c2c7c9aa9f569"
integrity sha512-ptdeAxwTY0zbj7AK8m+SH3z52uHVrt/qmOTSIGo/kyfnSp3h5HeKlywkJf5GEk09kuRrPHfWARVSXH1W3IGU7g==
dependencies:
bluebird "^3.5.0"
cls-bluebird "^2.1.0"
debug "^4.1.1"
dottie "^2.0.0"
inflection "1.12.0"
lodash "^4.17.11"
lodash "^4.17.15"
moment "^2.24.0"
moment-timezone "^0.5.21"
retry-as-promised "^3.1.0"
semver "^6.1.1"
retry-as-promised "^3.2.0"
semver "^6.3.0"
sequelize-pool "^2.3.0"
toposort-class "^1.0.1"
uuid "^3.2.1"
uuid "^3.3.3"
validator "^10.11.0"
wkx "^0.4.6"
wkx "^0.4.8"
serialize-error@2.1.0:
version "2.1.0"
@ -12848,11 +12848,16 @@ utils-merge@1.0.1, utils-merge@1.x.x:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.3.2, uuid@^3.0.0, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@~3.3.2:
uuid@3.3.2, uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2, uuid@~3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
v8-compile-cache@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe"
@ -13586,7 +13591,7 @@ winston@~3.2.1:
triple-beam "^1.3.0"
winston-transport "^4.3.0"
wkx@^0.4.6:
wkx@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.8.tgz#a092cf088d112683fdc7182fd31493b2c5820003"
integrity sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==
@ -13831,7 +13836,7 @@ yargs@13.2.4:
y18n "^4.0.0"
yargs-parser "^13.1.0"
yargs@^12.0.2, yargs@^12.0.5:
yargs@^12.0.2:
version "12.0.5"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
@ -13849,7 +13854,7 @@ yargs@^12.0.2, yargs@^12.0.5:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
yargs@^13.2.2, yargs@~13.3.0:
yargs@^13.1.0, yargs@^13.2.2, yargs@~13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==