From 41a36e2e1877b4a2ab6751c011e80582f8ccbcf2 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Wed, 23 May 2018 01:14:52 +0200 Subject: Add privacy and ToS links To be GDPR compliant we need to provide privacy statement. These should be linked on the index page. So as soon as a document exist under `public/docs/privacy.md` the link will show up. Since we already add legal links, we also add Terms of Use, which will show up as soon as `public/docs/terms-of-use.md` exists. This should allow everyone to provide the legal documents they need for GDPR and other privacy and business laws. Signed-off-by: Sheogorath --- lib/response.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/response.js b/lib/response.js index ae3e45fa..2ea2f1c6 100644 --- a/lib/response.js +++ b/lib/response.js @@ -2,6 +2,7 @@ // response // external modules var fs = require('fs') +var path = require('path') var markdownpdf = require('markdown-pdf') var shortId = require('shortid') var querystring = require('querystring') @@ -75,7 +76,9 @@ function showIndex (req, res, next) { allowPDFExport: config.allowPDFExport, signin: req.isAuthenticated(), infoMessage: req.flash('info'), - errorMessage: req.flash('error') + errorMessage: req.flash('error'), + privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), + termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')) }) } -- cgit v1.2.3 From 8aa5c03213fb8809c4d41f95ee6b8d8e2967812a Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 14:50:31 +0200 Subject: Use hard delete instead of soft delete Right now we only flag notes as deleted. This is no longer allowed under GDPR. Make sure you do regular backups! Signed-off-by: Sheogorath --- lib/models/note.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/models/note.js b/lib/models/note.js index 2a048e37..7b9a909a 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -85,7 +85,7 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DATE } }, { - paranoid: true, + paranoid: false, classMethods: { associate: function (models) { Note.belongsTo(models.User, { -- cgit v1.2.3 From 408ab7ae1dfa5d1c7dedb2f9fde239596520b2e6 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 14:54:00 +0200 Subject: Use cascaded deletes When we delete a user we should delete all the notes that belong to this user including the revisions of these notes. Signed-off-by: Sheogorath --- lib/models/author.js | 8 ++++++-- lib/models/note.js | 4 +++- lib/models/revision.js | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/models/author.js b/lib/models/author.js index 8b4f74e5..03f832a4 100644 --- a/lib/models/author.js +++ b/lib/models/author.js @@ -24,12 +24,16 @@ module.exports = function (sequelize, DataTypes) { Author.belongsTo(models.Note, { foreignKey: 'noteId', as: 'note', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) Author.belongsTo(models.User, { foreignKey: 'userId', as: 'user', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) } } diff --git a/lib/models/note.js b/lib/models/note.js index 7b9a909a..7d8e9625 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -91,7 +91,9 @@ module.exports = function (sequelize, DataTypes) { Note.belongsTo(models.User, { foreignKey: 'ownerId', as: 'owner', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) Note.belongsTo(models.User, { foreignKey: 'lastchangeuserId', diff --git a/lib/models/revision.js b/lib/models/revision.js index 9ecd14dc..8bc95cb1 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -102,7 +102,9 @@ module.exports = function (sequelize, DataTypes) { Revision.belongsTo(models.Note, { foreignKey: 'noteId', as: 'note', - constraints: false + constraints: false, + onDelete: 'CASCADE', + hooks: true }) }, getNoteRevisions: function (note, callback) { -- cgit v1.2.3 From 4229084c6211db3d22cd9abec99b957725650b9e Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 15:20:38 +0200 Subject: Add delete function for authenticated users Allow users to delete themselbes. This is require to be GDPR compliant. See: https://gdpr-info.eu/art-17-gdpr/ Signed-off-by: Sheogorath --- lib/web/userRouter.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'lib') diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index 963961c7..b8bd9154 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -3,6 +3,7 @@ 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') @@ -36,6 +37,29 @@ UserRouter.get('/me', function (req, res) { } }) +// delete the currently authenticated user +UserRouter.get('/me/delete', function (req, res) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + user.destroy().then(function () { + res.redirect(config.serverURL + '/') + }) + }).catch(function (err) { + logger.error('delete user failed: ' + err) + return response.errorInternalError(res) + }) + } else { + res.send({ + status: 'forbidden' + }) + } +}) + 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') -- cgit v1.2.3 From e31d204d747c6db0b39288fa55269e8a3311525c Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 16:15:10 +0200 Subject: Fix requests for deleted users When users are requested from the authorship which no longer exist, they shouldn't cause a 500. Signed-off-by: Sheogorath --- lib/models/user.js | 3 +++ lib/realtime.js | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/models/user.js b/lib/models/user.js index 4c823355..62ed5cc7 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -66,6 +66,9 @@ module.exports = function (sequelize, DataTypes) { }) }, getProfile: function (user) { + if (!user) { + return null + } return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) }, parseProfile: function (profile) { diff --git a/lib/realtime.js b/lib/realtime.js index 070bde2d..f6c62d4e 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -486,11 +486,13 @@ function startConnection (socket) { for (var i = 0; i < note.authors.length; i++) { var author = note.authors[i] var profile = models.User.getProfile(author.user) - authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: profile.photo, - name: profile.name + if (profile) { + authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: profile.photo, + name: profile.name + } } } -- cgit v1.2.3 From 70df29790a83db4abb40ed1e16cb05a3aa760672 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 18:19:31 +0200 Subject: Add token based security feature In the current setup users could be tricked into deleting their data by providing a malicious link like `[click me](/me/delete)`. This commit prevents such an easy attack and need the user's deleteToken to get his data deleted. In case someone requests his deletion by email you can also ask him for this token. We can add a GUI that shows it later on. Signed-off-by: Sheogorath --- .../20180525153000-user-add-delete-token.js | 13 +++++++++++ lib/models/user.js | 4 ++++ lib/response.js | 27 ++++++++++++++++++---- lib/web/userRouter.js | 20 +++++++++------- 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 lib/migrations/20180525153000-user-add-delete-token.js (limited to 'lib') diff --git a/lib/migrations/20180525153000-user-add-delete-token.js b/lib/migrations/20180525153000-user-add-delete-token.js new file mode 100644 index 00000000..642fa5d4 --- /dev/null +++ b/lib/migrations/20180525153000-user-add-delete-token.js @@ -0,0 +1,13 @@ +'use strict' +module.exports = { + up: function (queryInterface, Sequelize) { + return queryInterface.addColumn('Users', 'deleteToken', { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4 + }) + }, + + down: function (queryInterface, Sequelize) { + return queryInterface.removeColumn('Users', 'deleteToken') + } +} diff --git a/lib/models/user.js b/lib/models/user.js index 62ed5cc7..019aab7e 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -31,6 +31,10 @@ module.exports = function (sequelize, DataTypes) { refreshToken: { type: DataTypes.STRING }, + deleteToken: { + type: DataTypes.UUID, + defaultValue: Sequelize.UUIDV4 + }, email: { type: Sequelize.TEXT, validate: { diff --git a/lib/response.js b/lib/response.js index 2ea2f1c6..b1b89c78 100644 --- a/lib/response.js +++ b/lib/response.js @@ -56,7 +56,10 @@ function responseError (res, code, detail, msg) { } function showIndex (req, res, next) { - res.render(config.indexPath, { + var authStatus = req.isAuthenticated() + var deleteToken = '' + + var data = { url: config.serverURL, useCDN: config.useCDN, allowAnonymous: config.allowAnonymous, @@ -74,12 +77,28 @@ function showIndex (req, res, next) { email: config.isEmailEnable, allowEmailRegister: config.allowEmailRegister, allowPDFExport: config.allowPDFExport, - signin: req.isAuthenticated(), + 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')) - }) + 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(config.indexPath, data) + } + }) + } else { + res.render(config.indexPath, data) + } } function responseHackMD (res, note) { diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index b8bd9154..6832d901 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -38,25 +38,29 @@ UserRouter.get('/me', function (req, res) { }) // delete the currently authenticated user -UserRouter.get('/me/delete', function (req, res) { +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) } - user.destroy().then(function () { - res.redirect(config.serverURL + '/') - }) + 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 { - res.send({ - status: 'forbidden' - }) + return response.errorForbidden(res) } }) -- cgit v1.2.3 From bcbb8c67c9f8092643c318140f6613324f306bd2 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Sat, 26 May 2018 02:53:21 +0200 Subject: Add note export function This function is the first step to get out data following GDPR about the transportability of data. Details: https://gdpr-info.eu/art-20-gdpr/ Signed-off-by: Sheogorath --- lib/web/userRouter.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'lib') diff --git a/lib/web/userRouter.js b/lib/web/userRouter.js index 6832d901..db786d53 100644 --- a/lib/web/userRouter.js +++ b/lib/web/userRouter.js @@ -1,5 +1,7 @@ 'use strict' +const archiver = require('archiver') +const async = require('async') const Router = require('express').Router const response = require('../response') @@ -64,6 +66,60 @@ UserRouter.get('/me/delete/:token?', function (req, res) { } }) +// export the data of the authenticated user +UserRouter.get('/me/export', function (req, res) { + if (req.isAuthenticated()) { + // let output = fs.createWriteStream(__dirname + '/example.zip'); + let 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) { + let list = [] + async.each(notes, function (note, callback) { + let title + let extension = '' + do { + title = note.title + extension + extension++ + } while (list.indexOf(title) !== -1) + + list.push(title) + logger.debug('Write: ' + title + '.md') + archive.append(Buffer.from(note.content), { name: title + '.md', 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') -- cgit v1.2.3