Skip to content

Commit 56d78a7

Browse files
authored
Merge pull request #830 from SISheogorath/feature/GDPR
GDPR compliant part 1
2 parents f36b10a + fce735e commit 56d78a7

13 files changed

Lines changed: 216 additions & 30 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict'
2+
module.exports = {
3+
up: function (queryInterface, Sequelize) {
4+
return queryInterface.addColumn('Users', 'deleteToken', {
5+
type: Sequelize.UUID,
6+
defaultValue: Sequelize.UUIDV4
7+
})
8+
},
9+
10+
down: function (queryInterface, Sequelize) {
11+
return queryInterface.removeColumn('Users', 'deleteToken')
12+
}
13+
}

lib/models/author.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ module.exports = function (sequelize, DataTypes) {
2424
Author.belongsTo(models.Note, {
2525
foreignKey: 'noteId',
2626
as: 'note',
27-
constraints: false
27+
constraints: false,
28+
onDelete: 'CASCADE',
29+
hooks: true
2830
})
2931
Author.belongsTo(models.User, {
3032
foreignKey: 'userId',
3133
as: 'user',
32-
constraints: false
34+
constraints: false,
35+
onDelete: 'CASCADE',
36+
hooks: true
3337
})
3438
}
3539
}

lib/models/note.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,15 @@ module.exports = function (sequelize, DataTypes) {
8585
type: DataTypes.DATE
8686
}
8787
}, {
88-
paranoid: true,
88+
paranoid: false,
8989
classMethods: {
9090
associate: function (models) {
9191
Note.belongsTo(models.User, {
9292
foreignKey: 'ownerId',
9393
as: 'owner',
94-
constraints: false
94+
constraints: false,
95+
onDelete: 'CASCADE',
96+
hooks: true
9597
})
9698
Note.belongsTo(models.User, {
9799
foreignKey: 'lastchangeuserId',

lib/models/revision.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ module.exports = function (sequelize, DataTypes) {
102102
Revision.belongsTo(models.Note, {
103103
foreignKey: 'noteId',
104104
as: 'note',
105-
constraints: false
105+
constraints: false,
106+
onDelete: 'CASCADE',
107+
hooks: true
106108
})
107109
},
108110
getNoteRevisions: function (note, callback) {

lib/models/user.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ module.exports = function (sequelize, DataTypes) {
3131
refreshToken: {
3232
type: DataTypes.TEXT
3333
},
34+
deleteToken: {
35+
type: DataTypes.UUID,
36+
defaultValue: Sequelize.UUIDV4
37+
},
3438
email: {
3539
type: Sequelize.TEXT,
3640
validate: {
@@ -66,6 +70,9 @@ module.exports = function (sequelize, DataTypes) {
6670
})
6771
},
6872
getProfile: function (user) {
73+
if (!user) {
74+
return null
75+
}
6976
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
7077
},
7178
parseProfile: function (profile) {

lib/realtime.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -486,11 +486,13 @@ function startConnection (socket) {
486486
for (var i = 0; i < note.authors.length; i++) {
487487
var author = note.authors[i]
488488
var profile = models.User.getProfile(author.user)
489-
authors[author.userId] = {
490-
userid: author.userId,
491-
color: author.color,
492-
photo: profile.photo,
493-
name: profile.name
489+
if (profile) {
490+
authors[author.userId] = {
491+
userid: author.userId,
492+
color: author.color,
493+
photo: profile.photo,
494+
name: profile.name
495+
}
494496
}
495497
}
496498

lib/response.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// response
33
// external modules
44
var fs = require('fs')
5+
var path = require('path')
56
var markdownpdf = require('markdown-pdf')
67
var shortId = require('shortid')
78
var querystring = require('querystring')
@@ -61,7 +62,10 @@ function responseError (res, code, detail, msg) {
6162
}
6263

6364
function showIndex (req, res, next) {
64-
res.render(config.indexPath, {
65+
var authStatus = req.isAuthenticated()
66+
var deleteToken = ''
67+
68+
var data = {
6569
url: config.serverURL,
6670
useCDN: config.useCDN,
6771
allowAnonymous: config.allowAnonymous,
@@ -81,10 +85,28 @@ function showIndex (req, res, next) {
8185
email: config.isEmailEnable,
8286
allowEmailRegister: config.allowEmailRegister,
8387
allowPDFExport: config.allowPDFExport,
84-
signin: req.isAuthenticated(),
88+
signin: authStatus,
8589
infoMessage: req.flash('info'),
86-
errorMessage: req.flash('error')
87-
})
90+
errorMessage: req.flash('error'),
91+
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
92+
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
93+
deleteToken: deleteToken
94+
}
95+
96+
if (authStatus) {
97+
models.User.findOne({
98+
where: {
99+
id: req.user.id
100+
}
101+
}).then(function (user) {
102+
if (user) {
103+
data.deleteToken = user.deleteToken
104+
res.render(config.indexPath, data)
105+
}
106+
})
107+
} else {
108+
res.render(config.indexPath, data)
109+
}
88110
}
89111

90112
function responseHackMD (res, note) {

lib/web/userRouter.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use strict'
22

3+
const archiver = require('archiver')
4+
const async = require('async')
35
const Router = require('express').Router
46

57
const response = require('../response')
8+
const config = require('../config')
69
const models = require('../models')
710
const logger = require('../logger')
811
const {generateAvatar} = require('../letter-avatars')
@@ -36,6 +39,87 @@ UserRouter.get('/me', function (req, res) {
3639
}
3740
})
3841

42+
// delete the currently authenticated user
43+
UserRouter.get('/me/delete/:token?', function (req, res) {
44+
if (req.isAuthenticated()) {
45+
models.User.findOne({
46+
where: {
47+
id: req.user.id
48+
}
49+
}).then(function (user) {
50+
if (!user) {
51+
return response.errorNotFound(res)
52+
}
53+
if (user.deleteToken === req.params.token) {
54+
user.destroy().then(function () {
55+
res.redirect(config.serverURL + '/')
56+
})
57+
} else {
58+
return response.errorForbidden(res)
59+
}
60+
}).catch(function (err) {
61+
logger.error('delete user failed: ' + err)
62+
return response.errorInternalError(res)
63+
})
64+
} else {
65+
return response.errorForbidden(res)
66+
}
67+
})
68+
69+
// export the data of the authenticated user
70+
UserRouter.get('/me/export', function (req, res) {
71+
if (req.isAuthenticated()) {
72+
// let output = fs.createWriteStream(__dirname + '/example.zip');
73+
let archive = archiver('zip', {
74+
zlib: { level: 3 } // Sets the compression level.
75+
})
76+
res.setHeader('Content-Type', 'application/zip')
77+
res.attachment('archive.zip')
78+
archive.pipe(res)
79+
archive.on('error', function (err) {
80+
logger.error('export user data failed: ' + err)
81+
return response.errorInternalError(res)
82+
})
83+
models.User.findOne({
84+
where: {
85+
id: req.user.id
86+
}
87+
}).then(function (user) {
88+
models.Note.findAll({
89+
where: {
90+
ownerId: user.id
91+
}
92+
}).then(function (notes) {
93+
let list = []
94+
async.each(notes, function (note, callback) {
95+
let title
96+
let extension = ''
97+
do {
98+
title = note.title + extension
99+
extension++
100+
} while (list.indexOf(title) !== -1)
101+
102+
list.push(title)
103+
logger.debug('Write: ' + title + '.md')
104+
archive.append(Buffer.from(note.content), { name: title + '.md', date: note.lastchangeAt })
105+
callback(null, null)
106+
}, function (err) {
107+
if (err) {
108+
return response.errorInternalError(res)
109+
}
110+
111+
archive.finalize()
112+
})
113+
})
114+
}).catch(function (err) {
115+
logger.error('export user data failed: ' + err)
116+
return response.errorInternalError(res)
117+
})
118+
} else {
119+
return response.errorForbidden(res)
120+
}
121+
})
122+
39123
UserRouter.get('/user/:username/avatar.svg', function (req, res, next) {
40124
res.setHeader('Content-Type', 'image/svg+xml')
41125
res.setHeader('Cache-Control', 'public, max-age=86400')

locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,11 @@
105105
"Export to Snippet": "Export to Snippet",
106106
"Select Visibility Level": "Select Visibility Level",
107107
"Night Theme": "Night Theme",
108-
"Follow us on %s and %s.": "Follow us on %s, and %s."
108+
"Follow us on %s and %s.": "Follow us on %s, and %s.",
109+
"Privacy": "Privacy",
110+
"Terms of Use": "Terms of Use",
111+
"Do you really want to delete your user account?": "Do you really want to delete your user account?",
112+
"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.",
113+
"Delete user": "Delete user",
114+
"Export user data": "Export user data"
109115
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
},
1717
"dependencies": {
1818
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js",
19+
"archiver": "^2.1.1",
1920
"async": "^2.1.4",
2021
"aws-sdk": "^2.7.20",
2122
"base64url": "^3.0.0",

0 commit comments

Comments
 (0)