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 ++++- locales/en.json | 6 ++++-- public/views/index/body.ejs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) 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')) }) } diff --git a/locales/en.json b/locales/en.json index 1aef3f6d..b19089e4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -105,5 +105,7 @@ "Export to Snippet": "Export to Snippet", "Select Visibility Level": "Select Visibility Level", "Night Theme": "Night Theme", - "Follow us on %s and %s.": "Follow us on %s, and %s." -} + "Follow us on %s and %s.": "Follow us on %s, and %s.", + "Privacy": "Privacy", + "Terms of Use": "Terms of Use" +} \ No newline at end of file diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs index d8766fec..e3a3a85c 100644 --- a/public/views/index/body.ejs +++ b/public/views/index/body.ejs @@ -147,7 +147,7 @@

- © 2018 HackMD | <%= __('Releases') %> + © 2018 HackMD | <%= __('Releases') %><% if(privacyStatement) { %> | <%= __('Privacy') %><% } %><% if(termsOfUse) { %> | <%= __('Terms of Use') %><% } %>

<%- __('Follow us on %s and %s.', ' GitHub, Twitter', ' Facebook') %> -- 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(-) 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(-) 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(+) 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(-) 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 9fd09a8dfb8c59a44e9b2b51658e9e638a855635 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Fri, 25 May 2018 17:03:35 +0200 Subject: Add delete user UI This provides the UI for the delete user feature introduced in 4229084c6211db3d22cd9abec99b957725650b9e Placing of the user delete button is not perfect, but can be moved to an own user tab later on. Signed-off-by: Sheogorath --- locales/en.json | 7 +++++-- public/js/cover.js | 20 ++++++++++++-------- public/views/index/body.ejs | 33 +++++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/locales/en.json b/locales/en.json index b19089e4..8b2574a7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -107,5 +107,8 @@ "Night Theme": "Night Theme", "Follow us on %s and %s.": "Follow us on %s, and %s.", "Privacy": "Privacy", - "Terms of Use": "Terms of Use" -} \ No newline at end of file + "Terms of Use": "Terms of Use", + "Do you really want to delete your user account?": "Do you really want to delete your user account?", + "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.", + "Delete user": "Delete user" +} diff --git a/public/js/cover.js b/public/js/cover.js index c9c2b6cb..2a09b4c3 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -39,7 +39,7 @@ const options = { '' + '
' + '
' + - '
' + + '
' + '
' + '

' + '

' + @@ -208,8 +208,8 @@ function historyCloseClick (e) { e.preventDefault() const id = $(this).closest('a').siblings('span').html() const value = historyList.get('id', id)[0]._values - $('.ui-delete-modal-msg').text('Do you really want to delete below history?') - $('.ui-delete-modal-item').html(` ${value.text}
${value.time}`) + $('.ui-delete-history-modal-msg').text('Do you really want to delete below history?') + $('.ui-delete-history-modal-item').html(` ${value.text}
${value.time}`) clearHistory = false deleteId = id } @@ -277,7 +277,7 @@ function deleteHistory () { checkHistoryList() } } - $('.delete-modal').modal('hide') + $('.delete-history-modal').modal('hide') deleteId = null clearHistory = false }) @@ -297,12 +297,12 @@ function deleteHistory () { deleteId = null }) } - $('.delete-modal').modal('hide') + $('.delete-history-modal').modal('hide') clearHistory = false }) } -$('.ui-delete-modal-confirm').click(() => { +$('.ui-delete-history-modal-confirm').click(() => { deleteHistory() }) @@ -342,8 +342,8 @@ $('.ui-open-history').bind('change', e => { }) $('.ui-clear-history').click(() => { - $('.ui-delete-modal-msg').text('Do you really want to clear all history?') - $('.ui-delete-modal-item').html('There is no turning back.') + $('.ui-delete-history-modal-msg').text('Do you really want to clear all history?') + $('.ui-delete-history-modal-item').html('There is no turning back.') clearHistory = true deleteId = null }) @@ -371,6 +371,10 @@ $('.ui-refresh-history').click(() => { }) }) +$('.ui-delete-user-modal-cancel').click(() => { + $('.ui-delete-user').parent().removeClass('active') +}) + $('.ui-logout').click(() => { clearLoginState() location.href = `${serverurl}/logout` diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs index e3a3a85c..d4350540 100644 --- a/public/views/index/body.ejs +++ b/public/views/index/body.ejs @@ -27,6 +27,7 @@

@@ -108,7 +109,7 @@ - + @@ -157,8 +158,8 @@
- - -- 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(+) 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 From 75f28ca7f3600ab6483f38ad47a0c8430858f5c3 Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Sat, 26 May 2018 03:19:10 +0200 Subject: Add export data UI This adds the UI for the export feature introduced in bcbb8c67c9f8092643c318140f6613324f306bd2 It allows to download all notes from the main page in the default user submenu. Signed-off-by: Sheogorath --- locales/en.json | 3 ++- public/views/index/body.ejs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 8b2574a7..f9c29b53 100644 --- a/locales/en.json +++ b/locales/en.json @@ -110,5 +110,6 @@ "Terms of Use": "Terms of Use", "Do you really want to delete your user account?": "Do you really want to delete your user account?", "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "This will delete your account, all notes that are owned by you and remove all references to your account from other notes.", - "Delete user": "Delete user" + "Delete user": "Delete user", + "Export user data": "Export user data" } diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs index f28ab11d..a7c5a0b8 100644 --- a/public/views/index/body.ejs +++ b/public/views/index/body.ejs @@ -27,6 +27,7 @@ -- cgit v1.2.3 From 6f8bd8fdc90bf6e6f77591a28031edae4e58ceed Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Sun, 27 May 2018 15:28:09 +0200 Subject: Fix missing dependency To export the notes we need the archiver package that takes care of creating the zip files. Looks like I forgot this one in the initial commit. Signed-off-by: Sheogorath --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1d96de85..f676a4cf 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "Idle.Js": "git+https://github.com/shawnmclean/Idle.js", + "archiver": "^2.1.1", "async": "^2.1.4", "aws-sdk": "^2.7.20", "base64url": "^3.0.0", -- cgit v1.2.3 From fce735e833f91a0f1d17c518b65c4c724d1a4b4d Mon Sep 17 00:00:00 2001 From: Sheogorath Date: Mon, 28 May 2018 09:26:21 +0200 Subject: Add privacy policy example As we use various services and integration we should provide an example privacy policy. It has to be adjust when using it to match your setup. Signed-off-by: Sheogorath --- public/docs/privacy.md.example | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 public/docs/privacy.md.example diff --git a/public/docs/privacy.md.example b/public/docs/privacy.md.example new file mode 100644 index 00000000..08bf9091 --- /dev/null +++ b/public/docs/privacy.md.example @@ -0,0 +1,17 @@ +Privacy +=== + +We process the following data, for the following purposes: + +|your data|our usage| +|---------|---------| +|IP-Address|Used to communicate with your browser and our servers. It's may exposed to third-parties which provide resources for this service. These services are, depending on your login method, the document you visit and the setup of this instance: Google, Disqus, MathJax, GitHub, SlideShare/LinkedIn, yahoo, Gravatar, Imgur, Amazon, and Cloudflare.| +|Usernames and profiles|Your username as well as user profiles that are connected with it are transmitted and stored by us to provide a useful login integration with services like GitHub, Facebook, Twitter, GitLab, Dropbox, Google. Depending on the setup of this HackMD instance there are maybe other third-parties involved using SAML, LDAP or the integration with a Mattermost instance.| +|Profile pictures| Your profile picture is either loaded from the service you used to login, the HackMD instance or Gravatar.| +|Uploaded pictures| Pictures that are uploaded for documents are either uploaded to Amazon S3, Imgur, a minio instance or the local filesystem of the HackMD server.| + +All account data and notes are stored in a mysql/postgres/sqlite database. Besides the user accounts and the document themselves also relationships between the documents and the user accounts are stored. This includes ownership, authorship and revisions of all changes made during the creation of a note. + +To delete your account and all your notes owned by your user account, you can find a button in the drop down menu on the front page. + +The deletion of guest notes is not possible. These don't have any ownership and this means we can't connect these to you or anyone else. If you participated in a guest note or a note owned by someone else, your authorship for the revisions is removed from these notes as well. But the content you created will stay in place as the integrity of these notes has to stay untouched. -- cgit v1.2.3